Skip to content

Commit 2db203c

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 6bc7c26 commit 2db203c

25 files changed

Lines changed: 641 additions & 31 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Pipe assign operator basic usage with first-class callable
3+
--FILE--
4+
<?php
5+
6+
$x = "hello";
7+
$x |>= strtoupper(...);
8+
var_dump($x);
9+
10+
?>
11+
--EXPECT--
12+
string(5) "HELLO"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Pipe assign operator with closures and arrow functions
3+
--FILE--
4+
<?php
5+
6+
$x = 5;
7+
$x |>= (fn($v) => $v * 2);
8+
var_dump($x);
9+
10+
$y = "hello world";
11+
$y |>= (function($s) { return strtoupper($s); });
12+
var_dump($y);
13+
14+
$arr = [5, 2, 8, 1, 9];
15+
$arr |>= (fn($a) => array_filter($a, fn($n) => $n > 3));
16+
var_dump($arr);
17+
18+
?>
19+
--EXPECT--
20+
int(10)
21+
string(11) "HELLO WORLD"
22+
array(3) {
23+
[0]=>
24+
int(5)
25+
[2]=>
26+
int(8)
27+
[4]=>
28+
int(9)
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Pipe assign operator chain support (multiple |> on RHS)
3+
--FILE--
4+
<?php
5+
6+
$val = 10;
7+
$val |>= (fn($v) => $v * 2) |> (fn($v) => $v + 3) |> (fn($v) => $v * $v);
8+
var_dump($val);
9+
10+
$data = " the quick brown FOX ";
11+
$data |>= trim(...)
12+
|> strtolower(...)
13+
|> (fn($s) => explode(" ", $s))
14+
|> (fn($a) => array_map(ucfirst(...), $a))
15+
|> (fn($a) => implode("-", $a));
16+
var_dump($data);
17+
18+
?>
19+
--EXPECT--
20+
int(529)
21+
string(19) "The-Quick-Brown-Fox"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Pipe assign operator with all variable target types
3+
--FILE--
4+
<?php
5+
6+
$x = "hello";
7+
$x |>= strtoupper(...);
8+
var_dump($x);
9+
10+
$data = ["key" => "hello"];
11+
$data["key"] |>= strtoupper(...);
12+
var_dump($data["key"]);
13+
14+
$nested = ["a" => ["b" => "hello"]];
15+
$nested["a"]["b"] |>= strtoupper(...);
16+
var_dump($nested["a"]["b"]);
17+
18+
$obj = new stdClass;
19+
$obj->name = "hello";
20+
$obj->name |>= strtoupper(...);
21+
var_dump($obj->name);
22+
23+
class Foo {
24+
public static string $val = "hello";
25+
}
26+
Foo::$val |>= strtoupper(...);
27+
var_dump(Foo::$val);
28+
29+
?>
30+
--EXPECT--
31+
string(5) "HELLO"
32+
string(5) "HELLO"
33+
string(5) "HELLO"
34+
string(5) "HELLO"
35+
string(5) "HELLO"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Pipe assign operator single-evaluation semantics
3+
--FILE--
4+
<?php
5+
6+
$counter = 0;
7+
function track(): int {
8+
global $counter;
9+
$counter++;
10+
echo "track() called (call #", $counter, ")", PHP_EOL;
11+
return 0;
12+
}
13+
14+
$arr = ["hello"];
15+
$arr[track()] |>= strtoupper(...);
16+
17+
echo "track() was called ", $counter, " time(s)", PHP_EOL;
18+
var_dump($arr);
19+
20+
?>
21+
--EXPECT--
22+
track() called (call #1)
23+
track() was called 1 time(s)
24+
array(1) {
25+
[0]=>
26+
string(5) "HELLO"
27+
}
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
} catch (\Error $e) {
15+
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
16+
}
17+
18+
var_dump($a);
19+
20+
?>
21+
--EXPECTF--
22+
Error: modify_ref(): Argument #1 ($a) could not be passed by reference
23+
int(5)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Pipe assign operator expression result and precedence
3+
--FILE--
4+
<?php
5+
6+
function triple(int $x): int { return $x * 3; }
7+
function add_five(int $x): int { return $x + 5; }
8+
function to_string(int $x): string { return "value:$x"; }
9+
10+
$x = 5;
11+
$result = ($x |>= triple(...));
12+
var_dump($x, $result);
13+
14+
$e = 4;
15+
$f = ($e |>= triple(...)) |> add_five(...) |> to_string(...);
16+
var_dump($e, $f);
17+
18+
// Right-associativity
19+
// Parses as: $foo |>= ($bar |>= x(...))
20+
// 1. $bar |>= x(...) calls x(0), which prints int(0) and returns a Closure
21+
// 2. $bar is assigned the Closure
22+
// 3. $foo |>= <Closure> calls Closure(1) = 2
23+
// 4. $foo is assigned 2
24+
function x(int $bar): Closure {
25+
var_dump($bar);
26+
return function (int $foo): int { return $foo + 1; };
27+
}
28+
$bar = 0;
29+
$foo = 1;
30+
$foo |>= $bar |>= x(...);
31+
var_dump($bar instanceof Closure, $foo);
32+
33+
?>
34+
--EXPECT--
35+
int(15)
36+
int(15)
37+
int(12)
38+
string(8) "value:17"
39+
int(0)
40+
bool(true)
41+
int(2)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Pipe assign operator with mixed callable types
3+
--FILE--
4+
<?php
5+
6+
function double(int $x): int { return $x * 2; }
7+
8+
class Math {
9+
public function triple(int $x): int { return $x * 3; }
10+
public static function quadruple(int $x): int { return $x * 4; }
11+
}
12+
13+
$math = new Math();
14+
15+
$v1 = 5;
16+
$v1 |>= double(...);
17+
var_dump($v1);
18+
19+
$v2 = 5;
20+
$v2 |>= $math->triple(...);
21+
var_dump($v2);
22+
23+
$v3 = 5;
24+
$v3 |>= Math::quadruple(...);
25+
var_dump($v3);
26+
27+
class Multiplier {
28+
public function __construct(private int $factor) {}
29+
public function __invoke(int $x): int { return $x * $this->factor; }
30+
}
31+
32+
$v4 = 5;
33+
$v4 |>= new Multiplier(10);
34+
var_dump($v4);
35+
36+
$times6 = fn($x) => $x * 6;
37+
$v5 = 5;
38+
$v5 |>= $times6;
39+
var_dump($v5);
40+
41+
?>
42+
--EXPECT--
43+
int(10)
44+
int(15)
45+
int(20)
46+
int(50)
47+
int(30)

0 commit comments

Comments
 (0)