From 9c7e34bd56e2db961ff9750b59eb55d7681c493a Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 3 Feb 2025 10:59:44 +0300 Subject: [PATCH 1/4] feat: allow short function declaration --- .../short_function_declaration.phpt | 39 ++++++++++++++++++ .../short_method_declaration.phpt | 40 +++++++++++++++++++ .../short_method_declaration2.phpt | 14 +++++++ Zend/zend_language_parser.y | 14 +++++-- 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/short_function/short_function_declaration.phpt create mode 100644 Zend/tests/short_function/short_method_declaration.phpt create mode 100644 Zend/tests/short_function/short_method_declaration2.phpt diff --git a/Zend/tests/short_function/short_function_declaration.phpt b/Zend/tests/short_function/short_function_declaration.phpt new file mode 100644 index 0000000000000..42a99859cb4e6 --- /dev/null +++ b/Zend/tests/short_function/short_function_declaration.phpt @@ -0,0 +1,39 @@ +--TEST-- +Short function declaration +--FILE-- + $with: + return "greater"; + break; + case $what < $with: + return "less"; + break; + case $what == $with: + return "equals"; + break; + default: + throw new Exception("Unreachable statement"); + } +var_dump(compare(1, 2)); +var_dump(compare(20, 10)); +var_dump(compare(5, 5)); + +echo "done"; +?> +--EXPECT-- +123NULL +456 +string(4) "less" +string(7) "greater" +string(6) "equals" +done \ No newline at end of file diff --git a/Zend/tests/short_function/short_method_declaration.phpt b/Zend/tests/short_function/short_method_declaration.phpt new file mode 100644 index 0000000000000..5744e7fbffd2e --- /dev/null +++ b/Zend/tests/short_function/short_method_declaration.phpt @@ -0,0 +1,40 @@ +--TEST-- +Short method function declaration +--FILE-- +proxy->id; + function getName() = $this->proxy->name; + + function setId($value) = $this->proxy->id = $value; + function setName($value) = $this->proxy->name = $value; +} + +$decorated = new stdClass; +$decorated->id = null; +$decorated->name = null; + +$decorator = new Decorator; +$decorator->proxy = $decorated; + +var_dump($decorated); + +$decorator->setId(1); +$decorator->setName('Dmitrii'); + +var_dump($decorated); +?> +--EXPECT-- +object(stdClass)#1 (2) { + ["id"]=> + NULL + ["name"]=> + NULL +} +object(stdClass)#1 (2) { + ["id"]=> + int(1) + ["name"]=> + string(7) "Dmitrii" +} \ No newline at end of file diff --git a/Zend/tests/short_function/short_method_declaration2.phpt b/Zend/tests/short_function/short_method_declaration2.phpt new file mode 100644 index 0000000000000..8229c22eaca19 --- /dev/null +++ b/Zend/tests/short_function/short_method_declaration2.phpt @@ -0,0 +1,14 @@ +--TEST-- +Short method function declaration +--FILE-- +type; + function getTypeName(): string = match($type) { + "variable" => "Variable", + "function_return" => "Function Return Type", + default => "unknown" + }; +} +?> +--EXPECT-- diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index d2a29e670d8bf..8a6df586ea5c5 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -248,7 +248,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); /* Token used to force a parse error from the lexer */ %token T_ERROR -%type top_statement namespace_name name statement function_declaration_statement +%type top_statement namespace_name name statement function_declaration_statement function_body %type class_declaration_statement trait_declaration_statement legacy_namespace_name %type interface_declaration_statement interface_extends_list %type group_use_declaration inline_use_declarations inline_use_declaration @@ -578,9 +578,14 @@ function_name: function_declaration_statement: function returns_ref function_name backup_doc_comment '(' parameter_list ')' return_type - backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $13, $1, $4, - zend_ast_get_str($3), $6, NULL, $11, $8, NULL); CG(extra_fn_flags) = $9; } + backup_fn_flags function_body backup_fn_flags + { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $11, $1, $4, + zend_ast_get_str($3), $6, NULL, $10, $8, NULL); CG(extra_fn_flags) = $9; } +; + +function_body: + '{' inner_statement_list '}' { $$ = $2; } + | '=' statement { $$ = $2; } ; is_reference: @@ -1030,6 +1035,7 @@ absolute_trait_method_reference: method_body: ';' /* abstract method */ { $$ = NULL; } | '{' inner_statement_list '}' { $$ = $2; } + | '=' statement { $$ = $2; } ; property_modifiers: From 33a5943eed33fb05addcc9a2a9c81a76aae42207 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 3 Feb 2025 12:10:51 +0300 Subject: [PATCH 2/4] chore: adjust error message --- .../intersection_types/invalid_types/invalid_nullable_type.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt index 1c35dfdf91c70..8ae052cf285d7 100644 --- a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt +++ b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt @@ -7,4 +7,4 @@ function foo(): ?Countable&Iterator {} ?> --EXPECTF-- -Parse error: syntax error, unexpected token "&", expecting "{" in %s on line %d +Parse error: syntax error, unexpected token "&", expecting "=" or "{" in %s on line %d From a9cf551dff08b785fe830f50f38daae411de85d3 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 28 May 2025 10:23:00 +0300 Subject: [PATCH 3/4] test: allow only expressions --- .../short_function/global_declaration.phpt | 26 +++++++++++++ ...claration.phpt => method_declaration.phpt} | 10 ++--- ...aration2.phpt => method_declaration2.phpt} | 6 +-- .../short_function/possible_declaration.phpt | 11 ++++++ .../short_function_declaration.phpt | 39 ------------------- .../invalid_types/invalid_nullable_type.phpt | 2 +- Zend/zend_compile.c | 2 +- Zend/zend_compile.h | 5 ++- Zend/zend_language_parser.y | 8 ++-- 9 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 Zend/tests/short_function/global_declaration.phpt rename Zend/tests/short_function/{short_method_declaration.phpt => method_declaration.phpt} (66%) rename Zend/tests/short_function/{short_method_declaration2.phpt => method_declaration2.phpt} (56%) create mode 100644 Zend/tests/short_function/possible_declaration.phpt delete mode 100644 Zend/tests/short_function/short_function_declaration.phpt diff --git a/Zend/tests/short_function/global_declaration.phpt b/Zend/tests/short_function/global_declaration.phpt new file mode 100644 index 0000000000000..83de9fd0af593 --- /dev/null +++ b/Zend/tests/short_function/global_declaration.phpt @@ -0,0 +1,26 @@ +--TEST-- +Short global function declaration +--FILE-- + 123; +echo buz() . PHP_EOL; + +function compare(int $what, int $with): string => match(true) { + $what > $with => "greater", + $what < $with => "less", + $what == $with => "equals", + default => throw new Exception("Unreachable statement"), +}; + +var_dump(compare(1, 2)); +var_dump(compare(20, 10)); +var_dump(compare(5, 5)); + +echo "done"; +?> +--EXPECT-- +123 +string(4) "less" +string(7) "greater" +string(6) "equals" +done \ No newline at end of file diff --git a/Zend/tests/short_function/short_method_declaration.phpt b/Zend/tests/short_function/method_declaration.phpt similarity index 66% rename from Zend/tests/short_function/short_method_declaration.phpt rename to Zend/tests/short_function/method_declaration.phpt index 5744e7fbffd2e..00922cb978492 100644 --- a/Zend/tests/short_function/short_method_declaration.phpt +++ b/Zend/tests/short_function/method_declaration.phpt @@ -1,14 +1,14 @@ --TEST-- -Short method function declaration +Short function method declaration --FILE-- proxy->id; - function getName() = $this->proxy->name; + function getId() => $this->proxy->id; + function getName() => $this->proxy->name; - function setId($value) = $this->proxy->id = $value; - function setName($value) = $this->proxy->name = $value; + function setId($value) => $this->proxy->id = $value; + function setName($value) => $this->proxy->name = $value; } $decorated = new stdClass; diff --git a/Zend/tests/short_function/short_method_declaration2.phpt b/Zend/tests/short_function/method_declaration2.phpt similarity index 56% rename from Zend/tests/short_function/short_method_declaration2.phpt rename to Zend/tests/short_function/method_declaration2.phpt index 8229c22eaca19..16953efdad237 100644 --- a/Zend/tests/short_function/short_method_declaration2.phpt +++ b/Zend/tests/short_function/method_declaration2.phpt @@ -1,10 +1,10 @@ --TEST-- -Short method function declaration +Short function method declaration --FILE-- type; - function getTypeName(): string = match($type) { + function getType(): string => $this->type; + function getTypeName(): string => match($type) { "variable" => "Variable", "function_return" => "Function Return Type", default => "unknown" diff --git a/Zend/tests/short_function/possible_declaration.phpt b/Zend/tests/short_function/possible_declaration.phpt new file mode 100644 index 0000000000000..b6a9b20c603cc --- /dev/null +++ b/Zend/tests/short_function/possible_declaration.phpt @@ -0,0 +1,11 @@ +--TEST-- +Test possible declarations for expr&stmt options +--FILE-- + 123; // returns 123 + +var_dump(expr()); + +?> +--EXPECT-- +int(123) \ No newline at end of file diff --git a/Zend/tests/short_function/short_function_declaration.phpt b/Zend/tests/short_function/short_function_declaration.phpt deleted file mode 100644 index 42a99859cb4e6..0000000000000 --- a/Zend/tests/short_function/short_function_declaration.phpt +++ /dev/null @@ -1,39 +0,0 @@ ---TEST-- -Short function declaration ---FILE-- - $with: - return "greater"; - break; - case $what < $with: - return "less"; - break; - case $what == $with: - return "equals"; - break; - default: - throw new Exception("Unreachable statement"); - } -var_dump(compare(1, 2)); -var_dump(compare(20, 10)); -var_dump(compare(5, 5)); - -echo "done"; -?> ---EXPECT-- -123NULL -456 -string(4) "less" -string(7) "greater" -string(6) "equals" -done \ No newline at end of file diff --git a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt index 8ae052cf285d7..9b0f0788f6fa9 100644 --- a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt +++ b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt @@ -7,4 +7,4 @@ function foo(): ?Countable&Iterator {} ?> --EXPECTF-- -Parse error: syntax error, unexpected token "&", expecting "=" or "{" in %s on line %d +Parse error: syntax error, unexpected token "&", expecting "=>" or "{" in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index cb64729806a23..b2b9d4e523377 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8365,7 +8365,7 @@ static zend_op_array *zend_compile_func_decl_ex( zend_compile_closure_uses(uses_ast); } - if (ast->kind == ZEND_AST_ARROW_FUNC && decl->child[2]->kind != ZEND_AST_RETURN) { + if (decl->flags & ZEND_ACC_SHORT_DECLARATION && stmt_ast->kind != ZEND_AST_RETURN) { bool needs_return = true; if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { zend_arg_info *return_info = CG(active_op_array)->arg_info - 1; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 63572ab6623cc..06feca6402423 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -333,7 +333,7 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ -/* Function Flags (unused: 29-30) | | | */ +/* Function Flags (unused: 30) | | | */ /* ============== | | | */ /* | | | */ /* deprecation flag | | | */ @@ -395,6 +395,9 @@ typedef struct _zend_oparray_context { /* has #[\Override] attribute | | | */ #define ZEND_ACC_OVERRIDE (1 << 28) /* | X | | */ /* | | | */ +/* Function returning by reference | | | */ +#define ZEND_ACC_SHORT_DECLARATION (1 << 29) /* | X | | */ +/* | | | */ /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 8a6df586ea5c5..9e3fcd4248260 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -585,7 +585,8 @@ function_declaration_statement: function_body: '{' inner_statement_list '}' { $$ = $2; } - | '=' statement { $$ = $2; } + | T_DOUBLE_ARROW expr ';' + { $$ = $2; CG(extra_fn_flags) |= ZEND_ACC_SHORT_DECLARATION; } ; is_reference: @@ -1035,7 +1036,8 @@ absolute_trait_method_reference: method_body: ';' /* abstract method */ { $$ = NULL; } | '{' inner_statement_list '}' { $$ = $2; } - | '=' statement { $$ = $2; } + | T_DOUBLE_ARROW expr ';' + { $$ = $2; CG(extra_fn_flags) |= ZEND_ACC_SHORT_DECLARATION; } ; property_modifiers: @@ -1347,7 +1349,7 @@ inline_function: $5, $7, $11, $8, NULL); CG(extra_fn_flags) = $9; } | fn returns_ref backup_doc_comment '(' parameter_list ')' return_type T_DOUBLE_ARROW backup_fn_flags backup_lex_pos expr backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12, $1, $3, + { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12 | ZEND_ACC_SHORT_DECLARATION, $1, $3, NULL, $5, NULL, $11, $7, NULL); CG(extra_fn_flags) = $9; } ; From cd2b607dc69ab73b424ba85e063502463740390e Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 28 May 2025 13:56:23 +0300 Subject: [PATCH 4/4] test: allow inline function be short --- .../tests/short_function/inline_function.phpt | 17 +++++++++++ .../short_function/inline_function_deref.phpt | 28 +++++++++++++++++++ Zend/zend_language_parser.y | 14 +++++++--- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/short_function/inline_function.phpt create mode 100644 Zend/tests/short_function/inline_function_deref.phpt diff --git a/Zend/tests/short_function/inline_function.phpt b/Zend/tests/short_function/inline_function.phpt new file mode 100644 index 0000000000000..a18a59a524969 --- /dev/null +++ b/Zend/tests/short_function/inline_function.phpt @@ -0,0 +1,17 @@ +--TEST-- +Inline short declaration function +--FILE-- + 123; + +var_dump($f()); +var_dump((function() => null)()); +var_dump(get_debug_type(function() => null())); +var_dump(get_debug_type((function() => null)())); + +?> +--EXPECT-- +int(123) +NULL +string(7) "Closure" +string(4) "null" diff --git a/Zend/tests/short_function/inline_function_deref.phpt b/Zend/tests/short_function/inline_function_deref.phpt new file mode 100644 index 0000000000000..c58654a844fb5 --- /dev/null +++ b/Zend/tests/short_function/inline_function_deref.phpt @@ -0,0 +1,28 @@ +--TEST-- +Inline short declaration function dereference +--FILE-- + 123; + $b = function() => $a; + + return $b; +} + +var_dump(get_debug_type( a()() )); + +function b () { + $a = fn() => 123; + $b = fn() => $a; + + return $b; +} + +var_dump(get_debug_type( b()() )); + +?> +--EXPECTF-- +Warning: Undefined variable $a in %s on line %d +string(4) "null" +string(7) "Closure" \ No newline at end of file diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 9e3fcd4248260..8fcb2f8f84e85 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -248,7 +248,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); /* Token used to force a parse error from the lexer */ %token T_ERROR -%type top_statement namespace_name name statement function_declaration_statement function_body +%type top_statement namespace_name name statement function_declaration_statement function_body inline_function_body %type class_declaration_statement trait_declaration_statement legacy_namespace_name %type interface_declaration_statement interface_extends_list %type group_use_declaration inline_use_declarations inline_use_declaration @@ -1343,10 +1343,10 @@ expr: inline_function: function returns_ref backup_doc_comment '(' parameter_list ')' lexical_vars return_type - backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $13, $1, $3, + backup_fn_flags inline_function_body backup_fn_flags + { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $11, $1, $3, NULL, - $5, $7, $11, $8, NULL); CG(extra_fn_flags) = $9; } + $5, $7, $10, $8, NULL); CG(extra_fn_flags) = $9; } | fn returns_ref backup_doc_comment '(' parameter_list ')' return_type T_DOUBLE_ARROW backup_fn_flags backup_lex_pos expr backup_fn_flags { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12 | ZEND_ACC_SHORT_DECLARATION, $1, $3, @@ -1354,6 +1354,12 @@ inline_function: CG(extra_fn_flags) = $9; } ; +inline_function_body: + '{' inner_statement_list '}' { $$ = $2; } + | T_DOUBLE_ARROW expr + { $$ = $2; CG(extra_fn_flags) |= ZEND_ACC_SHORT_DECLARATION; } +; + fn: T_FN { $$ = CG(zend_lineno); } ;