From 3a0d8458dfb3c23e669344eb613ac87dd44d5023 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 16 Dec 2025 13:11:43 +0100 Subject: [PATCH 1/2] Support PFA syntax --- Zend/zend_ast.c | 128 ++++++++++++++++++++++++++++++-- Zend/zend_ast.h | 17 ++++- Zend/zend_language_parser.y | 27 ++++--- Zend/zend_types.h | 4 + ext/opcache/zend_file_cache.c | 4 +- ext/opcache/zend_persist.c | 1 + ext/opcache/zend_persist_calc.c | 2 + 7 files changed, 163 insertions(+), 20 deletions(-) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 9774cce39db2b..91d383c31e97f 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -54,13 +54,14 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_znode(const znode *node) { return (zend_ast *) ast; } -ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(void) { +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(zend_ast *args) { zend_ast_fcc *ast; ast = zend_ast_alloc(sizeof(zend_ast_fcc)); ast->kind = ZEND_AST_CALLABLE_CONVERT; ast->attr = 0; ast->lineno = CG(zend_lineno); + ast->args = args; ZEND_MAP_PTR_INIT(ast->fptr, NULL); return (zend_ast *) ast; @@ -157,6 +158,12 @@ ZEND_API zend_ast *zend_ast_create_decl( return (zend_ast *) ast; } +static bool zend_ast_is_placeholder_arg(zend_ast *arg) { + return arg->kind == ZEND_AST_PLACEHOLDER_ARG + || (arg->kind == ZEND_AST_NAMED_ARG + && arg->child[1]->kind == ZEND_AST_PLACEHOLDER_ARG); +} + #if ZEND_AST_SPEC ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_0(zend_ast_kind kind) { zend_ast *ast; @@ -400,6 +407,30 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_list_2(zend_ast_kind kind, zen return ast; } + +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_0(zend_ast_kind kind) { + return zend_ast_create_list(0, kind); +} + +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_1(zend_ast_kind kind, zend_ast *arg) { + zend_ast *list = zend_ast_create_list(1, kind, arg); + + if (zend_ast_is_placeholder_arg(arg)) { + return zend_ast_create_fcc(list); + } + + return list; +} + +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_2(zend_ast_kind kind, zend_ast *arg1, zend_ast *arg2) { + zend_ast *list = zend_ast_create_list(2, kind, arg1, arg2); + + if (zend_ast_is_placeholder_arg(arg1) || zend_ast_is_placeholder_arg(arg2)) { + return zend_ast_create_fcc(list); + } + + return list; +} #else static zend_ast *zend_ast_create_from_va_list(zend_ast_kind kind, zend_ast_attr attr, va_list va) { uint32_t i, children = kind >> ZEND_AST_NUM_CHILDREN_SHIFT; @@ -479,6 +510,41 @@ ZEND_API zend_ast *zend_ast_create_list(uint32_t init_children, zend_ast_kind ki return ast; } + +ZEND_API zend_ast *zend_ast_create_arg_list(uint32_t init_children, zend_ast_kind kind, ...) { + zend_ast *ast; + zend_ast_list *list; + bool has_placeholders = false; + + ast = zend_ast_alloc(zend_ast_list_size(4)); + list = (zend_ast_list *) ast; + list->kind = kind; + list->attr = 0; + list->lineno = CG(zend_lineno); + list->children = 0; + + { + va_list va; + uint32_t i; + va_start(va, kind); + for (i = 0; i < init_children; ++i) { + zend_ast *child = va_arg(va, zend_ast *); + ast = zend_ast_list_add(ast, child); + uint32_t lineno = zend_ast_get_lineno(child); + if (lineno < ast->lineno) { + ast->lineno = lineno; + } + has_placeholders = has_placeholders || zend_ast_is_placeholder_arg(child); + } + va_end(va); + } + + if (has_placeholders) { + return zend_ast_create_fcc(list); + } + + return ast; +} #endif zend_ast *zend_ast_create_concat_op(zend_ast *op0, zend_ast *op1) { @@ -508,6 +574,23 @@ ZEND_ATTRIBUTE_NODISCARD ZEND_API zend_ast * ZEND_FASTCALL zend_ast_list_add(zen return (zend_ast *) list; } +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_arg_list_add(zend_ast *list, zend_ast *arg) +{ + if (list->kind == ZEND_AST_CALLABLE_CONVERT) { + zend_ast_fcc *fcc_ast = (zend_ast_fcc*)list; + fcc_ast->args = zend_ast_list_add(fcc_ast->args, arg); + return (zend_ast*)fcc_ast; + } + + ZEND_ASSERT(list->kind == ZEND_AST_ARG_LIST); + + if (zend_ast_is_placeholder_arg(arg)) { + return zend_ast_create_fcc(zend_ast_list_add(list, arg)); + } + + return zend_ast_list_add(list, arg); +} + static zend_result zend_ast_add_array_element(const zval *result, zval *offset, zval *expr) { if (Z_TYPE_P(offset) == IS_UNDEF) { @@ -1060,6 +1143,14 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( case ZEND_AST_CALL: { ZEND_ASSERT(ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT); zend_ast_fcc *fcc_ast = (zend_ast_fcc*)ast->child[1]; + + zend_ast_list *args = zend_ast_get_list(fcc_ast->args); + ZEND_ASSERT(args->children > 0); + if (args->children != 1 || args->child[0]->attr != _IS_PLACEHOLDER_VARIADIC) { + /* TODO: PFAs */ + return FAILURE; + } + fptr = ZEND_MAP_PTR_GET(fcc_ast->fptr); if (!fptr) { @@ -1087,6 +1178,13 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( ZEND_ASSERT(ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT); zend_ast_fcc *fcc_ast = (zend_ast_fcc*)ast->child[2]; + zend_ast_list *args = zend_ast_get_list(fcc_ast->args); + ZEND_ASSERT(args->children > 0); + if (args->children != 1 || args->child[0]->attr != _IS_PLACEHOLDER_VARIADIC) { + /* TODO: PFAs */ + return FAILURE; + } + zend_class_entry *ce = zend_ast_fetch_class(ast->child[0], scope); if (!ce) { return FAILURE; @@ -1124,12 +1222,12 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( if (!(fptr->common.fn_flags & ZEND_ACC_STATIC)) { zend_non_static_method_call(fptr); - + return FAILURE; } if ((fptr->common.fn_flags & ZEND_ACC_ABSTRACT)) { zend_abstract_method_call(fptr); - + return FAILURE; } else if (fptr->common.scope->ce_flags & ZEND_ACC_TRAIT) { zend_error(E_DEPRECATED, @@ -1243,7 +1341,8 @@ static size_t ZEND_FASTCALL zend_ast_tree_size(zend_ast *ast) } else if (ast->kind == ZEND_AST_OP_ARRAY) { size = sizeof(zend_ast_op_array); } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { - size = sizeof(zend_ast_fcc); + zend_ast *args_ast = ((zend_ast_fcc*)ast)->args; + size = sizeof(zend_ast_fcc) + zend_ast_tree_size(args_ast); } else if (zend_ast_is_list(ast)) { uint32_t i; const zend_ast_list *list = zend_ast_get_list(ast); @@ -1320,6 +1419,8 @@ static void* ZEND_FASTCALL zend_ast_tree_copy(zend_ast *ast, void *buf) new->lineno = old->lineno; ZEND_MAP_PTR_INIT(new->fptr, ZEND_MAP_PTR(old->fptr)); buf = (void*)((char*)buf + sizeof(zend_ast_fcc)); + new->args = buf; + buf = zend_ast_tree_copy(old->args, buf); } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ ZEND_UNREACHABLE(); @@ -1403,6 +1504,10 @@ ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast) zend_ast_destroy(decl->child[3]); ast = decl->child[4]; goto tail_call; + } else if (EXPECTED(ast->kind == ZEND_AST_CALLABLE_CONVERT)) { + zend_ast_fcc *fcc_ast = (zend_ast_fcc*) ast; + + zend_ast_destroy(fcc_ast->args); } } @@ -2299,6 +2404,13 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio EMPTY_SWITCH_DEFAULT_CASE(); } break; + case ZEND_AST_PLACEHOLDER_ARG: + if (ast->attr == _IS_PLACEHOLDER_ARG) { + APPEND_STR("?"); + } else if (ast->attr == _IS_PLACEHOLDER_VARIADIC) { + APPEND_STR("..."); + } + break; /* 1 child node */ case ZEND_AST_VAR: @@ -2445,9 +2557,11 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_ex(str, ast->child[1], 0, indent); smart_str_appendc(str, ')'); break; - case ZEND_AST_CALLABLE_CONVERT: - smart_str_appends(str, "..."); - break; + case ZEND_AST_CALLABLE_CONVERT: { + zend_ast_fcc *fcc_ast = (zend_ast_fcc*)ast; + ast = fcc_ast->args; + goto simple_list; + } case ZEND_AST_CLASS_CONST: zend_ast_export_ns_name(str, ast->child[0], 0, indent); smart_str_appends(str, "::"); diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index fb48b187252b3..1c6d6f82fadf8 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -76,6 +76,7 @@ enum _zend_ast_kind { ZEND_AST_TYPE, ZEND_AST_CONSTANT_CLASS, ZEND_AST_CALLABLE_CONVERT, + ZEND_AST_PLACEHOLDER_ARG, /* 1 child node */ ZEND_AST_VAR = 1 << ZEND_AST_NUM_CHILDREN_SHIFT, @@ -229,10 +230,12 @@ typedef struct _zend_ast_decl { zend_ast *child[5]; } zend_ast_decl; +// TODO: rename typedef struct _zend_ast_fcc { zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */ zend_ast_attr attr; /* Additional attribute, use depending on node type */ uint32_t lineno; /* Line number */ + zend_ast *args; ZEND_MAP_PTR_DEF(zend_function *, fptr); } zend_ast_fcc; @@ -307,27 +310,39 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_list_0(zend_ast_kind kind); ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_list_1(zend_ast_kind kind, zend_ast *child); ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_list_2(zend_ast_kind kind, zend_ast *child1, zend_ast *child2); +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_0(zend_ast_kind kind); +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_1(zend_ast_kind kind, zend_ast *child); +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_arg_list_2(zend_ast_kind kind, zend_ast *child1, zend_ast *child2); + # define zend_ast_create(...) \ ZEND_AST_SPEC_CALL(zend_ast_create, __VA_ARGS__) # define zend_ast_create_ex(...) \ ZEND_AST_SPEC_CALL_EX(zend_ast_create_ex, __VA_ARGS__) # define zend_ast_create_list(init_children, ...) \ ZEND_AST_SPEC_CALL(zend_ast_create_list, __VA_ARGS__) +# define zend_ast_create_arg_list(init_children, ...) \ + ZEND_AST_SPEC_CALL(zend_ast_create_arg_list, __VA_ARGS__) #else ZEND_API zend_ast *zend_ast_create(zend_ast_kind kind, ...); ZEND_API zend_ast *zend_ast_create_ex(zend_ast_kind kind, zend_ast_attr attr, ...); ZEND_API zend_ast *zend_ast_create_list(uint32_t init_children, zend_ast_kind kind, ...); +ZEND_API zend_ast *zend_ast_create_arg_list(uint32_t init_children, zend_ast_kind kind, ...); #endif ZEND_ATTRIBUTE_NODISCARD ZEND_API zend_ast * ZEND_FASTCALL zend_ast_list_add(zend_ast *list, zend_ast *op); +/* Like zend_ast_list_add(), but wraps the list into a ZEND_AST_CALLABLE_CONVERT + * if any arg is a ZEND_AST_PLACEHOLDER_ARG. list can be a zend_ast_list, or a + * zend_ast_fcc. */ +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_arg_list_add(zend_ast *list, zend_ast *arg); + ZEND_API zend_ast *zend_ast_create_decl( zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4 ); -ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(void); +ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(zend_ast *args); typedef struct { bool had_side_effects; diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index e4d61006fe12f..faa63144bc651 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -901,16 +901,15 @@ return_type: ; argument_list: - '(' ')' { $$ = zend_ast_create_list(0, ZEND_AST_ARG_LIST); } + '(' ')' { $$ = zend_ast_create_arg_list(0, ZEND_AST_ARG_LIST); } | '(' non_empty_argument_list possible_comma ')' { $$ = $2; } - | '(' T_ELLIPSIS ')' { $$ = zend_ast_create_fcc(); } ; non_empty_argument_list: argument - { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1); } + { $$ = zend_ast_create_arg_list(1, ZEND_AST_ARG_LIST, $1); } | non_empty_argument_list ',' argument - { $$ = zend_ast_list_add($1, $3); } + { $$ = zend_ast_arg_list_add($1, $3); } ; /* `clone_argument_list` is necessary to resolve a parser ambiguity (shift-reduce conflict) @@ -923,25 +922,31 @@ non_empty_argument_list: * syntax. */ clone_argument_list: - '(' ')' { $$ = zend_ast_create_list(0, ZEND_AST_ARG_LIST); } + '(' ')' { $$ = zend_ast_create_arg_list(0, ZEND_AST_ARG_LIST); } | '(' non_empty_clone_argument_list possible_comma ')' { $$ = $2; } - | '(' expr ',' ')' { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2); } - | '(' T_ELLIPSIS ')' { $$ = zend_ast_create_fcc(); } + | '(' expr ',' ')' { $$ = zend_ast_create_arg_list(1, ZEND_AST_ARG_LIST, $2); } ; non_empty_clone_argument_list: expr ',' argument - { $$ = zend_ast_create_list(2, ZEND_AST_ARG_LIST, $1, $3); } + { $$ = zend_ast_create_arg_list(2, ZEND_AST_ARG_LIST, $1, $3); } | argument_no_expr - { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1); } + { $$ = zend_ast_create_arg_list(1, ZEND_AST_ARG_LIST, $1); } | non_empty_clone_argument_list ',' argument - { $$ = zend_ast_list_add($1, $3); } + { $$ = zend_ast_arg_list_add($1, $3); } ; argument_no_expr: identifier ':' expr { $$ = zend_ast_create(ZEND_AST_NAMED_ARG, $1, $3); } - | T_ELLIPSIS expr { $$ = zend_ast_create(ZEND_AST_UNPACK, $2); } + | T_ELLIPSIS + { $$ = zend_ast_create_ex(ZEND_AST_PLACEHOLDER_ARG, _IS_PLACEHOLDER_VARIADIC); } + | '?' + { $$ = zend_ast_create_ex(ZEND_AST_PLACEHOLDER_ARG, _IS_PLACEHOLDER_ARG); } + | identifier ':' '?' + { $$ = zend_ast_create(ZEND_AST_NAMED_ARG, $1, zend_ast_create_ex(ZEND_AST_PLACEHOLDER_ARG, _IS_PLACEHOLDER_ARG)); } + | T_ELLIPSIS expr + { $$ = zend_ast_create(ZEND_AST_UNPACK, $2); } ; argument: diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 43aa2aa86a00e..44c3b715183aa 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -638,6 +638,10 @@ struct _zend_ast_ref { #define _IS_BOOL 18 #define _IS_NUMBER 19 +/* used for PFAs/FCCs */ +#define _IS_PLACEHOLDER_ARG 20 +#define _IS_PLACEHOLDER_VARIADIC 21 + /* guard flags */ #define ZEND_GUARD_PROPERTY_GET (1<<0) #define ZEND_GUARD_PROPERTY_SET (1<<1) diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index d430f4833d3ca..fb6196abb93db 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -384,6 +384,7 @@ static void zend_file_cache_serialize_ast(zend_ast *ast, } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { zend_ast_fcc *fcc = (zend_ast_fcc*)ast; ZEND_MAP_PTR_INIT(fcc->fptr, NULL); + zend_file_cache_serialize_ast(fcc->args, script, info, buf); } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ ZEND_UNREACHABLE(); @@ -1304,6 +1305,7 @@ static void zend_file_cache_unserialize_ast(zend_ast *ast, } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { zend_ast_fcc *fcc = (zend_ast_fcc*)ast; ZEND_MAP_PTR_NEW(fcc->fptr); + zend_file_cache_unserialize_ast(fcc->args, script, buf); } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ ZEND_UNREACHABLE(); @@ -2109,7 +2111,7 @@ void zend_file_cache_invalidate(zend_string *full_path) if (ZCG(accel_directives).file_cache_read_only) { return; } - + char *filename; filename = zend_file_cache_get_bin_file_path(full_path); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index ef69cceb0250b..9bc2496837ce4 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -197,6 +197,7 @@ static zend_ast *zend_persist_ast(zend_ast *ast) node = (zend_ast *) copy; } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { zend_ast_fcc *copy = zend_shared_memdup(ast, sizeof(zend_ast_fcc)); + copy->args = zend_persist_ast(copy->args); node = (zend_ast *) copy; } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index c638d66619d0f..0b0ff51d0d4df 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -92,7 +92,9 @@ static void zend_persist_ast_calc(zend_ast *ast) ZVAL_PTR(&z, zend_ast_get_op_array(ast)->op_array); zend_persist_op_array_calc(&z); } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { + zend_ast_fcc *fcc_ast = (zend_ast_fcc*)ast; ADD_SIZE(sizeof(zend_ast_fcc)); + zend_persist_ast_calc(fcc_ast->args); } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ ZEND_UNREACHABLE(); From 75eddcbc383d73836eb7c3549c776f07c953397c Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 16 Dec 2025 18:10:07 +0100 Subject: [PATCH 2/2] Disallow FCC with more than one arg, or non-variadic placeholder --- .../first_class_callable_non_unary_error.phpt | 10 ++++++++++ .../first_class_callable_non_variadic_error.phpt | 10 ++++++++++ Zend/zend_compile.c | 5 +++++ 3 files changed, 25 insertions(+) create mode 100644 Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt create mode 100644 Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt diff --git a/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt b/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt new file mode 100644 index 0000000000000..74e36a9ad0df5 --- /dev/null +++ b/Zend/tests/first_class_callable/first_class_callable_non_unary_error.phpt @@ -0,0 +1,10 @@ +--TEST-- +First class callable error: more than one argument +--FILE-- + +--EXPECTF-- +Fatal error: Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders in %s on line %d diff --git a/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt b/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt new file mode 100644 index 0000000000000..efbd13b7593b6 --- /dev/null +++ b/Zend/tests/first_class_callable/first_class_callable_non_variadic_error.phpt @@ -0,0 +1,10 @@ +--TEST-- +First class callable error: non-variadic placeholder +--FILE-- + +--EXPECTF-- +Fatal error: Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 5eba2ec1366fa..ac3c834b406cf 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3950,6 +3950,11 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze zend_error_noreturn(E_COMPILE_ERROR, "Cannot create Closure for new expression"); } + zend_ast_list *args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args); + if (args->children != 1 || args->child[0]->attr != _IS_PLACEHOLDER_VARIADIC) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot create a Closure for call expression with more than one argument, or non-variadic placeholders"); + } + if (opcode == ZEND_INIT_FCALL) { opline->op1.num = zend_vm_calc_used_stack(0, fbc); }