From 6e193ca3ec36272f8b3d29358bfd7b3e2e42f0af Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 21 Apr 2025 15:58:09 +0200 Subject: [PATCH 01/13] Implement "clone with" --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 13 ++++++++ src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 1 + src/main/php/lang/ast/emit/PHP81.class.php | 1 + src/main/php/lang/ast/emit/PHP82.class.php | 1 + src/main/php/lang/ast/emit/PHP83.class.php | 2 +- src/main/php/lang/ast/emit/PHP84.class.php | 2 +- .../lang/ast/emit/RewriteCloneWith.class.php | 32 +++++++++++++++++++ .../ast/unittest/emit/CloningTest.class.php | 29 +++++++++++++++-- 10 files changed, 78 insertions(+), 6 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteCloneWith.class.php diff --git a/composer.json b/composer.json index f98e301e..0aeb3090 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", "xp-framework/reflection": "^3.2 | ^2.15", - "xp-framework/ast": "^11.5", + "xp-framework/ast": "dev-feature/clone-with as 11.6.0", "php" : ">=7.4.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 63c5fed8..d51abede 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1082,6 +1082,19 @@ protected function emitNewClass($result, $new) { $result->codegen->leave(); } + protected function emitClone($result, $clone) { + $result->out->write('clone '); + if (empty($clone->with)) { + $this->emitOne($result, $clone->expression); + } else { + $result->out->write('('); + $this->emitOne($result, $clone->expression); + $result->out->write(','); + $this->emitArguments($result, $clone->with); + $result->out->write(')'); + } + } + protected function emitCallable($result, $callable) { // Disambiguate the following: diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 6f97959c..7e2ee689 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -21,6 +21,7 @@ class PHP74 extends PHP { OmitConstantTypes, ReadonlyClasses, RewriteBlockLambdaExpressions, + RewriteCloneWith, RewriteEnums, RewriteExplicitOctals, RewriteProperties, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index db83e745..8f5464c3 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -24,6 +24,7 @@ class PHP80 extends PHP { OmitConstantTypes, ReadonlyClasses, RewriteBlockLambdaExpressions, + RewriteCloneWith, RewriteDynamicClassConstants, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index f93b4969..02207ac8 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -21,6 +21,7 @@ class PHP81 extends PHP { use RewriteBlockLambdaExpressions, + RewriteCloneWith, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, RewriteProperties, diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 89fa9cbd..12d4aec2 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -21,6 +21,7 @@ class PHP82 extends PHP { use RewriteBlockLambdaExpressions, + RewriteCloneWith, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, RewriteProperties, diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 7c7293b2..b73af417 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -18,7 +18,7 @@ * @see https://wiki.php.net/rfc#php_83 */ class PHP83 extends PHP { - use RewriteBlockLambdaExpressions, RewriteProperties; + use RewriteBlockLambdaExpressions, RewriteCloneWith, RewriteProperties; public $targetVersion= 80300; diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php index 71379f30..a0e42ac8 100755 --- a/src/main/php/lang/ast/emit/PHP84.class.php +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -18,7 +18,7 @@ * @see https://wiki.php.net/rfc#php_84 */ class PHP84 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, RewriteCloneWith; public $targetVersion= 80400; diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php new file mode 100755 index 00000000..382302b1 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -0,0 +1,32 @@ +with)) return parent::emitClone($result, $clone); + + // Wrap clone with, e.g. clone($x, id: 6100), inside an IIFE as follows: + // `function($args) { $this->id= $args['id']; return $this; }`, then bind + // this closure to the cloned instance before invoking it with the named + // arguments so we can access non-public members. + $t= $result->temp(); + $result->out->write('('.$t.'=clone '); + $this->emitOne($result, $clone->expression); + + $result->out->write(')?(function($a) {'); + foreach ($clone->with as $name => $argument) { + $result->out->write('$this->'.$name.'=$a["'.$name.'"];'); + } + + $result->out->write('return $this;})->bindTo('.$t.','.$t.')(['); + foreach ($clone->with as $name => $argument) { + $result->out->write('"'.$name.'"=>'); + $this->emitOne($result, $argument); + $result->out->write(','); + } + $result->out->write(']):null'); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 96292fb8..d0e10d38 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -9,7 +9,12 @@ class CloningTest extends EmittingTest { #[Before] public function fixture() { $this->fixture= new class() { - public $id= 1; + private $id= 1; + private $name= 'Test'; + + public function toString() { + return "id}, name: {$this->name}>"; + } public function with($id) { $this->id= $id; @@ -50,8 +55,26 @@ public function clone_interceptor_called() { public function run($in) { return clone $in; } - }', $this->fixture->with(id: 1)); + }', $this->fixture->with(1)); + + Assert::equals( + ['', ''], + [$this->fixture->toString(), $clone->toString()] + ); + } + + #[Test] + public function clone_with() { + $clone= $this->run('class %T { + private $id= 6100; + public function run($in) { + return clone($in, id: $this->id, name: "Changed"); + } + }', $this->fixture->with(1)); - Assert::equals([1, 2], [$this->fixture->id, $clone->id]); + Assert::equals( + ['', ''], + [$this->fixture->toString(), $clone->toString()] + ); } } \ No newline at end of file From 3a36357b7941aee902d861a06c77736b61ee24b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 15:00:37 +0200 Subject: [PATCH 02/13] Adjust to current version of RFC using `clone(, $properties)` --- src/main/php/lang/ast/emit/PHP.class.php | 13 ++------ .../lang/ast/emit/RewriteCloneWith.class.php | 33 ++++++++++--------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index c4b86da2..7bbcee4c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1083,16 +1083,9 @@ protected function emitNewClass($result, $new) { } protected function emitClone($result, $clone) { - $result->out->write('clone '); - if (empty($clone->with)) { - $this->emitOne($result, $clone->expression); - } else { - $result->out->write('('); - $this->emitOne($result, $clone->expression); - $result->out->write(','); - $this->emitArguments($result, $clone->with); - $result->out->write(')'); - } + $result->out->write('clone('); + $this->emitArguments($result, $clone->arguments); + $result->out->write(')'); } protected function emitCallable($result, $callable) { diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 382302b1..63b4d477 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -6,27 +6,28 @@ trait RewriteCloneWith { protected function emitClone($result, $clone) { - if (empty($clone->with)) return parent::emitClone($result, $clone); + $expr= $clone->arguments['object'] ?? $clone->arguments[0] ?? null; + $with= $clone->arguments['withProperties'] ?? $clone->arguments[1] ?? null; - // Wrap clone with, e.g. clone($x, id: 6100), inside an IIFE as follows: + // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE as follows: // `function($args) { $this->id= $args['id']; return $this; }`, then bind // this closure to the cloned instance before invoking it with the named // arguments so we can access non-public members. - $t= $result->temp(); - $result->out->write('('.$t.'=clone '); - $this->emitOne($result, $clone->expression); + if ($with) { + $c= $result->temp(); + $a= $result->temp(); - $result->out->write(')?(function($a) {'); - foreach ($clone->with as $name => $argument) { - $result->out->write('$this->'.$name.'=$a["'.$name.'"];'); + $result->out->write('['.$c.'=clone '); + $this->emitOne($result, $expr); + $result->out->write(','.$a.'='); + $this->emitOne($result, $with); + $result->out->write(']?(function($a) { foreach ($a as $p=>$v) { $this->$p= $v; }return $this;})'); + $result->out->write('->bindTo('.$c.','.$c.')('.$a.'):null'); + } else if (isset($clone->arguments['object'])) { + $result->out->write('clone '); + $this->emitOne($result, $expr); + } else { + return parent::emitClone($result, $clone); } - - $result->out->write('return $this;})->bindTo('.$t.','.$t.')(['); - foreach ($clone->with as $name => $argument) { - $result->out->write('"'.$name.'"=>'); - $this->emitOne($result, $argument); - $result->out->write(','); - } - $result->out->write(']):null'); } } \ No newline at end of file From 2818d8829ddcbe7a9be74ed42ae79c2558032bc4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 19:07:27 +0200 Subject: [PATCH 03/13] Simplify implementation --- .../lang/ast/emit/RewriteCloneWith.class.php | 16 +++++----------- .../ast/unittest/emit/CloningTest.class.php | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 63b4d477..239d463a 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -9,20 +9,14 @@ protected function emitClone($result, $clone) { $expr= $clone->arguments['object'] ?? $clone->arguments[0] ?? null; $with= $clone->arguments['withProperties'] ?? $clone->arguments[1] ?? null; - // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE as follows: - // `function($args) { $this->id= $args['id']; return $this; }`, then bind - // this closure to the cloned instance before invoking it with the named - // arguments so we can access non-public members. + // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE which + /// iterates over the property-value pairs, assigning them to the clone. if ($with) { - $c= $result->temp(); - $a= $result->temp(); - - $result->out->write('['.$c.'=clone '); + $result->out->write('(function($c, $a) { foreach ($a as $p=>$v) { $c->$p= $v; } return $c;})(clone '); $this->emitOne($result, $expr); - $result->out->write(','.$a.'='); + $result->out->write(','); $this->emitOne($result, $with); - $result->out->write(']?(function($a) { foreach ($a as $p=>$v) { $this->$p= $v; }return $this;})'); - $result->out->write('->bindTo('.$c.','.$c.')('.$a.'):null'); + $result->out->write(')'); } else if (isset($clone->arguments['object'])) { $result->out->write('clone '); $this->emitOne($result, $expr); diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 5dece99b..da5708a2 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -1,6 +1,7 @@ fixture= new class() { - private $id= 1; - private $name= 'Test'; + public $id= 1; + public $name= 'Test'; public function toString() { return "id}, name: {$this->name}>"; @@ -97,4 +98,15 @@ public function run($in) { [$this->fixture->toString(), $clone->toString()] ); } + + #[Test, Ignore('Could be done with reflection but with significant performance cost')] + public function clone_with_respects_visibility() { + $base= $this->type('class %T { private $id= 1; }'); + + Assert::throws(Error::class, fn() => $this->run('class %T extends '.$base.' { + public function run() { + clone($this, ["id" => 6100]); // Tries to set private member from base + } + }')); + } } \ No newline at end of file From 16aab10e50324ce842909bfc922c9457373c0bf5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 19:12:36 +0200 Subject: [PATCH 04/13] Add test verifying private & protected accessibility from "clone with" --- .../lang/ast/unittest/emit/CloningTest.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index da5708a2..06c16ad7 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -99,6 +99,21 @@ public function run($in) { ); } + #[Test, Values(['protected', 'private'])] + public function clone_with_can_access($modifiers) { + $clone= $this->run('class %T { + '.$modifiers.' $id= 1; + + public function id() { return $this->id; } + + public function run() { + return clone($this, ["id" => 6100]); + } + }'); + + Assert::equals(6100, $clone->id()); + } + #[Test, Ignore('Could be done with reflection but with significant performance cost')] public function clone_with_respects_visibility() { $base= $this->type('class %T { private $id= 1; }'); From f1e5bdc8b4838247b716370ac4c14cf831caf0fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 19:29:46 +0200 Subject: [PATCH 05/13] Raise an error when non-array is passed to `withProperties` --- .../lang/ast/emit/RewriteCloneWith.class.php | 2 +- .../ast/unittest/emit/CloningTest.class.php | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 239d463a..5aa20bb1 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -12,7 +12,7 @@ protected function emitClone($result, $clone) { // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE which /// iterates over the property-value pairs, assigning them to the clone. if ($with) { - $result->out->write('(function($c, $a) { foreach ($a as $p=>$v) { $c->$p= $v; } return $c;})(clone '); + $result->out->write('(function($c, array $a) { foreach ($a as $p=>$v) { $c->$p= $v; } return $c;})(clone '); $this->emitOne($result, $expr); $result->out->write(','); $this->emitOne($result, $with); diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 06c16ad7..3df7e108 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -1,7 +1,7 @@ run('class %T { + public function run() { + return clone(null); + } + }'); + } + + #[Test, Expect(Error::class)] + public function clone_with_null_properties() { + $this->run('class %T { + public function run() { + return clone($this, null); + } + }'); + } } \ No newline at end of file From 511d04f5a57f0b283c6e5940e87fb95cb386a6d0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 20:13:35 +0200 Subject: [PATCH 06/13] Add tests for clone used as callable --- .../lang/ast/unittest/emit/CloningTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 3df7e108..b2f3603d 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -1,6 +1,7 @@ =8.5.0')] + public function clone_callable($expression) { + $clone= $this->run('class %T { + public function run($in) { + $func= "clone"; + return array_map('.$expression.', [$in])[0]; + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + #[Test, Expect(Error::class)] public function clone_null_object() { $this->run('class %T { From f9c63e444c4571582438623804319032474c77f6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 20:57:47 +0200 Subject: [PATCH 07/13] Support `clone(...)` first-class callable syntax --- .../lang/ast/emit/CallablesAsClosures.class.php | 10 +++++++--- src/main/php/lang/ast/emit/PHP81.class.php | 1 + src/main/php/lang/ast/emit/PHP82.class.php | 1 + src/main/php/lang/ast/emit/PHP84.class.php | 2 +- .../lang/ast/emit/RewriteCallableClone.class.php | 15 +++++++++++++++ .../lang/ast/unittest/emit/CloningTest.class.php | 15 +++++++++++++-- 6 files changed, 38 insertions(+), 6 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteCallableClone.class.php diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index f2c2ce3f..1ea17e27 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -48,8 +48,12 @@ private function emitQuoted($result, $node) { } protected function emitCallable($result, $callable) { - $result->out->write('\Closure::fromCallable('); - $this->emitQuoted($result, $callable->expression); - $result->out->write(')'); + if ($callable->expression instanceof Literal && 'clone' === $callable->expression->expression) { + $result->out->write('fn($o) => clone $o'); + } else { + $result->out->write('\Closure::fromCallable('); + $this->emitQuoted($result, $callable->expression); + $result->out->write(')'); + } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index b65ffa10..36fc0b49 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -22,6 +22,7 @@ class PHP81 extends PHP { use EmulatePipelines, RewriteBlockLambdaExpressions, + RewriteCallableClone, RewriteCloneWith, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 7d0dfd1e..7e6df802 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -22,6 +22,7 @@ class PHP82 extends PHP { use EmulatePipelines, RewriteBlockLambdaExpressions, + RewriteCallableClone, RewriteCloneWith, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php index d8e0b7ce..2673be75 100755 --- a/src/main/php/lang/ast/emit/PHP84.class.php +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -19,7 +19,7 @@ * @see https://wiki.php.net/rfc#php_84 */ class PHP84 extends PHP { - use EmulatePipelines, RewriteCloneWith, RewriteBlockLambdaExpressions; + use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions; public $targetVersion= 80400; diff --git a/src/main/php/lang/ast/emit/RewriteCallableClone.class.php b/src/main/php/lang/ast/emit/RewriteCallableClone.class.php new file mode 100755 index 00000000..5379b720 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteCallableClone.class.php @@ -0,0 +1,15 @@ +expression instanceof Literal && 'clone' === $callable->expression->expression) { + $result->out->write('fn($o) => clone $o'); + } else { + parent::emitCallable($result, $callable); + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index b2f3603d..83b476d7 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -126,8 +126,19 @@ public function run() { }')); } - #[Test, Values(['clone(...)', '"clone"', '$func']), Runtime(php: '>=8.5.0')] - public function clone_callable($expression) { + #[Test] + public function clone_callable() { + $clone= $this->run('class %T { + public function run($in) { + return array_map(clone(...), [$in])[0]; + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + + #[Test, Values(['"clone"', '$func']), Runtime(php: '>=8.5.0')] + public function clone_callable_reference($expression) { $clone= $this->run('class %T { public function run($in) { $func= "clone"; From 513a199347523afb8722f6bc3158109d16a9f0f7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 21:03:30 +0200 Subject: [PATCH 08/13] Fix PHP 8.3 emitter --- src/main/php/lang/ast/emit/PHP83.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 3ab62d2b..4f62d440 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -19,7 +19,7 @@ * @see https://wiki.php.net/rfc#php_83 */ class PHP83 extends PHP { - use EmulatePipelines, RewriteCloneWith, RewriteBlockLambdaExpressions, RewriteProperties; + use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions, RewriteProperties; public $targetVersion= 80300; From 443aefe280d2282661e5f11cb5e74c52d686e640 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Jun 2025 21:21:30 +0200 Subject: [PATCH 09/13] Support clone with unpacking --- .../php/lang/ast/emit/RewriteCloneWith.class.php | 7 ++++++- .../php/lang/ast/unittest/emit/CloningTest.class.php | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 5aa20bb1..80a59a0c 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -1,6 +1,6 @@ arguments['object'])) { $result->out->write('clone '); $this->emitOne($result, $expr); + } else if ($expr instanceof UnpackExpression) { + $result->out->write('(function($u) { $c= clone $u["object"] ?? $u[0];'); + $result->out->write('foreach ($u["withProperties"] ?? $u[1] ?? [] as $p=>$v) { $c->$p= $v; } return $c;})('); + $this->emitOne($result, $expr->expression); + $result->out->write(')'); } else { return parent::emitClone($result, $clone); } diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 83b476d7..7dfdc2c1 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -86,6 +86,18 @@ public function run($in) { return '.$expression.'; } ); } + #[Test] + public function clone_unpack() { + $clone= $this->run('class %T { + public function run($in) { + $args= ["object" => $in]; + return clone(...$args); + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + #[Test] public function clone_with_named_argument() { $clone= $this->run('class %T { From 8feae1cc2da08840233d9021f12c0e91d68892b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Jun 2025 09:26:06 +0200 Subject: [PATCH 10/13] Implement support for partial unpacking --- .../lang/ast/emit/RewriteCloneWith.class.php | 5 ++-- .../ast/unittest/emit/CloningTest.class.php | 27 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 80a59a0c..829d1fd3 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -12,7 +12,8 @@ protected function emitClone($result, $clone) { // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE which /// iterates over the property-value pairs, assigning them to the clone. if ($with) { - $result->out->write('(function($c, array $a) { foreach ($a as $p=>$v) { $c->$p= $v; } return $c;})(clone '); + $result->out->write('(function($object, array $withProperties) {'); + $result->out->write('foreach ($withProperties as $p=>$v) { $object->$p=$v; } return $object;})(clone '); $this->emitOne($result, $expr); $result->out->write(','); $this->emitOne($result, $with); @@ -22,7 +23,7 @@ protected function emitClone($result, $clone) { $this->emitOne($result, $expr); } else if ($expr instanceof UnpackExpression) { $result->out->write('(function($u) { $c= clone $u["object"] ?? $u[0];'); - $result->out->write('foreach ($u["withProperties"] ?? $u[1] ?? [] as $p=>$v) { $c->$p= $v; } return $c;})('); + $result->out->write('foreach ($u["withProperties"] ?? $u[1] ?? [] as $p=>$v) { $c->$p=$v; } return $c;})('); $this->emitOne($result, $expr->expression); $result->out->write(')'); } else { diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 7dfdc2c1..b3e8788e 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -90,12 +90,33 @@ public function run($in) { return '.$expression.'; } public function clone_unpack() { $clone= $this->run('class %T { public function run($in) { - $args= ["object" => $in]; - return clone(...$args); + return clone(...["object" => $in]); } }', $this->fixture); - Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + Assert::equals('', $clone->toString()); + } + + #[Test] + public function clone_unpack_with_properties() { + $clone= $this->run('class %T { + public function run($in) { + return clone(...["object" => $in, "withProperties" => ["name" => "Changed"]]); + } + }', $this->fixture); + + Assert::equals('', $clone->toString()); + } + + #[Test] + public function clone_unpack_only_properties() { + $clone= $this->run('class %T { + public function run($in) { + return clone($in, ...["withProperties" => ["name" => "Changed"]]); + } + }', $this->fixture); + + Assert::equals('', $clone->toString()); } #[Test] From 7aba1b99d8ea67a8c6009d2b4a668ffd66d1c129 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Jun 2025 10:56:18 +0200 Subject: [PATCH 11/13] Support clone with unpack on PHP 7.4 --- .../lang/ast/emit/RewriteCloneWith.class.php | 30 ++++++++++--------- .../ast/unittest/emit/CloningTest.class.php | 11 +++++++ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php index 829d1fd3..5585a729 100755 --- a/src/main/php/lang/ast/emit/RewriteCloneWith.class.php +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -1,33 +1,35 @@ $v) { $c->$p=$v; } return $c;})'; + $expr= $clone->arguments['object'] ?? $clone->arguments[0] ?? null; $with= $clone->arguments['withProperties'] ?? $clone->arguments[1] ?? null; - // Wrap clone with, e.g. clone($x, ['id' => 6100]), inside an IIFE which - /// iterates over the property-value pairs, assigning them to the clone. - if ($with) { - $result->out->write('(function($object, array $withProperties) {'); - $result->out->write('foreach ($withProperties as $p=>$v) { $object->$p=$v; } return $object;})(clone '); + // Built ontop of a wrapper function which iterates over the property-value pairs, + // assigning them to the clone. Unwind unpack statements, e.g. `clone(...$args)`, + // into an array, manually unpacking it for invocation. + if ($expr instanceof UnpackExpression || $with instanceof UnpackExpression) { + $t= $result->temp(); + $result->out->write('('.$t.'='); + $this->emitOne($result, new ArrayLiteral($with ? [[null, $expr], [null, $with]] : [[null, $expr]], $clone->line)); + $result->out->write(')?'); + $result->out->write($wrapper.'(clone ('.$t.'["object"] ?? '.$t.'[0]), '.$t.'["withProperties"] ?? '.$t.'[1] ?? [])'); + $result->out->write(':null'); + } else if ($with) { + $result->out->write($wrapper.'(clone '); $this->emitOne($result, $expr); $result->out->write(','); $this->emitOne($result, $with); $result->out->write(')'); - } else if (isset($clone->arguments['object'])) { + } else { $result->out->write('clone '); $this->emitOne($result, $expr); - } else if ($expr instanceof UnpackExpression) { - $result->out->write('(function($u) { $c= clone $u["object"] ?? $u[0];'); - $result->out->write('foreach ($u["withProperties"] ?? $u[1] ?? [] as $p=>$v) { $c->$p=$v; } return $c;})('); - $this->emitOne($result, $expr->expression); - $result->out->write(')'); - } else { - return parent::emitClone($result, $clone); } } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index b3e8788e..4a3ad1d2 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -108,6 +108,17 @@ public function run($in) { Assert::equals('', $clone->toString()); } + #[Test] + public function clone_unpack_object_and_properties() { + $clone= $this->run('class %T { + public function run($in) { + return clone(...["object" => $in], ...["withProperties" => ["name" => "Changed"]]); + } + }', $this->fixture); + + Assert::equals('', $clone->toString()); + } + #[Test] public function clone_unpack_only_properties() { $clone= $this->run('class %T { From 959a5e83820c2247e0b0c685571c7813919ad2e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Jun 2025 11:55:03 +0200 Subject: [PATCH 12/13] Rewrite `clone with` and `clone(...)` callable TODO: Remove as soon as PR is merged --- src/main/php/lang/ast/emit/PHP85.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP85.class.php b/src/main/php/lang/ast/emit/PHP85.class.php index 4bb3e5e9..693a102a 100755 --- a/src/main/php/lang/ast/emit/PHP85.class.php +++ b/src/main/php/lang/ast/emit/PHP85.class.php @@ -19,7 +19,7 @@ * @see https://wiki.php.net/rfc#php_85 */ class PHP85 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, RewriteCallableClone, RewriteCloneWith; // TODO: Remove once PR is merged! public $targetVersion= 80500; From 11505679c72aa35f9e6619ade38a809018a23350 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Jun 2025 09:41:36 +0200 Subject: [PATCH 13/13] Use AST library release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 887770b8..6097b63e 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", "xp-framework/reflection": "^3.2 | ^2.15", - "xp-framework/ast": "dev-feature/clone-with as 11.7.0", + "xp-framework/ast": "^11.7", "php" : ">=7.4.0" }, "require-dev" : {