diff --git a/src/main/php/lang/ast/nodes/CallableExpression.class.php b/src/main/php/lang/ast/nodes/CallableExpression.class.php index 841f9fc..55b7152 100755 --- a/src/main/php/lang/ast/nodes/CallableExpression.class.php +++ b/src/main/php/lang/ast/nodes/CallableExpression.class.php @@ -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; } diff --git a/src/main/php/lang/ast/nodes/CallableNewExpression.class.php b/src/main/php/lang/ast/nodes/CallableNewExpression.class.php index 2fdba8c..1808a4b 100755 --- a/src/main/php/lang/ast/nodes/CallableNewExpression.class.php +++ b/src/main/php/lang/ast/nodes/CallableNewExpression.class.php @@ -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; } diff --git a/src/main/php/lang/ast/nodes/Placeholder.class.php b/src/main/php/lang/ast/nodes/Placeholder.class.php new file mode 100755 index 0000000..715948c --- /dev/null +++ b/src/main/php/lang/ast/nodes/Placeholder.class.php @@ -0,0 +1,29 @@ +literal= $literal; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 5140ecd..154f249 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -45,6 +45,7 @@ OffsetExpression, Parameter, PipeExpression, + Placeholder, Property, ReturnStatement, ScopeExpression, @@ -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); } @@ -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) { @@ -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)]; } @@ -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; @@ -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; @@ -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)); @@ -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: ) vs. positional arguments @@ -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) { @@ -1767,7 +1774,7 @@ public function arguments($parse) { break; } } - return $arguments; + return [$arguments, $callable]; } public function expressions($parse, $end) { diff --git a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php index b934bfc..a0e6e13 100755 --- a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php @@ -9,6 +9,7 @@ UnpackExpression, ScopeExpression, Literal, + Placeholder, Variable }; use lang\ast\types\IsValue; @@ -86,7 +87,11 @@ 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(...);' ); } @@ -94,7 +99,11 @@ public function first_class_callable_function() { #[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(...);' ); } @@ -102,11 +111,39 @@ public function first_class_callable_static() { #[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(