From 9585c04378be672ebfcae83f997ccd0feedb3ddf Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 11 Apr 2020 16:44:10 -0500 Subject: [PATCH 01/40] 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 8bdd29c5512cc..f12f73c4ef066 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2425,10 +2425,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 62d0fbcded2ee..745e831042c78 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1040,6 +1040,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 08b2ac6b3f39b..8b8197c600ede 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 @@ -237,6 +238,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. */ @@ -1292,6 +1294,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 4c883b81c5f7d..5e377249422a5 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1861,6 +1861,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 a1e131032bcfb..0900c51d3d95a 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -173,6 +173,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 c1e1fd254dfaa..57c8edad8acb6 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -742,6 +742,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 529ba112d84b86e9f8130f85658b30fbfba96093 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 12 Dec 2024 21:48:40 -0600 Subject: [PATCH 02/40] 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 0988adcc7c885fe51958e33b3ad78f3b0b2780c1 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 29 Dec 2024 22:54:26 -0600 Subject: [PATCH 03/40] 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 f12f73c4ef066..8bdd29c5512cc 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2425,17 +2425,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 9348c35f6cc07..c82ca66c9f573 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -154,6 +154,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 0669d106f15e9..c56e601489527 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6613,6 +6613,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]; @@ -11769,6 +11793,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 745e831042c78..62d0fbcded2ee 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1040,7 +1040,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 8b8197c600ede..658f57c174129 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1295,7 +1295,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 661236172fe7c2e13d71ebe38a1f4aad24697c78 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 30 Dec 2024 00:09:50 -0600 Subject: [PATCH 04/40] 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 8bdd29c5512cc..07cd1ab7e3760 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2720,6 +2720,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 499b1116cabb1590c6b17f122745d36a027b4568 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 30 Dec 2024 00:10:30 -0600 Subject: [PATCH 05/40] 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 b04e422a1078fcccd5aef920a574a67bfc75354e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 14 Jan 2025 13:10:02 -0600 Subject: [PATCH 06/40] 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 c56e601489527..679ae50061290 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6427,6 +6427,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]; @@ -6613,30 +6639,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 4dbf197b2e53ac31ce27b0b25eab2a0f18728741 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 8 Feb 2025 14:10:01 +0000 Subject: [PATCH 07/40] 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 97db4bc1f758382d0b6e9597760968b8130013f4 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:25:55 -0600 Subject: [PATCH 08/40] 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 f07fea2fd481bd861917ea8fca385b61b129abe0 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:29:33 -0600 Subject: [PATCH 09/40] 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 1443311a9d494143ab5407c05cb6c8532aab794c Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 8 Feb 2025 23:34:21 -0600 Subject: [PATCH 10/40] 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 3b6a9b38c3a131f8541d4103fdf0ae473a4be6fa Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:25:10 -0600 Subject: [PATCH 11/40] 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 46be67d9ad0cb70dbc4290cb54141827b6a526f4 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:28:17 -0600 Subject: [PATCH 12/40] 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 77af08790377c9f0c15b79135eae2675a74bcd1a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sun, 9 Feb 2025 22:36:54 -0600 Subject: [PATCH 13/40] 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 d3951813285e932ad1874ae0c9e447db990c8064 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 10 Feb 2025 01:58:28 -0600 Subject: [PATCH 14/40] 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 c8e00512536d44c6db3f4213d9daa420097cc41e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Mon, 10 Feb 2025 02:02:43 -0600 Subject: [PATCH 15/40] 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 679ae50061290..54c0d79832626 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11797,7 +11797,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 4ddf259991e4b5ca7e94e26223b462f52e425744 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 12 Feb 2025 22:18:25 -0600 Subject: [PATCH 16/40] 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 658f57c174129..a3732f7fa7058 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -238,7 +238,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 cf26b9feb900ac5d3acae223cfadb3278b030c53 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 26 Feb 2025 00:10:15 -0600 Subject: [PATCH 17/40] 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 a3732f7fa7058..816b8126cbf25 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 c02447c5f831f4e2937bc02a1e0663f529072fe8 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 27 Feb 2025 15:36:12 -0600 Subject: [PATCH 18/40] 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 ec945306597295758bd0ab5b251d9169c19d58dc Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:37:05 -0600 Subject: [PATCH 19/40] 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 f1cb86dfdc904514b7d5587330de57a69c1fc6c7 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:42:42 -0600 Subject: [PATCH 20/40] 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 9c488d19f1890..3a3cdaa468133 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: 19d25d22098f46283b517352cbb302db962b50fd */ + * Stub hash: c5235344b7c651d27c2c33c90696a418a9c96837 */ static void register_tokenizer_data_symbols(int module_number) { @@ -151,6 +151,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 0d9e407bc9d404289fb9d149c7897f384e70cb5b Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:44:30 -0600 Subject: [PATCH 21/40] 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 c93283724c97d5e91ae27634c96504a4b2962b4a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 13:54:31 -0600 Subject: [PATCH 22/40] 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 d6082507f4bd6b723e25ef22ff5f16822964e502 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 6 Mar 2025 14:03:39 -0600 Subject: [PATCH 23/40] 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 07cd1ab7e3760..dce91c00c7a2b 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2510,6 +2510,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); @@ -2720,11 +2721,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 241dda7eb3781418ddd396f5bb44283882d7d859 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 10:31:24 -0500 Subject: [PATCH 24/40] 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 63c7b63ed4e904c2abb9dd73dd370cca3f63cf5e Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:00:29 -0500 Subject: [PATCH 25/40] 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 54c0d79832626..793d4d3875d1d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6437,19 +6437,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 d7b7a99fa345575cb5c4540710e55a2a7a731463 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:39:22 -0500 Subject: [PATCH 26/40] 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 793d4d3875d1d..580fbd097c83a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6432,14 +6432,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). */ @@ -6451,13 +6457,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 82bae3c1b56f30a9b0980cea960017474181dabc Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:39:39 -0500 Subject: [PATCH 27/40] 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 0d432c640410a5ee7339fe151c34cfe7cd67d03c Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 18 Mar 2025 18:29:53 +0100 Subject: [PATCH 28/40] 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 dce91c00c7a2b..ec861b3ff6bf5 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2510,7 +2510,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 93ea77bd0b273f81e54a0a54db4f6f493b01aa3f Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 11:41:37 -0500 Subject: [PATCH 29/40] 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 d53cade68febb10297eae5af7068571e7c7176c8 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Tue, 18 Mar 2025 12:13:03 -0500 Subject: [PATCH 30/40] 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 7b13753156e40b5d1c41cb15093d00e1256401e8 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:01:39 -0500 Subject: [PATCH 31/40] 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 036cf3cf738c75c3545bc560f3233c8fc81be5b5 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:01:48 -0500 Subject: [PATCH 32/40] 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 cfa3e3f2af82aaaa35ac653ad1861a73a0552623 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:07:28 -0500 Subject: [PATCH 33/40] 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 2ba6512f1dcef13754d6aff4fd8e04fdfffedf17 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 00:12:13 -0500 Subject: [PATCH 34/40] 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 7b6365d39cd694e4b6bc835b2aa640052bf9b9a0 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 11:36:19 -0500 Subject: [PATCH 35/40] 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 580fbd097c83a..44fe8e4d6d932 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6453,6 +6453,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 ba646f538b7c488b1c83cd8a9f1c8eb0ddcf1f67 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 12:54:42 -0500 Subject: [PATCH 36/40] 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 44fe8e4d6d932..0fdd3f77d0be6 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6453,7 +6453,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 991592498edafea2186f97e5b363e4bcef471d88 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Thu, 20 Mar 2025 13:17:22 -0500 Subject: [PATCH 37/40] 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) From ffa7735baa2727a477641ed0208e6d2668e94b50 Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Wed, 28 May 2025 18:45:33 -0500 Subject: [PATCH 38/40] Make test output more portable. --- Zend/tests/pipe_operator/optimizations.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/tests/pipe_operator/optimizations.phpt b/Zend/tests/pipe_operator/optimizations.phpt index 48d8a7dfad1c8..afdc528337c13 100644 --- a/Zend/tests/pipe_operator/optimizations.phpt +++ b/Zend/tests/pipe_operator/optimizations.phpt @@ -41,7 +41,7 @@ $_main: 0000 V2 = NEW 0 string("Other") 0001 DO_FCALL 0002 ASSIGN CV0($o) V2 -0003 INIT_FCALL 1 112 string("_test1") +0003 INIT_FCALL 1 %d string("_test1") 0004 SEND_VAL int(5) 1 0005 T2 = DO_UCALL 0006 INIT_METHOD_CALL 1 CV0($o) string("foo") @@ -52,7 +52,7 @@ $_main: 0011 SEND_VAL T2 1 0012 V2 = DO_UCALL 0013 ASSIGN CV1($res1) V2 -0014 INIT_FCALL 1 96 string("var_dump") +0014 INIT_FCALL 1 %d string("var_dump") 0015 SEND_VAR CV1($res1) 1 0016 DO_ICALL 0017 RETURN int(1) From af73e57aebb7c2aebbaf6628c241d80ab161846a Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 31 May 2025 01:35:51 -0500 Subject: [PATCH 39/40] Revise all-callables test. --- .../pipe_operator/mixed_callable_call.phpt | 76 +++++++++++-------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt index 27f2b178335d2..13bef986defb7 100644 --- a/Zend/tests/pipe_operator/mixed_callable_call.phpt +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -3,60 +3,72 @@ Pipe operator handles all callable styles --FILE-- $x * $y; + function __invoke(int $x): int + { + return $x * 27; + } } -$test = new _Test(); + +$test = new Test(); $add3 = fn($x) => _add($x, 3); -$res1 = 2 - |> [$test, 'message'] - |> 'strlen' - |> $add3 - |> fn($x) => _area($x, 2) - |> _double(...) - |> multiplier(3) - |> StaticTest::oneMore(...) +$res1 = 1 + |> times3(...) + |> 'times5' + |> $test->times7(...) + |> [$test, 'times11'] + |> StaticTest::times13(...) + |> [StaticTest::class, 'times17'] + |> fn($x) => times23($x) + |> new Times27() ; var_dump($res1); ?> --EXPECT-- -int(121) +int(158513355) From c4dcbae1f23527b45a7fbe62a22e685495288cbf Mon Sep 17 00:00:00 2001 From: Larry Garfield Date: Sat, 31 May 2025 01:40:53 -0500 Subject: [PATCH 40/40] Formatting fix. --- Zend/zend_compile.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0fdd3f77d0be6..1deb81ca2a4cd 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6437,8 +6437,7 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) 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. - */ + * 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);