diff --git a/Zend/tests/closure_const_expr/attributes.phpt b/Zend/tests/closure_const_expr/attributes.phpt new file mode 100644 index 0000000000000..c83b821d12969 --- /dev/null +++ b/Zend/tests/closure_const_expr/attributes.phpt @@ -0,0 +1,57 @@ +--TEST-- +Allow defining closures in attributes +--EXTENSIONS-- +reflection +--FILE-- +getAttributes() as $reflectionAttribute) { + var_dump($reflectionAttribute->newInstance()); +} + +?> +--EXPECTF-- +object(Attr)#%d (1) { + ["value"]=> + object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(%d) + } +} +array(1) { + [0]=> + string(3) "foo" +} +object(Attr)#%d (1) { + ["value"]=> + object(Closure)#%d (4) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(%d) + ["parameter"]=> + array(1) { + ["$args"]=> + string(10) "" + } + } +} diff --git a/Zend/tests/closure_const_expr/attributes_ast_printing.phpt b/Zend/tests/closure_const_expr/attributes_ast_printing.phpt new file mode 100644 index 0000000000000..e87abf8c90611 --- /dev/null +++ b/Zend/tests/closure_const_expr/attributes_ast_printing.phpt @@ -0,0 +1,41 @@ +--TEST-- +AST printing for closures in attributes +--FILE-- +getMessage(), "\n"; +} + +try { + \assert( + ! + new #[Attr(static function ($foo) { + echo $foo; + })] + class {} + ); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +assert(!#[Attr(static function ($foo) { + echo $foo; +})] function () { +}) +assert(!new #[Attr(static function ($foo) { + echo $foo; +})] class { +}) diff --git a/Zend/tests/closure_const_expr/attributes_scope_001.phpt b/Zend/tests/closure_const_expr/attributes_scope_001.phpt new file mode 100644 index 0000000000000..6d89f410ca02b --- /dev/null +++ b/Zend/tests/closure_const_expr/attributes_scope_001.phpt @@ -0,0 +1,28 @@ +--TEST-- +Closure in attribute may access private variables +--EXTENSIONS-- +reflection +--FILE-- +secret, PHP_EOL; +})] +class C { + public function __construct( + private string $secret, + ) {} +} + +foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) { + ($reflectionAttribute->newInstance()->value)(new C('secret')); +} + +?> +--EXPECT-- +secret diff --git a/Zend/tests/closure_const_expr/attributes_scope_002.phpt b/Zend/tests/closure_const_expr/attributes_scope_002.phpt new file mode 100644 index 0000000000000..ac168fd02de46 --- /dev/null +++ b/Zend/tests/closure_const_expr/attributes_scope_002.phpt @@ -0,0 +1,35 @@ +--TEST-- +Closure in attribute may not access unrelated private variables +--EXTENSIONS-- +reflection +--FILE-- +secret, PHP_EOL; +})] +class C { +} + +class E { + public function __construct( + private string $secret, + ) {} +} + +foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) { + ($reflectionAttribute->newInstance()->value)(new E('secret')); +} + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot access private property E::$secret in %s:%d +Stack trace: +#0 %s(%d): C::{closure:%s:%d}(Object(E)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/closure_const_expr/basic.phpt b/Zend/tests/closure_const_expr/basic.phpt new file mode 100644 index 0000000000000..d1242c989b2d8 --- /dev/null +++ b/Zend/tests/closure_const_expr/basic.phpt @@ -0,0 +1,23 @@ +--TEST-- +Allow defining Closures in const expressions. +--FILE-- + +--EXPECTF-- +object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(3) +} +called diff --git a/Zend/tests/closure_const_expr/class_const.phpt b/Zend/tests/closure_const_expr/class_const.phpt new file mode 100644 index 0000000000000..d392d0d7dfdc6 --- /dev/null +++ b/Zend/tests/closure_const_expr/class_const.phpt @@ -0,0 +1,25 @@ +--TEST-- +Allow defining Closures in class constants. +--FILE-- + +--EXPECTF-- +object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(4) +} +called diff --git a/Zend/tests/closure_const_expr/complex_array.phpt b/Zend/tests/closure_const_expr/complex_array.phpt new file mode 100644 index 0000000000000..87044cb3659f5 --- /dev/null +++ b/Zend/tests/closure_const_expr/complex_array.phpt @@ -0,0 +1,41 @@ +--TEST-- +Allow defining Closures wrapped in an array in const expressions. +--FILE-- + +--EXPECTF-- +array(2) { + [0]=> + object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(3) + } + [1]=> + object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(5) + } +} +called +also called diff --git a/Zend/tests/closure_const_expr/complex_new.phpt b/Zend/tests/closure_const_expr/complex_new.phpt new file mode 100644 index 0000000000000..9437bbdc259f4 --- /dev/null +++ b/Zend/tests/closure_const_expr/complex_new.phpt @@ -0,0 +1,33 @@ +--TEST-- +Allow defining Closures passed as constructor arguments in const expressions. +--FILE-- +c)(); + +?> +--EXPECTF-- +object(Dummy)#%d (1) { + ["c"]=> + object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(9) + } +} +called diff --git a/Zend/tests/closure_const_expr/default_args.phpt b/Zend/tests/closure_const_expr/default_args.phpt new file mode 100644 index 0000000000000..f1a9ee48f1146 --- /dev/null +++ b/Zend/tests/closure_const_expr/default_args.phpt @@ -0,0 +1,22 @@ +--TEST-- +Closures in default argument +--FILE-- + +--EXPECT-- +default +explicit diff --git a/Zend/tests/closure_const_expr/disallows_non_static.phpt b/Zend/tests/closure_const_expr/disallows_non_static.phpt new file mode 100644 index 0000000000000..6784accc5119b --- /dev/null +++ b/Zend/tests/closure_const_expr/disallows_non_static.phpt @@ -0,0 +1,18 @@ +--TEST-- +Disallows using non-static closures. +--FILE-- +d); +($foo->d)(); + +?> +--EXPECTF-- +Fatal error: Closures in constant expressions must be "static" in %s on line %d diff --git a/Zend/tests/closure_const_expr/disallows_using_variables.phpt b/Zend/tests/closure_const_expr/disallows_using_variables.phpt new file mode 100644 index 0000000000000..4fb70b4eebe7d --- /dev/null +++ b/Zend/tests/closure_const_expr/disallows_using_variables.phpt @@ -0,0 +1,17 @@ +--TEST-- +Disallows using variables. +--FILE-- + +--EXPECTF-- +Fatal error: Cannot "use(...)" variables in constant expression in %s on line %d diff --git a/Zend/tests/closure_const_expr/property_initializer.phpt b/Zend/tests/closure_const_expr/property_initializer.phpt new file mode 100644 index 0000000000000..077a4e4f79a11 --- /dev/null +++ b/Zend/tests/closure_const_expr/property_initializer.phpt @@ -0,0 +1,27 @@ +--TEST-- +Closure in property initializer +--FILE-- +d); +($c->d)(); + + +?> +--EXPECTF-- +object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(4) +} +called diff --git a/Zend/tests/closure_const_expr/property_initializer_scope_001.phpt b/Zend/tests/closure_const_expr/property_initializer_scope_001.phpt new file mode 100644 index 0000000000000..bbd5b5b0ad84c --- /dev/null +++ b/Zend/tests/closure_const_expr/property_initializer_scope_001.phpt @@ -0,0 +1,22 @@ +--TEST-- +Closure in property initializer may access private variables +--FILE-- +secret, PHP_EOL; + }; + + public function __construct( + private string $secret, + ) {} +} + +$c = new C('secret'); +($c->d)($c); + + +?> +--EXPECTF-- +secret diff --git a/Zend/tests/closure_const_expr/property_initializer_scope_002.phpt b/Zend/tests/closure_const_expr/property_initializer_scope_002.phpt new file mode 100644 index 0000000000000..a1c6942da1765 --- /dev/null +++ b/Zend/tests/closure_const_expr/property_initializer_scope_002.phpt @@ -0,0 +1,30 @@ +--TEST-- +Closure in property initializer may not access unrelated private variables +--FILE-- +secret, PHP_EOL; + }; + + +} + +class E { + public function __construct( + private string $secret, + ) {} +} + +$c = new C(); +($c->d)(new E('secret')); + + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot access private property E::$secret in %s:%d +Stack trace: +#0 %s(%d): C::{closure:%s:%d}(Object(E)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/closure_const_expr/static_initalizer.phpt b/Zend/tests/closure_const_expr/static_initalizer.phpt new file mode 100644 index 0000000000000..65bfb37393d42 --- /dev/null +++ b/Zend/tests/closure_const_expr/static_initalizer.phpt @@ -0,0 +1,27 @@ +--TEST-- +Closure in static initializer +--FILE-- + +--EXPECTF-- +object(Closure)#%d (3) { + ["name"]=> + string(17) "{closure:foo():4}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(4) +} +called diff --git a/Zend/tests/closure_const_expr/static_property_initializer.phpt b/Zend/tests/closure_const_expr/static_property_initializer.phpt new file mode 100644 index 0000000000000..0b7249d78bd6f --- /dev/null +++ b/Zend/tests/closure_const_expr/static_property_initializer.phpt @@ -0,0 +1,26 @@ +--TEST-- +Closure in static property initializer +--FILE-- + +--EXPECTF-- +object(Closure)#%d (3) { + ["name"]=> + string(%d) "{closure:%s:%d}" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(4) +} +called diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index d33747412ef0a..53f6661ecf937 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -23,6 +23,7 @@ #include "zend_language_parser.h" #include "zend_smart_str.h" #include "zend_exceptions.h" +#include "zend_closures.h" #include "zend_constants.h" #include "zend_enum.h" @@ -989,6 +990,13 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_inner( } return SUCCESS; } + case ZEND_AST_OP_ARRAY: + { + zend_function *func = Z_PTR_P(&((zend_ast_zval*)(ast))->val); + + zend_create_closure(result, func, scope, scope, NULL); + return SUCCESS; + } case ZEND_AST_PROP: case ZEND_AST_NULLSAFE_PROP: { @@ -1068,7 +1076,7 @@ static size_t ZEND_FASTCALL zend_ast_tree_size(zend_ast *ast) { size_t size; - if (ast->kind == ZEND_AST_ZVAL || ast->kind == ZEND_AST_CONSTANT) { + if (ast->kind == ZEND_AST_ZVAL || ast->kind == ZEND_AST_CONSTANT || ast->kind == ZEND_AST_OP_ARRAY) { size = sizeof(zend_ast_zval); } else if (zend_ast_is_list(ast)) { uint32_t i; @@ -1109,6 +1117,13 @@ static void* ZEND_FASTCALL zend_ast_tree_copy(zend_ast *ast, void *buf) ZVAL_STR_COPY(&new->val, zend_ast_get_constant_name(ast)); Z_LINENO(new->val) = zend_ast_get_lineno(ast); buf = (void*)((char*)buf + sizeof(zend_ast_zval)); + } else if (ast->kind == ZEND_AST_OP_ARRAY) { + zend_ast_zval *new = (zend_ast_zval*)buf; + new->kind = ZEND_AST_OP_ARRAY; + new->attr = ast->attr; + ZVAL_COPY(&new->val, &((zend_ast_zval *) ast)->val); + Z_LINENO(new->val) = zend_ast_get_lineno(ast); + buf = (void*)((char*)buf + sizeof(zend_ast_zval)); } else if (zend_ast_is_list(ast)) { zend_ast_list *list = zend_ast_get_list(ast); zend_ast_list *new = (zend_ast_list*)buf; @@ -1176,6 +1191,8 @@ ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast) goto tail_call; } else if (EXPECTED(ast->kind == ZEND_AST_ZVAL)) { zval_ptr_dtor_nogc(zend_ast_get_zval(ast)); + } else if (EXPECTED(ast->kind == ZEND_AST_OP_ARRAY)) { + zval_ptr_dtor_nogc(&((zend_ast_zval*)(ast))->val); } else if (EXPECTED(zend_ast_is_list(ast))) { zend_ast_list *list = zend_ast_get_list(ast); if (list->children) { diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 8d1d93f2564a7..2303b8eba1313 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -35,6 +35,7 @@ enum _zend_ast_kind { /* special nodes */ ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT, ZEND_AST_CONSTANT, + ZEND_AST_OP_ARRAY, ZEND_AST_ZNODE, /* declaration nodes */ diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7e5351ddfafdb..d77bd47fa557b 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11049,7 +11049,8 @@ static bool zend_is_allowed_in_const_expr(zend_ast_kind kind) /* {{{ */ || kind == ZEND_AST_CONST_ENUM_INIT || kind == ZEND_AST_NEW || kind == ZEND_AST_ARG_LIST || kind == ZEND_AST_NAMED_ARG - || kind == ZEND_AST_PROP || kind == ZEND_AST_NULLSAFE_PROP; + || kind == ZEND_AST_PROP || kind == ZEND_AST_NULLSAFE_PROP + || kind == ZEND_AST_CLOSURE; } /* }}} */ @@ -11183,6 +11184,29 @@ static void zend_compile_const_expr_new(zend_ast **ast_ptr) class_ast->attr = fetch_type << ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT; } +static void zend_compile_const_expr_closure(zend_ast **ast_ptr) +{ + zend_ast_decl *closure_ast = (zend_ast_decl *) *ast_ptr; + zend_ast *uses_ast = closure_ast->child[1]; + if (!(closure_ast->flags & ZEND_ACC_STATIC)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Closures in constant expressions must be \"static\""); + } + if (uses_ast) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot \"use(...)\" variables in constant expression"); + } + + znode node; + zend_op_array *op = zend_compile_func_decl(&node, *ast_ptr, 1); + + zend_ast_destroy(*ast_ptr); + zval z; + ZVAL_PTR(&z, op); + *ast_ptr = zend_ast_create_zval(&z); + (*ast_ptr)->kind = ZEND_AST_OP_ARRAY; +} + static void zend_compile_const_expr_args(zend_ast **ast_ptr) { zend_ast_list *list = zend_ast_get_list(*ast_ptr); @@ -11245,6 +11269,9 @@ static void zend_compile_const_expr(zend_ast **ast_ptr, void *context) /* {{{ */ case ZEND_AST_ARG_LIST: zend_compile_const_expr_args(ast_ptr); break; + case ZEND_AST_CLOSURE: + zend_compile_const_expr_closure(ast_ptr); + break; } zend_ast_apply(ast, zend_compile_const_expr, context); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index c5ddc040b22d8..e829b4ed1a49b 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -178,6 +178,10 @@ static zend_ast *zend_persist_ast(zend_ast *ast) zend_ast_zval *copy = zend_shared_memdup(ast, sizeof(zend_ast_zval)); zend_persist_zval(©->val); node = (zend_ast *) copy; + } else if (ast->kind == ZEND_AST_OP_ARRAY) { + zend_ast_zval *copy = zend_shared_memdup(ast, sizeof(zend_ast_zval)); + zend_persist_op_array(©->val); + node = (zend_ast *) copy; } else if (zend_ast_is_list(ast)) { zend_ast_list *list = zend_ast_get_list(ast); zend_ast_list *copy = zend_shared_memdup(ast, diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index d7a0455e3e5c1..c007e1a6f697c 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -78,6 +78,9 @@ static void zend_persist_ast_calc(zend_ast *ast) if (ast->kind == ZEND_AST_ZVAL || ast->kind == ZEND_AST_CONSTANT) { ADD_SIZE(sizeof(zend_ast_zval)); zend_persist_zval_calc(&((zend_ast_zval*)(ast))->val); + } else if (ast->kind == ZEND_AST_OP_ARRAY) { + ADD_SIZE(sizeof(zend_ast_zval)); + zend_persist_op_array_calc(&((zend_ast_zval*)(ast))->val); } else if (zend_ast_is_list(ast)) { zend_ast_list *list = zend_ast_get_list(ast); ADD_SIZE(sizeof(zend_ast_list) - sizeof(zend_ast *) + sizeof(zend_ast *) * list->children);