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
12 changes: 12 additions & 0 deletions src/main/php/lang/ast/nodes/Generic.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php namespace lang\ast\nodes;

class Generic extends Literal {
public $kind= 'generic';
public $expression, $components;

public function __construct($expression, $components, $line= -1) {
$this->expression= $expression;
$this->components= $components;
$this->line= $line;
}
}
1 change: 1 addition & 0 deletions src/main/php/lang/ast/nodes/Signature.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class Signature extends Node {
public $kind= 'signature';
public $parameters, $returns, $byref;
public $generic= null;

public function __construct($parameters= [], $returns= null, $byref= false, $line= -1) {
$this->parameters= $parameters;
Expand Down
116 changes: 78 additions & 38 deletions src/main/php/lang/ast/syntax/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ForLoop,
ForeachLoop,
FunctionDeclaration,
Generic,
GotoStatement,
IfStatement,
InstanceExpression,
Expand Down Expand Up @@ -1186,6 +1187,8 @@ private function variable($parse, $rbp) {
}

private function member($parse) {
static $skip= ['<' => 1, '<?' => 1, '>' => 1, '>>' => 1, ',' => 1];

if ('{' === $parse->token->value) {
$line= $parse->token->line;
$parse->forward();
Expand All @@ -1197,6 +1200,34 @@ private function member($parse) {
} else if ('name' === $parse->token->kind) {
$expr= new Literal($parse->token->value, $parse->token->line);
$parse->forward();

// Resolve ambiguity involving generic method invocations:
//
// - $this->member<?string>()
// - $this->member<string>()
// - $this->member < PHP_VERSION
// - $this->member < time()
// - test($this->member<string, string>($arg))
// - test($this->member < CONST, CONST > $arg)
//
// Look ahead until `>` followed by `(`
if ('<' === $parse->token->value || '<?' === $parse->token->value) {
$skipped= [];
while ('name' === $parse->token->kind || isset($skip[$parse->token->value])) {
$skipped[]= $parse->token;
$parse->forward();
}

$last= $skipped[sizeof($skipped) - 1]->value;
$generic= '(' === $parse->token->value && ('>' === $last || '>>' === $last);

$skipped[]= $parse->token;
$parse->token= array_shift($skipped);
array_unshift($parse->queue, ...$skipped);

// \util\cmd\Console::writeLine($generic, ' && ', $parse->queue);
if ($generic) return new Generic($expr->expression, $this->generic($parse), $expr->line);
}
} else {
$parse->expecting('an expression in curly braces, a name or a variable', 'member');
$expr= new Literal($parse->token->value, $parse->token->line);
Expand All @@ -1205,6 +1236,44 @@ private function member($parse) {
return $expr;
}

private function generic($parse) {

// Handle `T<?C>` - generics with a nullable component type
if ('<?' === $parse->token->value) {
array_unshift($parse->queue, new Token(self::symbol('?'), '(operator)', '?'));
$parse->forward();
} else if ('<' === $parse->token->value) {
$parse->forward();
} else {
return null;
}

$components= [];
do {
// Resolve ambiguity between generic placeholders and nullable
if ('?' === $parse->token->value) {
$parse->forward();
if (',' === $parse->token->value || '>' === $parse->token->value) {
$components[]= new IsLiteral('?');
} else {
$components[]= new IsNullable($this->type($parse, false));
}
} else {
$components[]= $this->type($parse, false);
}

if ('>' === $parse->token->symbol->id) {
break;
} else if ('>>' === $parse->token->value) {
array_unshift($parse->queue, $parse->token= new Token(self::symbol('>')));
break;
}
} while (',' === $parse->token->value && true | $parse->forward());

$parse->expecting('>', 'generic');
return $components;
}

private function type0($parse, $optional) {
static $literal= [
'string' => true,
Expand Down Expand Up @@ -1253,38 +1322,7 @@ private function type0($parse, $optional) {
return null;
}

// Resolve ambiguity between short open tag and nullables as in <?int>
if ('<?' === $parse->token->value) {
array_unshift($parse->queue, new Token(self::symbol('?'), '(operator)', '?'));
$parse->token->value= '<';
}

if ('<' === $parse->token->value) {
$parse->forward();
$components= [];
do {

// Resolve ambiguity between generic placeholders and nullable
if ('?' === $parse->token->value) {
$parse->forward();
if (',' === $parse->token->value || '>' === $parse->token->value) {
$components[]= new IsLiteral('?');
} else {
$components[]= new IsNullable($this->type($parse, false));
}
} else {
$components[]= $this->type($parse, false);
}

if ('>' === $parse->token->symbol->id) {
break;
} else if ('>>' === $parse->token->value) {
array_unshift($parse->queue, $parse->token= new Token(self::symbol('>')));
break;
}
} while (',' === $parse->token->value && true | $parse->forward());
$parse->expecting('>', 'type');

if ($components= $this->generic($parse)) {
if ('array' === $type) {
return 1 === sizeof($components) ? new IsArray($components[0]) : new IsMap($components[0], $components[1]);
} else {
Expand Down Expand Up @@ -1467,19 +1505,21 @@ public function typeBody($parse) {
}

public function signature($parse, $byref= false) {
$line= $parse->token->line;
$signature= new Signature();
$signature->byref= $byref;
$signature->line= $parse->token->line;
$signature->generic= $this->generic($parse);

$parse->expecting('(', 'signature');
$parameters= $this->parameters($parse);
$signature->parameters= $this->parameters($parse);
$parse->expecting(')', 'signature');

if (':' === $parse->token->value) {
$parse->forward();
$return= $this->type($parse);
} else {
$return= null;
$signature->returns= $this->type($parse);
}

return new Signature($parameters, $return, $byref, $line);
return $signature;
}

public function closure($parse, $static) {
Expand Down
55 changes: 54 additions & 1 deletion src/test/php/lang/ast/unittest/parse/InvokeTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use lang\ast\nodes\{
CallableExpression,
CallableNewExpression,
Generic,
NewExpression,
InstanceExpression,
InvokeExpression,
Expand All @@ -11,7 +12,7 @@
Literal,
Variable
};
use lang\ast\types\IsValue;
use lang\ast\types\{IsValue, IsLiteral, IsGeneric, IsNullable};
use test\{Assert, Test};

/**
Expand Down Expand Up @@ -106,4 +107,56 @@ public function first_class_callable_object_creation() {
'new T(...);'
);
}

#[Test]
public function invoke_generic_method() {
$instance= new InstanceExpression(
new Variable('this', self::LINE),
new Generic('test', [new IsLiteral('string')], self::LINE),
self::LINE
);
$this->assertParsed(
[new InvokeExpression($instance, [], self::LINE)],
'$this->test<string>();'
);
}

#[Test]
public function invoke_generic_static_method() {
$invoke= new InvokeExpression(
new Generic('test', [new IsLiteral('string')], self::LINE),
[],
self::LINE
);
$this->assertParsed(
[new ScopeExpression('self', $invoke, self::LINE)],
'self::test<string>();'
);
}

#[Test]
public function invoke_with_nested_generic() {
$invoke= new InvokeExpression(
new Generic('test', [new IsGeneric('List', [new IsLiteral('string')])], self::LINE),
[],
self::LINE
);
$this->assertParsed(
[new ScopeExpression('self', $invoke, self::LINE)],
'self::test<List<string>>();'
);
}

#[Test]
public function invoke_with_nullable_generic() {
$invoke= new InvokeExpression(
new Generic('test', [new IsNullable(new IsLiteral('string'))], self::LINE),
[],
self::LINE
);
$this->assertParsed(
[new ScopeExpression('self', $invoke, self::LINE)],
'self::test<?string>();'
);
}
}
11 changes: 11 additions & 0 deletions src/test/php/lang/ast/unittest/parse/MembersTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ public function method_with_annotations() {
$this->assertParsed([$class], 'class A { #[Test, Ignore("Not implemented")] public function a() { } }');
}

#[Test]
public function generic_method() {
$signature= new Signature([], null, false, self::LINE);
$signature->generic= [new IsValue('T')];

$class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE);
$class->declare(new Method(['public'], 'a', $signature, [], null, null, self::LINE));

$this->assertParsed([$class], 'class A { public function a<T>() { } }');
}

#[Test]
public function instance_property_access() {
$this->assertParsed(
Expand Down
38 changes: 38 additions & 0 deletions src/test/php/lang/ast/unittest/parse/OperatorTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,42 @@ public function multiple_semicolons() {
';; $a= 1 ;;; $b= 2;'
);
}

#[Test]
public function member_smaller_than_generic_call_ambiguity() {
$this->assertParsed(
[new BinaryExpression(
new InstanceExpression(new Variable('this', self::LINE), new Literal('test', self::LINE), self::LINE),
'<',
new Literal('THRESHOLD', self::LINE),
self::LINE
)],
'$this->test < THRESHOLD;'
);
}

#[Test]
public function arguments_with_constants_and_smaller_greater_generic_ambiguity() {
$this->assertParsed(
[new InvokeExpression(
new Literal('test', self::LINE),
[
new BinaryExpression(
new InstanceExpression(new Variable('this', self::LINE), new Literal('test', self::LINE), self::LINE),
'<',
new Literal('THRESHOLD', self::LINE),
self::LINE
),
new BinaryExpression(
new Literal('THRESHOLD', self::LINE),
'>',
new Literal('1', self::LINE),
self::LINE
)
],
self::LINE
)],
'test($this->test < THRESHOLD, THRESHOLD > 1);'
);
}
}