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/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..572a82ba 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, 'emitOne($result, $assignment->expression); } + protected function emitLogicalAssignment($result, $assignment) { + 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); + } + protected function emitReturn($result, $return) { $result->out->write('return '); $return->expression && $this->emitOne($result, $return->expression); @@ -1208,15 +1223,4 @@ protected function emitFrom($result, $from) { $result->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. 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..ad37965e 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,48 @@ 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); + } + + #[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