From 60087a5ffac640022f186b3eec0cfdc75f5fbe45 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 8 Mar 2026 11:45:05 -0600 Subject: [PATCH 1/5] Refactor line handling to lang.ast.emit.Result base class This prevents a parent::emitOne() call for each node --- src/main/php/lang/ast/Emitter.class.php | 20 ++++++++-------- .../php/lang/ast/emit/GeneratedCode.class.php | 15 ------------ src/main/php/lang/ast/emit/PHP.class.php | 23 +++++-------------- src/main/php/lang/ast/emit/Result.class.php | 14 +++++++++++ 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 987a532f..79bce411 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -108,7 +108,7 @@ protected function emit() { /** * Standalone operators * - * @param lang.ast.Result $result + * @param lang.ast.emit.Result $result * @param lang.ast.Token $operator * @return void */ @@ -119,7 +119,7 @@ protected function emitOperator($result, $operator) { /** * Emit nodes seperated as statements * - * @param lang.ast.Result $result + * @param lang.ast.emit.Result $result * @param iterable $nodes * @return void */ @@ -133,23 +133,21 @@ public function emitAll($result, $nodes) { /** * Emit single nodes * - * @param lang.ast.Result $result + * @param lang.ast.emit.Result $result * @param lang.ast.Node $node * @return void */ public function emitOne($result, $node) { - - // Check for transformations - if (isset($this->transformations[$node->kind])) { - foreach ($this->transformations[$node->kind] as $transformation) { + if ($transformations= $this->transformations[$node->kind] ?? null) { + foreach ($transformations as $transformation) { $r= $transformation($result->codegen, $node); if ($r instanceof Node) { if ($r->kind === $node->kind) continue; - $this->{'emit'.$r->kind}($result, $r); + $this->{'emit'.$r->kind}($result->at($r->line), $r); return; } else if ($r) { foreach ($r as $s => $n) { - $this->{'emit'.$n->kind}($result, $n); + $this->{'emit'.$n->kind}($result->at($n->line), $n); null === $s || $result->out->write(';'); } return; @@ -158,14 +156,14 @@ public function emitOne($result, $node) { // Fall through, use default } - $this->{'emit'.$node->kind}($result, $node); + $this->{'emit'.$node->kind}($result->at($node->line), $node); } /** * Creates result * * @param io.streams.OutputStream $target - * @return lang.ast.Result + * @return lang.ast.emit.Result */ protected abstract function result($target); diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 9e5b36bd..5e17b2cb 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -2,7 +2,6 @@ class GeneratedCode extends Result { private $prolog, $epilog; - public $line= 1; /** * Starts a result stream, including an optional prolog and epilog @@ -36,20 +35,6 @@ protected function finalize() { '' === $this->epilog || $this->out->write($this->epilog); } - /** - * Forwards output line to given line number - * - * @param int $line - * @return self - */ - public function at($line) { - if ($line > $this->line) { - $this->out->write(str_repeat("\n", $line - $this->line)); - $this->line= $line; - } - return $this; - } - /** * Creates a temporary variable and returns its name * diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 4d413d72..a1913bff 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -32,7 +32,7 @@ abstract class PHP extends Emitter { * Creates result * * @param io.streams.OutputStream $target - * @return lang.ast.Result + * @return lang.ast.emit.Result */ protected function result($target) { return new GeneratedCode($target, 'out->write('yield from '); $this->emitOne($result, $from->iterable); } - - /** - * Emit single nodes - * - * @param lang.ast.Result $result - * @param lang.ast.Node $node - * @return void - */ - public function emitOne($result, $node) { - parent::emitOne($result->at($node->line), $node); - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php index 83a38911..76203df3 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -7,6 +7,7 @@ class Result implements Closeable { public $out; public $codegen; + public $line= 1; public $locals= []; /** @@ -31,6 +32,19 @@ public function from($file) { return $this; } + /** + * Forwards output line to given line number + * + * @param int $line + * @return self + */ + public function at($line) { + if ($line > $this->line) { + $this->out->write(str_repeat("\n", $line - $this->line)); + $this->line= $line; + } + return $this; + } /** * Initialize result. Guaranteed to be called *once* from constructor. From 30fa98ea8bf524c4082bf1f64903220ce52db089 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 23 Apr 2026 19:57:02 +0200 Subject: [PATCH 2/5] Implement logical AND and OR assignment operators (&&=, ||=) JavaScript baseline available; see #133 --- src/main/php/lang/ast/emit/PHP.class.php | 10 +++++++- .../ast/unittest/emit/OperatorTest.class.php | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a1913bff..dfc21ea1 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -820,7 +820,15 @@ protected function emitAssign($result, $target) { protected function emitAssignment($result, $assignment) { $this->emitAssign($result, $assignment->variable); - $result->out->write($assignment->operator); + + if ('||=' === $assignment->operator || '&&=' === $assignment->operator) { + $result->out->write(substr($assignment->operator, 0, 2)); + $this->emitAssign($result, $assignment->variable); + $result->out->write('='); + } else { + $result->out->write($assignment->operator); + } + $this->emitOne($result, $assignment->expression); } diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index 061a0d0e..2b736b2e 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -118,4 +118,28 @@ public function run($arg) { Assert::equals($expected, $r[0]); } + + #[Test, Values([[null, 'default'], ['test', 'test']])] + public function logical_or_assign($input, $expected) { + $r= $this->run('class %T { + public function run($a) { + $a||= "default"; + return $a; + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[null, null], ['test', 'changed']])] + public function logical_and_assign($input, $expected) { + $r= $this->run('class %T { + public function run($a) { + $a&&= "changed"; + return $a; + } + }', $input); + + Assert::equals($expected, $r); + } } \ No newline at end of file From 816e04c18b620b0cf8c89ff314d1e57ea4ff4911 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 23 Apr 2026 20:28:14 +0200 Subject: [PATCH 3/5] Implement emitter for `LogicalAssignment` nodes See https://github.com/xp-framework/compiler/pull/190#discussion_r3132806395 --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index f933b456..e0c519b4 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": "^12.0", + "xp-framework/ast": "^12.1", "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 dfc21ea1..640a451c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -820,15 +820,15 @@ protected function emitAssign($result, $target) { protected function emitAssignment($result, $assignment) { $this->emitAssign($result, $assignment->variable); + $result->out->write($assignment->operator); + $this->emitOne($result, $assignment->expression); + } - if ('||=' === $assignment->operator || '&&=' === $assignment->operator) { - $result->out->write(substr($assignment->operator, 0, 2)); - $this->emitAssign($result, $assignment->variable); - $result->out->write('='); - } else { - $result->out->write($assignment->operator); - } - + protected function emitLogicalAssignment($result, $assignment) { + $this->emitOne($result, $assignment->variable); + $result->out->write(substr($assignment->operator, 0, 2)); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); $this->emitOne($result, $assignment->expression); } From 16ec2a6e19ae5eabf777ac88750d9b21ac7d533e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 23 Apr 2026 20:37:31 +0200 Subject: [PATCH 4/5] Verify left-hand-side of `||=` is only evaluated once --- src/main/php/lang/ast/emit/PHP.class.php | 15 ++++++++++---- .../ast/unittest/emit/OperatorTest.class.php | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 640a451c..572a82ba 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -825,10 +825,17 @@ protected function emitAssignment($result, $assignment) { } protected function emitLogicalAssignment($result, $assignment) { - $this->emitOne($result, $assignment->variable); - $result->out->write(substr($assignment->operator, 0, 2)); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); + if ($assignment->variable instanceof Variable) { + $this->emitOne($result, $assignment->variable); + $result->out->write(substr($assignment->operator, 0, 2)); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); + } else { + $t= $result->temp(); + $result->out->write('('.$t.'= &'); + $this->emitOne($result, $assignment->variable); + $result->out->write(')'.substr($assignment->operator, 0, 2).$t.'='); + } $this->emitOne($result, $assignment->expression); } diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index 2b736b2e..eeeca2b6 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -142,4 +142,24 @@ public function run($a) { Assert::equals($expected, $r); } + + #[Test, Values(['??=', '||=', '&&='])] + public function assignment_lhs_evaluated_only_once($op) { + $r= $this->run('class %T { + private $evaluated= 0; + private $a= null; + + private function test() { + $this->evaluated++; + return $this; + } + + public function run() { + $this->test()->a '.$op.' "default"; + return $this->evaluated; + } + }'); + + Assert::equals(1, $r); + } } \ No newline at end of file From a5617d7e1cad0eac39ae42b36020d7f98355fce9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 23 Apr 2026 20:50:14 +0200 Subject: [PATCH 5/5] Remove test with `??=` See https://3v4l.org/7mk6b#v8.3.0 vs. https://3v4l.org/7mk6b#v8.2.30 --- src/test/php/lang/ast/unittest/emit/OperatorTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index eeeca2b6..ad37965e 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -143,7 +143,7 @@ public function run($a) { Assert::equals($expected, $r); } - #[Test, Values(['??=', '||=', '&&='])] + #[Test, Values(['||=', '&&='])] public function assignment_lhs_evaluated_only_once($op) { $r= $this->run('class %T { private $evaluated= 0;