Skip to content

Implement first-class callable syntax #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0",
"xp-framework/ast": "^7.3",
"xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0",
"php" : ">=7.0.0"
},
"require-dev" : {
Expand Down
35 changes: 35 additions & 0 deletions src/main/php/lang/ast/emit/CallablesAsClosures.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php namespace lang\ast\emit;

/**
* Rewrites callable expressions to regular closures
*
* @see https://wiki.php.net/rfc/first_class_callable_syntax
*/
trait CallablesAsClosures {

protected function emitCallable($result, $callable) {

// Use variables in the following cases:
//
// $closure(...); => use ($closure)
// $obj->method(...); => use ($obj)
// $obj->$method(...); => use ($obj, $method)
// ($obj->property)(...); => use ($obj)
// $class::$method(...); => use ($class, $method)
// [$obj, 'method'](...); => use ($obj)
// [Foo::class, $method](...); => use ($method)
$use= [];
foreach ($result->codegen->search($callable, 'variable') as $var) {
$use[$var->name]= true;
}
unset($use['this']);

// Create closure
$t= $result->temp();
$result->out->write('function(...'.$t.')');
$use && $result->out->write('use($'.implode(',$', array_keys($use)).')');
$result->out->write('{ return ');
$this->emitOne($result, $callable->expression);
$result->out->write('(...'.$t.'); }');
}
}
7 changes: 7 additions & 0 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,13 @@ protected function emitNewClass($result, $new) {
array_shift($result->type);
}

protected function emitCallable($result, $callable) {
$t= $result->temp();
$result->out->write('fn(...'.$t.')=>');
$this->emitOne($result, $callable->expression);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the RFC merged to PHP 8.1, we should move this to a trait for PHP 7.4 and 8.0 and emit native code on PHP 8.1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the only thing we need to do is to remove the use CallablesAsClosures from the PHP81 emitter.

$result->out->write('(...'.$t.')');
}

protected function emitInvoke($result, $invoke) {
$this->emitOne($result, $invoke->expression);
$result->out->write('(');
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP70.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_70
*/
class PHP70 extends PHP {
use OmitPropertyTypes, OmitConstModifiers;
use OmitPropertyTypes, OmitConstModifiers, CallablesAsClosures;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP71.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_71
*/
class PHP71 extends PHP {
use OmitPropertyTypes;
use OmitPropertyTypes, CallablesAsClosures;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP72.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_72
*/
class PHP72 extends PHP {
use OmitPropertyTypes;
use OmitPropertyTypes, CallablesAsClosures;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals;

/** Sets up type => literal mappings */
Expand Down
130 changes: 130 additions & 0 deletions src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php namespace lang\ast\unittest\emit;

use unittest\{Assert, Test, Values};

/**
* Tests for first-class callable syntax
*
* @see https://wiki.php.net/rfc/first_class_callable_syntax#proposal
*/
class CallableSyntaxTest extends EmittingTest {

/**
* Verification helper
*
* @param string $code
* @return void
* @throws unittest.AssertionFailedError
*/
private function verify($code) {
Assert::equals(4, $this->run($code)('Test'));
}

#[Test]
public function native_function() {
$this->verify('class <T> {
public function run() { return strlen(...); }
}');
}

#[Test]
public function instance_method() {
$this->verify('class <T> {
public function length($arg) { return strlen($arg); }
public function run() { return $this->length(...); }
}');
}

#[Test]
public function class_method() {
$this->verify('class <T> {
public static function length($arg) { return strlen($arg); }
public function run() { return self::length(...); }
}');
}

#[Test]
public function private_method() {
$this->verify('class <T> {
private function length($arg) { return strlen($arg); }
public function run() { return $this->length(...); }
}');
}

#[Test]
public function variable_function() {
$this->verify('class <T> {
public function run() {
$func= "strlen";
return $func(...);
}
}');
}

#[Test]
public function instance_method_reference() {
$this->verify('class <T> {
private $func= "strlen";
public function run() {
return ($this->func)(...);
}
}');
}

#[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])]
public function variable_instance_method($expr) {
$this->verify('class <T> {
private function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
return '.$expr.';
}
}');
}

#[Test, Values(['self::$func(...)', 'self::{$func}(...)'])]
public function variable_class_method($expr) {
$this->verify('class <T> {
private static function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
return '.$expr.';
}
}');
}

#[Test]
public function variable_class_method_with_variable_class() {
$this->verify('class <T> {
private static function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
$class= __CLASS__;
return $class::$func(...);
}
}');
}

#[Test]
public function string_function_reference() {
$this->verify('class <T> {
public function run() { return "strlen"(...); }
}');
}

#[Test]
public function array_instance_method_reference() {
$this->verify('class <T> {
public function length($arg) { return strlen($arg); }
public function run() { return [$this, "length"](...); }
}');
}

#[Test]
public function array_class_method_reference() {
$this->verify('class <T> {
public static function length($arg) { return strlen($arg); }
public function run() { return [self::class, "length"](...); }
}');
}
}