Skip to content

Commit 41f8a58

Browse files
authored
Merge pull request #185 from xp-framework/feature/clone-with
Implement "clone with"
2 parents 33cc068 + 1150567 commit 41f8a58

13 files changed

+231
-9
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"require" : {
99
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
1010
"xp-framework/reflection": "^3.2 | ^2.15",
11-
"xp-framework/ast": "^11.6",
11+
"xp-framework/ast": "^11.7",
1212
"php" : ">=7.4.0"
1313
},
1414
"require-dev" : {

src/main/php/lang/ast/emit/CallablesAsClosures.class.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ private function emitQuoted($result, $node) {
4848
}
4949

5050
protected function emitCallable($result, $callable) {
51-
$result->out->write('\Closure::fromCallable(');
52-
$this->emitQuoted($result, $callable->expression);
53-
$result->out->write(')');
51+
if ($callable->expression instanceof Literal && 'clone' === $callable->expression->expression) {
52+
$result->out->write('fn($o) => clone $o');
53+
} else {
54+
$result->out->write('\Closure::fromCallable(');
55+
$this->emitQuoted($result, $callable->expression);
56+
$result->out->write(')');
57+
}
5458
}
5559
}

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,12 @@ protected function emitNewClass($result, $new) {
10821082
$result->codegen->leave();
10831083
}
10841084

