From 1fd3f5b841aefd11203f1b58aa92948fc8f319c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:41:31 +0200 Subject: [PATCH 01/13] Implement first-class callable syntax See https://wiki.php.net/rfc/first_class_callable_syntax --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 7 +++++++ src/main/php/lang/ast/emit/PHP70.class.php | 2 +- src/main/php/lang/ast/emit/PHP71.class.php | 2 +- src/main/php/lang/ast/emit/PHP72.class.php | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index eada459d..1c12a90f 100755 --- a/composer.json +++ b/composer.json @@ -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" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 02207b71..3a10a602 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -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); + $result->out->write('(... '.$t.')'); + } + protected function emitInvoke($result, $invoke) { $this->emitOne($result, $invoke->expression); $result->out->write('('); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 4032fa26..45c85319 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -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 */ diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 2c9f4640..1148183d 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -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 */ diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index d717e5f7..0ce0e1e1 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -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 */ From 29c2a1c428822bb5648b8794a749cf46f4d74b5f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:42:21 +0200 Subject: [PATCH 02/13] Emit callable expressions as regular `function` closures for PHP < 7.4 --- .../ast/emit/CallablesAsClosures.class.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 src/main/php/lang/ast/emit/CallablesAsClosures.class.php diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php new file mode 100755 index 00000000..90ffc338 --- /dev/null +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -0,0 +1,34 @@ + 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) { + 'this' === $var->name || $use[$var->name]= true; + } + + // 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.'); }'); + } +} \ No newline at end of file From ab460eff5517025c2d24b8c401332bf93a715b47 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:42:34 +0200 Subject: [PATCH 03/13] Add tests for callable syntax --- .../emit/CallableSyntaxTest.class.php | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php new file mode 100755 index 00000000..3e413ea7 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -0,0 +1,139 @@ +run('class { + public function run() { return strlen(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function class_method() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return self::length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function private_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_function() { + $f= $this->run('class { + public function run() { + $func= "strlen"; + return $func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method_reference() { + $f= $this->run('class { + private $func= "strlen"; + public function run() { + return ($this->func)(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_instance_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return $this->$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return self::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method_with_variable_class() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + $class= __CLASS__; + return $class::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function string_function_reference() { + $f= $this->run('class { + public function run() { return "strlen"(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_instance_method_reference() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return [$this, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_class_method_reference() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return [self::class, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } +} \ No newline at end of file From 6b85e46ebcf91f55b532860f64b21049689fe383 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 23:08:45 +0200 Subject: [PATCH 04/13] Remove unused imports --- .../php/lang/ast/unittest/emit/CallableSyntaxTest.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 3e413ea7..e93022ba 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,7 +1,6 @@ Date: Sat, 3 Jul 2021 11:03:06 +0200 Subject: [PATCH 05/13] Add variations to variable method reference tests --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index e93022ba..f18bb60f 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,6 +1,6 @@ $func(...)', '$this->{$func}(...)'])] + public function variable_instance_method($expr) { $f= $this->run('class { private function length($arg) { return strlen($arg); } public function run() { $func= "length"; - return $this->$func(...); + return '.$expr.'; } }'); Assert::equals(4, $f('Test')); } - #[Test] - public function variable_class_method() { + #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] + public function variable_class_method($expr) { $f= $this->run('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; - return self::$func(...); + return '.$expr.'; } }'); From a9bc033dd8ce9a8e3dabe8b40fe605b3e1a2cdee Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Jul 2021 11:07:13 +0200 Subject: [PATCH 06/13] Extract assertion into helper method --- .../emit/CallableSyntaxTest.class.php | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index f18bb60f..a5275a1d 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -2,100 +2,100 @@ 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() { - $f= $this->run('class { + $this->verify('class { public function run() { return strlen(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function instance_method() { - $f= $this->run('class { + $this->verify('class { public function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function class_method() { - $f= $this->run('class { + $this->verify('class { public static function length($arg) { return strlen($arg); } public function run() { return self::length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function private_method() { - $f= $this->run('class { + $this->verify('class { private function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function variable_function() { - $f= $this->run('class { + $this->verify('class { public function run() { $func= "strlen"; return $func(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function instance_method_reference() { - $f= $this->run('class { + $this->verify('class { private $func= "strlen"; public function run() { return ($this->func)(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])] public function variable_instance_method($expr) { - $f= $this->run('class { + $this->verify('class { private function length($arg) { return strlen($arg); } public function run() { $func= "length"; return '.$expr.'; } }'); - - Assert::equals(4, $f('Test')); } #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] public function variable_class_method($expr) { - $f= $this->run('class { + $this->verify('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; return '.$expr.'; } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function variable_class_method_with_variable_class() { - $f= $this->run('class { + $this->verify('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; @@ -103,36 +103,28 @@ public function run() { return $class::$func(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function string_function_reference() { - $f= $this->run('class { + $this->verify('class { public function run() { return "strlen"(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function array_instance_method_reference() { - $f= $this->run('class { + $this->verify('class { public function length($arg) { return strlen($arg); } public function run() { return [$this, "length"](...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function array_class_method_reference() { - $f= $this->run('class { + $this->verify('class { public static function length($arg) { return strlen($arg); } public function run() { return [self::class, "length"](...); } }'); - - Assert::equals(4, $f('Test')); } } \ No newline at end of file From 12e517bf602f1de306ecae5ec626feab966000b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 11:36:58 +0200 Subject: [PATCH 07/13] Make code consistent with other places in emitter --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 90ffc338..92acd613 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -20,8 +20,9 @@ protected function emitCallable($result, $callable) { // [Foo::class, $method](...); => use ($method) $use= []; foreach ($result->codegen->search($callable, 'variable') as $var) { - 'this' === $var->name || $use[$var->name]= true; + $use[$var->name]= true; } + unset($use['this']); // Create closure $t= $result->temp(); From 1c2279bae22104d5fc7d8c6b97cffdd9b4caef08 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 11:39:39 +0200 Subject: [PATCH 08/13] QA: Compress whitespace --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 4 ++-- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 92acd613..4302ba81 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -27,9 +27,9 @@ protected function emitCallable($result, $callable) { // Create closure $t= $result->temp(); $result->out->write('function(...'.$t.')'); - $use && $result->out->write('use($'.implode(', $', array_keys($use)).')'); + $use && $result->out->write('use($'.implode(',$', array_keys($use)).')'); $result->out->write('{ return '); $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.'); }'); + $result->out->write('(...'.$t.'); }'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3a10a602..05e0241b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -990,9 +990,9 @@ protected function emitNewClass($result, $new) { protected function emitCallable($result, $callable) { $t= $result->temp(); - $result->out->write('fn(...'.$t.') => '); + $result->out->write('fn(...'.$t.')=>'); $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.')'); + $result->out->write('(...'.$t.')'); } protected function emitInvoke($result, $invoke) { From 0c84bef73ff9b34bbebcb02b2b9650c2bbc44124 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 12:14:45 +0200 Subject: [PATCH 09/13] Add changelog entry --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 0620677c..d9addf0c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #114: Implements first-class callable syntax: `strlen(...)` + now returns a closure which if invoked with a string argument, returns + its length. Includes support for static and instance methods as well as + indirect references like `$closure(...)` and `self::{$expression}(...)`, + see https://wiki.php.net/rfc/first_class_callable_syntax + (@thekid) + ## 6.5.0 / 2021-05-22 * Merged PR #111: Add support for directives using declare - @thekid From a3217345b791249e42b622076aec3333a8076253 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 13:08:44 +0200 Subject: [PATCH 10/13] Add test for `fn() => ...` style closures --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index a5275a1d..81889372 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -52,7 +52,7 @@ public function run() { return $this->length(...); } } #[Test] - public function variable_function() { + public function string_reference() { $this->verify('class { public function run() { $func= "strlen"; @@ -62,7 +62,17 @@ public function run() { } #[Test] - public function instance_method_reference() { + public function fn_reference() { + $this->verify('class { + public function run() { + $func= fn($arg) => strlen($arg); + return $func(...); + } + }'); + } + + #[Test] + public function instance_property_reference() { $this->verify('class { private $func= "strlen"; public function run() { From 8ef7349a1ba7ebafcf719299dbe9d7ba9d5d4fd9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 13:54:07 +0200 Subject: [PATCH 11/13] Ensure exceptions are raised for non-existant callables See https://github.com/xp-framework/compiler/pull/114#discussion_r663485667 --- .../ast/emit/CallablesAsClosures.class.php | 63 ++++++++++++------- src/main/php/lang/ast/emit/PHP.class.php | 4 +- src/main/php/lang/ast/emit/PHP70.class.php | 51 ++++++++++++++- src/main/php/lang/ast/emit/PHP74.class.php | 2 +- src/main/php/lang/ast/emit/PHP80.class.php | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- .../emit/CallableSyntaxTest.class.php | 14 ++++- 7 files changed, 108 insertions(+), 30 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 4302ba81..4ac1c2b4 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,35 +1,54 @@ out->write('\Closure::fromCallable('); + if ($callable->expression instanceof Literal) { - // 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']); + // Rewrite f() => "f" + $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); + } else if ($callable->expression instanceof InstanceExpression) { + + // Rewrite $this->f => [$this, "f"] + $result->out->write('['); + $this->emitOne($result, $callable->expression->expression); + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else if ($callable->expression instanceof ScopeExpression) { - // 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.'); }'); + // Rewrite self::f => ["self", "f"] + $result->out->write('['); + if ($callable->expression->type instanceof Node) { + $this->emitOne($result, $callable->expression->type); + } else { + $result->out->write('"'.$callable->expression->type.'"'); + } + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else { + + // Emit other expressions as-is + $this->emitOne($result, $callable->expression); + } + $result->out->write(')'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 05e0241b..6e1cd84f 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -989,10 +989,8 @@ protected function emitNewClass($result, $new) { } protected function emitCallable($result, $callable) { - $t= $result->temp(); - $result->out->write('fn(...'.$t.')=>'); $this->emitOne($result, $callable->expression); - $result->out->write('(...'.$t.')'); + $result->out->write('(...)'); } protected function emitInvoke($result, $invoke) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 45c85319..70f71279 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,5 +1,7 @@ literal mappings */ @@ -26,4 +28,51 @@ public function __construct() { }, ]; } + + protected function emitCallable($result, $callable) { + $t= $result->temp(); + $result->out->write('(is_callable('.$t.'='); + if ($callable->expression instanceof Literal) { + + // Rewrite f() => "f" + $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); + } else if ($callable->expression instanceof InstanceExpression) { + + // Rewrite $this->f => [$this, "f"] + $result->out->write('['); + $this->emitOne($result, $callable->expression->expression); + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else if ($callable->expression instanceof ScopeExpression) { + + // Rewrite self::f => [self::class, "f"] + $result->out->write('['); + if ($callable->expression->type instanceof Node) { + $this->emitOne($result, $callable->expression->type); + } else { + $result->out->write($callable->expression->type.'::class'); + } + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else { + + // Emit other expressions as-is + $this->emitOne($result, $callable->expression); + } + + // Emit equivalent of Closure::fromCallable() which doesn't exist until PHP 7.1 + $a= $result->temp(); + $result->out->write(')?function(...'.$a.') use('.$t.') { return '.$t.'(...'.$a.'); }:'); + $result->out->write('(function() { throw new \Error("Given argument is not callable"); })())'); + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 1e0918fa..e8336ff8 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index d8205039..d5eae2f8 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 6d8d5693..4b9882b8 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 81889372..39961358 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,6 +1,7 @@ nonexistant', 'self::nonexistant', '$nonexistant', '$null'])] + public function non_existant($expr) { + $this->run('class { + public function run() { + $null= null; + $nonexistant= "nonexistant"; + return '.$expr.'(...); + } + }'); + } } \ No newline at end of file From f25b9d2f1ff7d0a378562b990fdab73a391960d2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:10:44 +0200 Subject: [PATCH 12/13] Use release version of xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1c12a90f..73d49ba9 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0", + "xp-framework/ast": "^7.4", "php" : ">=7.0.0" }, "require-dev" : { From cf6f56a7e116741cf5815a785e71d607b5b09a45 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:18:30 +0200 Subject: [PATCH 13/13] Fix PHP 8.1 compatibility Calling static trait method T::__init is deprecated, it should only be called on a class using the trait --- src/main/php/lang/ast/emit/PHP.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d7d5621e..e20775b0 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -527,10 +527,9 @@ protected function emitTrait($result, $trait) { $this->emitOne($result, $member); $result->out->write("\n"); } + $result->out->write('}'); - $result->out->write('static function __init() {'); $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); - $result->out->write('}} '.$trait->name.'::__init();'); } protected function emitUse($result, $use) {