Skip to content

Commit 4357341

Browse files
committed
Fix pipeline execution order
1 parent bb30652 commit 4357341

File tree

2 files changed

+183
-27
lines changed

2 files changed

+183
-27
lines changed

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

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,42 +1158,40 @@ protected function emitNullsafeInstance($result, $instance) {
11581158
$this->emitOne($result, $instance->member);
11591159
}
11601160

1161-
protected function emitPipeTarget($result, $pipe, $argument) {
1162-
1163-
// $expr |> new T(...) => new T($expr)
1164-
if ($pipe->target instanceof CallableNewExpression) {
1165-
$pipe->target->type->arguments= [$argument];
1166-
$this->emitOne($result, $pipe->target->type);
1167-
$pipe->target->type->arguments= null;
1168-
return;
1169-
}
1170-
1171-
// $expr |> strtoupper(...) => strtoupper($expr)
1172-
// $expr |> fn($x) => $x * 2 => (fn($x) => $x * 2)($expr)
1173-
if ($pipe->target instanceof CallableExpression) {
1174-
$this->emitOne($result, $pipe->target->expression);
1161+
protected function emitPipeTarget($result, $target, $arg) {
1162+
if ($target instanceof CallableNewExpression) {
1163+
$target->type->arguments= [new Variable(substr($arg, 1))];
1164+
$this->emitOne($result, $target->type);
1165+
$target->type->arguments= null;
1166+
} else if ($target instanceof CallableExpression) {
1167+
$this->emitOne($result, $target->expression);
1168+
$result->out->write('('.$arg.')');
11751169
} else {
11761170
$result->out->write('(');
1177-
$this->emitOne($result, $pipe->target);
1178-
$result->out->write(')');
1171+
$this->emitOne($result, $target);
1172+
$result->out->write(')('.$arg.')');
11791173
}
1180-
1181-
$result->out->write('(');
1182-
$this->emitOne($result, $argument);
1183-
$result->out->write(')');
11841174
}
11851175

11861176
protected function emitPipe($result, $pipe) {
1187-
$this->emitPipeTarget($result, $pipe, $pipe->expression);
1177+
1178+
// $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1]
1179+
$t= $result->temp();
1180+
$result->out->write('['.$t.'=');
1181+
$this->emitOne($result, $pipe->expression);
1182+
$result->out->write(',');
1183+
$this->emitPipeTarget($result, $pipe->target, $t);
1184+
$result->out->write('][1]');
11881185
}
11891186

11901187
protected function emitNullsafePipe($result, $pipe) {
1188+
1189+
// $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg)
11911190
$t= $result->temp();
11921191
$result->out->write('null===('.$t.'=');
11931192
$this->emitOne($result, $pipe->expression);
11941193
$result->out->write(')?null:');
1195-
1196-
$this->emitPipeTarget($result, $pipe, new Variable(substr($t, 1)));
1194+
$this->emitPipeTarget($result, $pipe->target, $t);
11971195
}
11981196

11991197
protected function emitUnpack($result, $unpack) {

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

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?php namespace lang\ast\unittest\emit;
22

3-
use test\{Assert, Test, Values};
3+
use lang\Error;
4+
use test\verify\Runtime;
5+
use test\{Assert, Expect, Test, Values};
46

57
/** @see https://wiki.php.net/rfc/pipe-operator-v3 */
68
class PipelinesTest extends EmittingTest {
@@ -98,6 +100,24 @@ public function run() {
98100
Assert::equals('test: OK', $r);
99101
}
100102

103+
#[Test, Expect(Error::class)]
104+
public function pipe_to_throw() {
105+
$this->run('use lang\Error; class %T {
106+
public function run() {
107+
return "test" |> throw new Error("Test");
108+
}
109+
}');
110+
}
111+
112+
#[Test, Expect(Error::class)]
113+
public function pipe_to_missing() {
114+
$this->run('class %T {
115+
public function run() {
116+
return "test" |> "__missing";
117+
}
118+
}');
119+
}
120+
101121
#[Test]
102122
public function pipe_chain() {
103123
$r= $this->run('class %T {
@@ -109,7 +129,7 @@ public function run() {
109129
Assert::equals('TEST', $r);
110130
}
111131

112-
#[Test, Values([[['test'], 'TEST'], [[], null]])]
132+
#[Test, Values([[['test'], 'TEST'], [[''], ''], [[], null]])]
113133
public function nullsafe_pipe($input, $expected) {
114134
$r= $this->run('class %T {
115135
public function run($arg) {
@@ -120,7 +140,7 @@ public function run($arg) {
120140
Assert::equals($expected, $r);
121141
}
122142

123-
#[Test, Values([[null, null], ['test', 'TEST'], [' test ', 'TEST']])]
143+
#[Test, Values([[null, null], ['', ''], ['test', 'TEST'], [' test ', 'TEST']])]
124144
public function nullsafe_chain($input, $expected) {
125145
$r= $this->run('class %T {
126146
public function run($arg) {
@@ -132,7 +152,7 @@ public function run($arg) {
132152
}
133153

134154
#[Test]
135-
public function precedence() {
155+
public function concat_precedence() {
136156
$r= $this->run('class %T {
137157
public function run() {
138158
return "te"."st" |> strtoupper(...);
@@ -142,6 +162,54 @@ public function run() {
142162
Assert::equals('TEST', $r);
143163
}
144164

165+
#[Test]
166+
public function addition_precedence() {
167+
$r= $this->run('class %T {
168+
public function run() {
169+
return 5 + 2 |> fn($i) => $i * 2;
170+
}
171+
}');
172+
173+
Assert::equals(14, $r);
174+
}
175+
176+
#[Test]
177+
public function comparison_precedence() {
178+
$r= $this->run('class %T {
179+
public function run() {
180+
return 5 |> fn($i) => $i * 2 === 10;
181+
}
182+
}');
183+
184+
Assert::true($r);
185+
}
186+
187+
#[Test, Values([[0, 'even'], [1, 'odd'], [2, 'even']])]
188+
public function ternary_precedence($arg, $expected) {
189+
$r= $this->run('class %T {
190+
public function run($arg) {
191+
return $arg |> fn($i) => $i % 2 ? "odd" : "even";
192+
}
193+
}', $arg);
194+
195+
Assert::equals($expected, $r);
196+
}
197+
198+
#[Test, Values([[0, 'root'], [1001, 'test'], [1002, '#unknown']])]
199+
public function coalesce_precedence($arg, $expected) {
200+
$r= $this->run('class %T {
201+
private $users= [0 => "root", 1001 => "test"];
202+
203+
private function user($id) { return $this->users[$id] ?? null; }
204+
205+
public function run($arg) {
206+
return $arg |> $this->user(...) ?? "#unknown";
207+
}
208+
}', $arg);
209+
210+
Assert::equals($expected, $r);
211+
}
212+
145213
#[Test]
146214
public function rfc_example() {
147215
$r= $this->run('class %T {
@@ -156,4 +224,94 @@ public function run() {
156224
}');
157225
Assert::equals(['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D'], array_values($r));
158226
}
227+
228+
#[Test, Expect(Error::class), Runtime(php: '>=8.5.0')]
229+
public function rejects_by_reference_functions() {
230+
$this->run('class %T {
231+
private function modify(&$arg) { $arg++; }
232+
233+
public function run() {
234+
$val= 1;
235+
return $val |> $this->modify(...);
236+
}
237+
}');
238+
}
239+
240+
#[Test]
241+
public function accepts_prefer_by_reference_functions() {
242+
$r= $this->run('class %T {
243+
public function run() {
244+
return ["hello", "world"] |> array_multisort(...);
245+
}
246+
}');
247+
248+
Assert::true($r);
249+
}
250+
251+
#[Test]
252+
public function execution_order() {
253+
$r= $this->run('class %T {
254+
public function run() {
255+
$invoked= [];
256+
257+
$first= function() use(&$invoked) { $invoked[]= "first"; return 1; };
258+
$second= function() use(&$invoked) { $invoked[]= "second"; return false; };
259+
$skipped= function() use(&$invoked) { $invoked[]= "skipped"; return $in; };
260+
$third= function($in) use(&$invoked) { $invoked[]= "third"; return $in; };
261+
$capture= function($result) use(&$invoked) { $invoked[]= $result; };
262+
263+
$first() |> ($second() ? $skipped : $third) |> $capture;
264+
return $invoked;
265+
}
266+
}');
267+
268+
Assert::equals(['first', 'second', 'third', 1], $r);
269+
}
270+
271+
#[Test]
272+
public function interrupted_by_exception() {
273+
$r= $this->run('use lang\Error; class %T {
274+
public function run() {
275+
$invoked= [];
276+
277+
$provide= function() use(&$invoked) { $invoked[]= "provide"; return 1; };
278+
$transform= function($in) use(&$invoked) { $invoked[]= "transform"; return $in * 2; };
279+
$throw= function() use(&$invoked) { $invoked[]= "throw"; throw new Error("Break"); };
280+
281+
try {
282+
$provide() |> $transform |> $throw |> throw new Error("Unreachable");
283+
} catch (Error $e) {
284+
$invoked[]= $e->compoundMessage();
285+
}
286+
return $invoked;
287+
}
288+
}');
289+
290+
Assert::equals(['provide', 'transform', 'throw', 'Exception lang.Error (Break)'], $r);
291+
}
292+
293+
#[Test]
294+
public function generators() {
295+
$r= $this->run('class %T {
296+
private function range($lo, $hi) {
297+
for ($i= $lo; $i <= $hi; $i++) {
298+
yield $i;
299+
}
300+
}
301+
302+
private function map($fn) {
303+
return function($it) use($fn) {
304+
foreach ($it as $element) {
305+
yield $fn($element);
306+
}
307+
};
308+
}
309+
310+
public function run() {
311+
return $this->range(1, 3) |> $this->map(fn($e) => $e + 1) |> iterator_to_array(...);
312+
}
313+
}');
314+
315+
Assert::equals([2, 3, 4], $r);
316+
}
159317
}

0 commit comments

Comments
 (0)