Skip to content

Commit 46028af

Browse files
committed
feat: add pipe assignment operator (|>=)
Adds the `|>=` compound assignment operator for the pipe operator, allowing `$x |>= callable` to be used as shorthand for `$x = $x |> callable`. Supports pipe chains on the RHS: `$x |>= fn1(...) |> fn2(...) |> fn3(...)` is equivalent to `$x = $x |> fn1(...) |> fn2(...) |> fn3(...)`. Implementation modeled after `??=` (ZEND_AST_ASSIGN_COALESCE), with a dedicated AST node (ZEND_AST_ASSIGN_PIPE) and compiler function that uses memoization to avoid double-evaluating complex LHS expressions. Extracts shared callable dispatch logic into zend_pipe_build_fcall() helper used by both `|>` and `|>=` compilation. All variable target types supported: simple vars, array dims, object properties, static properties.
1 parent 9390b68 commit 46028af

25 files changed

Lines changed: 662 additions & 31 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Pipe assign operator basic usage with first-class callables
3+
--FILE--
4+
<?php
5+
6+
// Simple built-in function
7+
$arr = [3, 1, 4, 1, 5];
8+
$arr |>= array_unique(...);
9+
var_dump($arr);
10+
11+
// String function
12+
$str = " hello ";
13+
$str |>= trim(...);
14+
var_dump($str);
15+
16+
// array_values
17+
$data = ["a" => 1, "b" => 2, "c" => 3];
18+
$data |>= array_values(...);
19+
var_dump($data);
20+
21+
// strtoupper
22+
$s = "hello";
23+
$s |>= strtoupper(...);
24+
var_dump($s);
25+
26+
?>
27+
--EXPECT--
28+
array(4) {
29+
[0]=>
30+
int(3)
31+
[1]=>
32+
int(1)
33+
[2]=>
34+
int(4)
35+
[4]=>
36+
int(5)
37+
}
38+
string(5) "hello"
39+
array(3) {
40+
[0]=>
41+
int(1)
42+
[1]=>
43+
int(2)
44+
[2]=>
45+
int(3)
46+
}
47+
string(5) "HELLO"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Pipe assign operator with closures and arrow functions
3+
--FILE--
4+
<?php
5+
6+
// Parenthesized arrow function
7+
$x = 5;
8+
$x |>= (fn($v) => $v * 2);
9+
var_dump($x);
10+
11+
// Closure
12+
$y = "hello world";
13+
$y |>= (function($s) { return strtoupper($s); });
14+
var_dump($y);
15+
16+
// Arrow function with complex body
17+
$arr = [5, 2, 8, 1, 9];
18+
$arr |>= (fn($a) => array_filter($a, fn($n) => $n > 3));
19+
var_dump($arr);
20+
21+
?>
22+
--EXPECT--
23+
int(10)
24+
string(11) "HELLO WORLD"
25+
array(3) {
26+
[0]=>
27+
int(5)
28+
[2]=>
29+
int(8)
30+
[4]=>
31+
int(9)
32+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Pipe assign operator chain support (multiple |> on RHS)
3+
--FILE--
4+
<?php
5+
6+
// Two-step chain
7+
$x = [3, 1, 4, 1, 5];
8+
$x |>= array_unique(...) |> array_values(...);
9+
var_dump($x);
10+
11+
// Three-step chain
12+
$s = " Hello World ";
13+
$s |>= trim(...) |> strtolower(...) |> str_split(...);
14+
var_dump($s);
15+
16+
// Chain with closures
17+
$val = 10;
18+
$val |>= (fn($v) => $v * 2) |> (fn($v) => $v + 3) |> (fn($v) => $v * $v);
19+
var_dump($val);
20+
21+
?>
22+
--EXPECT--
23+
array(4) {
24+
[0]=>
25+
int(3)
26+
[1]=>
27+
int(1)
28+
[2]=>
29+
int(4)
30+
[3]=>
31+
int(5)
32+
}
33+
array(11) {
34+
[0]=>
35+
string(1) "h"
36+
[1]=>
37+
string(1) "e"
38+
[2]=>
39+
string(1) "l"
40+
[3]=>
41+
string(1) "l"
42+
[4]=>
43+
string(1) "o"
44+
[5]=>
45+
string(1) " "
46+
[6]=>
47+
string(1) "w"
48+
[7]=>
49+
string(1) "o"
50+
[8]=>
51+
string(1) "r"
52+
[9]=>
53+
string(1) "l"
54+
[10]=>
55+
string(1) "d"
56+
}
57+
int(529)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Pipe assign operator with five-step chain
3+
--FILE--
4+
<?php
5+
6+
$data = " the quick brown FOX ";
7+
$data |>= trim(...)
8+
|> strtolower(...)
9+
|> (fn($s) => explode(" ", $s))
10+
|> (fn($a) => array_map(ucfirst(...), $a))
11+
|> (fn($a) => implode("-", $a));
12+
13+
var_dump($data);
14+
15+
?>
16+
--EXPECT--
17+
string(19) "The-Quick-Brown-Fox"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Pipe assign operator with all variable target types
3+
--FILE--
4+
<?php
5+
6+
// Simple variable
7+
$x = "hello";
8+
$x |>= strtoupper(...);
9+
echo "Simple var: ", $x, PHP_EOL;
10+
11+
// Array dimension
12+
$data = ["key" => " hello "];
13+
$data["key"] |>= trim(...);
14+
echo "Array dim: ", $data['key'], PHP_EOL;
15+
16+
// Nested array dimension
17+
$nested = ["a" => ["b" => "hello"]];
18+
$nested["a"]["b"] |>= strtoupper(...);
19+
echo "Nested dim: ", $nested['a']['b'], PHP_EOL;
20+
21+
// Object property
22+
$obj = new stdClass;
23+
$obj->name = " hello ";
24+
$obj->name |>= trim(...);
25+
echo "Prop: ", $obj->name, PHP_EOL;
26+
27+
// Static property
28+
class Foo {
29+
public static string $val = "hello";
30+
}
31+
Foo::$val |>= strtoupper(...);
32+
echo "Static prop: ", Foo::$val, PHP_EOL;
33+
34+
?>
35+
--EXPECT--
36+
Simple var: HELLO
37+
Array dim: hello
38+
Nested dim: HELLO
39+
Prop: hello
40+
Static prop: HELLO
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Pipe assign operator single-evaluation semantics
3+
--FILE--
4+
<?php
5+
6+
// Verify that sub-expressions in the LHS are evaluated only once.
7+
8+
$counter = 0;
9+
function track(): int {
10+
global $counter;
11+
$counter++;
12+
echo "track() called (call #", $counter, ")", PHP_EOL;
13+
return 0;
14+
}
15+
16+
$arr = [["hello"]];
17+
$arr[track()] |>= (fn($a) => array_map(strtoupper(...), $a));
18+
19+
echo "track() was called ", $counter, " time(s)", PHP_EOL;
20+
var_dump($arr);
21+
22+
?>
23+
--EXPECT--
24+
track() called (call #1)
25+
track() was called 1 time(s)
26+
array(1) {
27+
[0]=>
28+
array(1) {
29+
[0]=>
30+
string(5) "HELLO"
31+
}
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
Pipe assign operator error: unparenthesized arrow function
3+
--FILE--
4+
<?php
5+
6+
$x = 5;
7+
$x |>= fn($v) => $v * 2;
8+
9+
?>
10+
--EXPECTF--
11+
Fatal error: Arrow functions on the right hand side of |>= must be parenthesized in %s on line %d
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Pipe assign operator error: cannot reassign $this
3+
--FILE--
4+
<?php
5+
6+
class Bar {
7+
public function test() {
8+
$this |>= trim(...);
9+
}
10+
}
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Cannot re-assign $this in %s on line %d
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe assign operator error: by-reference rejection
3+
--FILE--
4+
<?php
5+
6+
function modify_ref(int &$a): int {
7+
$a += 1;
8+
return $a;
9+
}
10+
11+
try {
12+
$a = 5;
13+
$a |>= modify_ref(...);
14+
var_dump($a);
15+
} catch (\Error $e) {
16+
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
17+
}
18+
19+
?>
20+
--EXPECTF--
21+
Error: modify_ref(): Argument #1 ($a) could not be passed by reference
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Pipe assign operator expression result value
3+
--FILE--
4+
<?php
5+
6+
// The expression result of |>= should be the assigned value
7+
$x = 5;
8+
$result = ($x |>= (fn($v) => $v * 3));
9+
echo "x=", $x, ", result=", $result, PHP_EOL;
10+
11+
// Nested assignment
12+
$a = 10;
13+
$b = ($a |>= (fn($v) => $v + 5));
14+
echo "a=", $a, ", b=", $b, PHP_EOL;
15+
16+
// Chain expression result
17+
$c = "hello";
18+
$d = ($c |>= strtoupper(...) |> str_split(...));
19+
echo "c="; var_dump($c);
20+
echo "d="; var_dump($d);
21+
22+
?>
23+
--EXPECT--
24+
x=15, result=15
25+
a=15, b=15
26+
c=array(5) {
27+
[0]=>
28+
string(1) "H"
29+
[1]=>
30+
string(1) "E"
31+
[2]=>
32+
string(1) "L"
33+
[3]=>
34+
string(1) "L"
35+
[4]=>
36+
string(1) "O"
37+
}
38+
d=array(5) {
39+
[0]=>
40+
string(1) "H"
41+
[1]=>
42+
string(1) "E"
43+
[2]=>
44+
string(1) "L"
45+
[3]=>
46+
string(1) "L"
47+
[4]=>
48+
string(1) "O"
49+
}

0 commit comments

Comments
 (0)