diff --git a/src/main/php/lang/ast/emit/Local.class.php b/src/main/php/lang/ast/emit/Local.class.php new file mode 100755 index 00000000..d90a7d5c --- /dev/null +++ b/src/main/php/lang/ast/emit/Local.class.php @@ -0,0 +1,10 @@ +type= $type; + $this->byRef= $byRef; + } +} \ 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 163ff27b..fffaacce 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -18,7 +18,7 @@ UnpackExpression, Variable }; -use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsExpression}; +use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsExpression, IsLiteral}; use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { @@ -27,6 +27,7 @@ abstract class PHP extends Emitter { const CONSTANT= 2; protected $literals= []; + public $extensions= []; /** * Creates result @@ -113,6 +114,24 @@ protected function constantType($type) { } } + /** + * Returns type for a given node + * + * @param lang.ast.emit.Result $result + * @param lang.ast.Node $node + * @return ?lang.ast.Type + */ + protected function typeOf($result, $node) { + if ($node instanceof Variable) { + $local= $result->locals[$node->pointer] ?? null; + return $local ? $local->type : null; + } else if ($node instanceof Literal) { + if ('"' === $node->expression[0] || "'" === $node->expression[0]) return new IsLiteral('string'); + } + + return null; // TBI + } + /** * Enclose a node inside a closure * @@ -126,7 +145,7 @@ protected function enclose($result, $node, $signature, $static, $emit) { $capture= []; foreach ($result->codegen->search($node, 'variable') as $var) { if (isset($result->locals[$var->pointer])) { - $capture[$var->pointer]??= ($result->locals[$var->pointer] ? '&$' : '$').$var->pointer; + $capture[$var->pointer]??= ($result->locals[$var->pointer]->byRef ? '&$' : '$').$var->pointer; } } unset($capture['this']); @@ -146,7 +165,7 @@ protected function enclose($result, $node, $signature, $static, $emit) { if ($capture) { $result->out->write('use('.implode(', ', $capture).')'); foreach ($capture as $name => $variable) { - $result->locals[$name]= '&' === $variable[0]; + $result->locals[$name]= $locals[$name]; } } @@ -299,7 +318,7 @@ protected function emitArray($result, $array) { } protected function emitParameter($result, $parameter) { - $result->locals[$parameter->name]= $parameter->reference; + $result->locals[$parameter->name]= new Local($parameter->type, $parameter->reference); $parameter->annotations && $this->emitOne($result, $parameter->annotations); // If we have a non-constant default and a type, emit a nullable type hint @@ -341,7 +360,7 @@ protected function emitSignature($result, $signature, $use= null) { if ($use) { $result->out->write(' use('.implode(',', $use).') '); foreach ($use as $variable) { - $result->locals[ltrim($variable, '&$')]= '&' === $variable[0]; + $result->locals[ltrim($variable, '&$')]= new Local(null, '&' === $variable[0]); } } @@ -689,7 +708,7 @@ protected function emitProperty($result, $property) { protected function emitMethod($result, $method) { $locals= $result->locals; - $result->locals= ['this' => false]; + $result->locals= ['this' => new Local(null, false)]; // FIXME: Type $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', DETAIL_ANNOTATIONS => $method->annotations, @@ -815,10 +834,10 @@ protected function emitOffset($result, $offset) { } } - protected function emitAssign($result, $target) { + protected function emitAssign($result, $target, $type= null) { if ($target instanceof Variable && $target->const) { $result->out->write('$'.$target->pointer); - $result->locals[$target->pointer]= false; + $result->locals[$target->pointer]= new Local($type, false); } else if ($target instanceof ArrayLiteral) { $result->out->write('['); foreach ($target->values as $pair) { @@ -838,7 +857,7 @@ protected function emitAssign($result, $target) { } protected function emitAssignment($result, $assignment) { - $this->emitAssign($result, $assignment->variable); + $this->emitAssign($result, $assignment->variable, $this->typeOf($result, $assignment->expression)); $result->out->write($assignment->operator); $this->emitOne($result, $assignment->expression); } @@ -1146,10 +1165,18 @@ protected function emitCallableNew($result, $callable) { } protected function emitInvoke($result, $invoke) { - $this->emitOne($result, $invoke->expression); - $result->out->write('('); - $this->emitArguments($result, $invoke->arguments); - $result->out->write(')'); + if ( + ($invoke->expression instanceof InstanceExpression) && + ($type= $this->typeOf($result, $invoke->expression->expression)) && + ($extension= $this->extensions[$type->literal()][$invoke->expression->member->expression] ?? null) + ) { + $this->emitOne($result, $extension($invoke->expression->expression, $invoke->arguments)); + } else { + $this->emitOne($result, $invoke->expression); + $result->out->write('('); + $this->emitArguments($result, $invoke->arguments); + $result->out->write(')'); + } } protected function emitScope($result, $scope) { diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index bfbc0eaf..96b5a982 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -13,6 +13,7 @@ abstract class EmittingTest { private static $id= 0; private $cl, $language, $emitter, $output; private $transformations= []; + private $extensions= []; /** * Constructor @@ -54,6 +55,18 @@ protected function transform($type, $function) { $this->transformations[]= $this->emitter->transform($type, $function); } + /** + * Register an extension. + * + * @param string $type + * @param [:var] $impl + * @return void + */ + protected function extension($type, $impl) { + $this->emitter->extensions[$type]= $impl; + $this->extensions[]= $type; + } + /** * Parse and emit given code * @@ -131,5 +144,9 @@ public function tearDown() { foreach ($this->transformations as $transformation) { $this->emitter->remove($transformation); } + + foreach ($this->extensions as $type) { + unset($this->emitter->extensions[$type]); + } } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ExtensionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExtensionsTest.class.php new file mode 100755 index 00000000..a3f35054 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ExtensionsTest.class.php @@ -0,0 +1,74 @@ + strlen($self); + // public function contains(string $self, string $needle) => false !== strpos($self, $needle); + // } + // } + // ``` + // + // Single-expression methods would be inlined at call-site! + $this->extension('string', [ + 'length' => function($self, $arguments) { + return new InvokeExpression(new Literal('strlen'), [$self]); + }, + 'contains' => function($self, $arguments) { + return new BinaryExpression( + new Literal('false'), + '!==', + new InvokeExpression(new Literal('strpos'), [$self, $arguments[0]]) + ); + } + ]); + } + + #[Test] + public function literal() { + Assert::equals(4, $this->run('class %T { + public function run() { + return "Test"->length(); + } + }')); + } + + #[Test] + public function local() { + Assert::equals(4, $this->run('class %T { + public function run() { + $test= "Test"; + return $test->length(); + } + }')); + } + + #[Test, Values([['', 0], ['Test', 4]])] + public function param_length($input, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run(string $input) { + return $input->length(); + } + }', $input)); + } + + #[Test, Values([['', false], ['Test', true]])] + public function param_contains($input, $expected) { + Assert::equals($expected, $this->run('class %T { + public function run(string $input) { + return $input->contains("Test"); + } + }', $input)); + } +} \ No newline at end of file