Skip to content

Commit a7fa148

Browse files
committed
Optimize pipelines with single-placeholder PFAs
1 parent 61c4476 commit a7fa148

File tree

3 files changed

+132
-17
lines changed

3 files changed

+132
-17
lines changed
Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php namespace lang\ast\emit;
22

3-
use lang\ast\nodes\{CallableExpression, CallableNewExpression, Variable, Placeholder};
3+
use lang\ast\nodes\{CallableExpression, CallableNewExpression, Literal, Placeholder, Variable};
44

55
/**
66
* Emulates pipelines / the pipe operator, including a null-safe version.
@@ -10,54 +10,87 @@
1010
* $in |> $expr;
1111
* ($expr)($in);
1212
*
13+
* // Optimize for string literals:
14+
* $in |> 'strlen';
15+
* strlen($in);
16+
*
1317
* // Optimize for first-class callables with single placeholder argument:
1418
* $in |> strlen(...);
1519
* strlen($in);
20+
*
21+
* // Optimize for partial functions with single placeholder argument:
22+
* $in |> str_replace('test', 'ok', ?);
23+
* strlen('test', 'ok', $in);
1624
* ```
1725
*
1826
* @see https://wiki.php.net/rfc/pipe-operator-v3
1927
* @see https://externals.io/message/107661#107670
28+
* @test lang.ast.unittest.emit.EmulatePipelinesTest
2029
* @test lang.ast.unittest.emit.PipelinesTest
2130
*/
2231
trait EmulatePipelines {
2332

24-
private function singlePlaceholder($arguments) {
25-
return 1 === sizeof($arguments) && $arguments[0] instanceof Placeholder;
33+
private function passSingle($arguments, $arg) {
34+
$placeholder= -1;
35+
foreach ($arguments as $n => $argument) {
36+
if ($argument instanceof Placeholder) {
37+
if ($placeholder > -1) return null;
38+
$placeholder= $n;
39+
}
40+
}
41+
42+
$r= $arguments;
43+
$r[$placeholder]= $arg;
44+
return $r;
2645
}
2746

2847
protected function emitPipeTarget($result, $target, $arg) {
29-
if ($target instanceof CallableNewExpression && $this->singlePlaceholder($target->arguments)) {
30-
$target->type->arguments= [new Variable(substr($arg, 1))];
48+
if ($target instanceof CallableNewExpression && ($pass= $this->passSingle($target->arguments, $arg))) {
49+
$target->type->arguments= $pass;
3150
$this->emitOne($result, $target->type);
3251
$target->type->arguments= null;
33-
} else if ($target instanceof CallableExpression && $this->singlePlaceholder($target->arguments)) {
52+
} else if ($target instanceof CallableExpression && ($pass= $this->passSingle($target->arguments, $arg))) {
3453
$this->emitOne($result, $target->expression);
35-
$result->out->write('('.$arg.')');
54+
$result->out->write('(');
55+
$this->emitArguments($result, $pass);
56+
$result->out->write(')');
57+
} else if ($target instanceof Literal) {
58+
$result->out->write(trim($target->expression, '"\''));
59+
$result->out->write('(');
60+
$this->emitOne($result, $arg);
61+
$result->out->write(')');
3662
} else {
3763
$result->out->write('(');
3864
$this->emitOne($result, $target);
39-
$result->out->write(')('.$arg.')');
65+
$result->out->write(')(');
66+
$this->emitOne($result, $arg);
67+
$result->out->write(')');
4068
}
4169
}
4270

4371
protected function emitPipe($result, $pipe) {
4472

45-
// $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1]
46-
$t= $result->temp();
47-
$result->out->write('['.$t.'=');
48-
$this->emitOne($result, $pipe->expression);
49-
$result->out->write(',');
50-
$this->emitPipeTarget($result, $pipe->target, $t);
51-
$result->out->write('][1]');
73+
// <const> |> strtoupper(...) => strtoupper(<const>)
74+
// <expr> |> strtoupper(...) => [$arg= <expr>, strtoupper($arg)][1]
75+
if ($this->isConstant($result, $pipe->expression)) {
76+
$this->emitPipeTarget($result, $pipe->target, $pipe->expression);
77+
} else {
78+
$t= $result->temp();
79+
$result->out->write('['.$t.'=');
80+
$this->emitOne($result, $pipe->expression);
81+
$result->out->write(',');
82+
$this->emitPipeTarget($result, $pipe->target, new Variable(substr($t, 1)));
83+
$result->out->write('][1]');
84+
}
5285
}
5386

5487
protected function emitNullsafePipe($result, $pipe) {
5588

56-
// $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg)
89+
// <expr> ?|> strtoupper(...) => null === ($arg= <expr>) ? null : strtoupper($arg)
5790
$t= $result->temp();
5891
$result->out->write('null===('.$t.'=');
5992
$this->emitOne($result, $pipe->expression);
6093
$result->out->write(')?null:');
61-
$this->emitPipeTarget($result, $pipe->target, $t);
94+
$this->emitPipeTarget($result, $pipe->target, new Variable(substr($t, 1)));
6295
}
6396
}

src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ public function run() {
323323
Assert::equals(['hi world'], $r);
324324
}
325325

326+
#[Test, Expect(class: Error::class, message: '/Too few arguments/')]
327+
public function partial_function_application_returned_by_pipe() {
328+
$this->run('class %T {
329+
public function run() {
330+
"hi" |> str_replace("hello", ?, ?);
331+
}
332+
}');
333+
}
334+
326335
#[Test]
327336
public function partial_function_application_pass_named() {
328337
$f= $this->run('class %T {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php namespace lang\ast\unittest\emit;
2+
3+
use test\{Assert, Test, Values};
4+
5+
class EmulatePipelinesTest extends EmittingTest {
6+
7+
/** @return string */
8+
protected function runtime() { return 'php:8.4.0'; }
9+
10+
#[Test]
11+
public function lhs_evaluation_order() {
12+
Assert::equals(
13+
'[$_0=result(),($expr)($_0)][1];',
14+
$this->emit('result() |> $expr;')
15+
);
16+
}
17+
18+
#[Test]
19+
public function to_expression() {
20+
Assert::equals(
21+
'($expr)("hi");',
22+
$this->emit('"hi" |> $expr;')
23+
);
24+
}
25+
26+
#[Test, Values(['"strlen"', "'strlen'"])]
27+
public function to_string_literal($notation) {
28+
Assert::equals(
29+
'strlen("hi");',
30+
$this->emit('"hi" |> '.$notation.';')
31+
);
32+
}
33+
34+
#[Test]
35+
public function to_array_literal() {
36+
Assert::equals(
37+
'([$this,"func",])("hi");',
38+
$this->emit('"hi" |> [$this, "func"];')
39+
);
40+
}
41+
42+
#[Test]
43+
public function to_first_class_callable() {
44+
Assert::equals(
45+
'strlen("hi");',
46+
$this->emit('"hi" |> strlen(...);')
47+
);
48+
}
49+
50+
#[Test]
51+
public function to_callable_new() {
52+
Assert::equals(
53+
'new \\util\\Date("2025-07-12");',
54+
$this->emit('"2025-07-12" |> new \\util\\Date(...);')
55+
);
56+
}
57+
58+
#[Test]
59+
public function to_partial_function_application() {
60+
Assert::equals(
61+
'str_replace("hi","hello","hi");',
62+
$this->emit('"hi" |> str_replace("hi", "hello", ?);')
63+
);
64+
}
65+
66+
#[Test]
67+
public function chained() {
68+
Assert::equals(
69+
'[$_0=strtoupper("hi"),trim($_0,"{}")][1];',
70+
$this->emit('"hi" |> strtoupper(...) |> trim(?, "{}");')
71+
);
72+
}
73+
}

0 commit comments

Comments
 (0)