From 2db203c8e9cf6b1d9512e53db04d35607c9fadd1 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 | 41 +++++ 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_ast.phpt | 80 ++++++++ Zend/zend_ast.c | 1 + Zend/zend_ast.h | 1 + Zend/zend_compile.c | 173 +++++++++++++++--- 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 +- 25 files changed, 641 insertions(+), 31 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_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..335befacff03 --- /dev/null +++ b/Zend/tests/pipe_operator/assign_pipe_009.phpt @@ -0,0 +1,41 @@ +--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(int $bar): Closure { + var_dump($bar); + return function (int $foo): int { return $foo + 1; }; +} +$bar = 0; +$foo = 1; +$foo |>= $bar |>= x(...); +var_dump($bar instanceof Closure, $foo); + +?> +--EXPECT-- +int(15) +int(15) +int(12) +string(8) "value:17" +int(0) +bool(true) +int(2) 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..c33c8f3d39f8 --- /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(10); +var_dump($v4); + +$times6 = fn($x) => $x * 6; +$v5 = 5; +$v5 |>= $times6; +var_dump($v5); + +?> +--EXPECT-- +int(10) +int(15) +int(20) +int(50) +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_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 3aa04de860a1..19ffea248525 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2594,6 +2594,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 86a68c1cbaf9..ce5151e45aaa 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 ac5a9d71a6ea..d66b405172bf 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5146,7 +5146,7 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg * breaking for the generated call. */ if (callback->kind == ZEND_AST_CALL - && callback->child[0]->kind == ZEND_AST_ZVAL + && callback->child[0]->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(callback->child[0])) == IS_STRING && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) { return FAILURE; @@ -6769,6 +6769,35 @@ static bool zend_is_pipe_optimizable_callable_name(zend_ast *ast) return true; } +/* Build a function call AST for a pipe operation: value |> callable. */ +static zend_ast *zend_pipe_build_fcall(zend_ast *callable_ast, zend_ast *arg_list) +{ + /* Turn bar(...) into bar($value). */ + if (callable_ast->kind == ZEND_AST_CALL + && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT + && zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) { + return zend_ast_create(ZEND_AST_CALL, + callable_ast->child[0], arg_list); + } + /* Turn Foo::bar(...) into Foo::bar($value). */ + if (callable_ast->kind == ZEND_AST_STATIC_CALL + && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { + return zend_ast_create(ZEND_AST_STATIC_CALL, + callable_ast->child[0], callable_ast->child[1], arg_list); + } + /* Turn $obj->bar(...) into $obj->bar($value). */ + if (callable_ast->kind == ZEND_AST_METHOD_CALL + && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { + return zend_ast_create(ZEND_AST_METHOD_CALL, + callable_ast->child[0], callable_ast->child[1], arg_list); + } + /* Fallback: compile $expr then call ($expr)($value). */ + 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); +} + static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type) { zend_ast *operand_ast = ast->child[0]; @@ -6792,35 +6821,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_list_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&wrapped_operand_result)); - - zend_ast *fcall_ast; - znode callable_result; - - /* Turn $foo |> bar(...) into bar($foo). */ - if (callable_ast->kind == ZEND_AST_CALL - && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT - && zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) { - fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast->child[0], arg_list_ast); - /* Turn $foo |> bar::baz(...) into bar::baz($foo). */ - } else if (callable_ast->kind == ZEND_AST_STATIC_CALL - && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { - fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); - /* Turn $foo |> $bar->baz(...) into $bar->baz($foo). */ - } else if (callable_ast->kind == ZEND_AST_METHOD_CALL - && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { - fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); - /* Turn $foo |> $expr into ($expr)($foo) */ - } else { - 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 *fcall_ast = zend_pipe_build_fcall(callable_ast, arg_list_ast); zend_do_extended_stmt(&operand_result); @@ -11019,6 +11021,116 @@ 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 *arg_list = zend_ast_create_list(1, ZEND_AST_ARG_LIST, + zend_ast_create_znode(current)); + zend_ast *fcall = zend_pipe_build_fcall(step, arg_list); + + 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; @@ -12212,6 +12324,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 07f2d44cb5c6..d4bfa9a82871 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1914,6 +1914,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);