From d7716ada74a31b5098ec723bd7812c759e80bfb7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 11 Apr 2020 16:44:10 -0500 Subject: [PATCH 01/37] Implement pipe operator. --- Zend/tests/pipe_operator/ast.phpt | 18 ++++++++ Zend/tests/pipe_operator/call_by_ref.phpt | 20 +++++++++ .../compound_userland_calls.phpt | 19 ++++++++ .../pipe_operator/function_not_found.phpt | 15 +++++++ .../pipe_operator/mixed_callable_call.phpt | 44 +++++++++++++++++++ .../pipe_operator/optional_parameters.phpt | 15 +++++++ .../pipe_operator/precedence_addition.phpt | 17 +++++++ .../pipe_operator/precedence_coalesce.phpt | 17 +++++++ .../pipe_operator/precedence_ternary.phpt | 21 +++++++++ .../pipe_operator/simple_builtin_call.phpt | 11 +++++ .../pipe_operator/simple_userland_call.phpt | 15 +++++++ .../pipe_operator/too_many_parameters.phpt | 21 +++++++++ Zend/tests/pipe_operator/type_mismatch.phpt | 20 +++++++++ Zend/tests/pipe_operator/void_return.phpt | 21 +++++++++ Zend/tests/pipe_operator/wrapped_chains.phpt | 21 +++++++++ Zend/zend_ast.c | 15 +++++-- Zend/zend_compile.h | 1 + Zend/zend_language_parser.y | 4 ++ Zend/zend_language_scanner.l | 4 ++ ext/tokenizer/tokenizer_data.c | 1 + ext/tokenizer/tokenizer_data.stub.php | 5 +++ 21 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/pipe_operator/ast.phpt create mode 100644 Zend/tests/pipe_operator/call_by_ref.phpt create mode 100644 Zend/tests/pipe_operator/compound_userland_calls.phpt create mode 100644 Zend/tests/pipe_operator/function_not_found.phpt create mode 100644 Zend/tests/pipe_operator/mixed_callable_call.phpt create mode 100644 Zend/tests/pipe_operator/optional_parameters.phpt create mode 100644 Zend/tests/pipe_operator/precedence_addition.phpt create mode 100644 Zend/tests/pipe_operator/precedence_coalesce.phpt create mode 100644 Zend/tests/pipe_operator/precedence_ternary.phpt create mode 100644 Zend/tests/pipe_operator/simple_builtin_call.phpt create mode 100644 Zend/tests/pipe_operator/simple_userland_call.phpt create mode 100644 Zend/tests/pipe_operator/too_many_parameters.phpt create mode 100644 Zend/tests/pipe_operator/type_mismatch.phpt create mode 100644 Zend/tests/pipe_operator/void_return.phpt create mode 100644 Zend/tests/pipe_operator/wrapped_chains.phpt diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt new file mode 100644 index 0000000000000..4a5be8e4ea662 --- /dev/null +++ b/Zend/tests/pipe_operator/ast.phpt @@ -0,0 +1,18 @@ +--TEST-- +Test that a pipe operator displays as a pipe operator when outputting syntax. +--FILE-- + '_test') == 99); +} catch (AssertionError $e) { + print $e->getMessage(); +} + +?> +--EXPECTF-- +assert(5 |> \_test == 99) diff --git a/Zend/tests/pipe_operator/call_by_ref.phpt b/Zend/tests/pipe_operator/call_by_ref.phpt new file mode 100644 index 0000000000000..e3320d0b1ffd6 --- /dev/null +++ b/Zend/tests/pipe_operator/call_by_ref.phpt @@ -0,0 +1,20 @@ +--TEST-- +Pipe operator accepts by-reference functions +--FILE-- + '_modify'; + +var_dump($res1); +var_dump($a); +?> +--EXPECT-- +string(3) "foo" +int(6) diff --git a/Zend/tests/pipe_operator/compound_userland_calls.phpt b/Zend/tests/pipe_operator/compound_userland_calls.phpt new file mode 100644 index 0000000000000..922c3c5ac23d8 --- /dev/null +++ b/Zend/tests/pipe_operator/compound_userland_calls.phpt @@ -0,0 +1,19 @@ +--TEST-- +Pipe operator chains +--FILE-- + '_test1' |> '_test2'; + +var_dump($res1); +?> +--EXPECT-- +int(12) diff --git a/Zend/tests/pipe_operator/function_not_found.phpt b/Zend/tests/pipe_operator/function_not_found.phpt new file mode 100644 index 0000000000000..cebb73fedcbb8 --- /dev/null +++ b/Zend/tests/pipe_operator/function_not_found.phpt @@ -0,0 +1,15 @@ +--TEST-- +Pipe operator throws normally on missing function +--FILE-- + '_test'; +} +catch (Throwable $e) { + printf("Expected %s thrown, got %s", Error::class, get_class($e)); +} + +?> +--EXPECT-- +Expected Error thrown, got Error diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt new file mode 100644 index 0000000000000..3f1a41a1db0f4 --- /dev/null +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -0,0 +1,44 @@ +--TEST-- +Pipe operator handles all callable styles +--FILE-- + _add($x, 3); + +$res1 = 2 + |> [$test, 'message'] + |> 'strlen' + |> $add3 + |> fn($x) => _area($x, 2) +; + +var_dump($res1); +?> +--EXPECT-- +int(20) diff --git a/Zend/tests/pipe_operator/optional_parameters.phpt b/Zend/tests/pipe_operator/optional_parameters.phpt new file mode 100644 index 0000000000000..53cce2e9972e8 --- /dev/null +++ b/Zend/tests/pipe_operator/optional_parameters.phpt @@ -0,0 +1,15 @@ +--TEST-- +Pipe operator accepts optional-parameter functions +--FILE-- + '_test'; + +var_dump($res1); +?> +--EXPECT-- +int(8) diff --git a/Zend/tests/pipe_operator/precedence_addition.phpt b/Zend/tests/pipe_operator/precedence_addition.phpt new file mode 100644 index 0000000000000..d047907a84bd2 --- /dev/null +++ b/Zend/tests/pipe_operator/precedence_addition.phpt @@ -0,0 +1,17 @@ +--TEST-- +Pipe binds lower than addition +--FILE-- + '_test1'; + +var_dump($res1); +?> +--EXPECT-- +int(8) diff --git a/Zend/tests/pipe_operator/precedence_coalesce.phpt b/Zend/tests/pipe_operator/precedence_coalesce.phpt new file mode 100644 index 0000000000000..812075d8a75ad --- /dev/null +++ b/Zend/tests/pipe_operator/precedence_coalesce.phpt @@ -0,0 +1,17 @@ +--TEST-- +Pipe binds lower than coalesce +--FILE-- + $bad_func ?? '_test1'; + +var_dump($res1); +?> +--EXPECT-- +int(10) diff --git a/Zend/tests/pipe_operator/precedence_ternary.phpt b/Zend/tests/pipe_operator/precedence_ternary.phpt new file mode 100644 index 0000000000000..9fe886be892d6 --- /dev/null +++ b/Zend/tests/pipe_operator/precedence_ternary.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe binds lower than ternary +--FILE-- + $bad_func ? '_test1' : '_test2'; + +var_dump($res1); +?> +--EXPECT-- +int(10) diff --git a/Zend/tests/pipe_operator/simple_builtin_call.phpt b/Zend/tests/pipe_operator/simple_builtin_call.phpt new file mode 100644 index 0000000000000..72f5968dd0b65 --- /dev/null +++ b/Zend/tests/pipe_operator/simple_builtin_call.phpt @@ -0,0 +1,11 @@ +--TEST-- +Pipe operator supports built-in functions +--FILE-- + 'strlen'; + +var_dump($res1); +?> +--EXPECT-- +int(5) diff --git a/Zend/tests/pipe_operator/simple_userland_call.phpt b/Zend/tests/pipe_operator/simple_userland_call.phpt new file mode 100644 index 0000000000000..7f311f9a10474 --- /dev/null +++ b/Zend/tests/pipe_operator/simple_userland_call.phpt @@ -0,0 +1,15 @@ +--TEST-- +Pipe operator supports user-defined functions +--FILE-- + '_test'; + +var_dump($res1); +?> +--EXPECT-- +int(6) diff --git a/Zend/tests/pipe_operator/too_many_parameters.phpt b/Zend/tests/pipe_operator/too_many_parameters.phpt new file mode 100644 index 0000000000000..34487aeb97565 --- /dev/null +++ b/Zend/tests/pipe_operator/too_many_parameters.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe operator fails on multi-parameter functions +--FILE-- + '_test'; +} +catch (Throwable $e) { + printf("Expected %s thrown, got %s", ArgumentCountError::class, get_class($e)); +} + + +?> +--EXPECT-- +Expected ArgumentCountError thrown, got ArgumentCountError diff --git a/Zend/tests/pipe_operator/type_mismatch.phpt b/Zend/tests/pipe_operator/type_mismatch.phpt new file mode 100644 index 0000000000000..6cac9473c84d9 --- /dev/null +++ b/Zend/tests/pipe_operator/type_mismatch.phpt @@ -0,0 +1,20 @@ +--TEST-- +Pipe operator respects types +--FILE-- + '_test'; + var_dump($res1); +} +catch (Throwable $e) { + printf("Expected %s thrown, got %s", TypeError::class, get_class($e)); +} + +?> +--EXPECT-- +Expected TypeError thrown, got TypeError diff --git a/Zend/tests/pipe_operator/void_return.phpt b/Zend/tests/pipe_operator/void_return.phpt new file mode 100644 index 0000000000000..e9a71dda44f85 --- /dev/null +++ b/Zend/tests/pipe_operator/void_return.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe operator fails void return chaining in strict mode +--FILE-- + 'nonReturnFunction' + |> 'strlen'; + var_dump($result); +} +catch (Throwable $e) { + printf("Expected %s thrown, got %s", TypeError::class, get_class($e)); +} + +?> +--EXPECT-- +Expected TypeError thrown, got TypeError diff --git a/Zend/tests/pipe_operator/wrapped_chains.phpt b/Zend/tests/pipe_operator/wrapped_chains.phpt new file mode 100644 index 0000000000000..e2b6f39f7f342 --- /dev/null +++ b/Zend/tests/pipe_operator/wrapped_chains.phpt @@ -0,0 +1,21 @@ +--TEST-- +Pipe operator chains saved as a closure +--FILE-- + $x |> '_test1' |> '_test2'; + +$res1 = $func(5); + +var_dump($res1); +?> +--EXPECT-- +int(12) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 95a67a95b7dde..700fc6ed18d8b 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2211,10 +2211,17 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_var(str, ast->child[1], 0, indent); break; case ZEND_AST_CALL: - zend_ast_export_ns_name(str, ast->child[0], 0, indent); - smart_str_appendc(str, '('); - zend_ast_export_ex(str, ast->child[1], 0, indent); - smart_str_appendc(str, ')'); + if (ast->attr & ZEND_CALL_SYNTAX_PIPE) { + zend_ast_export_ex(str, ast->child[1], 0, indent); + smart_str_appends(str, " |> "); + zend_ast_export_ns_name(str, ast->child[0], 0, indent); + } + else { + zend_ast_export_ns_name(str, ast->child[0], 0, indent); + smart_str_appendc(str, '('); + zend_ast_export_ex(str, ast->child[1], 0, indent); + smart_str_appendc(str, ')'); + } break; case ZEND_AST_PARENT_PROPERTY_HOOK_CALL: smart_str_append(str, Z_STR_P(zend_ast_get_zval(ast->child[0]))); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 1eaf3ef686e79..d78f63097ea59 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1030,6 +1030,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); /* These should not clash with ZEND_ACC_PPP_MASK and ZEND_ACC_PPP_SET_MASK */ #define ZEND_PARAM_REF (1<<3) #define ZEND_PARAM_VARIADIC (1<<4) +#define ZEND_CALL_SYNTAX_PIPE (1u << 2u) #define ZEND_NAME_FQ 0 #define ZEND_NAME_NOT_FQ 1 diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index d2a29e670d8bf..abb482ad2c478 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -62,6 +62,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %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 +%left T_PIPE %left '?' ':' %right T_COALESCE %left T_BOOLEAN_OR @@ -236,6 +237,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_COALESCE "'??'" %token T_POW "'**'" %token T_POW_EQUAL "'**='" +%token T_PIPE "|>" /* We need to split the & token in two to avoid a shift/reduce conflict. For T1&$v and T1&T2, * with only one token lookahead, bison does not know whether to reduce T1 as a complete type, * or shift to continue parsing an intersection type. */ @@ -1278,6 +1280,8 @@ expr: { $$ = zend_ast_create_binary_op(ZEND_IS_EQUAL, $1, $3); } | expr T_IS_NOT_EQUAL expr { $$ = zend_ast_create_binary_op(ZEND_IS_NOT_EQUAL, $1, $3); } + | expr T_PIPE expr + { $$ = zend_ast_create(ZEND_AST_CALL, $3, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1) ); $$->attr = ZEND_CALL_SYNTAX_PIPE; } | expr '<' expr { $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER, $1, $3); } | expr T_IS_SMALLER_OR_EQUAL expr diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 7ae73875926eb..70215be4000df 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1857,6 +1857,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN(T_COALESCE_EQUAL); } +"|>" { + RETURN_TOKEN(T_PIPE); +} + "||" { RETURN_TOKEN(T_BOOLEAN_OR); } diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index a046ab50e1498..64f702720b54c 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -172,6 +172,7 @@ char *get_token_type_name(int token_type) case T_COALESCE: return "T_COALESCE"; case T_POW: return "T_POW"; case T_POW_EQUAL: return "T_POW_EQUAL"; + case T_PIPE: return "T_PIPE"; case T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG: return "T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG"; case T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG: return "T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG"; case T_BAD_CHARACTER: return "T_BAD_CHARACTER"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 45f3c89f2de3a..b3981fea8bac6 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -737,6 +737,11 @@ * @cvalue T_POW_EQUAL */ const T_POW_EQUAL = UNKNOWN; +/** + * @var int + * @cvalue T_PIPE + */ +const T_PIPE = UNKNOWN; /** * @var int * @cvalue T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG From 0f8b8b5d34d85e76adda637f26fbba6e0064eebb Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 12 Dec 2024 21:48:40 -0600 Subject: [PATCH 02/37] Include FCC in tested callable formats. --- Zend/tests/pipe_operator/mixed_callable_call.phpt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt index 3f1a41a1db0f4..0ed5689ef51fa 100644 --- a/Zend/tests/pipe_operator/mixed_callable_call.phpt +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -27,6 +27,10 @@ class _Test } } +function _double(int $x): int { + return $x * 2; +} + $test = new _Test(); $add3 = fn($x) => _add($x, 3); @@ -36,9 +40,10 @@ $res1 = 2 |> 'strlen' |> $add3 |> fn($x) => _area($x, 2) + |> _double(...) ; var_dump($res1); ?> --EXPECT-- -int(20) +int(40) From 69a6febab5e481e1f4956e6c014d21f7eda17fb7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 29 Dec 2024 22:54:26 -0600 Subject: [PATCH 03/37] Move the code generation from the lexer to a compile function, courtesy Ilija. --- Zend/zend_ast.c | 15 ++++----------- Zend/zend_ast.h | 1 + Zend/zend_compile.c | 27 +++++++++++++++++++++++++++ Zend/zend_compile.h | 1 - Zend/zend_language_parser.y | 2 +- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 700fc6ed18d8b..95a67a95b7dde 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2211,17 +2211,10 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_var(str, ast->child[1], 0, indent); break; case ZEND_AST_CALL: - if (ast->attr & ZEND_CALL_SYNTAX_PIPE) { - zend_ast_export_ex(str, ast->child[1], 0, indent); - smart_str_appends(str, " |> "); - zend_ast_export_ns_name(str, ast->child[0], 0, indent); - } - else { - zend_ast_export_ns_name(str, ast->child[0], 0, indent); - smart_str_appendc(str, '('); - zend_ast_export_ex(str, ast->child[1], 0, indent); - smart_str_appendc(str, ')'); - } + zend_ast_export_ns_name(str, ast->child[0], 0, indent); + smart_str_appendc(str, '('); + zend_ast_export_ex(str, ast->child[1], 0, indent); + smart_str_appendc(str, ')'); break; case ZEND_AST_PARENT_PROPERTY_HOOK_CALL: smart_str_append(str, Z_STR_P(zend_ast_get_zval(ast->child[0]))); diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 6f68a961b7989..258dc7f894a78 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_PARENT_PROPERTY_HOOK_CALL, + ZEND_AST_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 9533ef8dfaa44..81a0c8b66de54 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6598,6 +6598,30 @@ static void zend_compile_match(znode *result, zend_ast *ast) efree(jmp_end_opnums); } +static void zend_compile_pipe(znode *result, zend_ast *ast) +{ + zend_ast *operand_ast = ast->child[0]; + zend_ast *callable_ast = ast->child[1]; + + znode operand_result; + zend_compile_expr(&operand_result, operand_ast); + + /* Turn $foo |> bar(...) into bar($foo). */ + if (callable_ast->kind == ZEND_AST_CALL + && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) { + callable_ast = callable_ast->child[0]; + } + + znode callable_result; + zend_compile_expr(&callable_result, callable_ast); + + zend_ast *fcall_ast = zend_ast_create(ZEND_AST_CALL, + zend_ast_create_znode(&callable_result), + zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&operand_result))); + + zend_compile_expr(result, fcall_ast); +} + static void zend_compile_try(zend_ast *ast) /* {{{ */ { zend_ast *try_ast = ast->child[0]; @@ -11603,6 +11627,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ case ZEND_AST_MATCH: zend_compile_match(result, ast); return; + case ZEND_AST_PIPE: + zend_compile_pipe(result, ast); + return; default: ZEND_ASSERT(0 /* not supported */); } diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index d78f63097ea59..1eaf3ef686e79 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1030,7 +1030,6 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); /* These should not clash with ZEND_ACC_PPP_MASK and ZEND_ACC_PPP_SET_MASK */ #define ZEND_PARAM_REF (1<<3) #define ZEND_PARAM_VARIADIC (1<<4) -#define ZEND_CALL_SYNTAX_PIPE (1u << 2u) #define ZEND_NAME_FQ 0 #define ZEND_NAME_NOT_FQ 1 diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index abb482ad2c478..7e0cb88fb10a7 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1281,7 +1281,7 @@ expr: | expr T_IS_NOT_EQUAL expr { $$ = zend_ast_create_binary_op(ZEND_IS_NOT_EQUAL, $1, $3); } | expr T_PIPE expr - { $$ = zend_ast_create(ZEND_AST_CALL, $3, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1) ); $$->attr = ZEND_CALL_SYNTAX_PIPE; } + { $$ = zend_ast_create(ZEND_AST_PIPE, $1, $3); } | expr '<' expr { $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER, $1, $3); } | expr T_IS_SMALLER_OR_EQUAL expr From 0b40c323318bdabffebaec67f93604597a3a2154 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 30 Dec 2024 00:09:50 -0600 Subject: [PATCH 04/37] Fix pipe error output. --- Zend/tests/pipe_operator/ast.phpt | 11 +++++++++-- Zend/zend_ast.c | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index 4a5be8e4ea662..c628b19ac8a99 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -10,9 +10,16 @@ function _test(int $a): int { try { assert((5 |> '_test') == 99); } catch (AssertionError $e) { - print $e->getMessage(); + print $e->getMessage() . PHP_EOL; +} + +try { + assert((5 |> _test(...)) == 99); +} catch (AssertionError $e) { + print $e->getMessage() . PHP_EOL; } ?> --EXPECTF-- -assert(5 |> \_test == 99) +assert(5 |> '_test' == 99) +assert(5 |> _test(...) == 99) \ No newline at end of file diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 95a67a95b7dde..e5152deaf1d98 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2506,6 +2506,11 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_name(str, ast->child[1], 0, indent); } break; + case ZEND_AST_PIPE: + zend_ast_export_ex(str, ast->child[0], 0, indent); + smart_str_appends(str, " |> "); + zend_ast_export_ex(str, ast->child[1], 0, indent); + break; case ZEND_AST_NAMED_ARG: smart_str_append(str, zend_ast_get_str(ast->child[0])); smart_str_appends(str, ": "); From 80ba4df3b23fd474acd1e83735cd1b708309cd3c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 30 Dec 2024 00:10:30 -0600 Subject: [PATCH 05/37] Remove dead comment. --- Zend/tests/pipe_operator/call_by_ref.phpt | 1 - 1 file changed, 1 deletion(-) diff --git a/Zend/tests/pipe_operator/call_by_ref.phpt b/Zend/tests/pipe_operator/call_by_ref.phpt index e3320d0b1ffd6..b2476d5fd55e8 100644 --- a/Zend/tests/pipe_operator/call_by_ref.phpt +++ b/Zend/tests/pipe_operator/call_by_ref.phpt @@ -8,7 +8,6 @@ function _modify(int &$a): string { return "foo"; } -//try { $a = 5; $res1 = $a |> '_modify'; From c8c036514ee06b6798d556a9a8f9e40101e1e6f1 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 14 Jan 2025 13:10:02 -0600 Subject: [PATCH 06/37] Wrap pipe LHS in a QM_ASSIGN opcode to implicitly block references. --- Zend/tests/pipe_operator/call_by_ref.phpt | 34 +++++++++++---- Zend/zend_compile.c | 50 ++++++++++++----------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/Zend/tests/pipe_operator/call_by_ref.phpt b/Zend/tests/pipe_operator/call_by_ref.phpt index b2476d5fd55e8..cd5acecafa7be 100644 --- a/Zend/tests/pipe_operator/call_by_ref.phpt +++ b/Zend/tests/pipe_operator/call_by_ref.phpt @@ -1,5 +1,5 @@ --TEST-- -Pipe operator accepts by-reference functions +Pipe operator rejects by-reference functions. --FILE-- '_modify'; +function _append(array &$a): string { + $a['bar'] = 'beep'; +} + +// Simple variables +try { + $a = 5; + $res1 = $a |> _modify(...); + var_dump($res1); +} catch (\Error $e) { + print $e->getMessage() . PHP_EOL; +} + +// Complex variables. +try { + $a = ['foo' => 'beep']; + $res2 = $a |> _append(...); + var_dump($res2); +} catch (\Error $e) { + print $e->getMessage() . PHP_EOL; +} + -var_dump($res1); -var_dump($a); ?> ---EXPECT-- -string(3) "foo" -int(6) +--EXPECTF-- +_modify(): Argument #1 ($a) could not be passed by reference +_append(): Argument #1 ($a) could not be passed by reference \ No newline at end of file diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 81a0c8b66de54..6c29d21f686fb 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6412,6 +6412,32 @@ static bool can_match_use_jumptable(zend_ast_list *arms) { return 1; } +static void zend_compile_pipe(znode *result, zend_ast *ast) +{ + zend_ast *operand_ast = ast->child[0]; + zend_ast *callable_ast = ast->child[1]; + + znode operand_result; + zend_compile_expr(&operand_result, operand_ast); + znode wrapped_operand_result; + zend_emit_op_tmp(&wrapped_operand_result, ZEND_QM_ASSIGN, &operand_result, NULL); + + /* Turn $foo |> bar(...) into bar($foo). */ + if (callable_ast->kind == ZEND_AST_CALL + && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) { + callable_ast = callable_ast->child[0]; + } + + znode callable_result; + zend_compile_expr(&callable_result, callable_ast); + + zend_ast *fcall_ast = zend_ast_create(ZEND_AST_CALL, + zend_ast_create_znode(&callable_result), + zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&wrapped_operand_result))); + + zend_compile_expr(result, fcall_ast); +} + static void zend_compile_match(znode *result, zend_ast *ast) { zend_ast *expr_ast = ast->child[0]; @@ -6598,30 +6624,6 @@ static void zend_compile_match(znode *result, zend_ast *ast) efree(jmp_end_opnums); } -static void zend_compile_pipe(znode *result, zend_ast *ast) -{ - zend_ast *operand_ast = ast->child[0]; - zend_ast *callable_ast = ast->child[1]; - - znode operand_result; - zend_compile_expr(&operand_result, operand_ast); - - /* Turn $foo |> bar(...) into bar($foo). */ - if (callable_ast->kind == ZEND_AST_CALL - && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) { - callable_ast = callable_ast->child[0]; - } - - znode callable_result; - zend_compile_expr(&callable_result, callable_ast); - - zend_ast *fcall_ast = zend_ast_create(ZEND_AST_CALL, - zend_ast_create_znode(&callable_result), - zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&operand_result))); - - zend_compile_expr(result, fcall_ast); -} - static void zend_compile_try(zend_ast *ast) /* {{{ */ { zend_ast *try_ast = ast->child[0]; From 48bb0de5d3b82df2ea9d4c39b2e4e5ff189ba4cd Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 8 Feb 2025 14:10:01 +0000 Subject: [PATCH 07/37] Add test to check behaviour of prefer-by-ref parameters --- .../tests/pipe_operator/call_prefer_by_ref.phpt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Zend/tests/pipe_operator/call_prefer_by_ref.phpt diff --git a/Zend/tests/pipe_operator/call_prefer_by_ref.phpt b/Zend/tests/pipe_operator/call_prefer_by_ref.phpt new file mode 100644 index 0000000000000..d25ac3d00edcc --- /dev/null +++ b/Zend/tests/pipe_operator/call_prefer_by_ref.phpt @@ -0,0 +1,17 @@ +--TEST-- +Pipe operator accepts prefer-by-reference functions. +--FILE-- + array_multisort(...); + var_dump($r); +} catch (\Error $e) { + print $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +bool(true) From bbb3fd6038ac5aa7d4c2f8b891bbc8989959656b Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:25:55 -0600 Subject: [PATCH 08/37] Use echo instead. --- Zend/tests/pipe_operator/ast.phpt | 4 ++-- Zend/tests/pipe_operator/call_by_ref.phpt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index c628b19ac8a99..acc5440c59e07 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -10,13 +10,13 @@ function _test(int $a): int { try { assert((5 |> '_test') == 99); } catch (AssertionError $e) { - print $e->getMessage() . PHP_EOL; + echo $e->getMessage(), PHP_EOL; } try { assert((5 |> _test(...)) == 99); } catch (AssertionError $e) { - print $e->getMessage() . PHP_EOL; + echo $e->getMessage(), PHP_EOL; } ?> diff --git a/Zend/tests/pipe_operator/call_by_ref.phpt b/Zend/tests/pipe_operator/call_by_ref.phpt index cd5acecafa7be..4a504b1da70b8 100644 --- a/Zend/tests/pipe_operator/call_by_ref.phpt +++ b/Zend/tests/pipe_operator/call_by_ref.phpt @@ -18,7 +18,7 @@ try { $res1 = $a |> _modify(...); var_dump($res1); } catch (\Error $e) { - print $e->getMessage() . PHP_EOL; + echo $e->getMessage(), PHP_EOL; } // Complex variables. @@ -27,7 +27,7 @@ try { $res2 = $a |> _append(...); var_dump($res2); } catch (\Error $e) { - print $e->getMessage() . PHP_EOL; + echo $e->getMessage(), PHP_EOL; } From 0794003ed718c6d2a448330d5b8c35b734763206 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:29:33 -0600 Subject: [PATCH 09/37] Don't use printf, either. --- Zend/tests/pipe_operator/ast.phpt | 2 +- Zend/tests/pipe_operator/function_not_found.phpt | 4 ++-- Zend/tests/pipe_operator/too_many_parameters.phpt | 6 +++--- Zend/tests/pipe_operator/type_mismatch.phpt | 6 +++--- Zend/tests/pipe_operator/void_return.phpt | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index acc5440c59e07..a2b274f9b200c 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -22,4 +22,4 @@ try { ?> --EXPECTF-- assert(5 |> '_test' == 99) -assert(5 |> _test(...) == 99) \ No newline at end of file +assert(5 |> _test(...) == 99) diff --git a/Zend/tests/pipe_operator/function_not_found.phpt b/Zend/tests/pipe_operator/function_not_found.phpt index cebb73fedcbb8..747f53ce3e3d1 100644 --- a/Zend/tests/pipe_operator/function_not_found.phpt +++ b/Zend/tests/pipe_operator/function_not_found.phpt @@ -7,9 +7,9 @@ try { $res1 = 5 |> '_test'; } catch (Throwable $e) { - printf("Expected %s thrown, got %s", Error::class, get_class($e)); + echo $e::class, ": ", $e->getMessage(), PHP_EOL; } ?> --EXPECT-- -Expected Error thrown, got Error +Error: Call to undefined function _test() diff --git a/Zend/tests/pipe_operator/too_many_parameters.phpt b/Zend/tests/pipe_operator/too_many_parameters.phpt index 34487aeb97565..b36046bde05c2 100644 --- a/Zend/tests/pipe_operator/too_many_parameters.phpt +++ b/Zend/tests/pipe_operator/too_many_parameters.phpt @@ -12,10 +12,10 @@ try { $res1 = 5 |> '_test'; } catch (Throwable $e) { - printf("Expected %s thrown, got %s", ArgumentCountError::class, get_class($e)); + echo $e::class, ": ", $e->getMessage(), PHP_EOL; } ?> ---EXPECT-- -Expected ArgumentCountError thrown, got ArgumentCountError +--EXPECTF-- +ArgumentCountError: Too few arguments to function %s, 1 passed in %s on line %s and exactly 2 expected diff --git a/Zend/tests/pipe_operator/type_mismatch.phpt b/Zend/tests/pipe_operator/type_mismatch.phpt index 6cac9473c84d9..2cee15bb47a0d 100644 --- a/Zend/tests/pipe_operator/type_mismatch.phpt +++ b/Zend/tests/pipe_operator/type_mismatch.phpt @@ -12,9 +12,9 @@ try { var_dump($res1); } catch (Throwable $e) { - printf("Expected %s thrown, got %s", TypeError::class, get_class($e)); + echo $e::class, ": ", $e->getMessage(), PHP_EOL; } ?> ---EXPECT-- -Expected TypeError thrown, got TypeError +--EXPECTF-- +TypeError: _test(): Argument #1 ($a) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/pipe_operator/void_return.phpt b/Zend/tests/pipe_operator/void_return.phpt index e9a71dda44f85..6173e616c31cc 100644 --- a/Zend/tests/pipe_operator/void_return.phpt +++ b/Zend/tests/pipe_operator/void_return.phpt @@ -13,9 +13,9 @@ try { var_dump($result); } catch (Throwable $e) { - printf("Expected %s thrown, got %s", TypeError::class, get_class($e)); + echo $e::class, ": ", $e->getMessage(), PHP_EOL; } ?> --EXPECT-- -Expected TypeError thrown, got TypeError +TypeError: strlen(): Argument #1 ($string) must be of type string, null given \ No newline at end of file From 594af65403c423ab2ecaf938ca42dd9f8ec1494e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:34:21 -0600 Subject: [PATCH 10/37] Correct addition-precedence test so it would be a different output if it was wrong. --- Zend/tests/pipe_operator/precedence_addition.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/tests/pipe_operator/precedence_addition.phpt b/Zend/tests/pipe_operator/precedence_addition.phpt index d047907a84bd2..3b3269f52fad1 100644 --- a/Zend/tests/pipe_operator/precedence_addition.phpt +++ b/Zend/tests/pipe_operator/precedence_addition.phpt @@ -4,7 +4,7 @@ Pipe binds lower than addition '_test1'; var_dump($res1); ?> --EXPECT-- -int(8) +int(14) From 37d75e23f2fe34f1f9ca8b2fe252f6430ba19373 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:25:10 -0600 Subject: [PATCH 11/37] Tweak style. --- Zend/tests/pipe_operator/call_prefer_by_ref.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/call_prefer_by_ref.phpt b/Zend/tests/pipe_operator/call_prefer_by_ref.phpt index d25ac3d00edcc..31750ac280d71 100644 --- a/Zend/tests/pipe_operator/call_prefer_by_ref.phpt +++ b/Zend/tests/pipe_operator/call_prefer_by_ref.phpt @@ -9,7 +9,7 @@ try { $r = $a |> array_multisort(...); var_dump($r); } catch (\Error $e) { - print $e->getMessage() . PHP_EOL; + echo $e->getMessage(), PHP_EOL; } ?> From ec753359ff0f8ac46a1e4d1cb6b506a6f7b80b5c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:28:17 -0600 Subject: [PATCH 12/37] Expand ternary test. --- .../pipe_operator/precedence_ternary.phpt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Zend/tests/pipe_operator/precedence_ternary.phpt b/Zend/tests/pipe_operator/precedence_ternary.phpt index 9fe886be892d6..67f2899ce9689 100644 --- a/Zend/tests/pipe_operator/precedence_ternary.phpt +++ b/Zend/tests/pipe_operator/precedence_ternary.phpt @@ -11,11 +11,32 @@ function _test2(int $a): int { return $a * 2; } -$bad_func = null; - -$res1 = 5 |> $bad_func ? '_test1' : '_test2'; +function _test3(int $a): int { + return $a * 100; +} +// $config is null, so the second function gets used. +$config = null; +$res1 = 5 |> $config ? _test1(...) : _test2(...); var_dump($res1); + +// $config is truthy, so the ternary binds first +// and evaluates to the first function. +$config = _test3(...); +$res2 = 5 |> $config ? _test1(...) : _test2(...); +var_dump($res2); + +// Binding the ternary first doesn't make logical sense, +// so the pipe runs first in this case. +$x = true; +$y = 'beep'; +$z = 'default'; +$ret3 = $x ? $y |> strlen(...) : $z; +var_dump($ret3); + + ?> --EXPECT-- int(10) +int(6) +int(4) From d836c1778bd4f2306afff17efb2253b58c0f338c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:36:54 -0600 Subject: [PATCH 13/37] Add a test for comparison operators. --- .../pipe_operator/precedence_comparison.phpt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Zend/tests/pipe_operator/precedence_comparison.phpt diff --git a/Zend/tests/pipe_operator/precedence_comparison.phpt b/Zend/tests/pipe_operator/precedence_comparison.phpt new file mode 100644 index 0000000000000..93f1236aebf9a --- /dev/null +++ b/Zend/tests/pipe_operator/precedence_comparison.phpt @@ -0,0 +1,17 @@ +--TEST-- +Pipe binds higher than comparison +--FILE-- + _test1(...)) == 10 ; +var_dump($res1); + +?> +--EXPECT-- +bool(true) From 8957cdd470943b54ca2ba06689cf999c33a963d0 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 10 Feb 2025 01:58:28 -0600 Subject: [PATCH 14/37] Update comparison precedence test. --- .../pipe_operator/precedence_comparison.phpt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Zend/tests/pipe_operator/precedence_comparison.phpt b/Zend/tests/pipe_operator/precedence_comparison.phpt index 93f1236aebf9a..19bded49f4ce5 100644 --- a/Zend/tests/pipe_operator/precedence_comparison.phpt +++ b/Zend/tests/pipe_operator/precedence_comparison.phpt @@ -7,11 +7,21 @@ function _test1(int $a): int { return $a * 2; } -// This will error without the parens, -// as it will try to compare a closure to an int first. $res1 = (5 |> _test1(...)) == 10 ; var_dump($res1); +// This will error without the parens, +// as it will try to compare a closure to an int first. +try { + $res1 = 5 |> _test1(...) == 10 ; +} +catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + ?> ---EXPECT-- +--EXPECTF-- bool(true) + +Notice: Object of class Closure could not be converted to int in %s line %d +Error: Value of type bool is not callable From 26629a813c3dc628f4a64db4fe9d0392010f553c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 10 Feb 2025 02:02:43 -0600 Subject: [PATCH 15/37] Whitespace fixes. --- Zend/tests/pipe_operator/call_by_ref.phpt | 2 +- Zend/tests/pipe_operator/void_return.phpt | 2 +- Zend/zend_compile.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Zend/tests/pipe_operator/call_by_ref.phpt b/Zend/tests/pipe_operator/call_by_ref.phpt index 4a504b1da70b8..026089b0a6547 100644 --- a/Zend/tests/pipe_operator/call_by_ref.phpt +++ b/Zend/tests/pipe_operator/call_by_ref.phpt @@ -34,4 +34,4 @@ try { ?> --EXPECTF-- _modify(): Argument #1 ($a) could not be passed by reference -_append(): Argument #1 ($a) could not be passed by reference \ No newline at end of file +_append(): Argument #1 ($a) could not be passed by reference diff --git a/Zend/tests/pipe_operator/void_return.phpt b/Zend/tests/pipe_operator/void_return.phpt index 6173e616c31cc..0dbba519297cd 100644 --- a/Zend/tests/pipe_operator/void_return.phpt +++ b/Zend/tests/pipe_operator/void_return.phpt @@ -18,4 +18,4 @@ catch (Throwable $e) { ?> --EXPECT-- -TypeError: strlen(): Argument #1 ($string) must be of type string, null given \ No newline at end of file +TypeError: strlen(): Argument #1 ($string) must be of type string, null given diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6c29d21f686fb..d89997cfe8280 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11631,7 +11631,7 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ return; case ZEND_AST_PIPE: zend_compile_pipe(result, ast); - return; + return; default: ZEND_ASSERT(0 /* not supported */); } From ceb45893dc549ec6b824c3cf38319fb0ced22ab9 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 12 Feb 2025 22:18:25 -0600 Subject: [PATCH 16/37] Add single quotes for consistency. --- Zend/zend_language_parser.y | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 7e0cb88fb10a7..ff54ac68ba6db 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -237,7 +237,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_COALESCE "'??'" %token T_POW "'**'" %token T_POW_EQUAL "'**='" -%token T_PIPE "|>" +%token T_PIPE "'|>'" /* We need to split the & token in two to avoid a shift/reduce conflict. For T1&$v and T1&T2, * with only one token lookahead, bison does not know whether to reduce T1 as a complete type, * or shift to continue parsing an intersection type. */ From e7890e393741e5e178235a00933e6f044426bdb7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Feb 2025 00:10:15 -0600 Subject: [PATCH 17/37] Increase precedence of pipe operator. --- .../pipe_operator/precedence_addition.phpt | 1 + .../pipe_operator/precedence_coalesce.phpt | 16 +++++++------- .../pipe_operator/precedence_comparison.phpt | 15 ++----------- .../pipe_operator/precedence_ternary.phpt | 22 +++++++------------ Zend/zend_language_parser.y | 2 +- 5 files changed, 20 insertions(+), 36 deletions(-) diff --git a/Zend/tests/pipe_operator/precedence_addition.phpt b/Zend/tests/pipe_operator/precedence_addition.phpt index 3b3269f52fad1..aa5782e42e086 100644 --- a/Zend/tests/pipe_operator/precedence_addition.phpt +++ b/Zend/tests/pipe_operator/precedence_addition.phpt @@ -9,6 +9,7 @@ function _test1(int $a): int { $bad_func = null; +// This should add 5+2 first, then pipe that to _test1. $res1 = 5 + 2 |> '_test1'; var_dump($res1); diff --git a/Zend/tests/pipe_operator/precedence_coalesce.phpt b/Zend/tests/pipe_operator/precedence_coalesce.phpt index 812075d8a75ad..daf699d9fe85d 100644 --- a/Zend/tests/pipe_operator/precedence_coalesce.phpt +++ b/Zend/tests/pipe_operator/precedence_coalesce.phpt @@ -1,17 +1,17 @@ --TEST-- -Pipe binds lower than coalesce +Pipe binds higher than coalesce --FILE-- get_username(...) + ?? 'default'; -$res1 = 5 |> $bad_func ?? '_test1'; - -var_dump($res1); +var_dump($user); ?> --EXPECT-- -int(10) +string(1) "5" diff --git a/Zend/tests/pipe_operator/precedence_comparison.phpt b/Zend/tests/pipe_operator/precedence_comparison.phpt index 19bded49f4ce5..84f71b74058a0 100644 --- a/Zend/tests/pipe_operator/precedence_comparison.phpt +++ b/Zend/tests/pipe_operator/precedence_comparison.phpt @@ -7,21 +7,10 @@ function _test1(int $a): int { return $a * 2; } -$res1 = (5 |> _test1(...)) == 10 ; +// The pipe should run before the comparison. +$res1 = 5 |> _test1(...) == 10 ; var_dump($res1); -// This will error without the parens, -// as it will try to compare a closure to an int first. -try { - $res1 = 5 |> _test1(...) == 10 ; -} -catch (Throwable $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; -} - ?> --EXPECTF-- bool(true) - -Notice: Object of class Closure could not be converted to int in %s line %d -Error: Value of type bool is not callable diff --git a/Zend/tests/pipe_operator/precedence_ternary.phpt b/Zend/tests/pipe_operator/precedence_ternary.phpt index 67f2899ce9689..207d15dab9d65 100644 --- a/Zend/tests/pipe_operator/precedence_ternary.phpt +++ b/Zend/tests/pipe_operator/precedence_ternary.phpt @@ -1,5 +1,5 @@ --TEST-- -Pipe binds lower than ternary +Pipe binds higher than ternary --FILE-- $config ? _test1(...) : _test2(...); -var_dump($res1); +function is_odd(int $a): bool { + return (bool)($a % 2); +} -// $config is truthy, so the ternary binds first -// and evaluates to the first function. -$config = _test3(...); -$res2 = 5 |> $config ? _test1(...) : _test2(...); -var_dump($res2); +$res1 = 5 |> is_odd(...) ? 'odd' : 'even'; +var_dump($res1); -// Binding the ternary first doesn't make logical sense, -// so the pipe runs first in this case. +// The pipe binds first, resulting in bool ? int : string, which is well-understood. $x = true; $y = 'beep'; $z = 'default'; @@ -37,6 +32,5 @@ var_dump($ret3); ?> --EXPECT-- -int(10) -int(6) +string(3) "odd" int(4) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index ff54ac68ba6db..7c4183ef73609 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -62,7 +62,6 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %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 -%left T_PIPE %left '?' ':' %right T_COALESCE %left T_BOOLEAN_OR @@ -72,6 +71,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %left T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG %nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP %nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL +%left T_PIPE %left '.' %left T_SL T_SR %left '+' '-' From 392d8b246d68de29df33f9bd53f9bb946cf40044 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 27 Feb 2025 15:36:12 -0600 Subject: [PATCH 18/37] Remove dead code. --- Zend/tests/pipe_operator/precedence_addition.phpt | 2 -- 1 file changed, 2 deletions(-) diff --git a/Zend/tests/pipe_operator/precedence_addition.phpt b/Zend/tests/pipe_operator/precedence_addition.phpt index aa5782e42e086..4fd64639b4518 100644 --- a/Zend/tests/pipe_operator/precedence_addition.phpt +++ b/Zend/tests/pipe_operator/precedence_addition.phpt @@ -7,8 +7,6 @@ function _test1(int $a): int { return $a * 2; } -$bad_func = null; - // This should add 5+2 first, then pipe that to _test1. $res1 = 5 + 2 |> '_test1'; From c15c082ec5540a4b85b137f59b26b70e519245a2 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:37:05 -0600 Subject: [PATCH 19/37] Add test for generator and pipe behavior. --- Zend/tests/pipe_operator/generators.phpt | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Zend/tests/pipe_operator/generators.phpt diff --git a/Zend/tests/pipe_operator/generators.phpt b/Zend/tests/pipe_operator/generators.phpt new file mode 100644 index 0000000000000..9607af581fcdc --- /dev/null +++ b/Zend/tests/pipe_operator/generators.phpt @@ -0,0 +1,30 @@ +--TEST-- +Generators +--FILE-- + map_incr(...) |> iterator_to_array(...); + +var_dump($result); +?> +--EXPECT-- +array(3) { + [0]=> + int(2) + [1]=> + int(3) + [2]=> + int(4) +} From 9cd04b4423240dd6e54bbfada4af76b8d99053d2 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:42:42 -0600 Subject: [PATCH 20/37] Add regenerated file. --- ext/tokenizer/tokenizer_data_arginfo.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index 61f6ac1ec3659..49f5bfeaa7b63 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 the .stub.php file instead. - * Stub hash: d917cab61a2b436a16d2227cdb438add45e42d69 */ + * Stub hash: fb68c3145dbbc92b99a8b107b861a6e5de46b591 */ static void register_tokenizer_data_symbols(int module_number) { @@ -150,6 +150,7 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_COALESCE", T_COALESCE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_POW", T_POW, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_POW_EQUAL", T_POW_EQUAL, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PIPE", T_PIPE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG", T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG", T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_BAD_CHARACTER", T_BAD_CHARACTER, CONST_PERSISTENT); From 8cf43d85b9c7a1615bb070d45c4d3109b689d507 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:44:30 -0600 Subject: [PATCH 21/37] Add test for complex ordering. --- .../tests/pipe_operator/complex_ordering.phpt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Zend/tests/pipe_operator/complex_ordering.phpt diff --git a/Zend/tests/pipe_operator/complex_ordering.phpt b/Zend/tests/pipe_operator/complex_ordering.phpt new file mode 100644 index 0000000000000..49f3d47ade4b9 --- /dev/null +++ b/Zend/tests/pipe_operator/complex_ordering.phpt @@ -0,0 +1,20 @@ +--TEST-- +Functions are executed in the expected order +--FILE-- + (bar() ? baz(...) : quux(...)) + |> var_dump(...); + +?> +--EXPECT-- +foo +bar +quux +int(1) From a264deb82ad750373de5bb5ed8ae9b1e12facb92 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:54:31 -0600 Subject: [PATCH 22/37] Add test for exceptions. --- .../pipe_operator/exception_interruption.phpt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Zend/tests/pipe_operator/exception_interruption.phpt diff --git a/Zend/tests/pipe_operator/exception_interruption.phpt b/Zend/tests/pipe_operator/exception_interruption.phpt new file mode 100644 index 0000000000000..5cd7dcb8cc7f6 --- /dev/null +++ b/Zend/tests/pipe_operator/exception_interruption.phpt @@ -0,0 +1,37 @@ +--TEST-- +A pipe interrupted by an exception, to demonstrate correct order of execution. +--FILE-- + (bar() ? baz(...) : quux(...)) + |> var_dump(...); +} +catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + $result = foo() + |> (throw new Exception('Break')) + |> (bar() ? baz(...) : quux(...)) + |> var_dump(...); +} +catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +foo +bar +quux +Exception: Oops +foo +Exception: Break From d5a5dc8d18a9f3d5f5399248f2de63301b540cdc Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 14:03:39 -0600 Subject: [PATCH 23/37] Use BINARY_OP macro for printing Pipe AST. --- Zend/tests/pipe_operator/ast.phpt | 15 +++++++++++++-- Zend/zend_ast.c | 6 +----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index a2b274f9b200c..2307c6ab5a466 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -7,6 +7,10 @@ function _test(int $a): int { return $a + 1; } +function abool(int $x): bool { + return false; +} + try { assert((5 |> '_test') == 99); } catch (AssertionError $e) { @@ -19,7 +23,14 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + assert(5 |> abool(...)); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + ?> --EXPECTF-- -assert(5 |> '_test' == 99) -assert(5 |> _test(...) == 99) +assert((5 |> '_test') == 99) +assert((5 |> _test(...)) == 99) +assert(5 |> abool(...)) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index e5152deaf1d98..bcdd048c2e8be 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2296,6 +2296,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio case ZEND_AST_GREATER_EQUAL: BINARY_OP(" >= ", 180, 181, 181); case ZEND_AST_AND: BINARY_OP(" && ", 130, 130, 131); case ZEND_AST_OR: BINARY_OP(" || ", 120, 120, 121); + case ZEND_AST_PIPE: BINARY_OP(" |> ", 130, 130, 131); case ZEND_AST_ARRAY_ELEM: if (ast->child[1]) { zend_ast_export_ex(str, ast->child[1], 80, indent); @@ -2506,11 +2507,6 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_name(str, ast->child[1], 0, indent); } break; - case ZEND_AST_PIPE: - zend_ast_export_ex(str, ast->child[0], 0, indent); - smart_str_appends(str, " |> "); - zend_ast_export_ex(str, ast->child[1], 0, indent); - break; case ZEND_AST_NAMED_ARG: smart_str_append(str, zend_ast_get_str(ast->child[0])); smart_str_appends(str, ": "); From 94f737e15ada48bf92accd9de589e68188f66849 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 10:31:24 -0500 Subject: [PATCH 24/37] Improve tests for AST printing, still doesn't work. --- Zend/tests/pipe_operator/ast.phpt | 78 +++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index 2307c6ab5a466..a43ac54690488 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -1,8 +1,8 @@ --TEST-- -Test that a pipe operator displays as a pipe operator when outputting syntax. +A pipe operator displays as a pipe operator when outputting syntax, with correct parens. --FILE-- getMessage(), PHP_EOL; } +*/ + +print "Concat, which binds higher\n"; + +try { + assert(false && foo() . bar() |> baz() . quux()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && (foo() . bar()) |> baz() . quux()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && foo() . (bar() |> baz()) . quux()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && foo() . bar() |> (baz() . quux())); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && (foo() . bar() |> baz()) . quux()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && foo() . (bar() |> baz() . quux())); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +print "<, which binds lower\n"; + +try { + assert(false && foo() < bar() |> baz()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + // Currently wrong + assert(false && (foo() < bar()) |> baz()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && foo() < (bar() |> baz())); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} ?> --EXPECTF-- -assert((5 |> '_test') == 99) -assert((5 |> _test(...)) == 99) -assert(5 |> abool(...)) +Concat, which binds higher +assert(false && (foo() . bar() |> baz() . quux())) +assert(false && (foo() . bar() |> baz() . quux())) +assert(false && foo() . (bar() |> baz()) . quux()) +assert(false && (foo() . bar() |> baz() . quux())) +assert(false && (foo() . bar() |> baz()) . quux()) +assert(false && foo() . (bar() |> baz() . quux())) +<, which binds lower +assert(false && foo() < bar() |> baz()) +assert(false && (foo() < bar()) |> baz()) +assert(false && foo() < (bar() |> baz())) From ef2d1a0bf17ee137f0ccb7cd2d8af0ba0e91d896 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:00:29 -0500 Subject: [PATCH 25/37] Improved optimzations and namespace handling, courtesy Arnaud. --- Zend/zend_compile.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d89997cfe8280..15654cf9c309b 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6422,19 +6422,29 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) znode wrapped_operand_result; zend_emit_op_tmp(&wrapped_operand_result, ZEND_QM_ASSIGN, &operand_result, NULL); + 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) { - callable_ast = 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_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); } - znode callable_result; - zend_compile_expr(&callable_result, callable_ast); - - zend_ast *fcall_ast = zend_ast_create(ZEND_AST_CALL, - zend_ast_create_znode(&callable_result), - zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create_znode(&wrapped_operand_result))); - zend_compile_expr(result, fcall_ast); } From 59873a7b28f2f1106fe0768af26ab0560a493d4f Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:39:22 -0500 Subject: [PATCH 26/37] Improve docs. --- Zend/zend_compile.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 15654cf9c309b..8077692a6b384 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6417,14 +6417,20 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) zend_ast *operand_ast = ast->child[0]; zend_ast *callable_ast = ast->child[1]; + /* Compile the left hand side down to a value first. */ znode operand_result; zend_compile_expr(&operand_result, operand_ast); + + /* Wrap the value in a ZEND_QM_ASSIGN opcode to ensure references + * always fail. Otherwise, they'd only fail in complex cases like arrays. + */ znode wrapped_operand_result; zend_emit_op_tmp(&wrapped_operand_result, ZEND_QM_ASSIGN, &operand_result, NULL); + /* 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; + zend_ast *fcall_ast; znode callable_result; /* Turn $foo |> bar(...) into bar($foo). */ @@ -6436,13 +6442,13 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) } 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); + 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); + callable_ast, arg_list_ast); } zend_compile_expr(result, fcall_ast); From 2beea97ef3e7668fa80a0be6c4929202679929bb Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:39:39 -0500 Subject: [PATCH 27/37] Ensure higher order functions stil work. --- Zend/tests/pipe_operator/mixed_callable_call.phpt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt index 0ed5689ef51fa..93f5fb99bae9c 100644 --- a/Zend/tests/pipe_operator/mixed_callable_call.phpt +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -31,6 +31,11 @@ function _double(int $x): int { return $x * 2; } +function multiplier(int $x): \Closure +{ + return fn($y) => $x * $y; +} + $test = new _Test(); $add3 = fn($x) => _add($x, 3); @@ -41,9 +46,10 @@ $res1 = 2 |> $add3 |> fn($x) => _area($x, 2) |> _double(...) + |> multiplier(3) ; var_dump($res1); ?> --EXPECT-- -int(40) +int(120) From 5700cb20a6d49e7ae04921bdd4c38d165c00daf7 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 18 Mar 2025 18:29:53 +0100 Subject: [PATCH 28/37] AST: Update priorities for ZEND_AST_PIPE Puts ZEND_AST_PIPE above comparison operators, and below math operators. --- Zend/tests/pipe_operator/ast.phpt | 11 +++++------ Zend/zend_ast.c | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index a43ac54690488..9fd40b8142b42 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -77,7 +77,6 @@ try { } try { - // Currently wrong assert(false && (foo() < bar()) |> baz()); } catch (AssertionError $e) { echo $e->getMessage(), PHP_EOL; @@ -90,15 +89,15 @@ try { } ?> ---EXPECTF-- +--EXPECT-- Concat, which binds higher -assert(false && (foo() . bar() |> baz() . quux())) -assert(false && (foo() . bar() |> baz() . quux())) +assert(false && foo() . bar() |> baz() . quux()) +assert(false && foo() . bar() |> baz() . quux()) assert(false && foo() . (bar() |> baz()) . quux()) -assert(false && (foo() . bar() |> baz() . quux())) +assert(false && foo() . bar() |> baz() . quux()) assert(false && (foo() . bar() |> baz()) . quux()) assert(false && foo() . (bar() |> baz() . quux())) <, which binds lower assert(false && foo() < bar() |> baz()) assert(false && (foo() < bar()) |> baz()) -assert(false && foo() < (bar() |> baz())) +assert(false && foo() < bar() |> baz()) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index bcdd048c2e8be..eac3e50f56398 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2296,7 +2296,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio case ZEND_AST_GREATER_EQUAL: BINARY_OP(" >= ", 180, 181, 181); case ZEND_AST_AND: BINARY_OP(" && ", 130, 130, 131); case ZEND_AST_OR: BINARY_OP(" || ", 120, 120, 121); - case ZEND_AST_PIPE: BINARY_OP(" |> ", 130, 130, 131); + case ZEND_AST_PIPE: BINARY_OP(" |> ", 183, 183, 184); case ZEND_AST_ARRAY_ELEM: if (ast->child[1]) { zend_ast_export_ex(str, ast->child[1], 80, indent); From f7027e7b689fdce18eaf7b13b75fd87bcd0e4679 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:41:37 -0500 Subject: [PATCH 29/37] Remove vestigial comment. --- Zend/tests/pipe_operator/exception_interruption.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/exception_interruption.phpt b/Zend/tests/pipe_operator/exception_interruption.phpt index 5cd7dcb8cc7f6..6711b652e7c6b 100644 --- a/Zend/tests/pipe_operator/exception_interruption.phpt +++ b/Zend/tests/pipe_operator/exception_interruption.phpt @@ -6,7 +6,7 @@ A pipe interrupted by an exception, to demonstrate correct order of execution. function foo() { echo __FUNCTION__, PHP_EOL; return 1; } function bar() { echo __FUNCTION__, PHP_EOL; return false; } function baz($in) { echo __FUNCTION__, PHP_EOL; return $in; } -function quux($in) { echo __FUNCTION__, PHP_EOL; throw new \Exception('Oops'); } // This is line 6. +function quux($in) { echo __FUNCTION__, PHP_EOL; throw new \Exception('Oops'); } try { $result = foo() From ee3d107370ed04f8ab955bd6bb98a20d49ab7b0a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 12:13:03 -0500 Subject: [PATCH 30/37] Add test for namespaced functions. --- .../pipe_operator/namespaced_functions.phpt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Zend/tests/pipe_operator/namespaced_functions.phpt diff --git a/Zend/tests/pipe_operator/namespaced_functions.phpt b/Zend/tests/pipe_operator/namespaced_functions.phpt new file mode 100644 index 0000000000000..2f9def7582282 --- /dev/null +++ b/Zend/tests/pipe_operator/namespaced_functions.phpt @@ -0,0 +1,19 @@ +--TEST-- +Pipe operator handles all callable styles +--FILE-- + test(...); +} +?> +--EXPECT-- +5 From 10fd080e955eaaa690615e2f64f3ae667a12bc83 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:01:39 -0500 Subject: [PATCH 31/37] Remove dead code --- Zend/tests/pipe_operator/ast.phpt | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index 9fd40b8142b42..f54ea070e0c12 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -2,33 +2,6 @@ A pipe operator displays as a pipe operator when outputting syntax, with correct parens. --FILE-- '_test') == 99); -} catch (AssertionError $e) { - echo $e->getMessage(), PHP_EOL; -} - -try { - assert((5 |> _test(...)) == 99); -} catch (AssertionError $e) { - echo $e->getMessage(), PHP_EOL; -} - -try { - assert(5 |> abool(...)); -} catch (AssertionError $e) { - echo $e->getMessage(), PHP_EOL; -} -*/ print "Concat, which binds higher\n"; From 55df27c4e81cfa20be45f992a67c17e273dd240a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:01:48 -0500 Subject: [PATCH 32/37] More namespace tests. --- Zend/tests/pipe_operator/namespaced_functions.phpt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/namespaced_functions.phpt b/Zend/tests/pipe_operator/namespaced_functions.phpt index 2f9def7582282..0c2b1ab52db77 100644 --- a/Zend/tests/pipe_operator/namespaced_functions.phpt +++ b/Zend/tests/pipe_operator/namespaced_functions.phpt @@ -5,7 +5,7 @@ Pipe operator handles all callable styles namespace Beep { function test(int $x) { - print $x; + echo $x, PHP_EOL; } } @@ -13,7 +13,10 @@ namespace Bar { use function \Beep\test; 5 |> test(...); + + 5 |> \Beep\test(...); } ?> --EXPECT-- 5 +5 From d275216d0b964cc76b4975ac168807f04036a24c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:07:28 -0500 Subject: [PATCH 33/37] Add test for static method FCC. --- Zend/tests/pipe_operator/mixed_callable_call.phpt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt index 93f5fb99bae9c..27f2b178335d2 100644 --- a/Zend/tests/pipe_operator/mixed_callable_call.phpt +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -27,6 +27,12 @@ class _Test } } +class StaticTest { + public static function oneMore(int $x): int { + return $x + 1; + } +} + function _double(int $x): int { return $x * 2; } @@ -47,9 +53,10 @@ $res1 = 2 |> fn($x) => _area($x, 2) |> _double(...) |> multiplier(3) + |> StaticTest::oneMore(...) ; var_dump($res1); ?> --EXPECT-- -int(120) +int(121) From 38f1309710ca8cc27a71ca8b6edacf9e5abbd3a1 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:12:13 -0500 Subject: [PATCH 34/37] Add more AST examples. --- Zend/tests/pipe_operator/ast.phpt | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Zend/tests/pipe_operator/ast.phpt b/Zend/tests/pipe_operator/ast.phpt index f54ea070e0c12..e8c088dabfdbd 100644 --- a/Zend/tests/pipe_operator/ast.phpt +++ b/Zend/tests/pipe_operator/ast.phpt @@ -61,6 +61,34 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + assert(false && foo() |> bar() < baz()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && (foo() |> bar()) < baz()); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && foo() |> (bar() < baz())); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + + + +print "misc examples\n"; + +try { + assert(false && foo() |> (bar() |> baz(...))); +} catch (AssertionError $e) { + echo $e->getMessage(), PHP_EOL; +} + ?> --EXPECT-- Concat, which binds higher @@ -74,3 +102,8 @@ assert(false && foo() . (bar() |> baz() . quux())) assert(false && foo() < bar() |> baz()) assert(false && (foo() < bar()) |> baz()) assert(false && foo() < bar() |> baz()) +assert(false && foo() |> bar() < baz()) +assert(false && foo() |> bar() < baz()) +assert(false && foo() |> (bar() < baz())) +misc examples +assert(false && foo() |> (bar() |> baz(...))) From d1115a8fd44f8de96b6e2fc181ab76d23721c6a7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 11:36:19 -0500 Subject: [PATCH 35/37] Optimize static method calls in pipes. --- Zend/zend_compile.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 8077692a6b384..3c86cdb95ee31 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6438,6 +6438,11 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) { 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) { From d717f25559e3df1aea88b120dea3a47f1cf8be78 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 12:54:42 -0500 Subject: [PATCH 36/37] Typo fix. --- Zend/zend_compile.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3c86cdb95ee31..e7964ce1b38e7 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6438,7 +6438,7 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) { fcall_ast = zend_ast_create(ZEND_AST_CALL, callable_ast->child[0], arg_list_ast); - /* Turn $foo |> bar::>baz(...) into bar::baz($foo). */ + /* 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, From 9f9e0540350ecf52da833546c6c88872b07d9c97 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 13:17:22 -0500 Subject: [PATCH 37/37] Add tests for pipe optimizations. --- Zend/tests/pipe_operator/optimizations.phpt | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 Zend/tests/pipe_operator/optimizations.phpt diff --git a/Zend/tests/pipe_operator/optimizations.phpt b/Zend/tests/pipe_operator/optimizations.phpt new file mode 100644 index 0000000000000..48d8a7dfad1c8 --- /dev/null +++ b/Zend/tests/pipe_operator/optimizations.phpt @@ -0,0 +1,89 @@ +--TEST-- +Pipe operator optimizes away most callables +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.opt_debug_level=0x20000 +--EXTENSIONS-- +opcache +--FILE-- + _test1(...) + |> $o->foo(...) + |> Other::bar(...) +; + +var_dump($res1); +?> +--EXPECTF-- +$_main: + ; (lines=18, args=0, vars=2, tmps=2) + ; (after optimizer) + ; %s:1-27 +0000 V2 = NEW 0 string("Other") +0001 DO_FCALL +0002 ASSIGN CV0($o) V2 +0003 INIT_FCALL 1 112 string("_test1") +0004 SEND_VAL int(5) 1 +0005 T2 = DO_UCALL +0006 INIT_METHOD_CALL 1 CV0($o) string("foo") +0007 SEND_VAL_EX T2 1 +0008 V3 = DO_FCALL +0009 T2 = QM_ASSIGN V3 +0010 INIT_STATIC_METHOD_CALL 1 string("Other") string("bar") +0011 SEND_VAL T2 1 +0012 V2 = DO_UCALL +0013 ASSIGN CV1($res1) V2 +0014 INIT_FCALL 1 96 string("var_dump") +0015 SEND_VAR CV1($res1) 1 +0016 DO_ICALL +0017 RETURN int(1) +LIVE RANGES: + 2: 0001 - 0002 (new) + 2: 0010 - 0011 (tmp/var) + +_test1: + ; (lines=4, args=1, vars=1, tmps=1) + ; (after optimizer) + ; %s:3-5 +0000 CV0($a) = RECV 1 +0001 T1 = ADD CV0($a) int(1) +0002 VERIFY_RETURN_TYPE T1 +0003 RETURN T1 + +Other::foo: + ; (lines=4, args=1, vars=1, tmps=1) + ; (after optimizer) + ; %s:8-10 +0000 CV0($a) = RECV 1 +0001 T1 = ADD CV0($a) CV0($a) +0002 VERIFY_RETURN_TYPE T1 +0003 RETURN T1 + +Other::bar: + ; (lines=4, args=1, vars=1, tmps=1) + ; (after optimizer) + ; %s:12-14 +0000 CV0($a) = RECV 1 +0001 T1 = SUB CV0($a) int(1) +0002 VERIFY_RETURN_TYPE T1 +0003 RETURN T1 +int(11)