From 06ea36d97d8eee370554476b9c97476dd9213728 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Tue, 7 Jul 2026 15:55:47 -0500 Subject: [PATCH] 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. --- Zend/tests/pipe_operator/assign_pipe_001.phpt | 12 ++ Zend/tests/pipe_operator/assign_pipe_002.phpt | 29 +++ Zend/tests/pipe_operator/assign_pipe_003.phpt | 21 +++ Zend/tests/pipe_operator/assign_pipe_004.phpt | 35 ++++ Zend/tests/pipe_operator/assign_pipe_005.phpt | 27 +++ Zend/tests/pipe_operator/assign_pipe_006.phpt | 11 ++ Zend/tests/pipe_operator/assign_pipe_007.phpt | 14 ++ Zend/tests/pipe_operator/assign_pipe_008.phpt | 23 +++ Zend/tests/pipe_operator/assign_pipe_009.phpt | 54 ++++++ Zend/tests/pipe_operator/assign_pipe_010.phpt | 47 +++++ Zend/tests/pipe_operator/assign_pipe_011.phpt | 18 ++ Zend/tests/pipe_operator/assign_pipe_012.phpt | 22 +++ Zend/tests/pipe_operator/assign_pipe_013.phpt | 22 +++ Zend/tests/pipe_operator/assign_pipe_014.phpt | 21 +++ Zend/tests/pipe_operator/assign_pipe_015.phpt | 12 ++ Zend/tests/pipe_operator/assign_pipe_016.phpt | 44 +++++ Zend/tests/pipe_operator/assign_pipe_017.phpt | 28 +++ Zend/tests/pipe_operator/assign_pipe_018.phpt | 59 ++++++ Zend/tests/pipe_operator/assign_pipe_019.phpt | 49 +++++ Zend/tests/pipe_operator/assign_pipe_ast.phpt | 80 ++++++++ Zend/zend_ast.c | 1 + Zend/zend_ast.h | 1 + Zend/zend_compile.c | 178 ++++++++++++++---- Zend/zend_language_parser.y | 5 +- Zend/zend_language_scanner.l | 4 + ext/tokenizer/tokenizer_data.c | 1 + ext/tokenizer/tokenizer_data.stub.php | 5 + ext/tokenizer/tokenizer_data_arginfo.h | 3 +- 28 files changed, 789 insertions(+), 37 deletions(-) create mode 100644 Zend/tests/pipe_operator/assign_pipe_001.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_002.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_003.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_004.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_005.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_006.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_007.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_008.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_009.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_010.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_011.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_012.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_013.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_014.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_015.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_016.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_017.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_018.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_019.phpt create mode 100644 Zend/tests/pipe_operator/assign_pipe_ast.phpt diff --git a/Zend/tests/pipe_operator/assign_pipe_001.phpt b/Zend/tests/pipe_operator/assign_pipe_001.phpt new file mode 100644 index 000000000000..9fdfc24e2f98 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_001.phpt @@ -0,0 +1,12 @@ +--TEST-- +Pipe assign operator basic usage with first-class callable +--FILE-- += strtoupper(...); +var_dump($x); + +?> +--EXPECT-- +string(5) "HELLO" diff --git a/Zend/tests/pipe_operator/assign_pipe_002.phpt b/Zend/tests/pipe_operator/assign_pipe_002.phpt new file mode 100644 index 000000000000..98ee51333a14 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_002.phpt @@ -0,0 +1,29 @@ +--TEST-- +Pipe assign operator with closures and arrow functions +--FILE-- += (fn($v) => $v * 2); +var_dump($x); + +$y = "hello world"; +$y |>= (function($s) { return strtoupper($s); }); +var_dump($y); + +$arr = [5, 2, 8, 1, 9]; +$arr |>= (fn($a) => array_filter($a, fn($n) => $n > 3)); +var_dump($arr); + +?> +--EXPECT-- +int(10) +string(11) "HELLO WORLD" +array(3) { + [0]=> + int(5) + [2]=> + int(8) + [4]=> + int(9) +} diff --git a/Zend/tests/pipe_operator/assign_pipe_003.phpt b/Zend/tests/pipe_operator/assign_pipe_003.phpt new file mode 100644 index 000000000000..83285480dafb --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_003.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe assign operator chain support (multiple |> on RHS) +--FILE-- += (fn($v) => $v * 2) |> (fn($v) => $v + 3) |> (fn($v) => $v * $v); +var_dump($val); + +$data = " the quick brown FOX "; +$data |>= trim(...) + |> strtolower(...) + |> (fn($s) => explode(" ", $s)) + |> (fn($a) => array_map(ucfirst(...), $a)) + |> (fn($a) => implode("-", $a)); +var_dump($data); + +?> +--EXPECT-- +int(529) +string(19) "The-Quick-Brown-Fox" diff --git a/Zend/tests/pipe_operator/assign_pipe_004.phpt b/Zend/tests/pipe_operator/assign_pipe_004.phpt new file mode 100644 index 000000000000..917d769553ba --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_004.phpt @@ -0,0 +1,35 @@ +--TEST-- +Pipe assign operator with all variable target types +--FILE-- += strtoupper(...); +var_dump($x); + +$data = ["key" => "hello"]; +$data["key"] |>= strtoupper(...); +var_dump($data["key"]); + +$nested = ["a" => ["b" => "hello"]]; +$nested["a"]["b"] |>= strtoupper(...); +var_dump($nested["a"]["b"]); + +$obj = new stdClass; +$obj->name = "hello"; +$obj->name |>= strtoupper(...); +var_dump($obj->name); + +class Foo { + public static string $val = "hello"; +} +Foo::$val |>= strtoupper(...); +var_dump(Foo::$val); + +?> +--EXPECT-- +string(5) "HELLO" +string(5) "HELLO" +string(5) "HELLO" +string(5) "HELLO" +string(5) "HELLO" diff --git a/Zend/tests/pipe_operator/assign_pipe_005.phpt b/Zend/tests/pipe_operator/assign_pipe_005.phpt new file mode 100644 index 000000000000..2041663d6c56 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_005.phpt @@ -0,0 +1,27 @@ +--TEST-- +Pipe assign operator single-evaluation semantics +--FILE-- += strtoupper(...); + +echo "track() was called ", $counter, " time(s)", PHP_EOL; +var_dump($arr); + +?> +--EXPECT-- +track() called (call #1) +track() was called 1 time(s) +array(1) { + [0]=> + string(5) "HELLO" +} diff --git a/Zend/tests/pipe_operator/assign_pipe_006.phpt b/Zend/tests/pipe_operator/assign_pipe_006.phpt new file mode 100644 index 000000000000..969811d6e245 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_006.phpt @@ -0,0 +1,11 @@ +--TEST-- +Pipe assign operator error: unparenthesized arrow function +--FILE-- += fn($v) => $v * 2; + +?> +--EXPECTF-- +Fatal error: Arrow functions on the right hand side of |>= must be parenthesized in %s on line %d diff --git a/Zend/tests/pipe_operator/assign_pipe_007.phpt b/Zend/tests/pipe_operator/assign_pipe_007.phpt new file mode 100644 index 000000000000..e00bc28b62b2 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_007.phpt @@ -0,0 +1,14 @@ +--TEST-- +Pipe assign operator error: cannot reassign $this +--FILE-- += trim(...); + } +} + +?> +--EXPECTF-- +Fatal error: Cannot re-assign $this in %s on line %d diff --git a/Zend/tests/pipe_operator/assign_pipe_008.phpt b/Zend/tests/pipe_operator/assign_pipe_008.phpt new file mode 100644 index 000000000000..7f8864572274 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_008.phpt @@ -0,0 +1,23 @@ +--TEST-- +Pipe assign operator error: by-reference rejection +--FILE-- += modify_ref(...); +} catch (\Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +var_dump($a); + +?> +--EXPECTF-- +Error: modify_ref(): Argument #1 ($a) could not be passed by reference +int(5) diff --git a/Zend/tests/pipe_operator/assign_pipe_009.phpt b/Zend/tests/pipe_operator/assign_pipe_009.phpt new file mode 100644 index 000000000000..f01123bf9b5f --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_009.phpt @@ -0,0 +1,54 @@ +--TEST-- +Pipe assign operator expression result and precedence +--FILE-- += triple(...)); +var_dump($x, $result); + +$e = 4; +$f = ($e |>= triple(...)) |> add_five(...) |> to_string(...); +var_dump($e, $f); + +// Right-associativity +// Parses as: $foo |>= ($bar |>= x(...)) +// 1. $bar |>= x(...) calls x(0), which prints int(0) and returns a Closure +// 2. $bar is assigned the Closure +// 3. $foo |>= calls Closure(1) = 2 +// 4. $foo is assigned 2 +function x($bar) { + var_dump($bar); + return function ($foo) { return $foo + 1; }; +} +$bar = "bar"; +$foo = 1; +$foo |>= $bar |>= x(...); +var_dump($bar instanceof Closure, $foo); + +$a = 10; +$b = 5; +$a += $b |>= triple(...); +var_dump($a, $b); + +$c = 4; +$d = ($c |>= (fn($v) => $v * 2)) |> triple(...); +var_dump($c, $d); + +?> +--EXPECT-- +int(15) +int(15) +int(12) +string(8) "value:17" +string(3) "bar" +bool(true) +int(2) +int(25) +int(15) +int(8) +int(24) diff --git a/Zend/tests/pipe_operator/assign_pipe_010.phpt b/Zend/tests/pipe_operator/assign_pipe_010.phpt new file mode 100644 index 000000000000..9e8cc5909af1 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_010.phpt @@ -0,0 +1,47 @@ +--TEST-- +Pipe assign operator with mixed callable types +--FILE-- += double(...); +var_dump($v1); + +$v2 = 5; +$v2 |>= $math->triple(...); +var_dump($v2); + +$v3 = 5; +$v3 |>= Math::quadruple(...); +var_dump($v3); + +class Multiplier { + public function __construct(private int $factor) {} + public function __invoke(int $x): int { return $x * $this->factor; } +} + +$v4 = 5; +$v4 |>= new Multiplier(5); +var_dump($v4); + +$times6 = fn($x) => $x * 6; +$v5 = 5; +$v5 |>= $times6; +var_dump($v5); + +?> +--EXPECT-- +int(10) +int(15) +int(20) +int(25) +int(30) diff --git a/Zend/tests/pipe_operator/assign_pipe_011.phpt b/Zend/tests/pipe_operator/assign_pipe_011.phpt new file mode 100644 index 000000000000..f502c313c097 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_011.phpt @@ -0,0 +1,18 @@ +--TEST-- +Pipe assign operator with mixed callable types in chains +--FILE-- += add_prefix(...) |> Transform::upper(...) |> (fn($s) => $s . "_done"); +var_dump($s); + +?> +--EXPECT-- +string(17) "PREFIX_HELLO_done" diff --git a/Zend/tests/pipe_operator/assign_pipe_012.phpt b/Zend/tests/pipe_operator/assign_pipe_012.phpt new file mode 100644 index 000000000000..ba516888e106 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_012.phpt @@ -0,0 +1,22 @@ +--TEST-- +Pipe assign operator sequential usage equivalent to chain +--FILE-- += trim(...); +$s |>= strtolower(...); +$s |>= (fn($s) => str_replace(" ", "-", $s)); +var_dump($s); + +$s2 = " Hello World "; +$s2 |>= trim(...) |> strtolower(...) |> (fn($s) => str_replace(" ", "-", $s)); +var_dump($s2); + +var_dump($s === $s2); + +?> +--EXPECT-- +string(11) "hello-world" +string(11) "hello-world" +bool(true) diff --git a/Zend/tests/pipe_operator/assign_pipe_013.phpt b/Zend/tests/pipe_operator/assign_pipe_013.phpt new file mode 100644 index 000000000000..18a1b8f3084b --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_013.phpt @@ -0,0 +1,22 @@ +--TEST-- +Pipe assign operator exception interruption +--FILE-- += (fn($v) => $v * 2) |> will_throw(...); +} catch (RuntimeException $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +var_dump($x); + +?> +--EXPECT-- +RuntimeException: interrupted at 84 +int(42) diff --git a/Zend/tests/pipe_operator/assign_pipe_014.phpt b/Zend/tests/pipe_operator/assign_pipe_014.phpt new file mode 100644 index 000000000000..de9beb97b7f9 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_014.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe assign operator with userland function chains +--FILE-- += double(...); +var_dump($a); + +$b = 3; +$b |>= double(...) |> increment(...) |> square(...); +var_dump($b); + +?> +--EXPECT-- +int(10) +int(49) diff --git a/Zend/tests/pipe_operator/assign_pipe_015.phpt b/Zend/tests/pipe_operator/assign_pipe_015.phpt new file mode 100644 index 000000000000..a16ed529360b --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_015.phpt @@ -0,0 +1,12 @@ +--TEST-- +Pipe assign operator error: cannot use function call as target +--FILE-- += strtoupper(...); + +?> +--EXPECTF-- +Fatal error: Can't use function return value in write context in %s on line %d diff --git a/Zend/tests/pipe_operator/assign_pipe_016.phpt b/Zend/tests/pipe_operator/assign_pipe_016.phpt new file mode 100644 index 000000000000..ec845529fb99 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_016.phpt @@ -0,0 +1,44 @@ +--TEST-- +Pipe assign operator nested and compound expressions +--FILE-- += triple(...)) |> add_five(...) |> to_string(...); +echo "e=", $e, ", f=", $f, PHP_EOL; + +$g = 3; +$h = 10; +$h |>= (fn($v) => $v + ($g |>= double(...))); +echo "g=", $g, ", h=", $h, PHP_EOL; + +$a = 2; +$b = 3; +$c = ($a |>= double(...)) + ($b |>= triple(...)); +echo "a=", $a, ", b=", $b, ", c=", $c, PHP_EOL; + +$x = 5; +$y = 2; +$y |>= (fn($v) => $v + ($x |>= triple(...) |> add_five(...))); +echo "x=", $x, ", y=", $y, PHP_EOL; + +$p = 2; +$q = 3; +$r = 4; +$r |>= (fn($v) => $v * ($q |>= (fn($w) => $w + ($p |>= double(...))))); +echo "p=", $p, ", q=", $q, ", r=", $r, PHP_EOL; + +?> +--EXPECT-- +e=12, f=result=17 +g=3, h=16 +a=4, b=9, c=13 +x=5, y=22 +p=2, q=3, r=28 diff --git a/Zend/tests/pipe_operator/assign_pipe_017.phpt b/Zend/tests/pipe_operator/assign_pipe_017.phpt new file mode 100644 index 000000000000..94ab318ce6db --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_017.phpt @@ -0,0 +1,28 @@ +--TEST-- +Pipe assign operator typed property handling +--FILE-- +foo |>= strlen(...); +var_dump($f->foo); + +class Bar { + public array $bar = ["bar"]; +} + +$b = new Bar(); +try { + $b->bar |>= count(...); +} catch (TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +string(1) "3" +Cannot assign int to property Bar::$bar of type array diff --git a/Zend/tests/pipe_operator/assign_pipe_018.phpt b/Zend/tests/pipe_operator/assign_pipe_018.phpt new file mode 100644 index 000000000000..0430c71d6681 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_018.phpt @@ -0,0 +1,59 @@ +--TEST-- +Pipe assign operator with property hooks +--FILE-- +name = $value; + echo "set hook called\n"; + } + get { + echo "get hook called\n"; + return $this->name; + } + } + + public string $upper { + get => strtoupper($this->name); + } +} + +$obj = new WithHooks(); +$obj->name |>= strtoupper(...); +echo $obj->name, "\n"; + +$obj->name = 'world'; +$obj->name |>= strtoupper(...) |> trim(...); +echo $obj->name, "\n"; + +echo $obj->upper, "\n"; + +class WithReadonly { + public function __construct( + public readonly string $value = 'hello', + ) {} +} + +$ro = new WithReadonly(); +try { + $ro->value |>= strtoupper(...); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +get hook called +set hook called +get hook called +HELLO +set hook called +get hook called +set hook called +get hook called +WORLD +get hook called +WORLD +Cannot modify readonly property WithReadonly::$value diff --git a/Zend/tests/pipe_operator/assign_pipe_019.phpt b/Zend/tests/pipe_operator/assign_pipe_019.phpt new file mode 100644 index 000000000000..fc567bea929b --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_019.phpt @@ -0,0 +1,49 @@ +--TEST-- +Pipe assign operator with partial function application +--FILE-- += str_replace('World', 'PHP', ?); +var_dump($x); + +$x = " trimmed "; +$x |>= trim(?); +var_dump($x); + +$x = [3, 1, 4, 1, 5]; +$x |>= array_slice(?, 1, 3); +var_dump($x); + +$x = "hello"; +$x |>= str_pad(?, 10, '.'); +var_dump($x); + +$x = "hello world"; +$x |>= str_replace(' ', '-', ?) |> strtoupper(...); +var_dump($x); + +$data = ["key" => "hello world"]; +$data["key"] |>= str_replace(' ', '_', ?) |> strtoupper(...); +var_dump($data["key"]); + +$x = "abc"; +$x |>= substr(?, 1); +var_dump($x); + +?> +--EXPECT-- +string(9) "Hello PHP" +string(7) "trimmed" +array(3) { + [0]=> + int(1) + [1]=> + int(4) + [2]=> + int(1) +} +string(10) "hello....." +string(11) "HELLO-WORLD" +string(11) "HELLO_WORLD" +string(2) "bc" diff --git a/Zend/tests/pipe_operator/assign_pipe_ast.phpt b/Zend/tests/pipe_operator/assign_pipe_ast.phpt new file mode 100644 index 000000000000..8e6d559c043a --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_ast.phpt @@ -0,0 +1,80 @@ +--TEST-- +Pipe assign operator AST printing +--INI-- +zend.assertions=1 +assert.exception=1 +--FILE-- += foo(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $x |>= foo(...) |> bar(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $x |>= (fn($v) => $v * 2)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $arr[0] |>= foo(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $obj->prop |>= foo(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $x |>= ($callback ?? fallback(...))); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $x |>= ($cond ? foo(...) : bar(...))); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $x |>= $y |>= foo(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $z = $x |>= foo(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && ($x |>= foo(...)) |> bar(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +assert(false && ($x |>= foo(...))) +assert(false && ($x |>= foo(...) |> bar(...))) +assert(false && ($x |>= (fn($v) => $v * 2))) +assert(false && ($arr[0] |>= foo(...))) +assert(false && ($obj->prop |>= foo(...))) +assert(false && ($x |>= $callback ?? fallback(...))) +assert(false && ($x |>= $cond ? foo(...) : bar(...))) +assert(false && ($x |>= $y |>= foo(...))) +assert(false && ($z = $x |>= foo(...))) +assert(false && ($x |>= foo(...)) |> bar(...)) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index adcb62a51d1f..ba4261b910bf 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2604,6 +2604,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio } break; case ZEND_AST_ASSIGN_COALESCE: BINARY_OP(" \?\?= ", 90, 91, 90); + case ZEND_AST_ASSIGN_PIPE: BINARY_OP(" |>= ", 90, 91, 90); case ZEND_AST_BINARY_OP: switch (ast->attr) { case ZEND_ADD: BINARY_OP(" + ", 200, 200, 201); diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 26f54102a143..4913bf886ccc 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -153,6 +153,7 @@ enum _zend_ast_kind { ZEND_AST_MATCH_ARM, ZEND_AST_NAMED_ARG, ZEND_AST_PIPE, + ZEND_AST_ASSIGN_PIPE, /* 3 child nodes */ ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index bb24d73fb240..cba1f6165471 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6949,6 +6949,35 @@ static zend_ast *zend_partial_apply(zend_ast *callable_ast, zend_ast *pipe_arg) return new_arg_list; } +static zend_ast *zend_pipe_build_fcall(zend_ast *callable_ast, zend_ast *pipe_arg) +{ + zend_ast *pfa_arg_list_ast = NULL; + + if ((pfa_arg_list_ast = zend_partial_apply(callable_ast, pipe_arg))) { + switch (callable_ast->kind) { + case ZEND_AST_CALL: + return zend_ast_create(ZEND_AST_CALL, + callable_ast->child[0], pfa_arg_list_ast); + case ZEND_AST_STATIC_CALL: + return zend_ast_create(ZEND_AST_STATIC_CALL, + callable_ast->child[0], callable_ast->child[1], + pfa_arg_list_ast); + case ZEND_AST_METHOD_CALL: + return zend_ast_create(ZEND_AST_METHOD_CALL, + callable_ast->child[0], callable_ast->child[1], + pfa_arg_list_ast); + default: + ZEND_UNREACHABLE(); + } + } + + zend_ast *arg_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, pipe_arg); + znode callable_result; + zend_compile_expr(&callable_result, callable_ast); + return zend_ast_create(ZEND_AST_CALL, + zend_ast_create_znode(&callable_result), arg_list_ast); +} + static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type) { zend_ast *operand_ast = ast->child[0]; @@ -6972,41 +7001,8 @@ static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type) wrapped_operand_result = operand_result; } - /* Turn the operand into a function parameter list. */ - zend_ast *arg = zend_ast_create_znode(&wrapped_operand_result); - - zend_ast *fcall_ast; - znode callable_result; - zend_ast *pfa_arg_list_ast = NULL; - - /* Turn $foo |> PFA into plain function call if possible */ - if ((pfa_arg_list_ast = zend_partial_apply(callable_ast, arg))) { - switch (callable_ast->kind) { - case ZEND_AST_CALL: - fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast->child[0], pfa_arg_list_ast); - break; - case ZEND_AST_STATIC_CALL: - fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL, - callable_ast->child[0], callable_ast->child[1], - pfa_arg_list_ast); - break; - case ZEND_AST_METHOD_CALL: - fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL, - callable_ast->child[0], callable_ast->child[1], - pfa_arg_list_ast); - break; - default: - ZEND_UNREACHABLE(); - } - /* Turn $foo |> $expr into ($expr)($foo) */ - } else { - zend_ast *arg_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, arg); - zend_compile_expr(&callable_result, callable_ast); - callable_ast = zend_ast_create_znode(&callable_result); - fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast, arg_list_ast); - } + zend_ast *pipe_arg = zend_ast_create_znode(&wrapped_operand_result); + zend_ast *fcall_ast = zend_pipe_build_fcall(callable_ast, pipe_arg); zend_do_extended_stmt(&operand_result); @@ -11205,6 +11201,115 @@ static void zend_compile_assign_coalesce(znode *result, zend_ast *ast) /* {{{ */ } /* }}} */ +/* Recursively compile a pipe chain for |>=, processing steps left-to-right. + * Walks the left spine of the PIPE tree via recursion, which naturally + * gives left-to-right ordering without needing to collect into an array. */ +static void zend_compile_assign_pipe_chain(znode *current, zend_ast *callable_ast) +{ + zend_ast *step; + + if (callable_ast->kind == ZEND_AST_PIPE) { + /* Recurse into the left child first (processes left-to-right). */ + zend_compile_assign_pipe_chain(current, callable_ast->child[0]); + + /* Wrap intermediate result to prevent references. */ + if (current->op_type & (IS_CV|IS_VAR)) { + znode wrapped; + zend_emit_op_tmp(&wrapped, ZEND_QM_ASSIGN, current, NULL); + *current = wrapped; + } + step = callable_ast->child[1]; + } else { + step = callable_ast; + } + + if (step->kind == ZEND_AST_ARROW_FUNC + && !(step->attr & ZEND_PARENTHESIZED_ARROW_FUNC)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Arrow functions on the right hand side of |>= must be parenthesized"); + } + + zend_ast *pipe_arg = zend_ast_create_znode(current); + zend_ast *fcall = zend_pipe_build_fcall(step, pipe_arg); + + zend_do_extended_stmt(current); + zend_compile_var(current, fcall, BP_VAR_R, false); +} + +static void zend_compile_assign_pipe(znode *result, zend_ast *ast) /* {{{ */ +{ + zend_ast *var_ast = ast->child[0]; + zend_ast *callable_ast = ast->child[1]; + + znode var_node_r, var_node_w, pipe_result; + zend_op *opline; + + /* Use memoization to avoid double-evaluating sub-expressions in the LHS variable. */ + HashTable *orig_memoized_exprs = CG(memoized_exprs); + const zend_memoize_mode orig_memoize_mode = CG(memoize_mode); + + zend_ensure_writable_variable(var_ast); + if (is_this_fetch(var_ast)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this"); + } + + ALLOC_HASHTABLE(CG(memoized_exprs)); + zend_hash_init(CG(memoized_exprs), 0, NULL, znode_dtor, 0); + + CG(memoize_mode) = ZEND_MEMOIZE_COMPILE; + zend_compile_var(&var_node_r, var_ast, BP_VAR_R, false); + CG(memoize_mode) = ZEND_MEMOIZE_NONE; + + /* Wrap initial value in QM_ASSIGN to prevent references. */ + if (var_node_r.op_type & (IS_CV|IS_VAR)) { + zend_emit_op_tmp(&pipe_result, ZEND_QM_ASSIGN, &var_node_r, NULL); + } else { + pipe_result = var_node_r; + } + + zend_compile_assign_pipe_chain(&pipe_result, callable_ast); + + CG(memoize_mode) = ZEND_MEMOIZE_FETCH; + zend_compile_var(&var_node_w, var_ast, BP_VAR_W, false); + + opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1]; + zend_ast_kind kind = is_global_var_fetch(var_ast) ? ZEND_AST_VAR : var_ast->kind; + switch (kind) { + case ZEND_AST_VAR: + zend_emit_op_tmp(result, ZEND_ASSIGN, &var_node_w, &pipe_result); + break; + case ZEND_AST_STATIC_PROP: + opline->opcode = ZEND_ASSIGN_STATIC_PROP; + opline->result_type = IS_TMP_VAR; + var_node_w.op_type = IS_TMP_VAR; + zend_emit_op_data(&pipe_result); + *result = var_node_w; + break; + case ZEND_AST_DIM: + opline->opcode = ZEND_ASSIGN_DIM; + opline->result_type = IS_TMP_VAR; + var_node_w.op_type = IS_TMP_VAR; + zend_emit_op_data(&pipe_result); + *result = var_node_w; + break; + case ZEND_AST_PROP: + case ZEND_AST_NULLSAFE_PROP: + opline->opcode = ZEND_ASSIGN_OBJ; + opline->result_type = IS_TMP_VAR; + var_node_w.op_type = IS_TMP_VAR; + zend_emit_op_data(&pipe_result); + *result = var_node_w; + break; + default: ZEND_UNREACHABLE(); + } + + zend_hash_destroy(CG(memoized_exprs)); + FREE_HASHTABLE(CG(memoized_exprs)); + CG(memoized_exprs) = orig_memoized_exprs; + CG(memoize_mode) = orig_memoize_mode; +} +/* }}} */ + static void zend_compile_print(znode *result, const zend_ast *ast) /* {{{ */ { zend_op *opline; @@ -12415,6 +12520,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ case ZEND_AST_ASSIGN_COALESCE: zend_compile_assign_coalesce(result, ast); return; + case ZEND_AST_ASSIGN_PIPE: + zend_compile_assign_pipe(result, ast); + return; case ZEND_AST_PRINT: zend_compile_print(result, ast); return; diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index b4dda00404ea..a8481aaa7d7c 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -60,7 +60,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %precedence T_YIELD %precedence T_DOUBLE_ARROW %precedence T_YIELD_FROM -%precedence '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL T_POW_EQUAL T_COALESCE_EQUAL +%precedence '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL T_POW_EQUAL T_COALESCE_EQUAL T_PIPE_EQUAL %left '?' ':' %right T_COALESCE %left T_BOOLEAN_OR @@ -197,6 +197,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_SL_EQUAL "'<<='" %token T_SR_EQUAL "'>>='" %token T_COALESCE_EQUAL "'??='" +%token T_PIPE_EQUAL "'|>='" %token T_BOOLEAN_OR "'||'" %token T_BOOLEAN_AND "'&&'" %token T_IS_EQUAL "'=='" @@ -1296,6 +1297,8 @@ expr: { $$ = zend_ast_create_assign_op(ZEND_SR, $1, $3); } | variable T_COALESCE_EQUAL expr { $$ = zend_ast_create(ZEND_AST_ASSIGN_COALESCE, $1, $3); } + | variable T_PIPE_EQUAL expr + { $$ = zend_ast_create(ZEND_AST_ASSIGN_PIPE, $1, $3); } | variable T_INC { $$ = zend_ast_create(ZEND_AST_POST_INC, $1); } | T_INC variable { $$ = zend_ast_create(ZEND_AST_PRE_INC, $2); } | variable T_DEC { $$ = zend_ast_create(ZEND_AST_POST_DEC, $1); } diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index bf6bbe7f9d90..18a36de7fdec 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1945,6 +1945,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN(T_COALESCE_EQUAL); } +"|>=" { + RETURN_TOKEN(T_PIPE_EQUAL); +} + "|>" { RETURN_TOKEN(T_PIPE); } diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index 87b15b8bb345..7c994c94ccd0 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -131,6 +131,7 @@ char *get_token_type_name(int token_type) case T_SL_EQUAL: return "T_SL_EQUAL"; case T_SR_EQUAL: return "T_SR_EQUAL"; case T_COALESCE_EQUAL: return "T_COALESCE_EQUAL"; + case T_PIPE_EQUAL: return "T_PIPE_EQUAL"; case T_BOOLEAN_OR: return "T_BOOLEAN_OR"; case T_BOOLEAN_AND: return "T_BOOLEAN_AND"; case T_IS_EQUAL: return "T_IS_EQUAL"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 57c8edad8acb..910b20a7db1e 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -542,6 +542,11 @@ * @cvalue T_COALESCE_EQUAL */ const T_COALESCE_EQUAL = UNKNOWN; +/** + * @var int + * @cvalue T_PIPE_EQUAL + */ +const T_PIPE_EQUAL = UNKNOWN; /** * @var int * @cvalue T_BOOLEAN_OR diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index b82842ede0f1..fc70aff190bc 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit tokenizer_data.stub.php instead. - * Stub hash: c5235344b7c651d27c2c33c90696a418a9c96837 */ + * Stub hash: a4fe4e44aa2a76d99cd1db19f7d91ff9ba1a1198 */ static void register_tokenizer_data_symbols(int module_number) { @@ -111,6 +111,7 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_SL_EQUAL", T_SL_EQUAL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_SR_EQUAL", T_SR_EQUAL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_COALESCE_EQUAL", T_COALESCE_EQUAL, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PIPE_EQUAL", T_PIPE_EQUAL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_BOOLEAN_OR", T_BOOLEAN_OR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_BOOLEAN_AND", T_BOOLEAN_AND, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_IS_EQUAL", T_IS_EQUAL, CONST_PERSISTENT);