Skip to content

Implement partial function application #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
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
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": "dev-feature/pfa as 11.8.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 @@ -22,7 +22,7 @@ class PHP81 extends PHP {
use
EmulatePipelines,
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 @@ -22,7 +22,7 @@ class PHP82 extends PHP {
use
EmulatePipelines,
RewriteBlockLambdaExpressions,
RewriteCallableClone,
RewriteCallables,
RewriteCloneWith,
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
Expand Down
8 changes: 7 additions & 1 deletion src/main/php/lang/ast/emit/PHP83.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
* @see https://wiki.php.net/rfc#php_83
*/
class PHP83 extends PHP {
use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions, RewriteProperties;
use
EmulatePipelines,
RewriteCallables,
RewriteCloneWith,
RewriteBlockLambdaExpressions,
RewriteProperties
;

public $targetVersion= 80300;

Expand Down
7 changes: 6 additions & 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,12 @@
* @see https://wiki.php.net/rfc#php_84
*/
class PHP84 extends PHP {
use EmulatePipelines, RewriteCallableClone, RewriteCloneWith, RewriteBlockLambdaExpressions;
use
EmulatePipelines,
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, RewriteCloneWith; // TODO: Remove once PR is merged!
use RewriteBlockLambdaExpressions, RewriteCallables, RewriteCloneWith; // TODO: Remove once PR is merged!

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,76 @@
<?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(',');
}
}

// 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(')');
$init && $result->out->write(']['.sizeof($init).']');
} else {
parent::emitCallable($result, $callable);
}
}
}
Loading
Loading