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 e222e4bd74019..d0002c5419e7a 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1861,10 +1861,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_CLASS_CONST: zend_ast_export_ns_name(str, ast->child[0], 0, indent); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 76405a3689dc9..2819d9d3bc7f6 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -922,6 +922,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); /* These should not clash with ZEND_ACC_(PUBLIC|PROTECTED|PRIVATE) */ #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 e783a7d9aa1e7..fc89e8ad3c9b2 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 @@ -231,6 +232,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_COALESCE "'??'" %token T_POW "'**'" %token T_POW_EQUAL "'**='" +%token T_PIPE "|>" %token T_BAD_CHARACTER "invalid character" /* Token used to force a parse error from the lexer */ @@ -1114,6 +1116,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 b127c3bc6b80c..eb45d14bf3e97 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1826,6 +1826,10 @@ NEWLINE ("\r"|"\n"|"\r\n") 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 db5a425406ac6..da28322ef6c39 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -167,6 +167,7 @@ void tokenizer_register_constants(INIT_FUNC_ARGS) { REGISTER_LONG_CONSTANT("T_COALESCE", T_COALESCE, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_POW", T_POW, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_POW_EQUAL", T_POW_EQUAL, CONST_CS | CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_PIPE", T_PIPE, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_BAD_CHARACTER", T_BAD_CHARACTER, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_DOUBLE_COLON", T_PAAMAYIM_NEKUDOTAYIM, CONST_CS | CONST_PERSISTENT); } @@ -317,6 +318,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_BAD_CHARACTER: return "T_BAD_CHARACTER"; }