diff --git a/composer.json b/composer.json index 4ce18abd..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": "^11.6", + "xp-framework/ast": "^11.7", "php" : ">=7.4.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 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/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 56d0be09..7bbcee4c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1082,6 +1082,12 @@ protected function emitNewClass($result, $new) { $result->codegen->leave(); } + protected function emitClone($result, $clone) { + $result->out->write('clone('); + $this->emitArguments($result, $clone->arguments); + $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 d8feaec8..b36e2b49 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -22,6 +22,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 02c67c29..13f90ef7 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -25,6 +25,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 f0062593..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,8 @@ class PHP81 extends PHP { use EmulatePipelines, RewriteBlockLambdaExpressions, + RewriteCallableClone, + 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 c8da1649..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,8 @@ class PHP82 extends PHP { use EmulatePipelines, RewriteBlockLambdaExpressions, + RewriteCallableClone, + 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 101ed9f1..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, RewriteBlockLambdaExpressions, RewriteProperties; + use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions, 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 54c0face..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, RewriteBlockLambdaExpressions; + use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions; public $targetVersion= 80400; 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; 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/main/php/lang/ast/emit/RewriteCloneWith.class.php b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php new file mode 100755 index 00000000..5585a729 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteCloneWith.class.php @@ -0,0 +1,35 @@ +$v) { $c->$p=$v; } return $c;})'; + + $expr= $clone->arguments['object'] ?? $clone->arguments[0] ?? null; + $with= $clone->arguments['withProperties'] ?? $clone->arguments[1] ?? null; + + // 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 { + $result->out->write('clone '); + $this->emitOne($result, $expr); + } + } +} \ 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 0dddf242..4a3ad1d2 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -1,15 +1,30 @@ $this->id, "name" => "Changed"])']; + yield ['clone($in, withProperties: ["id" => $this->id, "name" => "Changed"])']; + yield ['clone(object: $in, withProperties: ["id" => $this->id, "name" => "Changed"])']; + yield ['clone(withProperties: ["id" => $this->id, "name" => "Changed"], object: $in)']; + } + #[Before] public function fixture() { $this->fixture= new class() { public $id= 1; + public $name= 'Test'; + + public function toString() { + return "id}, name: {$this->name}>"; + } public function with($id) { $this->id= $id; @@ -52,6 +67,147 @@ public function run($in) { } }', $this->fixture->with(1)); - Assert::equals([1, 2], [$this->fixture->id, $clone->id]); + Assert::equals( + ['', ''], + [$this->fixture->toString(), $clone->toString()] + ); + } + + #[Test, Values(from: 'arguments')] + public function clone_with($expression) { + $clone= $this->run('class %T { + private $id= 6100; + public function run($in) { return '.$expression.'; } + }', $this->fixture->with(1)); + + Assert::equals( + ['', ''], + [$this->fixture->toString(), $clone->toString()] + ); + } + + #[Test] + public function clone_unpack() { + $clone= $this->run('class %T { + public function run($in) { + return clone(...["object" => $in]); + } + }', $this->fixture); + + 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_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 { + public function run($in) { + return clone($in, ...["withProperties" => ["name" => "Changed"]]); + } + }', $this->fixture); + + Assert::equals('', $clone->toString()); + } + + #[Test] + public function clone_with_named_argument() { + $clone= $this->run('class %T { + public function run($in) { + return clone(object: $in); + } + }', $this->fixture->with(1)); + + Assert::equals( + ['', ''], + [$this->fixture->toString(), $clone->toString()] + ); + } + + #[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; }'); + + 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 + } + }')); + } + + #[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"; + 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 { + 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