Skip to content

Commit 7bc8237

Browse files
authored
Merge pull request #57 from xp-framework/feature/pfa
Add syntactic support for partial function application
2 parents 63eaec3 + fdfd8f2 commit 7bc8237

File tree

5 files changed

+126
-49
lines changed

5 files changed

+126
-49
lines changed

src/main/php/lang/ast/nodes/CallableExpression.class.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
class CallableExpression extends Node {
66
public $kind= 'callable';
77
public $expression;
8+
public $arguments;
89

9-
public function __construct($expression, $line= -1) {
10+
public function __construct($expression, $arguments= [], $line= -1) {
1011
$this->expression= $expression;
12+
$this->arguments= $arguments;
1113
$this->line= $line;
1214
}
1315

src/main/php/lang/ast/nodes/CallableNewExpression.class.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
class CallableNewExpression extends Node {
66
public $kind= 'callablenew';
77
public $type;
8+
public $arguments;
89

9-
public function __construct($type, $line= -1) {
10+
public function __construct($type, $arguments= [], $line= -1) {
1011
$this->type= $type;
12+
$this->arguments= $arguments;
1113
$this->line= $line;
1214
}
1315

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php namespace lang\ast\nodes;
2+
3+
use lang\ast\Node;
4+
5+
/**
6+
* These two placeholder symbols exist:
7+
*
8+
* - The argument place holder `?` means that exactly one argument is
9+
* expected at this position.
10+
* - The variadic place holder `...` means that zero or more arguments
11+
* may be supplied at this position.
12+
*
13+
* @see https://wiki.php.net/rfc/partial_function_application_v2
14+
*/
15+
class Placeholder extends Node {
16+
public static $ARGUMENT, $VARIADIC;
17+
public $literal;
18+
public $kind= 'placeholder';
19+
20+
static function __static() {
21+
self::$ARGUMENT= new self('?');
22+
self::$VARIADIC= new self('...');
23+
}
24+
25+
/** @param string $literal */
26+
private function __construct($literal) {
27+
$this->literal= $literal;
28+
}
29+
}

src/main/php/lang/ast/syntax/PHP.class.php

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
OffsetExpression,
4646
Parameter,
4747
PipeExpression,
48+
Placeholder,
4849
Property,
4950
ReturnStatement,
5051
ScopeExpression,
@@ -151,15 +152,18 @@ public function __construct() {
151152
$scope= $left instanceof Literal ? $parse->scope->resolve($left->expression) : $left;
152153
$expr= $this->member($parse);
153154

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

157-
if ($this->callable($parse)) {
158-
return new CallableExpression(new ScopeExpression($scope, $expr, $token->line), $token->line);
159-
}
161+
if ($callable) return new CallableExpression(
162+
new ScopeExpression($scope, $expr, $token->line),
163+
$arguments,
164+
$token->line
165+
);
160166

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

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

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

181-
// Resolve ambiguity by looking ahead: `func(...)` which is a first-class
182-
// callable reference vs. `func(...$it)` - a call with an unpacked argument
183-
if ($this->callable($parse)) {
184-
return new CallableExpression($left, $token->line);
187+
if ($callable) {
188+
return new CallableExpression($left, $arguments, $token->line);
189+
} else {
190+
return new InvokeExpression($left, $arguments, $left->line);
185191
}
186-
187-
$arguments= $this->arguments($parse);
188-
$parse->expecting(')', 'invoke expression');
189-
return new InvokeExpression($left, $arguments, $left->line);
190192
});
191193

192194
$this->infix('[', 100, function($parse, $token, $left) {
@@ -281,12 +283,14 @@ public function __construct() {
281283
// clone $x vs. clone($x) or clone($x, ["id" => 6100])
282284
if ('(' === $parse->token->value) {
283285
$parse->forward();
284-
if ($this->callable($parse)) {
285-
return new CallableExpression(new Literal('clone', $token->line), $token->line);
286-
}
287-
288-
$arguments= $this->arguments($parse);
286+
[$arguments, $callable]= $this->arguments($parse);
289287
$parse->expecting(')', 'clone arguments');
288+
289+
if ($callable) return new CallableExpression(
290+
new Literal('clone', $token->line),
291+
$arguments,
292+
$token->line
293+
);
290294
} else {
291295
$arguments= [$this->expression($parse, 90)];
292296
}
@@ -336,10 +340,12 @@ public function __construct() {
336340
}
337341

338342
$parse->expecting('(', 'new arguments');
343+
[$arguments, $callable]= $this->arguments($parse);
344+
$parse->expecting(')', 'new arguments');
339345

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

351-
return new CallableNewExpression($new, $token->line);
357+
return new CallableNewExpression($new, $arguments, $token->line);
352358
}
353359

354-
$arguments= $this->arguments($parse);
355-
$parse->expecting(')', 'new arguments');
356-
357360
if (null === $type) {
358361
$class= $this->class($parse, null);
359362
$class->annotations= $annotations;
@@ -1465,7 +1468,8 @@ private function annotations($parse, $context) {
14651468

14661469
if ('(' === $parse->token->value) {
14671470
$parse->expecting('(', $context);
1468-
$annotations->add(new Annotation($name, $this->arguments($parse), $parse->token->line));
1471+
[$arguments, $callable]= $this->arguments($parse);
1472+
$annotations->add(new Annotation($name, $arguments, $parse->token->line));
14691473
$parse->expecting(')', $context);
14701474
} else {
14711475
$annotations->add(new Annotation($name, [], $parse->token->line));
@@ -1722,24 +1726,9 @@ public function class($parse, $name, $comment= null, $modifiers= []) {
17221726
return $decl;
17231727
}
17241728

1725-
public function callable($parse) {
1726-
if ('...' === $parse->token->value) {
1727-
$dots= $parse->token;
1728-
$parse->forward();
1729-
if (')' === $parse->token->value) {
1730-
$parse->forward();
1731-
return true;
1732-
}
1733-
1734-
// Not first-class callable syntax but unpack
1735-
array_unshift($parse->queue, $parse->token);
1736-
$parse->token= $dots;
1737-
}
1738-
return false;
1739-
}
1740-
17411729
public function arguments($parse) {
17421730
$arguments= [];
1731+
$callable= false;
17431732
while (')' !== $parse->token->value) {
17441733

17451734
// Named arguments (name: <argument>) vs. positional arguments
@@ -1748,14 +1737,32 @@ public function arguments($parse) {
17481737
$parse->forward();
17491738
if (':' === $parse->token->value) {
17501739
$parse->forward();
1751-
$arguments[$token->value]= $this->expression($parse, 0);
1740+
$offset= $token->value;
17521741
} else {
17531742
array_unshift($parse->queue, $parse->token);
17541743
$parse->token= $token;
1755-
$arguments[]= $this->expression($parse, 0);
1744+
$offset= sizeof($arguments);
1745+
}
1746+
} else {
1747+
$offset= sizeof($arguments);
1748+
}
1749+
1750+
if ('?' === $parse->token->value) {
1751+
$callable= true;
1752+
$arguments[$offset]= Placeholder::$ARGUMENT;
1753+
$parse->forward();
1754+
} else if ('...' === $parse->token->value) {
1755+
$parse->forward();
1756+
1757+
// Resolve ambiguity between unpack and variadic placeholder at the end of arguments
1758+
if (')' === $parse->token->value || ',' === $parse->token->value) {
1759+
$callable= true;
1760+
$arguments[$offset]= Placeholder::$VARIADIC;
1761+
} else {
1762+
$arguments[$offset]= new UnpackExpression($this->expression($parse, 0), $parse->token->line);
17561763
}
17571764
} else {
1758-
$arguments[]= $this->expression($parse, 0);
1765+
$arguments[$offset]= $this->expression($parse, 0);
17591766
}
17601767

17611768
if (',' === $parse->token->value) {
@@ -1767,7 +1774,7 @@ public function arguments($parse) {
17671774
break;
17681775
}
17691776
}
1770-
return $arguments;
1777+
return [$arguments, $callable];
17711778
}
17721779

17731780
public function expressions($parse, $end) {

src/test/php/lang/ast/unittest/parse/InvokeTest.class.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
UnpackExpression,
1010
ScopeExpression,
1111
Literal,
12+
Placeholder,
1213
Variable
1314
};
1415
use lang\ast\types\IsValue;
@@ -86,27 +87,63 @@ public function argument_unpacking() {
8687
#[Test]
8788
public function first_class_callable_function() {
8889
$this->assertParsed(
89-
[new CallableExpression(new Literal('strlen', self::LINE), self::LINE)],
90+
[new CallableExpression(
91+
new Literal('strlen', self::LINE),
92+
[Placeholder::$VARIADIC],
93+
self::LINE
94+
)],
9095
'strlen(...);'
9196
);
9297
}
9398

9499
#[Test]
95100
public function first_class_callable_static() {
96101
$this->assertParsed(
97-
[new CallableExpression(new ScopeExpression('self', new Literal('length', self::LINE), self::LINE), self::LINE)],
102+
[new CallableExpression(
103+
new ScopeExpression('self', new Literal('length', self::LINE), self::LINE),
104+
[Placeholder::$VARIADIC],
105+
self::LINE
106+
)],
98107
'self::length(...);'
99108
);
100109
}
101110

102111
#[Test]
103112
public function first_class_callable_object_creation() {
104113
$this->assertParsed(
105-
[new CallableNewExpression(new NewExpression(new IsValue('\\T'), null, self::LINE), self::LINE)],
114+
[new CallableNewExpression(
115+
new NewExpression(new IsValue('\\T'), null, self::LINE),
116+
[Placeholder::$VARIADIC],
117+
self::LINE
118+
)],
106119
'new T(...);'
107120
);
108121
}
109122

123+
#[Test]
124+
public function partial_function_application() {
125+
$this->assertParsed(
126+
[new CallableExpression(
127+
new Literal('str_replace', self::LINE),
128+
[new Literal('"test"', self::LINE), new Literal('"ok"', self::LINE), Placeholder::$ARGUMENT],
129+
self::LINE
130+
)],
131+
'str_replace("test", "ok", ?);'
132+
);
133+
}
134+
135+
#[Test]
136+
public function partial_function_application_named() {
137+
$this->assertParsed(
138+
[new CallableExpression(
139+
new Literal('str_replace', self::LINE),
140+
[new Literal('"test"', self::LINE), new Literal('"ok"', self::LINE), 'subject' => Placeholder::$ARGUMENT],
141+
self::LINE
142+
)],
143+
'str_replace("test", "ok", subject: ?);'
144+
);
145+
}
146+
110147
#[Test]
111148
public function chained_invocation_spanning_multiple_lines() {
112149
$expr= new InvokeExpression(

0 commit comments

Comments
 (0)