Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
10eaf45
Implement partial function application
thekid Jul 12, 2025
02aa85b
Add test showing by-ref arguments do not work
thekid Jul 12, 2025
fa9bde8
Add test showing execution order differs
thekid Jul 12, 2025
15452e6
Make evaluation order consistent with PHP
thekid Jul 12, 2025
c833be0
Optimize passing constant expressions
thekid Jul 12, 2025
ea7d77e
QA: APIdoc readability
thekid Jul 12, 2025
6970210
Add test for PFA inside annotations
thekid Jul 12, 2025
c31dedb
Fix PFA in combination with pipeline operator optimizations
thekid Jul 12, 2025
bfa707c
Verify `?` placeholder also works in `new`
thekid Jul 12, 2025
4bc33f5
Implement support for named arguments
thekid Jul 12, 2025
61c4476
Show how the `...` placeholder may appear in the middle of arguments
thekid Jul 12, 2025
a7fa148
Optimize pipelines with single-placeholder PFAs
thekid Jul 12, 2025
e0bdbd9
Add test for partial constructor application
thekid Jul 12, 2025
eef8baf
Add test for static methods used w/ partial function applications
thekid Jul 12, 2025
c247e5f
Test PFAs work with __invoke() and __call()
thekid Jul 12, 2025
5134c3f
Verify first-class callable and PFAs both return `Closure` instances
thekid Jul 12, 2025
314383b
QA: Remove unused import
thekid Jul 12, 2025
a8d1dde
MFH
thekid Jul 13, 2025
fb30ef3
MFH
thekid Feb 14, 2026
138a73b
PFA currently scheduled for PHP 8.6
thekid Feb 14, 2026
b3c7d7b
Fix "Arrow functions on the right hand side of |> must be parenthesized"
thekid Feb 14, 2026
9454821
Fix "Arrow functions on the right hand side of |> must be parenthesiz…
thekid Feb 14, 2026
be50429
Use AST release version
thekid Feb 14, 2026
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"require" : {
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
"xp-framework/reflection": "^3.2 | ^2.15",
"xp-framework/ast": "^11.7",
"xp-framework/ast": "^12.0",
"php" : ">=7.4.0"
},
"require-dev" : {
Expand Down
15 changes: 10 additions & 5 deletions src/main/php/lang/ast/emit/CallablesAsClosures.class.php
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
<?php namespace lang\ast\emit;

use lang\ast\Node;
use lang\ast\nodes\{Expression, InstanceExpression, ScopeExpression, Literal};
use lang\ast\nodes\{Expression, InstanceExpression, ScopeExpression, Literal, Placeholder};

/**
* Rewrites callable expressions to `Callable::fromClosure()`
*
* @see https://www.php.net/manual/de/closure.fromcallable.php
* @see https://wiki.php.net/rfc/clone_with_v2
* @see https://wiki.php.net/rfc/first_class_callable_syntax
* @see https://wiki.php.net/rfc/partial_function_application_v2
*/
trait CallablesAsClosures {
use RewritePartialFunctionApplications { emitCallable as emitPartial; }

private function emitQuoted($result, $node) {
if ($node instanceof Literal) {

// Rewrite f() => "f"
// Rewrite f(...) => "f"
$result->out->write('"'.trim($node, '"\'').'"');
} else if ($node instanceof InstanceExpression) {

// Rewrite $this->f => [$this, "f"]
// Rewrite $this->f(...) => [$this, "f"]
$result->out->write('[');
$this->emitOne($result, $node->expression);
$result->out->write(',');
$this->emitQuoted($result, $node->member);
$result->out->write(']');
} else if ($node instanceof ScopeExpression) {

// Rewrite T::f => [T::class, "f"]
// Rewrite T::f(...) => [T::class, "f"]
$result->out->write('[');
if ($node->type instanceof Node) {
$this->emitOne($result, $node->type);
Expand All @@ -38,7 +41,7 @@ private function emitQuoted($result, $node) {
$result->out->write(']');
} else if ($node instanceof Expression) {

// Rewrite T::{<f>} => [T::class, <f>]
// Rewrite T::{<f>}(...) => [T::class, <f>]
$this->emitOne($result, $node->inline);
} else {

Expand All @@ -50,6 +53,8 @@ private function emitQuoted($result, $node) {
protected function emitCallable($result, $callable) {
if ($callable->expression instanceof Literal && 'clone' === $callable->expression->expression) {
$result->out->write('fn($o) => clone $o');
} else if ([Placeholder::$VARIADIC] !== $callable->arguments) {
$this->emitPartial($result, $callable);
} else {
$result->out->write('\Closure::fromCallable(');
$this->emitQuoted($result, $callable->expression);
Expand Down
30 changes: 24 additions & 6 deletions src/main/php/lang/ast/emit/EmulatePipelines.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{CallableExpression, CallableNewExpression, Literal, Variable};
use lang\ast\nodes\{CallableExpression, CallableNewExpression, Literal, Placeholder, Variable};

/**
* Emulates pipelines / the pipe operator, including a null-safe version.
Expand All @@ -14,9 +14,13 @@
* $in |> 'strlen';
* strlen($in);
*
* // Optimize for first-class callables:
* // Optimize for first-class callables with single placeholder argument:
* $in |> strlen(...);
* strlen($in);
*
* // Optimize for partial functions with single placeholder argument:
* $in |> str_replace('test', 'ok', ?);
* strlen('test', 'ok', $in);
* ```
*
* @see https://wiki.php.net/rfc/pipe-operator-v3
Expand All @@ -26,15 +30,29 @@
*/
trait EmulatePipelines {

private function passSingle($arguments, $arg) {
$placeholder= -1;
foreach ($arguments as $n => $argument) {
if ($argument instanceof Placeholder) {
if ($placeholder > -1) return null;
$placeholder= $n;
}
}

$r= $arguments;
$r[$placeholder]= $arg;
return $r;
}

protected function emitPipeTarget($result, $target, $arg) {
if ($target instanceof CallableNewExpression) {
$target->type->arguments= [$arg];
if ($target instanceof CallableNewExpression && ($pass= $this->passSingle($target->arguments, $arg))) {
$target->type->arguments= $pass;
$this->emitOne($result, $target->type);
$target->type->arguments= null;
} else if ($target instanceof CallableExpression) {
} else if ($target instanceof CallableExpression && ($pass= $this->passSingle($target->arguments, $arg))) {
$this->emitOne($result, $target->expression);
$result->out->write('(');
$this->emitOne($result, $arg);
$this->emitArguments($result, $pass);
$result->out->write(')');
} else if ($target instanceof Literal) {
$result->out->write(trim($target->expression, '"\''));
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP81.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PHP81 extends PHP {
EmulatePipelines,
RemoveVoidCasts,
RewriteBlockLambdaExpressions,
RewriteCallableClone,
RewriteCallables,
RewriteCloneWith,
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP82.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PHP82 extends PHP {
EmulatePipelines,
RemoveVoidCasts,
RewriteBlockLambdaExpressions,
RewriteCallableClone,
RewriteCallables,
RewriteCloneWith,
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
Expand Down
4 changes: 2 additions & 2 deletions src/main/php/lang/ast/emit/PHP83.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
* @see https://wiki.php.net/rfc#php_83
*/
class PHP83 extends PHP {
use
use
EmulatePipelines,
RemoveVoidCasts,
RewriteCallableClone,
RewriteCallables,
RewriteCloneWith,
RewriteBlockLambdaExpressions,
RewriteProperties
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP84.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @see https://wiki.php.net/rfc#php_84
*/
class PHP84 extends PHP {
use EmulatePipelines, RemoveVoidCasts, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions;
use EmulatePipelines, RemoveVoidCasts, RewriteCallables, RewriteCloneWith, RewriteBlockLambdaExpressions;

public $targetVersion= 80400;

Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP85.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @see https://wiki.php.net/rfc#php_85
*/
class PHP85 extends PHP {
use RewriteBlockLambdaExpressions, RewriteCallableClone;
use RewriteBlockLambdaExpressions, RewriteCallables;

public $targetVersion= 80500;

Expand Down
15 changes: 0 additions & 15 deletions src/main/php/lang/ast/emit/RewriteCallableClone.class.php

This file was deleted.

21 changes: 21 additions & 0 deletions src/main/php/lang/ast/emit/RewriteCallables.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\Literal;

/**
* Rewrites `clone(...)` and partial function applications
*
* @see https://wiki.php.net/rfc/clone_with_v2
* @see https://wiki.php.net/rfc/partial_function_application_v2
*/
trait RewriteCallables {
use RewritePartialFunctionApplications { emitCallable as emitPartial; }

protected function emitCallable($result, $callable) {
if ($callable->expression instanceof Literal && 'clone' === $callable->expression->expression) {
$result->out->write('(fn($o) => clone $o)');
} else {
$this->emitPartial($result, $callable);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{Placeholder, Variable, UnpackExpression};

/**
* Rewrites partial function application as follows:
*
* ```php
* // Input:
* $f= str_replace('test', 'ok', ?);
*
* // Output:
* $f= fn($arg) => str_replace('test', 'ok', $arg);
* ```
*
* Keeps evaluation order consistent with native implementation:
*
* ```php
* // Input:
* $f= str_replace('test', result(), ?);
*
* // Output:
* $f= [
* $temp= result(),
* fn($arg) => str_replace('test', $temp, $arg)
* ][1];
* ```
*
* @see https://wiki.php.net/rfc/partial_function_application_v2
*/
trait RewritePartialFunctionApplications {

protected function emitCallable($result, $callable) {
if ([Placeholder::$VARIADIC] !== $callable->arguments) {
$sig= '';
$pass= $init= [];
foreach ($callable->arguments as $name => $argument) {
if (Placeholder::$VARIADIC === $argument) {
$t= $result->temp();
$sig.= ',...'.$t;
$pass[$name]= new UnpackExpression(new Variable(substr($t, 1)));
} else if (Placeholder::$ARGUMENT === $argument) {
$t= $result->temp();
$sig.= ','.$t;
$pass[$name]= new Variable(substr($t, 1));
} else if ($this->isConstant($result, $argument)) {
$pass[$name]= $argument;
} else {
$t= $result->temp();
$pass[$name]= new Variable(substr($t, 1));
$init[$t]= $argument;
}
}

// Initialize any non-constant expressions in place
if ($init) {
$result->out->write('[');
foreach ($init as $t => $argument) {
$result->out->write($t.'=');
$this->emitOne($result, $argument);
$result->out->write(',');
}
} else {
$result->out->write('(');
}

// Emit closure invoking the callable expression
$result->out->write('fn('.substr($sig, 1).')=>');
$this->emitOne($result, $callable->expression);
$result->out->write('(');
$this->emitArguments($result, $pass);
$result->out->write(')');

if ($init) {
$result->out->write(']['.sizeof($init).']');
} else {
$result->out->write(')');
}
} else {
parent::emitCallable($result, $callable);
}
}
}
Loading
Loading