diff --git a/ChangeLog.md b/ChangeLog.md index dc57fc52..81bf0309 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ 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 + ## 6.6.0 / 2021-07-10 * Emit null-coalesce operator as `$a ?? $a= expression` instead of as diff --git a/composer.json b/composer.json index eada459d..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": "^7.3", + "xp-framework/ast": "^7.4", "php" : ">=7.0.0" }, "require-dev" : { 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..4ac1c2b4 --- /dev/null +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -0,0 +1,54 @@ +out->write('\Closure::fromCallable('); + 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", "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 e9056ebf..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) { @@ -946,6 +945,11 @@ protected function emitNewClass($result, $new) { array_shift($result->type); } + protected function emitCallable($result, $callable) { + $this->emitOne($result, $callable->expression); + $result->out->write('(...)'); + } + 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 abeb39cf..68d190d3 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 @@ 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/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 52446c1e..7331a97f 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, RewriteEnums; /** 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 0a264943..8e33fe69 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, RewriteEnums; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 1a252e4d..04f51259 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, RewriteEnums; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, 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 f114218c..6c88ec1e 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, RewriteEnums; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, 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 fd95d873..0b287fc2 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 new file mode 100755 index 00000000..39961358 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -0,0 +1,152 @@ +run($code)('Test')); + } + + #[Test] + public function native_function() { + $this->verify('class { + public function run() { return strlen(...); } + }'); + } + + #[Test] + public function instance_method() { + $this->verify('class { + public function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + } + + #[Test] + public function class_method() { + $this->verify('class { + public static function length($arg) { return strlen($arg); } + public function run() { return self::length(...); } + }'); + } + + #[Test] + public function private_method() { + $this->verify('class { + private function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + } + + #[Test] + public function string_reference() { + $this->verify('class { + public function run() { + $func= "strlen"; + return $func(...); + } + }'); + } + + #[Test] + 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() { + return ($this->func)(...); + } + }'); + } + + #[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])] + public function variable_instance_method($expr) { + $this->verify('class { + 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 { + 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 { + 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 { + public function run() { return "strlen"(...); } + }'); + } + + #[Test] + public function array_instance_method_reference() { + $this->verify('class { + public function length($arg) { return strlen($arg); } + public function run() { return [$this, "length"](...); } + }'); + } + + #[Test] + public function array_class_method_reference() { + $this->verify('class { + public static function length($arg) { return strlen($arg); } + public function run() { return [self::class, "length"](...); } + }'); + } + + #[Test, Expect(Error::class), Values(['nonexistant', '$this->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