From 1692bfdf59232c4cfca50c36057ef46b563d84a9 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Jun 2026 12:08:52 +0700 Subject: [PATCH 1/2] preload inference and compile time monomorphization without explicit turbofish --- Zend/Optimizer/dfa_pass.c | 600 ++++++++++++++++++ Zend/Optimizer/zend_optimizer.c | 4 +- Zend/Optimizer/zend_optimizer.h | 2 + Zend/Optimizer/zend_optimizer_internal.h | 2 + .../inference_all_defaulted_no_crash.phpt | 27 + .../reification/inference_literal_arg.phpt | 26 + .../reification/inference_optimizer_ssa.phpt | 45 ++ .../reification/preload_aot_call_lowering.inc | 28 + .../preload_aot_call_lowering.phpt | 36 ++ .../preload_class_monomorph_new.inc | 27 + .../preload_class_monomorph_new.phpt | 27 + .../reification/preload_class_typed_param.inc | 18 + .../preload_class_typed_param.phpt | 35 + .../reification/preload_generic_call_ops.inc | 10 + .../reification/preload_generic_call_ops.phpt | 25 + .../turbofish_namespace_class_arg.phpt | 36 ++ Zend/zend_compile.c | 191 +++++- Zend/zend_compile.h | 8 + Zend/zend_execute.c | 5 +- Zend/zend_inheritance.c | 266 +++++++- Zend/zend_inheritance.h | 6 + Zend/zend_opcode.c | 7 +- Zend/zend_vm_def.h | 19 +- Zend/zend_vm_execute.h | 38 +- ext/opcache/ZendAccelerator.c | 15 +- ext/opcache/zend_persist.c | 8 +- ext/opcache/zend_persist_calc.c | 6 + 27 files changed, 1479 insertions(+), 38 deletions(-) create mode 100644 Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt create mode 100644 Zend/tests/generics/reification/inference_literal_arg.phpt create mode 100644 Zend/tests/generics/reification/inference_optimizer_ssa.phpt create mode 100644 Zend/tests/generics/reification/preload_aot_call_lowering.inc create mode 100644 Zend/tests/generics/reification/preload_aot_call_lowering.phpt create mode 100644 Zend/tests/generics/reification/preload_class_monomorph_new.inc create mode 100644 Zend/tests/generics/reification/preload_class_monomorph_new.phpt create mode 100644 Zend/tests/generics/reification/preload_class_typed_param.inc create mode 100644 Zend/tests/generics/reification/preload_class_typed_param.phpt create mode 100644 Zend/tests/generics/reification/preload_generic_call_ops.inc create mode 100644 Zend/tests/generics/reification/preload_generic_call_ops.phpt create mode 100644 Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 76ca8e5145f0..fde6642af247 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -27,6 +27,7 @@ #include "zend_call_graph.h" #include "zend_inference.h" #include "zend_dump.h" +#include "zend_inheritance.h" #ifndef ZEND_DEBUG_DFA # define ZEND_DEBUG_DFA ZEND_DEBUG @@ -403,6 +404,601 @@ static bool variable_defined_or_used_in_range(zend_ssa *ssa, int var, int start, return false; } +/* SSA arg types reflect the call-site value, not the declared type, so inferring T from them is sound. */ +static bool zend_dfa_send_concrete_type( + const zend_op_array *op_array, const zend_ssa *ssa, + const zend_op *send, zend_type *out) +{ + /* By-ref / unusual sends: the callee could observe a reference; skip. */ + if (send->opcode == ZEND_SEND_REF + || send->opcode == ZEND_SEND_VAR_NO_REF + || send->opcode == ZEND_SEND_VAR_NO_REF_EX + || send->opcode == ZEND_SEND_USER) { + return false; + } + + if (send->op1_type == IS_CONST) { + const zval *zv = CT_CONSTANT_EX(op_array, send->op1.constant); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } + } + + int var = ssa->ops[send - op_array->opcodes].op1_use; + if (var < 0) { + return false; + } + const zend_ssa_var_info *info = &ssa->var_info[var]; + if (info->type & (MAY_BE_UNDEF | MAY_BE_REF)) { + return false; + } + uint32_t pure = info->type & MAY_BE_ANY; + switch (pure) { + case MAY_BE_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case MAY_BE_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case MAY_BE_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + } + /* Exact class only: !is_instanceof matches what runtime inference binds T to. */ + if (pure == MAY_BE_OBJECT && info->ce && !info->is_instanceof) { + *out = (zend_type) ZEND_TYPE_INIT_CLASS(zend_string_copy(info->ce->name), 0, 0); + return true; + } + return false; +} + +static zend_op *zend_dfa_find_call_verify(zend_op_array *op_array, const zend_call_info *call_info) +{ + if (!call_info->caller_call_opline || !call_info->caller_init_opline) { + return NULL; + } + zend_op *p = call_info->caller_call_opline; + while (p > call_info->caller_init_opline) { + p--; + if (p->opcode == ZEND_NOP || p->opcode == ZEND_EXT_NOP + || p->opcode == ZEND_EXT_FCALL_BEGIN) { + continue; + } + /* VERIFY extended_value 0 = speculative non-turbofish site, != 0 = turbofish; INSTALL always turbofish. */ + if ((p->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS + || p->opcode == ZEND_INSTALL_GENERIC_ARGS) + && p->op1_type == IS_UNUSED) { + return p; + } + return NULL; + } + return NULL; +} + +static bool zend_dfa_try_direct_dispatch(zend_op_array *op_array, + const zend_call_info *ci, zend_op *site, const zend_function *fbc) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args) { + return false; + } + zend_op *init = ci->caller_init_opline; + if (!init || (init->opcode != ZEND_INIT_FCALL + && init->opcode != ZEND_INIT_FCALL_BY_NAME + && init->opcode != ZEND_INIT_NS_FCALL_BY_NAME)) { + return false; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, site->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + return false; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) { + return false; + } + } + + zend_string *mangled = zend_generic_canonical_class_name( + fbc->common.function_name, nwa->args, nwa->count); + zend_string *lc = zend_string_tolower(mangled); + zend_string_hash_val(mangled); + zend_string_hash_val(lc); + zval zv; + ZVAL_STR(&zv, mangled); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* orig name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc name at lit+1 */ + + init->opcode = ZEND_INIT_FCALL_BY_NAME; + init->op1_type = IS_UNUSED; + init->op1.num = 0; + init->op2_type = IS_CONST; + init->op2.constant = lit; + /* result.num keeps the existing 1-slot function cache. */ + MAKE_NOP(site); + return true; +} + +/* On a final class the called scope is always one of its own monomorphs, so static ≡ the own-params turbofish. */ +static uint32_t zend_dfa_selfize_generic_new(zend_op_array *op_array) +{ + zend_class_entry *scope = op_array->scope; + if (!scope || !scope->generic_parameters + || !(scope->ce_flags & ZEND_ACC_FINAL) + || !op_array->generic_types || !op_array->generic_types->turbofish_args) { + return 0; + } + uint32_t pcount = scope->generic_parameters->count; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; + } + zend_string *cname = Z_STR(op_array->literals[newop->op1.constant]); + if (!zend_string_equals_ci(cname, scope->name)) { + continue; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count != pcount) { + continue; + } + bool identity = true; + for (uint32_t a = 0; a < nwa->count; a++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[a]) + || (ZEND_TYPE_FULL_MASK(nwa->args[a]) & MAY_BE_NULL)) { + identity = false; + break; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[a]); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index != a) { + identity = false; + break; + } + } + if (!identity) { + continue; + } + newop->op1_type = IS_UNUSED; + newop->op1.num = ZEND_FETCH_CLASS_STATIC | ZEND_FETCH_CLASS_EXCEPTION; + newop->op2_type = IS_UNUSED; + newop->op2.num = 0; + MAKE_NOP(verify); + changed++; + } + return changed; +} + +static uint32_t zend_dfa_optimize_generic_calls(zend_op_array *op_array, zend_ssa *ssa) +{ + const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); + uint32_t changed = 0; + + if (!func_info || !func_info->callee_info) { + return 0; + } + + for (const zend_call_info *ci = func_info->callee_info; ci; ci = ci->next_callee) { + const zend_function *fbc = ci->callee_func; + if (!fbc || ci->is_prototype || ci->named_args || ci->send_unpack) { + continue; + } + if (!ZEND_USER_CODE(fbc->common.type)) { + continue; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0) { + continue; + } + + zend_op *verify = zend_dfa_find_call_verify(op_array, ci); + if (!verify) { + continue; + } + + /* Turbofish site needs no value inference, so runs even for callees with no inferable params. */ + if (verify->extended_value != 0) { + if (zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc)) { + changed++; + continue; + } + uint8_t opcode = zend_generic_try_install_resolved_turbofish( + op_array, fbc, verify->extended_value, verify->op2.num); + if (opcode != ZEND_NOP) { + verify->opcode = opcode; + changed++; + } + continue; + } + + if (params->inferable_mask == 0 + || !fbc->op_array.generic_types || !fbc->op_array.generic_types->parameters) { + continue; + } + + uint32_t total = params->count; + uint32_t required = 0; + while (required < total + && !ZEND_TYPE_IS_SET(params->parameters[required].default_type)) { + required++; + } + if (total > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + + /* Bare top-level T only, matching zend_build_generic_call_type_args. */ + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + HashTable *pre = fbc->op_array.generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe)) continue; + if (ZEND_TYPE_FULL_MASK(*pe) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < ci->num_args) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0 || !ci->arg_info[pos].opline) { + break; + } + zend_type entry; + if (!zend_dfa_send_concrete_type(op_array, ssa, ci->arg_info[pos].opline, &entry)) { + break; + } + inferred[arity++] = entry; + } + /* arity 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE; stay generic. */ + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + continue; + } + + uint32_t args_id = 0; + uint8_t opcode = zend_generic_install_inferred_call( + op_array, fbc, inferred, arity, &args_id); + if (opcode == ZEND_NOP) { + continue; + } + verify->opcode = opcode; + verify->op2.num = arity; + verify->extended_value = args_id; + zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc); + changed++; + } + + return changed; +} + +static uint32_t zend_aot_rewrite_new_sites(zend_op_array *op_array) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args + || !op_array->generic_types->monomorph_type_args) { + return 0; + } + const zend_type_arg_table *binds = op_array->generic_types->monomorph_type_args; + if (binds->count > ZEND_GENERIC_MAX_PARAMS) { + return 0; + } + zend_type bindv[ZEND_GENERIC_MAX_PARAMS]; + uint32_t bindc = binds->count; + for (uint32_t i = 0; i < bindc; i++) { + const zend_type *t = zend_type_arg_entry_type(&binds->entries[i]); + bindv[i] = t ? *t : (zend_type) ZEND_TYPE_INIT_NONE(0); + } + + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; /* self/static/dynamic base: leave the runtime VERIFY. */ + } + + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count == 0 || nwa->count > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + for (uint32_t a = 0; a < nwa->count; a++) { + zend_type r = bindc + ? zend_substitute_function_type_param(nwa->args[a], bindv, bindc) + : nwa->args[a]; + if (zend_type_contains_type_parameter(r)) { + ok = false; + break; + } + resolved[a] = r; + } + if (!ok) { + continue; + } + + zend_string *base_name = Z_STR(op_array->literals[newop->op1.constant]); + zend_string *canonical = zend_generic_canonical_class_name( + base_name, resolved, nwa->count); + zend_string *lc = zend_string_tolower(canonical); + zend_string_hash_val(canonical); + zend_string_hash_val(lc); + + zval zv; + ZVAL_STR(&zv, canonical); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* full name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc at lit+1 */ + + newop->op1.constant = lit; /* keep op2.num cache slot (caches mono ce) */ + MAKE_NOP(verify); + changed++; + } + return changed; +} + +/* Per-monomorph ONCE: re-running zend_optimize_script would corrupt SSA. */ +static void zend_aot_optimize_monomorph(zend_op_array *op_array, zend_script *script, zend_long opt_level) +{ + if (op_array->last_try_catch) { + /* dfa bails on try/catch; do the SSA-free class-`new` rewrite, leave the rest generic. */ + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_redo_pass_two(op_array); + return; + } + zend_optimizer_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.arena = zend_arena_create(64 * 1024); + ctx.script = script; + ctx.optimization_level = opt_level; + + zend_func_info *func_info = zend_arena_calloc(&ctx.arena, 1, sizeof(zend_func_info)); + ZEND_SET_FUNC_INFO(op_array, func_info); + + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_analyze_calls(&ctx.arena, script, 0, op_array, func_info); + func_info->call_map = zend_build_call_map(&ctx.arena, func_info, op_array); + if (zend_dfa_analyze_op_array(op_array, &ctx, &func_info->ssa) == SUCCESS) { + zend_dfa_optimize_op_array(op_array, &ctx, &func_info->ssa, func_info->call_map); + } + zend_redo_pass_two(op_array); + + ZEND_SET_FUNC_INFO(op_array, NULL); + zend_arena_destroy(ctx.arena); +} + +/* Returns the case-preserved (+0) name, not the lc one: arg_info needs the original case. */ +static zend_string *zend_aot_mangled_call_name(const zend_op_array *op_array, const zend_op *op) +{ + if (op->op2_type != IS_CONST) { + return NULL; + } + zend_string *display; + if (op->opcode == ZEND_INIT_FCALL_BY_NAME || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { + display = Z_STR_P(RT_CONSTANT(op, op->op2)); + } else { + return NULL; + } + return memchr(ZSTR_VAL(display), '<', ZSTR_LEN(display)) ? display : NULL; +} + +/* Synthesizes each direct-dispatch callee; its own calls become the next round's work (fixpoint). */ +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level) +{ + /* Collect names first: synthesis rehashes function_table mid-iteration. */ + zend_string *wanted[4096]; + uint32_t want_count = 0; + zend_op_array *op_array; + /* wanted[] holds display names; table is keyed by lc, so lowercase only for the existence check. */ + #define ZEND_AOT_SCAN_OPS(oa) do { \ + zend_op_array *zoa = (oa); \ + if (zoa->type == ZEND_USER_FUNCTION && zoa->opcodes) { \ + for (uint32_t _i = 0; _i < zoa->last && want_count < (sizeof(wanted)/sizeof(wanted[0])); _i++) { \ + zend_string *_disp = zend_aot_mangled_call_name(zoa, &zoa->opcodes[_i]); \ + if (_disp) { \ + zend_string *_lc = zend_string_tolower(_disp); \ + if (!zend_hash_exists(&script->function_table, _lc)) { \ + wanted[want_count++] = _disp; \ + } \ + zend_string_release(_lc); \ + } \ + } \ + } \ + } while (0) + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + ZEND_AOT_SCAN_OPS(op_array); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + ZEND_AOT_SCAN_OPS(&m->op_array); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + #undef ZEND_AOT_SCAN_OPS + + zend_op_array *new_monos[4096]; + uint32_t new_count = 0; + for (uint32_t i = 0; i < want_count; i++) { + zend_string *lc = zend_string_tolower(wanted[i]); + bool exists = zend_hash_exists(&script->function_table, lc); + zend_string_release(lc); + if (exists) { + continue; /* synthesized as a duplicate request earlier this round. */ + } + zend_function *mono = zend_synthesize_specialized_monomorph_by_name( + &script->function_table, wanted[i]); + if (mono && new_count < (sizeof(new_monos)/sizeof(new_monos[0]))) { + new_monos[new_count++] = &mono->op_array; + } + } + + for (uint32_t i = 0; i < new_count; i++) { + zend_aot_optimize_monomorph(new_monos[i], script, opt_level); + } + return new_count; +} + +/* Upgrade INIT_FCALL_BY_NAME -> INIT_FCALL (+ DO_UCALL): safe since AOT persists the callee. */ +static zend_always_inline bool zend_aot_is_call_open(uint8_t opcode) +{ + switch (opcode) { + case ZEND_INIT_FCALL: + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + return true; + default: + return false; + } +} + +static uint32_t zend_aot_upgrade_op_array_to_ucall(zend_op_array *op_array, zend_script *script) +{ + if (!op_array->opcodes) { + return 0; + } + /* Nesting stack matches each call-open to its closing DO_FCALL*; fbc resolved at INIT for the SENDs. */ + struct { uint32_t init_i; zend_function *fbc; } stack[512]; + uint32_t sp = 0; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *op = &op_array->opcodes[i]; + if (zend_aot_is_call_open(op->opcode)) { + if (sp >= sizeof(stack)/sizeof(stack[0])) { + return changed; /* pathological nesting — stop, stay correct. */ + } + /* Both by-name forms keep the registered lc name at the +1 literal. */ + zend_function *fbc = NULL; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->op2_type == IS_CONST) { + zend_string *lc = Z_STR_P(RT_CONSTANT(op, op->op2) + 1); + if (memchr(ZSTR_VAL(lc), '<', ZSTR_LEN(lc))) { + zend_function *f = zend_hash_find_ptr(&script->function_table, lc); + if (f && f->type == ZEND_USER_FUNCTION) { + fbc = f; + } + } + } + stack[sp].init_i = i; + stack[sp].fbc = fbc; + sp++; + continue; + } + if (op->opcode == ZEND_CALLABLE_CONVERT) { + if (sp > 0) sp--; /* first-class callable closes its frame, no DO. */ + continue; + } + if (sp > 0 && stack[sp - 1].fbc + && op->opcode == ZEND_SEND_VAR_EX + && op->op2_type != IS_CONST + && !ARG_SHOULD_BE_SENT_BY_REF(stack[sp - 1].fbc, op->op2.num)) { + op->opcode = ZEND_SEND_VAR; + zend_vm_set_opcode_handler(op); + } + bool is_do = op->opcode == ZEND_DO_FCALL || op->opcode == ZEND_DO_FCALL_BY_NAME + || op->opcode == ZEND_DO_ICALL || op->opcode == ZEND_DO_UCALL; + if (!is_do || sp == 0) { + continue; + } + zend_function *fbc = stack[sp - 1].fbc; + zend_op *init = &op_array->opcodes[stack[sp - 1].init_i]; + sp--; + if (!fbc) { + continue; /* not a resolved monomorph dispatch site. */ + } + init->opcode = ZEND_INIT_FCALL; + init->op1_type = IS_UNUSED; + init->op1.num = zend_vm_calc_used_stack(init->extended_value, fbc); + /* INIT_FCALL reads op2 directly: advance one zval to the +1 lc literal it resolves. */ + init->op2.constant += sizeof(zval); + zend_vm_set_opcode_handler(init); + + /* Skip deprecated/nodiscard (as zend_get_call_op does) so warnings still fire. */ + if (op->opcode == ZEND_DO_FCALL_BY_NAME + && !(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED | ZEND_ACC_NODISCARD))) { + op->opcode = ZEND_DO_UCALL; + zend_vm_set_opcode_handler(op); + } + changed++; + } + return changed; +} + +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script) +{ + uint32_t changed = 0; + zend_op_array *op_array; + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + changed += zend_aot_upgrade_op_array_to_ucall(op_array, script); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + if (m->type == ZEND_USER_FUNCTION) { + changed += zend_aot_upgrade_op_array_to_ucall(&m->op_array, script); + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + return changed; +} + static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) { const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); @@ -1070,6 +1666,10 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx if (zend_dfa_optimize_calls(op_array, ssa)) { remove_nops = 1; } + zend_dfa_optimize_generic_calls(op_array, ssa); + if (zend_dfa_selfize_generic_new(op_array)) { + remove_nops = 1; + } } if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_8) { zend_dump_op_array(op_array, ZEND_DUMP_SSA, "after sccp pass", ssa); diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index 5ffdefac49a3..26d6f1f425bb 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -1306,7 +1306,7 @@ static void zend_optimize(zend_op_array *op_array, } } -static void zend_revert_pass_two(zend_op_array *op_array) +void zend_revert_pass_two(zend_op_array *op_array) { zend_op *opline; @@ -1336,7 +1336,7 @@ static void zend_revert_pass_two(zend_op_array *op_array) op_array->fn_flags &= ~ZEND_ACC_DONE_PASS_TWO; } -static void zend_redo_pass_two(zend_op_array *op_array) +void zend_redo_pass_two(zend_op_array *op_array) { zend_op *opline, *end; #if ZEND_USE_ABS_JMP_ADDR && !ZEND_USE_ABS_CONST_ADDR diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index d2847c92869c..6d3ac2423e5d 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -92,6 +92,8 @@ typedef void (*zend_optimizer_pass_t)(zend_script *, void *context); BEGIN_EXTERN_C() ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_level, zend_long debug_level); +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level); +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script); ZEND_API int zend_optimizer_register_pass(zend_optimizer_pass_t pass); ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); diff --git a/Zend/Optimizer/zend_optimizer_internal.h b/Zend/Optimizer/zend_optimizer_internal.h index d01df56260bc..9592183c503f 100644 --- a/Zend/Optimizer/zend_optimizer_internal.h +++ b/Zend/Optimizer/zend_optimizer_internal.h @@ -111,6 +111,8 @@ void zend_optimizer_pass3(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_cfg(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_dfa(zend_op_array *op_array, zend_optimizer_ctx *ctx); +void zend_revert_pass_two(zend_op_array *op_array); +void zend_redo_pass_two(zend_op_array *op_array); zend_result zend_dfa_analyze_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa); void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa, zend_call_info **call_map); void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx); diff --git a/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt new file mode 100644 index 000000000000..f72df5890e73 --- /dev/null +++ b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a non-turbofish call to an all-defaulted generic callee with non-literal args infers nothing and stays generic (must not build an empty type-arg box) +--FILE-- + crash). +function neighbors(mixed $graph, TNode $node): array { + return [$node]; +} + +function caller(mixed $g, mixed $n): array { + return neighbors($g, $n); +} + +var_dump(caller('graph', 'x')); +var_dump(caller('graph', 42)); +echo "ok\n"; +?> +--EXPECT-- +array(1) { + [0]=> + string(1) "x" +} +array(1) { + [0]=> + int(42) +} +ok diff --git a/Zend/tests/generics/reification/inference_literal_arg.phpt b/Zend/tests/generics/reification/inference_literal_arg.phpt new file mode 100644 index 000000000000..14829462fb20 --- /dev/null +++ b/Zend/tests/generics/reification/inference_literal_arg.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a scalar literal argument monomorphizes a non-turbofish generic call at compile time (no opcache needed); the reified return type enforces the inferred T +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function lit_int() { return retStr(42); } +function lit_float() { return retStr(3.14); } +function lit_string() { return retInt("hi"); } + +foreach (['lit_int', 'lit_float', 'lit_string'] as $fn) { + try { + $fn(); + echo "$fn: NO error (not monomorphized!)\n"; + } catch (TypeError $e) { + echo "$fn: ", $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +lit_int: retStr(): Return value must be of type int, string returned +lit_float: retStr(): Return value must be of type float, string returned +lit_string: retInt(): Return value must be of type string, int returned diff --git a/Zend/tests/generics/reification/inference_optimizer_ssa.phpt b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt new file mode 100644 index 000000000000..716957ccd5bf --- /dev/null +++ b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt @@ -0,0 +1,45 @@ +--TEST-- +Reification: the opcache DFA optimizer monomorphizes non-turbofish generic calls from each argument's SSA-inferred type; reassignment binds the actual type, not the declared one, and unknown types stay generic +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function from_int(int $i) { return retStr($i); } +function from_float(float $f) { return retStr($f); } +function local_literal() { $v = 1.5; return retStr($v); } + +// Reassignment must bind T from the SSA type (string), not the declared int. +function reassigned(int $val) { $val = 'x'; return retInt($val); } + +function from_unknown(array $a) { $v = $a[0]; return retInt($v); } + +function check(string $name, callable $fn): void { + try { + $r = $fn(); + echo "$name: ", var_export($r, true), "\n"; + } catch (TypeError $e) { + echo "$name: TypeError\n"; + } +} + +check('from_int', fn() => from_int(1)); +check('from_float', fn() => from_float(1.5)); +check('local_literal', fn() => local_literal()); +check('reassigned', fn() => reassigned(1)); +check('from_unknown', fn() => from_unknown([42])); +?> +--EXPECT-- +from_int: TypeError +from_float: TypeError +local_literal: TypeError +reassigned: TypeError +from_unknown: 7 diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.inc b/Zend/tests/generics/reification/preload_aot_call_lowering.inc new file mode 100644 index 000000000000..b86db5d7f720 --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.inc @@ -0,0 +1,28 @@ +(T $x): T { return $x; } + +function add(T $a, T $b): T { return $a + $b; } + +// No value params: dispatch must fire from the turbofish alone, nothing inferable from args. +function zero(): string { return "z"; } + +class Runner { + public static function ints(int $n): int { + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = add::($acc, id::($i)); + } + return $acc; + } + + public static function strs(): string { + return id::("hello") . ":" . zero::(); + } + + public static function badType(): void { + add::(1, []); + } +} diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.phpt b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt new file mode 100644 index 000000000000..f35cd4e034af --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: AOT-lowered preloaded generic calls (INIT_FCALL/DO_UCALL + concrete RECV + by-value SEND) stay correct and type-safe +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_aot_call_lowering.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + 5. +var_dump(Bench\add::("5", 2)); + +try { + Runner::badType(); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +int(10) +string(7) "hello:z" +int(7) +Bench\add(): Argument #2 ($b) must be of type int, array given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.inc b/Zend/tests/generics/reification/preload_class_monomorph_new.inc new file mode 100644 index 000000000000..15ca454caaed --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.inc @@ -0,0 +1,27 @@ + { + public function __construct(public array $items = []) {} + public function with(T $x): Vec { + $n = $this->items; + $n[] = $x; + return new Vec::($n); + } + public function sum(): int { + $s = 0; + foreach ($this->items as $v) { $s += $v; } + return $s; + } + public function size(): int { return count($this->items); } +} + +class Driver { + public static function build(): string { + $v = new Vec::([1, 2]); + $v = $v->with(3); + $v = $v->with(4); + return $v->sum() . ':' . $v->size() . ':' . get_class($v); + } +} diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.phpt b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt new file mode 100644 index 000000000000..44c9911d2cf2 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a compile-time class monomorph (`new C::`) inside a preloaded method preloads cleanly and behaves correctly +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_monomorph_new.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + SEGV at preload. +echo Driver::build(), "\n"; + +$s = new Vec::(['a']); +$s = $s->with('b'); +echo $s->size(), ':', get_class($s), "\n"; +echo "done\n"; +?> +--EXPECT-- +10:4:Vec +2:Vec +done diff --git a/Zend/tests/generics/reification/preload_class_typed_param.inc b/Zend/tests/generics/reification/preload_class_typed_param.inc new file mode 100644 index 000000000000..d48ba9710738 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.inc @@ -0,0 +1,18 @@ +(T $x): T { return $x; } + +class Keeper { + public static function take(Animal $a): string { + return get_class(keep::($a)); + } + public static function bad(): void { + keep::("not an animal"); + } +} diff --git a/Zend/tests/generics/reification/preload_class_typed_param.phpt b/Zend/tests/generics/reification/preload_class_typed_param.phpt new file mode 100644 index 000000000000..cb1fc27f1505 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reification: an AOT-preloaded monomorph of a bare class-typed parameter accepts subclasses and rejects non-instances +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_typed_param.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +Zoo\Dog +Zoo\Animal +Zoo\keep(): Argument #1 ($x) must be of type Zoo\Animal, string given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.inc b/Zend/tests/generics/reification/preload_generic_call_ops.inc new file mode 100644 index 000000000000..b02304b58327 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.inc @@ -0,0 +1,10 @@ +(T $x): T { + return $x; +} + +function outer(T $x): T { + return inner($x); +} diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.phpt b/Zend/tests/generics/reification/preload_generic_call_ops.phpt new file mode 100644 index 000000000000..871747b8e8d6 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reification: a preloaded generic function that contains a generic call op resolves correctly and shuts down cleanly (smoke test for the destroy_op_array runtime-cache cleanup; the underlying UAF is caught deterministically under valgrind/ASAN) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_generic_call_ops.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +(5)); +var_dump(outer(7)); +var_dump(inner::("ok")); +echo "done\n"; +?> +--EXPECT-- +int(5) +int(7) +string(2) "ok" +done diff --git a/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt new file mode 100644 index 000000000000..790d0bf63333 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: an unqualified class name in a turbofish type argument resolves against the current namespace +--FILE-- +(T $x): T { return $x; } + +$dog = identity::(new Dog()); +echo get_class($dog), "\n"; + +$animal = identity::(new Animal()); +echo get_class($animal), "\n"; + +try { + identity::("not an animal"); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} + +function wrap(T $x): T { return $x; } +$d2 = wrap::(new Dog()); +echo get_class($d2), "\n"; + +echo "done\n"; +?> +--EXPECTF-- +Acme\Dog +Acme\Animal +Acme\identity(): Argument #1 ($x) must be of type Acme\Animal, string given, called in %s on line %d +Acme\Dog +done diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 9688ec242fca..2adea3d829be 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -438,6 +438,9 @@ void zend_init_compiler_data_structures(void) /* {{{ */ CG(generic_scope) = NULL; CG(in_static_member_type) = false; CG(inheritance_binding_cache) = NULL; + CG(inheritance_binding_hint).target = NULL; + CG(inheritance_binding_hint).args = NULL; + CG(inheritance_binding_hint).arity = 0; } /* }}} */ @@ -1757,7 +1760,7 @@ static zend_type_arg_table *zend_build_concrete_call_type_args( /* If this turbofish CALL site is concrete, build the resolved type-arg table now * and stash it on the turbofish entry so persist can relocate it into SHM. */ -static bool zend_try_attach_concrete_call_table( +static bool zend_try_attach_concrete_call_table_for(zend_op_array *caller, const zend_function *fbc, const zend_type *args_box, uint32_t args_id) { if (!fbc || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { @@ -1776,7 +1779,7 @@ static bool zend_try_attach_concrete_call_table( if (!ct) { return false; } - zend_generic_type_table *gtt = CG(active_op_array)->generic_types; + zend_generic_type_table *gtt = caller->generic_types; zend_turbofish_args_entry *entry = zend_hash_index_find_ptr(gtt->turbofish_args, args_id); ZEND_ASSERT(entry != NULL); @@ -1789,7 +1792,169 @@ static bool zend_try_attach_concrete_call_table( return true; } -static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) +/* Literals only: inferring T from a variable's declared type is unsound, as the + * variable may be reassigned before the call. */ +static bool zend_infer_literal_arg_pre_erasure(zend_ast *arg, zend_type *out) +{ + if (arg->kind != ZEND_AST_ZVAL) { + return false; + } + const zval *zv = zend_ast_get_zval(arg); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } +} + +static const zend_type *zend_infer_call_type_args( + const zend_function *fbc, zend_ast *args_ast, + uint32_t *out_arity, uint32_t *out_args_id) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) { + return NULL; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0 || params->inferable_mask == 0) { + return NULL; + } + const zend_op_array *callee = &fbc->op_array; + if (!callee->generic_types || !callee->generic_types->parameters) { + return NULL; + } + if (!args_ast || args_ast->kind != ZEND_AST_ARG_LIST) { + return NULL; + } + zend_ast_list *args = zend_ast_get_list(args_ast); + for (uint32_t i = 0; i < args->children; i++) { + zend_ast_kind k = args->child[i]->kind; + if (k == ZEND_AST_UNPACK || k == ZEND_AST_NAMED_ARG) { + return NULL; + } + } + + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + if (total == 0 || total > ZEND_GENERIC_MAX_PARAMS) { + return NULL; + } + + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + { + HashTable *pre = callee->generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_FULL_MASK(*pe_type_ptr) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < (zend_ulong) args->children) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + } + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0) { + break; + } + zend_type entry; + if (!zend_infer_literal_arg_pre_erasure(args->child[pos], &entry)) { + break; + } + inferred[arity++] = entry; + } + + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + return NULL; + } + + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = inferred[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + + *out_arity = arity; + *out_args_id = args_id; + return zend_hash_index_find_ptr(table->turbofish_args, args_id); +} + +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id) +{ + /* arity == 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE (count-1). */ + if (arity == 0) { + return ZEND_NOP; + } + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = arg_types[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + if (!zend_can_install_call_args_statically(fbc, &box, arity)) { + zend_type_release(box, /* persistent */ false); + return ZEND_NOP; + } + + zend_generic_type_table *table = zend_generic_get_or_create_op_array_table(caller); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + const zend_type *args_box = zend_hash_index_find_ptr(table->turbofish_args, args_id); + zend_try_attach_concrete_call_table_for(caller, fbc, args_box, args_id); + + *out_args_id = args_id; + return ZEND_INSTALL_GENERIC_ARGS; +} + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity) +{ + if (!caller->generic_types || !caller->generic_types->turbofish_args) { + return ZEND_NOP; + } + const zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(caller->generic_types->turbofish_args, args_id); + if (!entry) { + return ZEND_NOP; + } + if (!zend_can_install_call_args_statically(fbc, &entry->args_box, arity)) { + return ZEND_NOP; + } + zend_try_attach_concrete_call_table_for(caller, fbc, &entry->args_box, args_id); + return ZEND_INSTALL_GENERIC_ARGS; +} + +static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc, zend_ast *call_args_ast) { uint32_t arity = 0; uint32_t args_id = 0; @@ -1817,6 +1982,13 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t || !fbc->op_array.generic_parameters)) { return; } + if (fbc) { + const zend_type *inferred = + zend_infer_call_type_args(fbc, call_args_ast, &arity, &args_id); + if (inferred) { + args_box = inferred; + } + } } /* Replace the runtime VERIFY with INSTALL when arity+bound check is @@ -1826,11 +1998,11 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t if (kind == ZEND_VERIFY_ARITY_KIND_CALL && zend_can_install_call_args_statically(fbc, args_box, arity)) { opcode = ZEND_INSTALL_GENERIC_ARGS; - zend_try_attach_concrete_call_table(fbc, args_box, args_id); - } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && turbofish_ast && args_id) { + zend_try_attach_concrete_call_table_for(CG(active_op_array), fbc, args_box, args_id); + } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && args_id) { /* Concrete args but VERIFY stays (bounds not statically decidable); * still attach the invariant table for the handler to install. */ - zend_try_attach_concrete_call_table(fbc, args_box, args_id); + zend_try_attach_concrete_call_table_for(CG(active_op_array), fbc, args_box, args_id); } zend_op *opline = get_next_op(); @@ -2201,7 +2373,8 @@ static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) result = (zend_type) ZEND_TYPE_INIT_CODE(code, 0, 0); } } else { - zend_string_addref(name); + /* Resolve against namespace/use, else `f::` in `namespace N` stays "C" not "N\C". */ + name = zend_resolve_class_name(name, ast->attr); result = (zend_type) ZEND_TYPE_INIT_CLASS(name, 0, 0); } } @@ -6242,7 +6415,7 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze * monomorph's substituted arg_info; CALL emits it after args so inference * from arg values can fill type-parameter slots. */ if (verify_kind == ZEND_VERIFY_ARITY_KIND_NEW) { - zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, NULL); } if (args_ast->kind == ZEND_AST_CALLABLE_CONVERT) { @@ -6279,7 +6452,7 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze uint32_t arg_count = zend_compile_args(args_ast, fbc, &may_have_extra_named_args); if (verify_kind == ZEND_VERIFY_ARITY_KIND_CALL) { - zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc); + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, args_ast); } zend_do_extended_fcall_begin(); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index cf748fa4ce69..3781d3cb740e 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -211,6 +211,14 @@ ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type); ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type); +/* Takes ownership of arg_types. */ +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id); + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity); + ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast); ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 2502878ff8ea..2ea5c6d349f6 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4303,7 +4303,10 @@ static zend_always_inline void i_free_compiled_variables(zend_execute_data *exec count--; } if (UNEXPECTED(execute_data->type_args)) { - zend_type_arg_table_destroy(execute_data->type_args); + /* Persisted monomorph tables are not frame-owned; only free request-built tables. */ + if (!execute_data->type_args->persisted) { + zend_type_arg_table_destroy(execute_data->type_args); + } execute_data->type_args = NULL; } } diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index a961d8b4a2e9..cb68f34cf1d1 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -7771,7 +7771,7 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( /* Build a concrete arg_info block by substituting the FUNCTION_LIKE T-refs in * the base's pre-erasure generic types with the concrete args. */ static zend_arg_info *zend_monomorph_build_arg_info( - const zend_op_array *base, const zend_type *args, uint32_t arity) + const zend_op_array *base, const zend_type *args, uint32_t arity, bool use_arena) { uint32_t num_args = base->num_args; bool has_return = (base->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; @@ -7782,7 +7782,9 @@ static zend_arg_info *zend_monomorph_build_arg_info( } const zend_arg_info *orig_block = base->arg_info - (has_return ? 1 : 0); - zend_arg_info *new_block = zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total); + zend_arg_info *new_block = use_arena + ? zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total) + : emalloc(sizeof(zend_arg_info) * total); memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); const HashTable *pre_params = base->generic_types ? base->generic_types->parameters : NULL; @@ -7823,10 +7825,11 @@ static zend_arg_info *zend_monomorph_build_arg_info( } } if (substitute) { - zend_type_copy_ctor(&sub, /* use_arena */ true, /* persistent */ false); + /* use_arena false on the AOT/preload path: emalloc, not arena. */ + zend_type_copy_ctor(&sub, use_arena, /* persistent */ false); new_block[slot].type = sub; } else { - zend_type_copy_ctor(&new_block[slot].type, /* use_arena */ true, /* persistent */ false); + zend_type_copy_ctor(&new_block[slot].type, use_arena, /* persistent */ false); } if (new_block[slot].name) { zend_string_addref(new_block[slot].name); @@ -7902,12 +7905,16 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( zend_function *existing = zend_hash_find_ptr(EG(function_table), lc_name); if (existing) { + /* Preload reaches INSTALL via here, not zend_fetch_function: init run_time_cache or cached opcodes hit NULL. */ + if (existing->type == ZEND_USER_FUNCTION) { + zend_init_func_run_time_cache(&existing->op_array); + } zend_string_release(display_name); zend_string_release(lc_name); return existing; } - zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity); + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity, true); zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); memcpy(mono, &base->op_array, sizeof(zend_op_array)); @@ -7983,6 +7990,214 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( return mono_fn; } +ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity) +{ +#if ZEND_USE_ABS_CONST_ADDR + /* The combined opcode+literal buffer below assumes relative literal layout; caller uses shared synth. */ + (void) fn_table; (void) base; (void) args; (void) arity; + return NULL; +#else + if (!fn_table) { + fn_table = EG(function_table); + } + if (!base || base->type != ZEND_USER_FUNCTION) { + return NULL; + } + const zend_generic_parameter_list *params = base->op_array.generic_parameters; + if (!params || params->count == 0) { + return NULL; + } + + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + uint32_t total = params->count; + if (arity > total) { + return NULL; + } + if (arity < total) { + for (uint32_t i = 0; i < arity; i++) filled[i] = args[i]; + for (uint32_t i = arity; i < total; i++) { + const zend_generic_parameter *p = ¶ms->parameters[i]; + const zend_type *def = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? &p->default_pre_erasure + : (ZEND_TYPE_IS_SET(p->default_type) ? &p->default_type : NULL); + if (!def) { + return NULL; + } + filled[i] = *def; + } + args = filled; + arity = total; + } + for (uint32_t i = 0; i < arity; i++) { + if (zend_type_contains_type_parameter(args[i])) { + return NULL; + } + } + + zend_string *display_name = zend_generic_canonical_class_name( + base->common.function_name, args, arity); + zend_string *lc_name = zend_string_tolower(display_name); + + zend_function *existing = zend_hash_find_ptr(fn_table, lc_name); + if (existing) { + zend_string_release(display_name); + zend_string_release(lc_name); + return existing; + } + + /* Unlike the shared synth, opcodes+literals are a fresh OWN copy the optimizer rewrites in place. */ + const zend_op_array *src = &base->op_array; + zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(mono, src, sizeof(zend_op_array)); + /* reserved[] memcpy'd from base may point into a transient arena: clear to avoid a dangling pointer. */ + memset(mono->reserved, 0, sizeof(mono->reserved)); + + /* persist efree()s opcodes and _put_free()s arg_info/vars/etc., so each must be an OWN heap alloc. */ + size_t ops_sz = ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op) * src->last, 16); + size_t lit_sz = sizeof(zval) * src->last_literal; + char *buf = emalloc(ops_sz + lit_sz); + memcpy(buf, src->opcodes, ops_sz); + mono->opcodes = (zend_op *) buf; + if (src->last_literal) { + zval *dst_lit = (zval *) (buf + ops_sz); + memcpy(dst_lit, src->literals, lit_sz); + for (uint32_t i = 0; i < src->last_literal; i++) { + Z_TRY_ADDREF(dst_lit[i]); + } + mono->literals = dst_lit; + } + + if (src->last_var) { + mono->vars = emalloc(sizeof(zend_string *) * src->last_var); + for (uint32_t i = 0; i < src->last_var; i++) { + mono->vars[i] = zend_string_copy(src->vars[i]); + } + } else { + mono->vars = NULL; + } + + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(src, args, arity, /* use_arena */ false); + + zend_type_arg_table *mono_targs = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(total)); + mono_targs->count = total; + mono_targs->generation = 0; + mono_targs->persisted = false; + for (uint32_t i = 0; i < total; i++) { + mono_targs->entries[i].name = NULL; + mono_targs->entries[i].type_ref = NULL; + mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + if (i < arity && ZEND_TYPE_IS_SET(args[i])) { + zend_type owned = args[i]; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + mono_targs->entries[i].owned_type = owned; + mono_targs->entries[i].name = zend_type_arg_canonical_name(args[i]); + } + } + { + zend_generic_type_table *gt = emalloc(sizeof(zend_generic_type_table)); + memset(gt, 0, sizeof(*gt)); + /* Deep-copy base's turbofish_args: the copied body references its boxes and persist frees the table (else double-free). */ + if (src->generic_types && src->generic_types->turbofish_args) { + zend_ulong tf_idx; + zend_turbofish_args_entry *tf_entry; + ZEND_HASH_FOREACH_NUM_KEY_PTR(src->generic_types->turbofish_args, tf_idx, tf_entry) { + zend_type box = tf_entry->args_box; + zend_type_copy_ctor(&box, /* use_arena */ false, /* persistent */ false); + zend_generic_type_table_set_turbofish_args(gt, (uint32_t) tf_idx, box); + } ZEND_HASH_FOREACH_END(); + } + gt->persisted = false; + gt->monomorph_type_args = mono_targs; + mono->generic_types = gt; + } + mono->generic_parameters = NULL; /* concrete; and persist would free a shared list */ + + /* persist _put_free's these: own live_range/try_catch, drop doc_comment/attributes. */ + if (src->last_live_range) { + mono->live_range = emalloc(sizeof(zend_live_range) * src->last_live_range); + memcpy(mono->live_range, src->live_range, sizeof(zend_live_range) * src->last_live_range); + } else { + mono->live_range = NULL; + } + if (src->last_try_catch) { + mono->try_catch_array = emalloc(sizeof(zend_try_catch_element) * src->last_try_catch); + memcpy(mono->try_catch_array, src->try_catch_array, sizeof(zend_try_catch_element) * src->last_try_catch); + } else { + mono->try_catch_array = NULL; + } + mono->doc_comment = NULL; + mono->attributes = NULL; + + /* Own the dynamic_func_defs array (persist _put_free's it); the closures stay shared (persist xlat-dedups). */ + if (src->num_dynamic_func_defs) { + mono->dynamic_func_defs = emalloc(sizeof(zend_function *) * src->num_dynamic_func_defs); + memcpy(mono->dynamic_func_defs, src->dynamic_func_defs, + sizeof(zend_function *) * src->num_dynamic_func_defs); + } else { + mono->dynamic_func_defs = NULL; + } + + mono->fn_flags2 |= ZEND_ACC2_MONOMORPH_TYPE_ARGS; + + mono->refcount = NULL; + mono->fn_flags &= ~(ZEND_ACC_IMMUTABLE | ZEND_ACC_HEAP_RT_CACHE | ZEND_ACC_PRELOADED); + mono->fn_flags |= ZEND_ACC_TRAIT_CLONE; + mono->function_name = zend_string_copy(src->function_name); + /* Own a filename ref: persist consumes one, and base's is shared with EG(included_files) (else double-free). */ + mono->filename = zend_string_copy(src->filename); + if (new_arg_info) { + mono->arg_info = new_arg_info; + + /* Bake each RECV mask to the concrete arg_info and drop TRAIT_CLONE, only if all RECV params are mask-only (no NAMED). */ + bool can_fast_recv = !(mono->fn_flags & ZEND_ACC_VARIADIC); + for (uint32_t i = 0; can_fast_recv && i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode != ZEND_RECV) { + continue; + } + uint32_t an = op->op1.num; + if (an == 0 || an > mono->num_args) { + can_fast_recv = false; + break; + } + zend_type *t = &new_arg_info[an - 1].type; + if (ZEND_TYPE_IS_SET(*t) && !ZEND_TYPE_IS_ONLY_MASK(*t)) { + can_fast_recv = false; + break; + } + } + if (can_fast_recv) { + for (uint32_t i = 0; i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode == ZEND_RECV) { + op->op2.num = (uint32_t) ZEND_TYPE_FULL_MASK(new_arg_info[op->op1.num - 1].type); + } + } + mono->fn_flags &= ~ZEND_ACC_TRAIT_CLONE; + } + } + + mono->static_variables = src->static_variables + ? zend_array_dup(src->static_variables) : NULL; + /* No run_time_cache slot here: persist assigns it; allocating now corrupts the per-request map_ptr table. */ + ZEND_MAP_PTR_INIT(mono->run_time_cache, NULL); + ZEND_MAP_PTR_INIT(mono->static_variables_ptr, NULL); + + zend_function *mono_fn = (zend_function *) mono; + if (!zend_hash_add_ptr(fn_table, lc_name, mono_fn)) { + existing = zend_hash_find_ptr(fn_table, lc_name); + zend_string_release(display_name); + zend_string_release(lc_name); + return existing ? existing : NULL; + } + + zend_string_release(display_name); + zend_string_release(lc_name); + return mono_fn; +#endif +} + ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name) { size_t lt_pos, args_len; @@ -8038,3 +8253,44 @@ ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_stri efree(args); return mono; } + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *name) +{ + /* `name` is case-preserved: lowercase only the base part for the table lookup, parse args as-is. */ + size_t lt_pos, args_len; + if (!zend_mp_split_name(name, <_pos, &args_len)) return NULL; + + zend_string *base_part = zend_string_init(ZSTR_VAL(name), lt_pos, 0); + zend_string *base_lc = zend_string_tolower(base_part); + zend_string_release(base_part); + zend_function *base = zend_hash_find_ptr(fn_table, base_lc); + zend_string_release(base_lc); + if (!base || base->type != ZEND_USER_FUNCTION || !base->op_array.generic_parameters) { + return NULL; + } + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(name) + lt_pos + 1, + .end = ZSTR_VAL(name) + ZSTR_LEN(name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_function *mono = zend_synthesize_specialized_monomorph_into(fn_table, base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; +} diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index 3e5c6f313146..4e8e648b9fb7 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -126,6 +126,12 @@ ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t); ZEND_API zend_function *zend_synthesize_function_monomorph( zend_function *base, const zend_type *args, uint32_t arity); +ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity); + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *lc_name); + ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name); void zend_verify_abstract_class(zend_class_entry *ce); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index bc4d718337fe..920590a0cb3b 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -880,8 +880,13 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) * The NEW form of the same opcodes (op1_type != IS_UNUSED) instead caches a * resolved monomorph zend_class_entry* in slot[0]. That entry is owned by * EG(class_table), NOT by the cache slot, so it must be left untouched here - * — freeing it as a type_arg_table corrupts the heap. */ + * — freeing it as a type_arg_table corrupts the heap. + * + * Gate on ZEND_ACC_HEAP_RT_CACHE: SHM/preload op_arrays share the per-request + * map_ptr cache region, already freed by the time they are destroyed — reading + * the stale cache here would be a use-after-free. */ if ((op_array->fn_flags2 & ZEND_ACC2_HAS_GENERIC_CALL_OPS) + && (op_array->fn_flags & ZEND_ACC_HEAP_RT_CACHE) && op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); if (cache_buf) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 52bce47d103e..7392a1a7f487 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -5836,10 +5836,14 @@ ZEND_VM_HOT_HANDLER(63, ZEND_RECV, NUM, UNUSED) /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } } ZEND_VM_NEXT_OPCODE(); @@ -5855,11 +5859,14 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_RECV, op->op2.num == MAY_BE_ANY, ZEND_RECV_NO } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } } ZEND_VM_NEXT_OPCODE(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 581b3e8f2a2b..9cf84035d820 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -2908,11 +2908,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -4458,10 +4461,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -56808,11 +56815,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_N } /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic - * inheritance clone may carry a substituted type in arg_info that we must - * verify against. */ + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zval *param = EX_VAR(opline->result.var); - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); @@ -58264,10 +58274,14 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_S /* The inline op2.num mask was baked in at the origin's compile time. A * generic-inheritance clone may carry a tighter substituted arg_info that - * the mask doesn't reflect, so force the slow path when the function is a - * clone with substituted types. */ + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } } ZEND_VM_NEXT_OPCODE(); diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..3555158b1da1 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4371,7 +4371,11 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) } const zend_op_array *orig_op_array = zend_shared_alloc_get_xlat_entry(op_array->refcount); - ZEND_ASSERT(orig_op_array && "Must be in xlat table"); + if (!orig_op_array) { + /* Monomorph clones reuse ZEND_ACC_TRAIT_CLONE but have no original; re-sync would clobber their substituted arg_info. */ + ZEND_ASSERT(zend_class_is_monomorph(op_array->scope)); + return; + } zend_string *function_name = op_array->function_name; zend_class_entry *scope = op_array->scope; @@ -4435,6 +4439,15 @@ static void preload_optimize(zend_persistent_script *script) zend_optimize_script(&script->script, ZCG(accel_directives).optimization_level, ZCG(accel_directives).opt_debug_level); zend_accel_finalize_delayed_early_binding_list(script); + /* Loop to fixpoint over the transitive closure of reachable instantiations. */ + for (int aot_round = 0; aot_round < 32; aot_round++) { + if (zend_aot_monomorphize_script(&script->script, + ZCG(accel_directives).optimization_level) == 0) { + break; + } + } + zend_aot_upgrade_dispatch_to_ucall(&script->script); + ZEND_HASH_MAP_FOREACH_PTR(&script->script.class_table, ce) { preload_fix_trait_methods(ce); } ZEND_HASH_FOREACH_END(); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 5b68782c6ebb..3c41bffd75d2 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -601,7 +601,8 @@ static zend_type_arg_table *zend_persist_type_arg_table( if (!table) { return NULL; } - const zend_type_named_with_args *new_nwa = zend_persist_mono_binding_nwa(ce); + /* ce == NULL: function-level table with concrete owned_type entries, no binding to rebind against. */ + const zend_type_named_with_args *new_nwa = ce ? zend_persist_mono_binding_nwa(ce) : NULL; for (uint32_t i = 0; i < table->count; i++) { if (table->entries[i].name) { zend_accel_store_interned_string(table->entries[i].name); @@ -660,6 +661,11 @@ static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_typ persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); } + if (persisted->monomorph_type_args) { + persisted->monomorph_type_args = + zend_persist_type_arg_table(persisted->monomorph_type_args, NULL); + } + /* Precompute the value-check plan into SHM (relocated via the persist arena, * which also works in file_cache mode where no SHM segment is locked). */ if (persisted->parameters) { diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index be1deda11ca7..d4558c16eab9 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -347,6 +347,8 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) ADD_SIZE(sizeof(HashTable)); } +static void zend_persist_type_arg_table_calc(zend_type_arg_table *table); + static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) { if (!table) { @@ -388,6 +390,10 @@ static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) zend_persist_turbofish_args_ht_calc(table->turbofish_args); } + if (table->monomorph_type_args) { + zend_persist_type_arg_table_calc(table->monomorph_type_args); + } + /* Mirror the value-check plan reservation in zend_persist_generic_type_table. */ if (table->parameters) { uint32_t cnt = zend_count_generic_value_checks(table->parameters); From 81c5215cc0d79c93eb2ad31cb16d8d51c4238e7f Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 21 Jun 2026 00:18:46 +0000 Subject: [PATCH 2/2] fix CI --- Zend/zend_closures.c | 5 +++ Zend/zend_compile.c | 18 +++++++--- Zend/zend_compile.h | 2 ++ Zend/zend_execute.h | 7 ++++ Zend/zend_inheritance.c | 53 ++++++++++++++++-------------- Zend/zend_opcode.c | 73 +++++++++++++++++++++++++++++++++++++++++ Zend/zend_vm_def.h | 6 ++++ Zend/zend_vm_execute.h | 28 ++++++++++++++++ 8 files changed, 162 insertions(+), 30 deletions(-) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index c0ceee2ea756..9666fd000205 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -803,6 +803,11 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en if (closure->func.op_array.refcount) { (*closure->func.op_array.refcount)++; } + if ((closure->func.common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && closure->func.op_array.generic_types + && closure->func.op_array.generic_types->monomorph_type_args) { + closure->func.op_array.generic_types->monomorph_type_args->refcount++; + } /* For fake closures, we want to reuse the static variables of the original function. */ HashTable *ht = ZEND_MAP_PTR_GET(func->op_array.static_variables_ptr); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 2adea3d829be..df7831ac1481 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -794,7 +794,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( return (zend_type_arg_table *)cache_slot[0]; } zend_type_arg_table *nt = zend_build_generic_call_type_args(call, NULL); - if (nt && nkey && !cache_slot[0]) { + if (nt && nkey && !cache_slot[0] + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { cache_slot[0] = nt; cache_slot[1] = (void *)nkey; nt->persisted = true; @@ -808,7 +809,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( } zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); if (t && key && cache_slot && !cache_slot[0] - && zend_call_is_cacheable_against_args(call->func, args_box)) { + && zend_call_is_cacheable_against_args(call->func, args_box) + && (caller->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { cache_slot[0] = t; cache_slot[1] = (void *)key; t->persisted = true; @@ -831,7 +833,8 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH && cache_slot[3] == (void *) base) { - *out_type_args = (zend_type_arg_table *) cache_slot[4]; + zend_type_arg_table *cached = (zend_type_arg_table *) cache_slot[4]; + *out_type_args = cached ? cached : zend_build_concrete_call_type_args(base, args_box); return (zend_function *) cache_slot[0]; } @@ -861,7 +864,9 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( } zend_type_arg_table *table = zend_build_concrete_call_type_args(base, args_box); - if (table) { + bool can_cache_table = table && cache_slot && EG(current_execute_data) + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE); + if (can_cache_table) { table->persisted = true; } @@ -869,7 +874,7 @@ ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( cache_slot[0] = mono; cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; cache_slot[3] = (void *) base; - cache_slot[4] = table; + cache_slot[4] = can_cache_table ? table : NULL; } *out_type_args = table; return mono; @@ -1783,6 +1788,9 @@ static bool zend_try_attach_concrete_call_table_for(zend_op_array *caller, zend_turbofish_args_entry *entry = zend_hash_index_find_ptr(gtt->turbofish_args, args_id); ZEND_ASSERT(entry != NULL); + if (entry->concrete_table && !entry->concrete_table->persisted) { + zend_type_arg_table_destroy(entry->concrete_table); + } entry->concrete_table = ct; entry->concrete_skip_value_check = fbc->op_array.generic_types diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 3781d3cb740e..d05ff5770173 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -261,6 +261,7 @@ typedef struct _zend_type_arg_table { * as part of a persisted class entry's `generic_type_args`. Tells the * destroy path to leave the table and its name/type contents alone. */ bool persisted; + uint32_t refcount; zend_type_arg_entry entries[1]; } zend_type_arg_table; @@ -335,6 +336,7 @@ static zend_always_inline zend_turbofish_args_entry *zend_generic_get_or_cache_a ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count); ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table); +ZEND_API void zend_type_arg_table_release(zend_type_arg_table *table); ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index ff9aff51b9d9..815b4917c721 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -345,9 +345,16 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, * call-frame teardown skips destroying it. VERIFY_GENERIC_ARGUMENTS may * still overwrite type_args later for explicit-turbofish calls. */ if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CLOSURE)) { +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Warray-bounds" +#endif const zend_closure *closure = (const zend_closure *) ZEND_CLOSURE_OBJECT(func); call->type_args = closure->captured_type_args; +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +#endif } else if (UNEXPECTED(ZEND_USER_CODE(func->type) && (func->op_array.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS))) { /* Monomorph reached via by-name dispatch: bind its concrete type-arg table. */ diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index cb68f34cf1d1..8e1a9e292b49 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2939,6 +2939,9 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke clone->hooks = clone_hooks; } + if (ZEND_TYPE_HAS_LIST(sub) || ZEND_TYPE_HAS_NAMED_WITH_ARGS(sub)) { + zend_type_release(sub, /* persistent */ false); + } info = clone; } } @@ -6107,6 +6110,8 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zval *zv; zend_string *synthesized_lc_parent = NULL; zend_class_entry *cache_key_proto = NULL; + zend_class_name *detached_interface_names = NULL; + uint32_t detached_interface_count = 0; ALLOCA_FLAG(use_heap) SET_ALLOCA_FLAG(use_heap); @@ -6142,10 +6147,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zend_string_addref(ce->interface_names[k].name); zend_string_addref(ce->interface_names[k].lc_name); } + detached_interface_names = ce->interface_names; + detached_interface_count = ce->num_interfaces; } if (ce->num_traits) { zend_class_name *src = ce->trait_names; - ce->trait_names = emalloc(sizeof(zend_class_name) * ce->num_traits); + ce->trait_names = zend_arena_alloc(&CG(arena), sizeof(zend_class_name) * ce->num_traits); memcpy(ce->trait_names, src, sizeof(zend_class_name) * ce->num_traits); for (uint32_t k = 0; k < ce->num_traits; k++) { zend_string_addref(ce->trait_names[k].name); @@ -6230,9 +6237,8 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ? ce->generic_types->trait_uses : NULL; bool ce_is_mono_for_trait = zend_class_is_monomorph(ce); bool *trait_skip_mono = NULL; - ALLOCA_FLAG(trait_skip_use_heap) if (trait_uses_table_for_synth && ce->num_traits > 1) { - trait_skip_mono = do_alloca(sizeof(bool) * ce->num_traits, trait_skip_use_heap); + trait_skip_mono = emalloc(sizeof(bool) * ce->num_traits); zend_mark_duplicate_lc_names(ce->trait_names, ce->num_traits, trait_skip_mono); } @@ -6257,14 +6263,14 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(base_trait == NULL)) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(!(base_trait->ce_flags & ZEND_ACC_TRAIT))) { zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(base_trait->name)); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (base_trait->generic_parameters) { @@ -6273,7 +6279,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (EG(exception)) { check_unrecoverable_load_failure(ce); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } zend_class_entry *mono = zend_synthesize_monomorph( @@ -6281,7 +6287,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (!mono) { check_unrecoverable_load_failure(ce); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } zend_string_release(ce->trait_names[i].name); @@ -6295,20 +6301,20 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ce->trait_names[i].lc_name, ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); if (UNEXPECTED(trait == NULL)) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(!(trait->ce_flags & ZEND_ACC_TRAIT))) { zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(trait->name)); free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { zend_use_of_deprecated_trait(trait, ce->name); if (UNEXPECTED(EG(exception))) { free_alloca(traits_and_interfaces, use_heap); - if (trait_skip_mono) free_alloca(trait_skip_mono, trait_skip_use_heap); + if (trait_skip_mono) efree(trait_skip_mono); return NULL; } } @@ -6339,7 +6345,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } if (trait_skip_mono) { - free_alloca(trait_skip_mono, trait_skip_use_heap); + efree(trait_skip_mono); } } @@ -6559,6 +6565,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string sizeof(zend_class_entry *) * ce->num_interfaces); zend_do_implement_interfaces(ce, interfaces); + if (detached_interface_names) { + for (uint32_t k = 0; k < detached_interface_count; k++) { + zend_string_release_ex(detached_interface_names[k].name, 0); + zend_string_release_ex(detached_interface_names[k].lc_name, 0); + } + efree(detached_interface_names); + } } else if (parent && parent->num_interfaces) { zend_do_inherit_interfaces(ce, parent); } @@ -7925,17 +7938,12 @@ ZEND_API zend_function *zend_synthesize_function_monomorph( zend_type_arg_table *mono_targs = NULL; { uint32_t tcount = params->count; - mono_targs = zend_arena_alloc(&CG(arena), ZEND_TYPE_ARG_TABLE_SIZE(tcount)); - mono_targs->count = tcount; - mono_targs->generation = 0; + mono_targs = zend_type_arg_table_alloc(tcount); mono_targs->persisted = true; for (uint32_t i = 0; i < tcount; i++) { - mono_targs->entries[i].name = NULL; - mono_targs->entries[i].type_ref = NULL; - mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); if (i < arity && ZEND_TYPE_IS_SET(args[i])) { zend_type owned = args[i]; - zend_type_copy_ctor(&owned, /* use_arena */ true, /* persistent */ false); + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); mono_targs->entries[i].owned_type = owned; zend_string *cname = zend_type_arg_canonical_name(args[i]); mono_targs->entries[i].name = cname; @@ -8079,14 +8087,9 @@ ZEND_API zend_function *zend_synthesize_specialized_monomorph_into( zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(src, args, arity, /* use_arena */ false); - zend_type_arg_table *mono_targs = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(total)); - mono_targs->count = total; - mono_targs->generation = 0; - mono_targs->persisted = false; + zend_type_arg_table *mono_targs = zend_type_arg_table_alloc(total); + mono_targs->persisted = true; for (uint32_t i = 0; i < total; i++) { - mono_targs->entries[i].name = NULL; - mono_targs->entries[i].type_ref = NULL; - mono_targs->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); if (i < arity && ZEND_TYPE_IS_SET(args[i])) { zend_type owned = args[i]; zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 920590a0cb3b..1a7236680d83 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -335,6 +335,7 @@ ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { table->count = count; table->generation = zend_type_arg_table_generation_counter++; table->persisted = false; + table->refcount = 1; for (uint32_t i = 0; i < count; i++) { table->entries[i].name = NULL; table->entries[i].type_ref = NULL; @@ -343,6 +344,21 @@ ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { return table; } +ZEND_API void zend_type_arg_table_release(zend_type_arg_table *table) { + if (!table || table->refcount == 0 || --table->refcount > 0) { + return; + } + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_string_release(table->entries[i].name); + } + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_type_release(table->entries[i].owned_type, false); + } + } + efree(table); +} + ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { if (!table || table->persisted) { return; @@ -657,6 +673,12 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->num_traits > 0) { _destroy_zend_class_traits_info(ce); } + } else if (ce->num_traits > 0) { + uint32_t i; + for (i = 0; i < ce->num_traits; i++) { + zend_string_release_ex(ce->trait_names[i].name, 0); + zend_string_release_ex(ce->trait_names[i].lc_name, 0); + } } if (ce->default_properties_table) { @@ -698,6 +720,10 @@ ZEND_API void destroy_zend_class(zval *zv) } } } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + zend_type_release(prop_info->type, /* persistent */ false); + if (prop_info->name) { + zend_string_release_ex(prop_info->name, 0); + } if (prop_info->hooks) { for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { if (prop_info->hooks[i]) { @@ -867,6 +893,36 @@ ZEND_API void zend_destroy_static_vars(zend_op_array *op_array) } } +static void zend_release_transient_monomorph(zend_function *cm) +{ + if (!cm || cm->type != ZEND_USER_FUNCTION + || !(cm->common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + || (cm->common.fn_flags & ZEND_ACC_IMMUTABLE)) { + return; + } + zend_op_array *moa = &cm->op_array; + if (moa->generic_types && moa->generic_types->monomorph_type_args) { + zend_type_arg_table_release(moa->generic_types->monomorph_type_args); + moa->generic_types->monomorph_type_args = NULL; + } + if (moa->arg_info) { + bool has_ret = (moa->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = moa->num_args + (has_ret ? 1 : 0) + + ((moa->fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + zend_arg_info *block = moa->arg_info - (has_ret ? 1 : 0); + for (uint32_t a = 0; a < total; a++) { + zend_type_release(block[a].type, 0); + if (block[a].name) { + zend_string_release(block[a].name); + } + if (block[a].doc_comment) { + zend_string_release(block[a].doc_comment); + } + } + moa->arg_info = NULL; + } +} + ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; @@ -892,6 +948,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) if (cache_buf) { for (uint32_t op_idx = 0; op_idx < op_array->last; op_idx++) { const zend_op *op = &op_array->opcodes[op_idx]; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->result.num) { + void **cache_slot = (void **) (cache_buf + op->result.num); + zend_release_transient_monomorph((zend_function *) cache_slot[0]); + cache_slot[0] = NULL; + continue; + } if ((op->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS || op->opcode == ZEND_INSTALL_GENERIC_ARGS) && op->op1_type == IS_UNUSED @@ -906,6 +970,7 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) zend_type_arg_table_destroy(mt); cache_slot[4] = NULL; } + zend_release_transient_monomorph((zend_function *) cache_slot[0]); cache_slot[0] = NULL; cache_slot[1] = NULL; continue; @@ -931,6 +996,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) zend_string_release_ex(op_array->function_name, 0); } + if ((op_array->fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && !(op_array->fn_flags & ZEND_ACC_IMMUTABLE) + && op_array->generic_types + && op_array->generic_types->monomorph_type_args) { + zend_type_arg_table_release(op_array->generic_types->monomorph_type_args); + op_array->generic_types->monomorph_type_args = NULL; + } + if (!op_array->refcount || --(*op_array->refcount) > 0) { return; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 7392a1a7f487..9be9d25a2b2b 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4419,6 +4419,8 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -9356,6 +9358,10 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 9cf84035d820..e475449738e7 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1994,6 +1994,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -2156,6 +2158,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -2318,6 +2322,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -22538,6 +22544,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -38330,6 +38340,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -55923,6 +55937,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -56085,6 +56101,8 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -56247,6 +56265,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FC } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); } EX(call) = call->prev_execute_data; zend_vm_stack_free_call_frame(call); @@ -76149,6 +76169,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { @@ -91941,6 +91965,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR generic_install_check_exception: if (UNEXPECTED(EG(exception))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } zend_vm_stack_free_args(call); if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) {