1085+
protected function emitClone($result, $clone) {
1086+
$result->out->write('clone(');
1087+
$this->emitArguments($result, $clone->arguments);
1088+
$result->out->write(')');
1089+
}
1090+
10851091
protected function emitCallable($result, $callable) {
10861092

10871093
// Disambiguate the following:

src/main/php/lang/ast/emit/PHP74.class.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class PHP74 extends PHP {
2222
OmitConstantTypes,
2323
ReadonlyClasses,
2424
RewriteBlockLambdaExpressions,
25+
RewriteCloneWith,
2526
RewriteEnums,
2627
RewriteExplicitOctals,
2728
RewriteProperties,

src/main/php/lang/ast/emit/PHP80.class.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class PHP80 extends PHP {
2525
OmitConstantTypes,
2626
ReadonlyClasses,
2727
RewriteBlockLambdaExpressions,
28+
RewriteCloneWith,
2829
RewriteDynamicClassConstants,
2930
RewriteEnums,
3031
RewriteExplicitOctals,

src/main/php/lang/ast/emit/PHP81.class.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class PHP81 extends PHP {
2222
use
2323
EmulatePipelines,
2424
RewriteBlockLambdaExpressions,
25+
RewriteCallableClone,
26+
RewriteCloneWith,
2527
RewriteDynamicClassConstants,
2628
RewriteStaticVariableInitializations,
2729
RewriteProperties,

src/main/php/lang/ast/emit/PHP82.class.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class PHP82 extends PHP {
2222
use
2323
EmulatePipelines,
2424
RewriteBlockLambdaExpressions,
25+
RewriteCallableClone,
26+
RewriteCloneWith,
2527
RewriteDynamicClassConstants,
2628
RewriteStaticVariableInitializations,
2729
RewriteProperties,

src/main/php/lang/ast/emit/PHP83.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* @see https://wiki.php.net/rfc#php_83
2020
*/
2121
class PHP83 extends PHP {
22-
use EmulatePipelines, RewriteBlockLambdaExpressions, RewriteProperties;
22+
use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions, RewriteProperties;
2323

2424
public $targetVersion= 80300;
2525

src/main/php/lang/ast/emit/PHP84.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* @see https://wiki.php.net/rfc#php_84
2020
*/
2121
class PHP84 extends PHP {
22-
use EmulatePipelines, RewriteBlockLambdaExpressions;
22+
use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions;
2323

2424
public $targetVersion= 80400;
2525

src/main/php/lang/ast/emit/PHP85.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* @see https://wiki.php.net/rfc#php_85
2020
*/
2121
class PHP85 extends PHP {
22-
use RewriteBlockLambdaExpressions;
22+
use RewriteBlockLambdaExpressions, RewriteCallableClone, RewriteCloneWith; // TODO: Remove once PR is merged!
2323

2424
public $targetVersion= 80500;
2525

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\nodes\Literal;
4+
5+
/** @see https://wiki.php.net/rfc/clone_with_v2 */
6+
trait RewriteCallableClone {
7+
8+
protected function emitCallable($result, $callable) {
9+
if ($callable->expression instanceof Literal && 'clone' === $callable->expression->expression) {
10+
$result->out->write('fn($o) => clone $o');
11+
} else {
12+
parent::emitCallable($result, $callable);
13+
}
14+
}
15+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\nodes\{ArrayLiteral, UnpackExpression};
4+
5+
/** @see https://wiki.php.net/rfc/clone_with_v2 */
6+
trait RewriteCloneWith {
7+
8+
protected function emitClone($result, $clone) {
9+
static $wrapper= '(function($c, array $w) { foreach ($w as $p=>$v) { $c->$p=$v; } return $c;})';
10+
11+
$expr= $clone->arguments['object'] ?? $clone->arguments[0] ?? null;
12+
$with= $clone->arguments['withProperties'] ?? $clone->arguments[1] ?? null;
13+
14+
// Built ontop of a wrapper function which iterates over the property-value pairs,
15+
// assigning them to the clone. Unwind unpack statements, e.g. `clone(...$args)`,
16+
// into an array, manually unpacking it for invocation.
17+
if ($expr instanceof UnpackExpression || $with instanceof UnpackExpression) {
18+
$t= $result->temp();
19+
$result->out->write('('.$t.'=');
20+
$this->emitOne($result, new ArrayLiteral($with ? [[null, $expr], [null, $with]] : [[null, $expr]], $clone->line));
21+
$result->out->write(')?');
22+
$result->out->write($wrapper.'(clone ('.$t.'["object"] ?? '.$t.'[0]), '.$t.'["withProperties"] ?? '.$t.'[1] ?? [])');
23+
$result->out->write(':null');
24+
} else if ($with) {
25+
$result->out->write($wrapper.'(clone ');
26+
$this->emitOne($result, $expr);
27+
$result->out->write(',');
28+
$this->emitOne($result, $with);
29+
$result->out->write(')');
30+
} else {
31+
$result->out->write('clone ');
32+
$this->emitOne($result, $expr);
33+
}
34+
}
35+
}

src/test/php/lang/ast/unittest/emit/CloningTest.class.php

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
<?php namespace lang\ast\unittest\emit;
22

3-
use test\{Assert, Before, Test};
3+
use lang\Error;
4+
use test\verify\Runtime;
5+
use test\{Assert, Before, Expect, Ignore, Test, Values};
46

57
/** @see https://www.php.net/manual/en/language.oop5.cloning.php */
68
class CloningTest extends EmittingTest {
79
private $fixture;
810

11+
/** @return iterable */
12+
private function arguments() {
13+
yield ['clone($in, ["id" => $this->id, "name" => "Changed"])'];
14+
yield ['clone($in, withProperties: ["id" => $this->id, "name" => "Changed"])'];
15+
yield ['clone(object: $in, withProperties: ["id" => $this->id, "name" => "Changed"])'];
16+
yield ['clone(withProperties: ["id" => $this->id, "name" => "Changed"], object: $in)'];
17+
}
18+
919
#[Before]
1020
public function fixture() {
1121
$this->fixture= new class() {
1222
public $id= 1;
23+
public $name= 'Test';
24+
25+
public function toString() {
26+
return "<id: {$this->id}, name: {$this->name}>";
27+
}
1328

1429
public function with($id) {
1530
$this->id= $id;
@@ -52,6 +67,147 @@ public function run($in) {
5267
}
5368
}', $this->fixture->with(1));
5469

55-
Assert::equals([1, 2], [$this->fixture->id, $clone->id]);
70+
Assert::equals(
71+
['<id: 1, name: Test>', '<id: 2, name: Test>'],
72+
[$this->fixture->toString(), $clone->toString()]
73+
);
74+
}
75+
76+
#[Test, Values(from: 'arguments')]
77+
public function clone_with($expression) {
78+
$clone= $this->run('class %T {
79+
private $id= 6100;
80+
public function run($in) { return '.$expression.'; }
81+
}', $this->fixture->with(1));
82+
83+
Assert::equals(
84+
['<id: 1, name: Test>', '<id: 6100, name: Changed>'],
85+
[$this->fixture->toString(), $clone->toString()]
86+
);
87+
}
88+
89+
#[Test]
90+
public function clone_unpack() {
91+
$clone= $this->run('class %T {
92+
public function run($in) {
93+
return clone(...["object" => $in]);
94+
}
95+
}', $this->fixture);
96+
97+
Assert::equals('<id: 2, name: Test>', $clone->toString());
98+
}
99+
100+
#[Test]
101+
public function clone_unpack_with_properties() {
102+
$clone= $this->run('class %T {
103+
public function run($in) {
104+
return clone(...["object" => $in, "withProperties" => ["name" => "Changed"]]);
105+
}
106+
}', $this->fixture);
107+
108+
Assert::equals('<id: 2, name: Changed>', $clone->toString());
109+
}
110+
111+
#[Test]
112+
public function clone_unpack_object_and_properties() {
113+
$clone= $this->run('class %T {
114+
public function run($in) {
115+
return clone(...["object" => $in], ...["withProperties" => ["name" => "Changed"]]);
116+
}
117+
}', $this->fixture);
118+
119+
Assert::equals('<id: 2, name: Changed>', $clone->toString());
120+
}
121+
122+
#[Test]
123+
public function clone_unpack_only_properties() {
124+
$clone= $this->run('class %T {
125+
public function run($in) {
126+
return clone($in, ...["withProperties" => ["name" => "Changed"]]);
127+
}
128+
}', $this->fixture);
129+
130+
Assert::equals('<id: 2, name: Changed>', $clone->toString());
131+
}
132+
133+
#[Test]
134+
public function clone_with_named_argument() {
135+
$clone= $this->run('class %T {
136+
public function run($in) {
137+
return clone(object: $in);
138+
}
139+
}', $this->fixture->with(1));
140+
141+
Assert::equals(
142+
['<id: 1, name: Test>', '<id: 2, name: Test>'],
143+
[$this->fixture->toString(), $clone->toString()]
144+
);
145+
}
146+
147+
#[Test, Values(['protected', 'private'])]
148+
public function clone_with_can_access($modifiers) {
149+
$clone= $this->run('class %T {
150+
'.$modifiers.' $id= 1;
151+
152+
public function id() { return $this->id; }
153+
154+
public function run() {
155+
return clone($this, ["id" => 6100]);
156+
}
157+
}');
158+
159+
Assert::equals(6100, $clone->id());
160+
}
161+
162+
#[Test, Ignore('Could be done with reflection but with significant performance cost')]
163+
public function clone_with_respects_visibility() {
164+
$base= $this->type('class %T { private $id= 1; }');
165+
166+
Assert::throws(Error::class, fn() => $this->run('class %T extends '.$base.' {
167+
public function run() {
168+
clone($this, ["id" => 6100]); // Tries to set private member from base
169+
}
170+
}'));
171+
}
172+
173+
#[Test]
174+
public function clone_callable() {
175+
$clone= $this->run('class %T {
176+
public function run($in) {
177+
return array_map(clone(...), [$in])[0];
178+
}
179+
}', $this->fixture);
180+
181+
Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone);
182+
}
183+
184+
#[Test, Values(['"clone"', '$func']), Runtime(php: '>=8.5.0')]
185+
public function clone_callable_reference($expression) {
186+
$clone= $this->run('class %T {
187+
public function run($in) {
188+
$func= "clone";
189+
return array_map('.$expression.', [$in])[0];
190+
}
191+
}', $this->fixture);
192+
193+
Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone);
194+
}
195+
196+
#[Test, Expect(Error::class)]
197+
public function clone_null_object() {
198+
$this->run('class %T {
199+
public function run() {
200+
return clone(null);
201+
}
202+
}');
203+
}
204+
205+
#[Test, Expect(Error::class)]
206+
public function clone_with_null_properties() {
207+
$this->run('class %T {
208+
public function run() {
209+
return clone($this, null);
210+
}
211+
}');
56212
}
57213
}

0 commit comments

Comments
 (0)