From d0b6ce612161ef81b55fb9322790172837389435 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 21 Apr 2025 15:56:25 +0200 Subject: [PATCH 1/3] Implement "clone with" See https://wiki.php.net/rfc/clone_with_v2 --- .../lang/ast/nodes/CloneExpression.class.php | 22 ++++++++++++++++ src/main/php/lang/ast/syntax/PHP.class.php | 25 ++++++++++++++++++- .../ast/unittest/parse/OperatorTest.class.php | 12 ++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100755 src/main/php/lang/ast/nodes/CloneExpression.class.php diff --git a/src/main/php/lang/ast/nodes/CloneExpression.class.php b/src/main/php/lang/ast/nodes/CloneExpression.class.php new file mode 100755 index 0000000..ccd79fc --- /dev/null +++ b/src/main/php/lang/ast/nodes/CloneExpression.class.php @@ -0,0 +1,22 @@ +expression= $expression; + $this->with= $with; + } + + /** @return iterable */ + public function children() { + yield &$this->expression; + foreach ($this->with as &$expr) { + yield &$expr; + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 80186a6..d89df11 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -13,6 +13,7 @@ CastExpression, CatchStatement, ClassDeclaration, + CloneExpression, ClosureExpression, Comment, Constant, @@ -222,7 +223,6 @@ public function __construct() { $this->prefix('-', 90); $this->prefix('++', 90); $this->prefix('--', 90); - $this->prefix('clone', 90); $this->assignment('='); $this->assignment('&='); @@ -283,6 +283,29 @@ public function __construct() { } }); + $this->prefix('clone', 90, function($parse, $token) { + + // clone $x vs. clone($x) or clone($x, id: 6100) + if ('(' === $parse->token->value) { + $parse->forward(); + $expression= $this->expression($parse, 90); + + if (',' === $parse->token->value) { + $parse->forward(); + $with= $this->arguments($parse); + } else { + $expression= new Braced($expression, $expression->line); + $with= []; + } + $parse->expecting(')', 'clone arguments'); + } else { + $expression= $this->expression($parse, 90); + $with= []; + } + + return new CloneExpression($expression, $with, $token->line); + }); + $this->prefix('{', 0, function($parse, $token) { $statements= $this->statements($parse); $parse->expecting('}', 'block'); diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index e2b9f6a..cc28c45 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -6,6 +6,7 @@ BinaryExpression, Braced, ClassDeclaration, + CloneExpression, InstanceExpression, InstanceOfExpression, InvokeExpression, @@ -115,11 +116,20 @@ public function append_array() { #[Test] public function clone_expression() { $this->assertParsed( - [new UnaryExpression('prefix', new Variable('a', self::LINE), 'clone', self::LINE)], + [new CloneExpression(new Variable('a', self::LINE), [], self::LINE)], 'clone $a;' ); } + #[Test] + public function clone_with() { + $with= ['id' => new Literal('6100', self::LINE)]; + $this->assertParsed( + [new CloneExpression(new Variable('a', self::LINE), $with, self::LINE)], + 'clone($a, id: 6100);' + ); + } + #[Test] public function error_suppression() { $this->assertParsed( From abf47cbb95107a464f9b325221dcc38d06eade5a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 14:35:14 +0200 Subject: [PATCH 2/3] Adjust to current version of RFC using `clone(, $properties)` --- .../lang/ast/nodes/CloneExpression.class.php | 12 +++++------- src/main/php/lang/ast/syntax/PHP.class.php | 17 ++++------------- .../ast/unittest/parse/OperatorTest.class.php | 8 ++++---- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/main/php/lang/ast/nodes/CloneExpression.class.php b/src/main/php/lang/ast/nodes/CloneExpression.class.php index ccd79fc..65f2d66 100755 --- a/src/main/php/lang/ast/nodes/CloneExpression.class.php +++ b/src/main/php/lang/ast/nodes/CloneExpression.class.php @@ -4,18 +4,16 @@ class CloneExpression extends Node { public $kind= 'clone'; - public $expression; - public $with; + public $arguments; - public function __construct($expression, $with, $line= -1) { - $this->expression= $expression; - $this->with= $with; + public function __construct($arguments, $line= -1) { + $this->arguments= $arguments; + $this->line= $line; } /** @return iterable */ public function children() { - yield &$this->expression; - foreach ($this->with as &$expr) { + foreach ($this->arguments as &$expr) { yield &$expr; } } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 483ebcf..962d5f5 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -296,25 +296,16 @@ public function __construct() { $this->prefix('clone', 90, function($parse, $token) { - // clone $x vs. clone($x) or clone($x, id: 6100) + // clone $x vs. clone($x) or clone($x, ["id" => 6100]) if ('(' === $parse->token->value) { $parse->forward(); - $expression= $this->expression($parse, 90); - - if (',' === $parse->token->value) { - $parse->forward(); - $with= $this->arguments($parse); - } else { - $expression= new Braced($expression, $expression->line); - $with= []; - } + $arguments= $this->arguments($parse); $parse->expecting(')', 'clone arguments'); } else { - $expression= $this->expression($parse, 90); - $with= []; + $arguments= [$this->expression($parse, 90)]; } - return new CloneExpression($expression, $with, $token->line); + return new CloneExpression($arguments, $token->line); }); $this->prefix('{', 0, function($parse, $token) { diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index cc28c45..89569bc 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -116,17 +116,17 @@ public function append_array() { #[Test] public function clone_expression() { $this->assertParsed( - [new CloneExpression(new Variable('a', self::LINE), [], self::LINE)], + [new CloneExpression([new Variable('a', self::LINE)], self::LINE)], 'clone $a;' ); } #[Test] public function clone_with() { - $with= ['id' => new Literal('6100', self::LINE)]; + $with= [[new Literal('"id"', self::LINE), new Literal('6100', self::LINE)]]; $this->assertParsed( - [new CloneExpression(new Variable('a', self::LINE), $with, self::LINE)], - 'clone($a, id: 6100);' + [new CloneExpression([new Variable('a', self::LINE), new ArrayLiteral($with, self::LINE)], self::LINE)], + 'clone($a, ["id" => 6100]);' ); } From ddc6edd4ed9417e12e0c585902dca41ea13592b7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 20:09:52 +0200 Subject: [PATCH 3/3] Create callable helper to disambiguate `f(...)` from `f(...$it)` --- src/main/php/lang/ast/syntax/PHP.class.php | 71 ++++++++++------------ 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 962d5f5..a55ea24 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -154,18 +154,8 @@ public function __construct() { if ('(' === $parse->token->value) { $parse->expecting('(', 'invoke expression'); - // Resolve ambiguity by looking ahead: `func(...)` which is a first-class - // callable reference vs. `func(...$it)` - a call with an unpacked argument - if ('...' === $parse->token->value) { - $dots= $parse->token; - $parse->forward(); - if (')' === $parse->token->value) { - $parse->forward(); - return new CallableExpression(new ScopeExpression($scope, $expr, $token->line), $left->line); - } - - array_unshift($parse->queue, $parse->token); - $parse->token= $dots; + if ($this->callable($parse)) { + return new CallableExpression(new ScopeExpression($scope, $expr, $token->line), $token->line); } $arguments= $this->arguments($parse); @@ -190,16 +180,8 @@ public function __construct() { // Resolve ambiguity by looking ahead: `func(...)` which is a first-class // callable reference vs. `func(...$it)` - a call with an unpacked argument - if ('...' === $parse->token->value) { - $dots= $parse->token; - $parse->forward(); - if (')' === $parse->token->value) { - $parse->forward(); - return new CallableExpression($left, $left->line); - } - - array_unshift($parse->queue, $parse->token); - $parse->token= $dots; + if ($this->callable($parse)) { + return new CallableExpression($left, $token->line); } $arguments= $this->arguments($parse); @@ -299,6 +281,10 @@ public function __construct() { // clone $x vs. clone($x) or clone($x, ["id" => 6100]) if ('(' === $parse->token->value) { $parse->forward(); + if ($this->callable($parse)) { + return new CallableExpression(new Literal('clone', $token->line), $token->line); + } + $arguments= $this->arguments($parse); $parse->expecting(')', 'clone arguments'); } else { @@ -353,25 +339,16 @@ public function __construct() { // Resolve ambiguity by looking ahead: `new T(...)` which is a first-class // callable reference vs. `new T(...$it)` - a call with an unpacked argument - if ('...' === $parse->token->value) { - $dots= $parse->token; - $parse->forward(); - if (')' === $parse->token->value) { - $parse->forward(); - - if (null === $type) { - $class= $this->class($parse, null); - $class->annotations= $annotations; - $new= new NewClassExpression($class, null, $token->line); - } else { - $new= new NewExpression($type, null, $token->line); - } - - return new CallableNewExpression($new, $token->line); + if ($this->callable($parse)) { + if (null === $type) { + $class= $this->class($parse, null); + $class->annotations= $annotations; + $new= new NewClassExpression($class, null, $token->line); + } else { + $new= new NewExpression($type, null, $token->line); } - array_unshift($parse->queue, $parse->token); - $parse->token= $dots; + return new CallableNewExpression($new, $token->line); } $arguments= $this->arguments($parse); @@ -1739,6 +1716,22 @@ public function class($parse, $name, $comment= null, $modifiers= []) { return $decl; } + public function callable($parse) { + if ('...' === $parse->token->value) { + $dots= $parse->token; + $parse->forward(); + if (')' === $parse->token->value) { + $parse->forward(); + return true; + } + + // Not first-class callable syntax but unpack + array_unshift($parse->queue, $parse->token); + $parse->token= $dots; + } + return false; + } + public function arguments($parse) { $arguments= []; while (')' !== $parse->token->value) {