From 0bd000f9b2faccb5fe4a5ffa58ee34717fa59085 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 20 Jul 2026 15:15:49 +0200 Subject: [PATCH 1/5] PFA: Optimize constant pre-bound arguments Pre-bound arguments are bound to the generated closure's lexical vars: ``` function f($a, $b) {} $f = f(1, ?); // Generates: $tmp = 1; $f = function ($b) use ($tmp) { return f($tmp, $b); }; ``` Detect which pre-bound arguments are constant and burn them into the generated closure instead: ``` function f($a, $b) {} $f = f(1, ?); // Generates: $f = function ($b) { return f(1, $b); }; ``` --- Zend/Optimizer/dfa_pass.c | 28 +++ .../partial_application/const_arg_opt.phpt | 162 ++++++++++++++++++ Zend/zend_ast.c | 2 +- Zend/zend_partial.c | 59 ++++--- Zend/zend_partial.h | 2 +- Zend/zend_vm_def.h | 9 +- Zend/zend_vm_execute.h | 36 +++- 7 files changed, 268 insertions(+), 30 deletions(-) create mode 100644 Zend/tests/partial_application/const_arg_opt.phpt diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 77dc322fbdec..d5ff9f494624 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -469,6 +469,34 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) } } } + + if (call_info->caller_call_opline && call_info->caller_call_opline->opcode == ZEND_CALLABLE_CONVERT_PARTIAL) { + /* Build a bitset of constant pre-bound PFA args: These are args whose value is alway the same for all + * instances of a PFA. */ + uint32_t const_args = 0; + for (uint32_t i = 0, l = MIN(sizeof(const_args)*CHAR_BIT, call_info->num_args); i < l; i++) { + zend_op *send_opline = call_info->arg_info[i].opline; + if (send_opline->op1_type == IS_CONST) { + zval *value = CT_CONSTANT_EX(op_array, send_opline->op1.constant); + if (Z_TYPE_P(value) == IS_CONSTANT_AST) { + /* Const exprs can evaluate to non-cont zvals (e.g. objects), and are not idempotent */ + continue; + } + const_args |= (1 << i); + } + } + + /* Pass the bitset to the ZEND_CALLABLE_CONVERT_PARTIAL opline. */ + zend_op *call_opline = call_info->caller_call_opline; + if (call_opline->op2_type == IS_UNUSED) { + call_opline->op2.num = const_args; + } else { + ZEND_ASSERT(call_opline->op2_type == IS_CONST); + zval *zv = CT_CONSTANT_EX(op_array, call_opline->op2.constant); + Z_EXTRA_P(zv) = const_args; + } + } + call_info = call_info->next_callee; } while (call_info); } diff --git a/Zend/tests/partial_application/const_arg_opt.phpt b/Zend/tests/partial_application/const_arg_opt.phpt new file mode 100644 index 000000000000..133b0f119a24 --- /dev/null +++ b/Zend/tests/partial_application/const_arg_opt.phpt @@ -0,0 +1,162 @@ +--TEST-- +Constant argument optimization +--DESCRIPTION-- +Pre-bound arguments that are constant can be burned into the generated +op_array instead of being passed via the Closure's lexical vars. +--ENV-- +A=1 +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--FILE-- +getClosureUsedVariables(); + if ($vars === []) { + echo "no lexical vars\n"; + } else { + $varNames = array_keys($vars); + echo 'lexical vars: ', implode(', ', $varNames), "\n"; + } +} + +echo "# Non-constant pre-bound argument:\n"; +$f = f(getenv('A'), ?); +print_lexical_vars($f); +$f(2); + +echo "# Constant pre-bound argument:\n"; +$f = f(2, ?); +print_lexical_vars($f); +$f(2); + +echo "# Constant pre-bound argument (inverted):\n"; +$f = f(?, 2); +print_lexical_vars($f); +$f(2); + +echo "# Inlined pre-bound argument:\n"; +$f = f(g(), ?); +print_lexical_vars($f); +$f(2); + +echo "# Constexpr pre-bound argument:\n"; +const B = 4; +$f = f(B, ?); +print_lexical_vars($f); +$f(2); + +echo "# Mixed arguments:\n"; +$f = h(5, getenv('A'), ?); +print_lexical_vars($f); +$f(2); + +echo "# Constant array pre-bound argument:\n"; +$f = f(['foo' => 'bar'], ?); +print_lexical_vars($f); +$f(2); + +echo "# Named arguments:\n"; +$f = h(1, c: ?, b: ?); +print_lexical_vars($f); +$f(2, 3); + +echo "# Many arguments (optimization can not be applied for all args) :\n"; +$f = i(?, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33); +print_lexical_vars($f); +$f(33); + +?> +--EXPECT-- +# Non-constant pre-bound argument: +lexical vars: a +array(2) { + [0]=> + string(1) "1" + [1]=> + int(2) +} +# Constant pre-bound argument: +no lexical vars +array(2) { + [0]=> + int(2) + [1]=> + int(2) +} +# Constant pre-bound argument (inverted): +no lexical vars +array(2) { + [0]=> + int(2) + [1]=> + int(2) +} +# Inlined pre-bound argument: +no lexical vars +array(2) { + [0]=> + int(3) + [1]=> + int(2) +} +# Constexpr pre-bound argument: +lexical vars: a +array(2) { + [0]=> + int(4) + [1]=> + int(2) +} +# Mixed arguments: +lexical vars: b +array(3) { + [0]=> + int(5) + [1]=> + string(1) "1" + [2]=> + int(2) +} +# Constant array pre-bound argument: +no lexical vars +array(2) { + [0]=> + array(1) { + ["foo"]=> + string(3) "bar" + } + [1]=> + int(2) +} +# Named arguments: +no lexical vars +array(3) { + [0]=> + int(1) + [1]=> + int(3) + [2]=> + int(2) +} +# Many arguments (optimization can not be applied for all args) : +lexical vars: a33 +int(33) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 8e735c6aba74..a17cb3d91b91 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1352,7 +1352,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1), extra_named_params, named_positions, fcc_ast->filename, &ast->lineno, - (void**)cache_slot, fcc_ast->name, flags); + (void**)cache_slot, fcc_ast->name, flags, 0); if (named_positions) { zend_array_release(named_positions); diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c index a78db3f51ef1..e911a2925552 100644 --- a/Zend/zend_partial.c +++ b/Zend/zend_partial.c @@ -66,6 +66,14 @@ static zend_always_inline bool zp_is_non_static_closure(const zend_function *fun return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == ZEND_ACC_CLOSURE); } +/* Whether argument at offset 'offset' is const. Such arguments can be burned into the generated op_array */ +static inline bool zp_is_const_arg(uint32_t const_args, uint32_t offset) { + if (offset < sizeof(const_args) * CHAR_BIT) { + return const_args & (1 << offset); + } + return false; +} + static zend_never_inline ZEND_COLD void zp_args_underflow( const zend_function *function, uint32_t args, uint32_t expected) { @@ -179,7 +187,7 @@ static zend_string *zp_get_func_param_name(const zend_function *function, uint32 * including params and used vars. */ static zp_names *zp_assign_names(uint32_t argc, zval *argv, zend_function *function, bool variadic_partial, - zend_array *extra_named_params) + zend_array *extra_named_params, uint32_t const_args) { zp_names *names = zend_arena_calloc(&CG(ast_arena), 1, zend_safe_address_guarded(argc, sizeof(*names->params), offsetof(zp_names, params))); @@ -226,7 +234,7 @@ static zp_names *zp_assign_names(uint32_t argc, zval *argv, /* Assign names for pre-bound params (lexical vars). * There may be clashes, we ensure to generate unique names. */ for (uint32_t offset = 0; offset < argc; offset++) { - if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset])) { + if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset]) || zp_is_const_arg(const_args, offset)) { continue; } uint32_t n = 2; @@ -510,7 +518,7 @@ static zend_ast *zp_compile_forwarding_call( zp_names *var_names, bool uses_variadic_placeholder, uint32_t num_args, zend_class_entry *called_scope, zend_type return_type, bool forward_superfluous_args, - zend_ast *stmts_ast) + zend_ast *stmts_ast, uint32_t const_args) { bool is_assert = zend_string_equals(function->common.function_name, ZSTR_KNOWN(ZEND_STR_ASSERT)); @@ -551,6 +559,9 @@ static zend_ast *zp_compile_forwarding_call( default_value_ast = zend_ast_create_zval(&default_value); } args_ast = zend_ast_list_add(args_ast, default_value_ast); + } else if (zp_is_const_arg(const_args, offset)) { + ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT); + args_ast = zend_ast_list_add(args_ast, zend_ast_create_zval(&argv[offset])); } else { args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_VAR, zend_ast_create_zval_from_str(zend_string_copy(var_names->params[offset])))); @@ -661,7 +672,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, const zend_array *named_positions, zend_string *declaring_filename, const uint32_t *declaring_lineno_ptr, void **cache_slot, - zend_string *pfa_name, uint32_t flags) { + zend_string *pfa_name, uint32_t flags, uint32_t const_args) { zend_op_array *op_array = NULL; @@ -771,7 +782,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, /* Assign variable names */ zp_names *var_names = zp_assign_names(argc, argv, function, - uses_variadic_placeholder, extra_named_params); + uses_variadic_placeholder, extra_named_params, const_args); /* Generate AST */ @@ -825,15 +836,17 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, default_value_ast, attributes_ast, NULL, NULL); } else if (!Z_ISUNDEF(argv[offset])) { - // TODO: If the pre-bound parameter is a literal, it can be a - // literal in the function body instead of a lexical var. - zend_ast *lexical_var_ast = zend_ast_create_zval_from_str( - zend_string_copy(var_names->params[offset])); - if (zp_arg_must_be_sent_by_ref(function, offset+1)) { - lexical_var_ast->attr = ZEND_BIND_REF; + if (zp_is_const_arg(const_args, offset)) { + /* Will be burned into the op_array */ + } else { + zend_ast *lexical_var_ast = zend_ast_create_zval_from_str( + zend_string_copy(var_names->params[offset])); + if (zp_arg_must_be_sent_by_ref(function, offset+1)) { + lexical_var_ast->attr = ZEND_BIND_REF; + } + lexical_vars_ast = zend_ast_list_add( + lexical_vars_ast, lexical_var_ast); } - lexical_vars_ast = zend_ast_list_add( - lexical_vars_ast, lexical_var_ast); } } @@ -894,7 +907,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, no_forwarding_ast = zp_compile_forwarding_call(this_ptr, function, argc, argv, extra_named_params, var_names, uses_variadic_placeholder, num_params, - called_scope, return_type, false, no_forwarding_ast); + called_scope, return_type, false, no_forwarding_ast, const_args); if (!no_forwarding_ast) { ZEND_ASSERT(EG(exception)); @@ -904,7 +917,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, forwarding_ast = zp_compile_forwarding_call(this_ptr, function, argc, argv, extra_named_params, var_names, uses_variadic_placeholder, num_params, - called_scope, return_type, true, forwarding_ast); + called_scope, return_type, true, forwarding_ast, const_args); if (!forwarding_ast) { ZEND_ASSERT(EG(exception)); @@ -931,7 +944,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function, stmts_ast = zp_compile_forwarding_call(this_ptr, function, argc, argv, extra_named_params, var_names, uses_variadic_placeholder, num_params, - called_scope, return_type, false, stmts_ast); + called_scope, return_type, false, stmts_ast, const_args); if (!stmts_ast) { ZEND_ASSERT(EG(exception)); @@ -1000,7 +1013,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct const zend_array *named_positions, zend_string *declaring_filename, const uint32_t *declaring_lineno_ptr, void **cache_slot, - zend_string *pfa_name, uint32_t flags) { + zend_string *pfa_name, uint32_t flags, uint32_t const_args) { if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION ? cache_slot[0] == function @@ -1015,7 +1028,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct if (UNEXPECTED(!op_array)) { op_array = zp_compile(this_ptr, function, argc, argv, extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr, - cache_slot, pfa_name, flags); + cache_slot, pfa_name, flags, const_args); } if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) { @@ -1037,7 +1050,7 @@ static void zp_free_unbound_args(uint32_t start, uint32_t argc, zval *argv) /* Bind pre-bound arguments as lexical vars */ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *argv, - zend_array *extra_named_params) { + zend_array *extra_named_params, uint32_t const_args) { zend_arg_info *arg_infos = function->common.arg_info; uint32_t bind_offset = 0; @@ -1052,7 +1065,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval * for (uint32_t offset = 0; offset < argc; offset++) { zval *var = &argv[offset]; - if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var)) { + if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var) || zp_is_const_arg(const_args, offset)) { continue; } zend_arg_info *arg_info; @@ -1095,14 +1108,14 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, const zend_array *named_positions, zend_string *declaring_filename, const uint32_t *declaring_lineno_ptr, void **cache_slot, - zend_string *pfa_name, uint32_t flags) { + zend_string *pfa_name, uint32_t flags, uint32_t const_args) { ZEND_ASSERT(pfa_name); const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv, extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr, - cache_slot, pfa_name, flags); + cache_slot, pfa_name, flags, const_args); if (UNEXPECTED(!op_array)) { ZEND_ASSERT(EG(exception)); @@ -1130,7 +1143,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, function->common.scope, called_scope, &object, (function->common.fn_flags & ZEND_ACC_CLOSURE) != 0); - zp_bind(result, function, argc, argv, extra_named_params); + zp_bind(result, function, argc, argv, extra_named_params, const_args); } void zend_partial_op_array_dtor(zval *pDest) diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h index 386823261e2b..d3fcdae6afc8 100644 --- a/Zend/zend_partial.h +++ b/Zend/zend_partial.h @@ -36,7 +36,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function, const zend_array *named_positions, zend_string *declaring_filename, const uint32_t *declaring_lineno_ptr, void **cache_slot, - zend_string *pfa_name, uint32_t flags); + zend_string *pfa_name, uint32_t flags, uint32_t const_args); void zend_partial_op_array_dtor(zval *pDest); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 3466bc68d694..d14230514b34 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9875,6 +9875,13 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM) void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS); zval *named_positions = GET_OP2_ZVAL_PTR(); zend_string *pfa_name = Z_STR_P(GET_OP1_ZVAL_PTR()); + uint32_t const_args; + + if (OP2_TYPE == IS_UNUSED) { + const_args = opline->op2.num; + } else { + const_args = Z_EXTRA_P(named_positions); + } zend_partial_create(EX_VAR(opline->result.var), &call->This, call->func, @@ -9883,7 +9890,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM) call->extra_named_params : NULL, OP2_TYPE == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL, EX(func)->op_array.filename, &opline->lineno, cache_slot, - pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS); + pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args); if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_array_release(call->extra_named_params); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index c0a23e49d02b..53bcdccd9719 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -8672,6 +8672,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS); zval *named_positions = RT_CONSTANT(opline, opline->op2); zend_string *pfa_name = Z_STR_P(RT_CONSTANT(opline, opline->op1)); + uint32_t const_args; + + if (IS_CONST == IS_UNUSED) { + const_args = opline->op2.num; + } else { + const_args = Z_EXTRA_P(named_positions); + } zend_partial_create(EX_VAR(opline->result.var), &call->This, call->func, @@ -8680,7 +8687,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV call->extra_named_params : NULL, IS_CONST == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL, EX(func)->op_array.filename, &opline->lineno, cache_slot, - pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS); + pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args); if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_array_release(call->extra_named_params); @@ -12038,6 +12045,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS); zval *named_positions = NULL; zend_string *pfa_name = Z_STR_P(RT_CONSTANT(opline, opline->op1)); + uint32_t const_args; + + if (IS_UNUSED == IS_UNUSED) { + const_args = opline->op2.num; + } else { + const_args = Z_EXTRA_P(named_positions); + } zend_partial_create(EX_VAR(opline->result.var), &call->This, call->func, @@ -12046,7 +12060,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV call->extra_named_params : NULL, IS_UNUSED == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL, EX(func)->op_array.filename, &opline->lineno, cache_slot, - pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS); + pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args); if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_array_release(call->extra_named_params); @@ -61490,6 +61504,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS); zval *named_positions = RT_CONSTANT(opline, opline->op2); zend_string *pfa_name = Z_STR_P(RT_CONSTANT(opline, opline->op1)); + uint32_t const_args; + + if (IS_CONST == IS_UNUSED) { + const_args = opline->op2.num; + } else { + const_args = Z_EXTRA_P(named_positions); + } zend_partial_create(EX_VAR(opline->result.var), &call->This, call->func, @@ -61498,7 +61519,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P call->extra_named_params : NULL, IS_CONST == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL, EX(func)->op_array.filename, &opline->lineno, cache_slot, - pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS); + pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args); if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_array_release(call->extra_named_params); @@ -64754,6 +64775,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P void **cache_slot = CACHE_ADDR(opline->extended_value & ~ZEND_PARTIAL_FLAGS); zval *named_positions = NULL; zend_string *pfa_name = Z_STR_P(RT_CONSTANT(opline, opline->op1)); + uint32_t const_args; + + if (IS_UNUSED == IS_UNUSED) { + const_args = opline->op2.num; + } else { + const_args = Z_EXTRA_P(named_positions); + } zend_partial_create(EX_VAR(opline->result.var), &call->This, call->func, @@ -64762,7 +64790,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_P call->extra_named_params : NULL, IS_UNUSED == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL, EX(func)->op_array.filename, &opline->lineno, cache_slot, - pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS); + pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args); if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_array_release(call->extra_named_params); From 36f7c6e37d1f79c43f2170bf96aa85b4be34fde1 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 24 Jul 2026 12:24:31 +0200 Subject: [PATCH 2/5] Review --- Zend/Optimizer/dfa_pass.c | 2 +- Zend/tests/partial_application/const_arg_opt.phpt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index d5ff9f494624..971847565260 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -479,7 +479,7 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) if (send_opline->op1_type == IS_CONST) { zval *value = CT_CONSTANT_EX(op_array, send_opline->op1.constant); if (Z_TYPE_P(value) == IS_CONSTANT_AST) { - /* Const exprs can evaluate to non-cont zvals (e.g. objects), and are not idempotent */ + /* Const exprs can evaluate to non-const zvals (e.g. objects), and are not idempotent */ continue; } const_args |= (1 << i); diff --git a/Zend/tests/partial_application/const_arg_opt.phpt b/Zend/tests/partial_application/const_arg_opt.phpt index 133b0f119a24..d63456a96a5d 100644 --- a/Zend/tests/partial_application/const_arg_opt.phpt +++ b/Zend/tests/partial_application/const_arg_opt.phpt @@ -51,7 +51,7 @@ $f(2); echo "# Constant pre-bound argument (inverted):\n"; $f = f(?, 2); print_lexical_vars($f); -$f(2); +$f(1); echo "# Inlined pre-bound argument:\n"; $f = f(g(), ?); @@ -106,7 +106,7 @@ array(2) { no lexical vars array(2) { [0]=> - int(2) + int(1) [1]=> int(2) } From 57793f1746ae0b0c66e46a8741333dd4ecd099e9 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 24 Jul 2026 12:24:36 +0200 Subject: [PATCH 3/5] UB --- Zend/Optimizer/dfa_pass.c | 2 +- Zend/zend_compile.c | 2 ++ Zend/zend_partial.c | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 971847565260..dcb4e8bc8412 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -482,7 +482,7 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) /* Const exprs can evaluate to non-const zvals (e.g. objects), and are not idempotent */ continue; } - const_args |= (1 << i); + const_args |= (UINT32_C(1) << i); } } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 420a7cc08658..9c80cccb7cb9 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4162,6 +4162,8 @@ static void zend_compile_call_partial(znode *result, zend_ast_fcc *fcc_ast, uint if (!Z_ISUNDEF_P(named_positions)) { opline->op2.constant = zend_add_literal(named_positions); opline->op2_type = IS_CONST; + } else { + opline->op2.num = 0; } } diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c index e911a2925552..ec60383a2bc8 100644 --- a/Zend/zend_partial.c +++ b/Zend/zend_partial.c @@ -69,7 +69,7 @@ static zend_always_inline bool zp_is_non_static_closure(const zend_function *fun /* Whether argument at offset 'offset' is const. Such arguments can be burned into the generated op_array */ static inline bool zp_is_const_arg(uint32_t const_args, uint32_t offset) { if (offset < sizeof(const_args) * CHAR_BIT) { - return const_args & (1 << offset); + return const_args & (UINT32_C(1) << offset); } return false; } From 75c21470ea54149ac53f25d9fe768f0dd13d2fc1 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 24 Jul 2026 12:40:31 +0200 Subject: [PATCH 4/5] Fix tests --- Zend/tests/partial_application/references_004.phpt | 5 ++--- .../partial_application/variation_debug_001.phpt | 4 +++- .../partial_application/variation_debug_002.phpt | 11 +++++++++-- .../partial_application/variation_variadics_001.phpt | 7 ++----- .../partial_application/variation_variadics_002.phpt | 5 ----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Zend/tests/partial_application/references_004.phpt b/Zend/tests/partial_application/references_004.phpt index e5163c710da2..78a347909e1b 100644 --- a/Zend/tests/partial_application/references_004.phpt +++ b/Zend/tests/partial_application/references_004.phpt @@ -26,9 +26,8 @@ var_dump($b, $c, $d); Closure [ static function {closure:%s:%d} ] { @@ %sreferences_004.php 13 - 13 - - Bound Variables [2] { - Variable #0 [ $a ] - Variable #1 [ $args2 ] + - Bound Variables [1] { + Variable #0 [ $args2 ] } - Parameters [2] { diff --git a/Zend/tests/partial_application/variation_debug_001.phpt b/Zend/tests/partial_application/variation_debug_001.phpt index 04b63f3c4010..21cfe4b84790 100644 --- a/Zend/tests/partial_application/variation_debug_001.phpt +++ b/Zend/tests/partial_application/variation_debug_001.phpt @@ -1,12 +1,14 @@ --TEST-- PFA variation: var_dump(), user function +--ENV-- +A=20 --FILE-- --EXPECTF-- object(Closure)#%d (5) { diff --git a/Zend/tests/partial_application/variation_debug_002.phpt b/Zend/tests/partial_application/variation_debug_002.phpt index a7c4c2d76e49..f1f323a91c04 100644 --- a/Zend/tests/partial_application/variation_debug_002.phpt +++ b/Zend/tests/partial_application/variation_debug_002.phpt @@ -1,8 +1,15 @@ --TEST-- PFA variation: var_dump(), internal function +--ENV-- +A=1 --FILE-- --EXPECTF-- object(Closure)#%d (5) { @@ -11,7 +18,7 @@ object(Closure)#%d (5) { ["file"]=> string(%d) "%svariation_debug_002.php" ["line"]=> - int(2) + int(7) ["static"]=> array(3) { ["array"]=> diff --git a/Zend/tests/partial_application/variation_variadics_001.phpt b/Zend/tests/partial_application/variation_variadics_001.phpt index 850f0eda149d..6a75310d1660 100644 --- a/Zend/tests/partial_application/variation_variadics_001.phpt +++ b/Zend/tests/partial_application/variation_variadics_001.phpt @@ -1,5 +1,7 @@ --TEST-- PFA variation: variadics, user function +--ENV-- +A=1 --FILE-- static function {closure:%s:%d} ] { @@ %s 6 - 6 - - Bound Variables [2] { - Variable #0 [ $a ] - Variable #1 [ $b2 ] - } - - Parameters [1] { Parameter #0 [ ...$b ] } diff --git a/Zend/tests/partial_application/variation_variadics_002.phpt b/Zend/tests/partial_application/variation_variadics_002.phpt index 21d8169fc42c..f3fb4c23ce10 100644 --- a/Zend/tests/partial_application/variation_variadics_002.phpt +++ b/Zend/tests/partial_application/variation_variadics_002.phpt @@ -12,11 +12,6 @@ echo $sprintf(1000, 10000); Closure [ static function {closure:%s:%d} ] { @@ %svariation_variadics_002.php 2 - 2 - - Bound Variables [2] { - Variable #0 [ $format ] - Variable #1 [ $values2 ] - } - - Parameters [1] { Parameter #0 [ mixed ...$values ] } From 62324a80021cf18223dd0e198e7cd66bc74c09a9 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 24 Jul 2026 15:12:54 +0200 Subject: [PATCH 5/5] Fix tests --- Zend/tests/partial_application/references_004.phpt | 5 +++++ Zend/tests/partial_application/variation_debug_001.phpt | 5 +++++ Zend/tests/partial_application/variation_debug_002.phpt | 5 +++++ Zend/tests/partial_application/variation_variadics_001.phpt | 5 +++++ Zend/tests/partial_application/variation_variadics_002.phpt | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/Zend/tests/partial_application/references_004.phpt b/Zend/tests/partial_application/references_004.phpt index 78a347909e1b..8d56d7dceaf4 100644 --- a/Zend/tests/partial_application/references_004.phpt +++ b/Zend/tests/partial_application/references_004.phpt @@ -1,5 +1,10 @@ --TEST-- PFA receives variadic param by ref if the actual function does +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.file_update_protection=0 --FILE--