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
4 changes: 3 additions & 1 deletion src/main/php/lang/ast/nodes/CallableExpression.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
class CallableExpression extends Node {
public $kind= 'callable';
public $expression;
public $arguments;

public function __construct($expression, $line= -1) {
public function __construct($expression, $arguments= [], $line= -1) {
$this->expression= $expression;
$this->arguments= $arguments;
$this->line= $line;
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/php/lang/ast/nodes/CallableNewExpression.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
class CallableNewExpression extends Node {
public $kind= 'callablenew';
public $type;
public $arguments;

public function __construct($type, $line= -1) {
public function __construct($type, $arguments= [], $line= -1) {
$this->type= $type;
$this->arguments= $arguments;
$this->line= $line;
}

Expand Down
29 changes: 29 additions & 0 deletions src/main/php/lang/ast/nodes/Placeholder.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php namespace lang\ast\nodes;

use lang\ast\Node;

/**
* These two placeholder symbols exist:
*
* - The argument place holder `?` means that exactly one argument is
* expected at this position.
* - The variadic place holder `...` means that zero or more arguments
* may be supplied at this position.
*
* @see https://wiki.php.net/rfc/partial_function_application_v2
*/
class Placeholder extends Node {
public static $ARGUMENT, $VARIADIC;
public $literal;
public $kind= 'placeholder';

static function __static() {
self::$ARGUMENT= new self('?');
self::$VARIADIC= new self('...');
}

/** @param string $literal */
private function __construct($literal) {
$this->literal= $literal;
}
}
95 changes: 51 additions & 44 deletions src/main/php/lang/ast/syntax/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
OffsetExpression,
Parameter,
PipeExpression,
Placeholder,
Property,
ReturnStatement,
ScopeExpression,
Expand Down Expand Up @@ -151,15 +152,18 @@ public function __construct() {
$scope= $left instanceof Literal ? $parse->scope->resolve($left->expression) : $left;
$expr= $this->member($parse);

// Wrap self::member() into an invoke expression
if ('(' === $parse->token->value) {
$parse->expecting('(', 'invoke expression');
[$arguments, $callable]= $this->arguments($parse);
$parse->expecting(')', 'invoke expression');

if ($this->callable($parse)) {
return new CallableExpression(new ScopeExpression($scope, $expr, $token->line), $token->line);
}
if ($callable) return new CallableExpression(
new ScopeExpression($scope, $expr, $token->line),
$arguments,
$token->line
);

$arguments= $this->arguments($parse);
$parse->expecting(')', 'invoke expression');
$expr= new InvokeExpression($expr, $arguments, $token->line);
}

Expand All @@ -177,16 +181,14 @@ public function __construct() {
});

$this->infix('(', 100, function($parse, $token, $left) {
[$arguments, $callable]= $this->arguments($parse);
$parse->expecting(')', 'invoke expression');

// Resolve ambiguity by looking ahead: `func(...)` which is a first-class
// callable reference vs. `func(...$it)` - a call with an unpacked argument
if ($this->callable($parse)) {
return new CallableExpression($left, $token->line);
if ($callable) {
return new CallableExpression($left, $arguments, $token->line);
} else {
return new InvokeExpression($left, $arguments, $left->line);
}

$arguments= $this->arguments($parse);
$parse->expecting(')', 'invoke expression');
return new InvokeExpression($left, $arguments, $left->line);
});

$this->infix('[', 100, function($parse, $token, $left) {
Expand Down Expand Up @@ -281,12 +283,14 @@ public function __construct() {
// clone $x vs. clone($x) or clone($x, ["id" => 6100])
if ('(' === $parse->token->value) {
$parse->forward();
if ($this->callable($parse)) {
return new CallableExpression(new Literal('clone', $token->line), $token->line);
}

$arguments= $this->arguments($parse);
[$arguments, $callable]= $this->arguments($parse);
$parse->expecting(')', 'clone arguments');

if ($callable) return new CallableExpression(
new Literal('clone', $token->line),
$arguments,
$token->line
);
} else {
$arguments= [$this->expression($parse, 90)];
}
Expand Down Expand Up @@ -336,10 +340,12 @@ public function __construct() {
}

$parse->expecting('(', 'new arguments');
[$arguments, $callable]= $this->arguments($parse);
$parse->expecting(')', 'new arguments');

// Resolve ambiguity by looking ahead: `new T(...)` which is a first-class
// callable reference vs. `new T(...$it)` - a call with an unpacked argument
if ($this->callable($parse)) {
if ($callable) {
if (null === $type) {
$class= $this->class($parse, null);
$class->annotations= $annotations;
Expand All @@ -348,12 +354,9 @@ public function __construct() {
$new= new NewExpression($type, null, $token->line);
}

return new CallableNewExpression($new, $token->line);
return new CallableNewExpression($new, $arguments, $token->line);
}

$arguments= $this->arguments($parse);
$parse->expecting(')', 'new arguments');

if (null === $type) {
$class= $this->class($parse, null);
$class->annotations= $annotations;
Expand Down Expand Up @@ -1465,7 +1468,8 @@ private function annotations($parse, $context) {

if ('(' === $parse->token->value) {
$parse->expecting('(', $context);
$annotations->add(new Annotation($name, $this->arguments($parse), $parse->token->line));
[$arguments, $callable]= $this->arguments($parse);
$annotations->add(new Annotation($name, $arguments, $parse->token->line));
$parse->expecting(')', $context);
} else {
$annotations->add(new Annotation($name, [], $parse->token->line));
Expand Down Expand Up @@ -1722,24 +1726,9 @@ public function class($parse, $name, $comment= null, $modifiers= []) {
return $decl;
}

public function callable($parse) {
if ('...' === $parse->token->value) {
$dots= $parse->token;
$parse->forward();
if (')' === $parse->token->value) {
$parse->forward();
return true;
}

// Not first-class callable syntax but unpack
array_unshift($parse->queue, $parse->token);
$parse->token= $dots;
}
return false;
}

public function arguments($parse) {
$arguments= [];
$callable= false;
while (')' !== $parse->token->value) {

// Named arguments (name: <argument>) vs. positional arguments
Expand All @@ -1748,14 +1737,32 @@ public function arguments($parse) {
$parse->forward();
if (':' === $parse->token->value) {
$parse->forward();
$arguments[$token->value]= $this->expression($parse, 0);
$offset= $token->value;
} else {
array_unshift($parse->queue, $parse->token);
$parse->token= $token;
$arguments[]= $this->expression($parse, 0);
$offset= sizeof($arguments);
}
} else {
$offset= sizeof($arguments);
}

if ('?' === $parse->token->value) {
$callable= true;
$arguments[$offset]= Placeholder::$ARGUMENT;
$parse->forward();
} else if ('...' === $parse->token->value) {
$parse->forward();

// Resolve ambiguity between unpack and variadic placeholder at the end of arguments
if (')' === $parse->token->value || ',' === $parse->token->value) {
$callable= true;
$arguments[$offset]= Placeholder::$VARIADIC;
} else {
$arguments[$offset]= new UnpackExpression($this->expression($parse, 0), $parse->token->line);
}
} else {
$arguments[]= $this->expression($parse, 0);
$arguments[$offset]= $this->expression($parse, 0);
}

if (',' === $parse->token->value) {
Expand All @@ -1767,7 +1774,7 @@ public function arguments($parse) {
break;
}
}
return $arguments;
return [$arguments, $callable];
}

public function expressions($parse, $end) {
Expand Down
43 changes: 40 additions & 3 deletions src/test/php/lang/ast/unittest/parse/InvokeTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
UnpackExpression,
ScopeExpression,
Literal,
Placeholder,
Variable
};
use lang\ast\types\IsValue;
Expand Down Expand Up @@ -86,27 +87,63 @@ public function argument_unpacking() {
#[Test]
public function first_class_callable_function() {
$this->assertParsed(
[new CallableExpression(new Literal('strlen', self::LINE), self::LINE)],
[new CallableExpression(
new Literal('strlen', self::LINE),
[Placeholder::$VARIADIC],
self::LINE
)],
'strlen(...);'
);
}

#[Test]
public function first_class_callable_static() {
$this->assertParsed(
[new CallableExpression(new ScopeExpression('self', new Literal('length', self::LINE), self::LINE), self::LINE)],
[new CallableExpression(
new ScopeExpression('self', new Literal('length', self::LINE), self::LINE),
[Placeholder::$VARIADIC],
self::LINE
)],
'self::length(...);'
);
}

#[Test]
public function first_class_callable_object_creation() {
$this->assertParsed(
[new CallableNewExpression(new NewExpression(new IsValue('\\T'), null, self::LINE), self::LINE)],
[new CallableNewExpression(
new NewExpression(new IsValue('\\T'), null, self::LINE),
[Placeholder::$VARIADIC],
self::LINE
)],
'new T(...);'
);
}

#[Test]
public function partial_function_application() {
$this->assertParsed(
[new CallableExpression(
new Literal('str_replace', self::LINE),
[new Literal('"test"', self::LINE), new Literal('"ok"', self::LINE), Placeholder::$ARGUMENT],
self::LINE
)],
'str_replace("test", "ok", ?);'
);
}

#[Test]
public function partial_function_application_named() {
$this->assertParsed(
[new CallableExpression(
new Literal('str_replace', self::LINE),
[new Literal('"test"', self::LINE), new Literal('"ok"', self::LINE), 'subject' => Placeholder::$ARGUMENT],
self::LINE
)],
'str_replace("test", "ok", subject: ?);'
);
}

#[Test]
public function chained_invocation_spanning_multiple_lines() {
$expr= new InvokeExpression(
Expand Down
Loading