Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/main/php/lang/ast/emit/Local.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php namespace lang\ast\emit;

class Local {
public $type, $byRef;

public function __construct($type, $byRef= false) {
$this->type= $type;
$this->byRef= $byRef;
}
}
53 changes: 40 additions & 13 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,6 +27,7 @@ abstract class PHP extends Emitter {
const CONSTANT= 2;

protected $literals= [];
public $extensions= [];

/**
* Creates result
Expand Down Expand Up @@ -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
*
Expand All @@ -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']);
Expand All @@ -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];
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]);
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions src/test/php/lang/ast/unittest/emit/EmittingTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstract class EmittingTest {
private static $id= 0;
private $cl, $language, $emitter, $output;
private $transformations= [];
private $extensions= [];

/**
* Constructor
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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]);
}
}
}
74 changes: 74 additions & 0 deletions src/test/php/lang/ast/unittest/emit/ExtensionsTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php namespace lang\ast\unittest\emit;

use lang\ast\nodes\{Literal, BinaryExpression, InvokeExpression};
use test\{Assert, Before, Test, Values};

class ExtensionsTest extends EmittingTest {

#[Before]
public function install() {

// See https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extension,
// this is what we might parse this from as an idea:
//
// ```php
// abstract class StringExtensions {
// extension(string) {
// public function length(string $self) => 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));
}
}
Loading