Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand Down
20 changes: 9 additions & 11 deletions src/main/php/lang/ast/Emitter.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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;
Expand All @@ -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);

Expand Down
15 changes: 0 additions & 15 deletions src/main/php/lang/ast/emit/GeneratedCode.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

class GeneratedCode extends Result {
private $prolog, $epilog;
public $line= 1;

/**
* Starts a result stream, including an optional prolog and epilog
Expand Down Expand Up @@ -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
*
Expand Down
38 changes: 21 additions & 17 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<?php ');
Expand All @@ -57,7 +57,7 @@ public function literal($type) {
* - Binary expression where left- and right hand side are literals
*
* @see https://wiki.php.net/rfc/const_scalar_exprs
* @param lang.ast.Result $result
* @param lang.ast.emit.Result $result
* @param lang.ast.Node $node
* @return bool
*/
Expand Down Expand Up @@ -116,11 +116,11 @@ protected function constantType($type) {
/**
* Enclose a node inside a closure
*
* @param lang.ast.Result $result
* @param lang.ast.emit.Result $result
* @param lang.ast.Node $node
* @param ?lang.ast.nodes.Signature $signature
* @param bool $static
* @param function(lang.ast.Result, lang.ast.Node): void $emit
* @param function(lang.ast.emit.Result, lang.ast.Node): void $emit
*/
protected function enclose($result, $node, $signature, $static, $emit) {
$capture= [];
Expand Down Expand Up @@ -159,7 +159,7 @@ protected function enclose($result, $node, $signature, $static, $emit) {
/**
* Emits local initializations
*
* @param lang.ast.Result $result
* @param lang.ast.emit.Result $result
* @param [:lang.ast.Node] $init
* @return void
*/
Expand All @@ -175,7 +175,7 @@ protected function emitInitializations($result, $init) {
* Convert blocks to IIFEs to allow a list of statements where PHP syntactically
* doesn't, e.g. `fn`-style lambdas or match expressions.
*
* @param lang.ast.Result $result
* @param lang.ast.emit.Result $result
* @param lang.ast.Node $expression
* @return void
*/
Expand Down Expand Up @@ -824,6 +824,21 @@ protected function emitAssignment($result, $assignment) {
$this->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);
Expand Down Expand Up @@ -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);
}
}
14 changes: 14 additions & 0 deletions src/main/php/lang/ast/emit/Result.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
class Result implements Closeable {
public $out;
public $codegen;
public $line= 1;
public $locals= [];

/**
Expand All @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions src/test/php/lang/ast/unittest/emit/OperatorTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading