diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index 04345bd1c7ea..49bcf7b13196 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -696,14 +696,14 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx break; case ZEND_VERIFY_GENERIC_ARGUMENTS: case ZEND_INSTALL_GENERIC_ARGS: - /* When this site has a turbofish (args_id in extended_value), - * the compiler allocated a 2-slot inline cache for the - * (zend_type_arg_table*, cache key) pair — re-allocate it - * here so the offset stays in sync with compact_literals' - * fresh cache_size. */ - if (opline->extended_value != 0) { + /* When this site has a turbofish (args_id in extended_value) + * or is a call (op1_type == IS_UNUSED, which caches its table + * regardless), the compiler allocated a 5-slot inline cache — + * re-allocate it here so the offset stays in sync with + * compact_literals' fresh cache_size. */ + if (opline->extended_value != 0 || opline->op1_type == IS_UNUSED) { opline->result.num = cache_size; - cache_size += 2 * sizeof(void *); + cache_size += 5 * sizeof(void *); } break; case ZEND_CATCH: diff --git a/Zend/tests/generics/reification/inference_basic.phpt b/Zend/tests/generics/reification/inference_basic.phpt index 240d9ed4da20..9c05c291b024 100644 --- a/Zend/tests/generics/reification/inference_basic.phpt +++ b/Zend/tests/generics/reification/inference_basic.phpt @@ -18,7 +18,7 @@ try { echo "TypeError: ", $e->getMessage(), "\n"; } ?> ---EXPECT-- +--EXPECTF-- Foo Bar -TypeError: kind(): Argument #1 ($x) must be of type Foo, Bar given +TypeError: kind(): Argument #1 ($x) must be of type Foo, Bar given, called in %s on line %d diff --git a/Zend/tests/generics/reification/variadic_t_reified.phpt b/Zend/tests/generics/reification/variadic_t_reified.phpt index 87b01a3001d0..9cdd17bcf6fb 100644 --- a/Zend/tests/generics/reification/variadic_t_reified.phpt +++ b/Zend/tests/generics/reification/variadic_t_reified.phpt @@ -67,9 +67,9 @@ try { ?> --EXPECTF-- int(6) -1: sum(): Argument #2 ($xs) must be of type int, string given -2: sum(): Argument #3 ($xs) must be of type int, string given +1: sum(): Argument #2 must be of type int, string given, called in %s on line %d +2: sum(): Argument #3 must be of type int, string given, called in %s on line %d string(6) ":: 123" -3: concat(): Argument #3 ($xs) must be of type int, array given +3: concat(): Argument #3 must be of type int, array given, called in %s on line %d int(3) -4: herd(): Argument #2 ($xs) must be of type Dog, Cat given +4: herd(): Argument #2 must be of type Dog, Cat given, called in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 842667fea21a..602a63627d6d 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -734,11 +734,65 @@ static bool zend_call_is_cacheable_against_args( * executing op_array — slot[0] = cached zend_type_arg_table*, slot[1] = * cache key. Runtime cache memory is allocated per-process and writable * even when the op_array's persistent metadata lives in opcache SHM. */ +/* Cache key for a generic call made WITHOUT turbofish (args_box == NULL). The + * resolved type-arg table is then a function only of the callee's declared + * defaults (plus bound fall-backs for unset slots) — provided no slot is + * filled by value-directed inference and no default itself references an outer + * type parameter (whose binding would make the table caller-dependent and, via + * the borrowed caller-frame type_ref, unsafe to persist). When both hold the + * table is invariant across every call at this site, so a constant key turns + * the per-call-site cache slot into a build-once memo. Returns 0 ("uncacheable") + * otherwise, falling back to the original rebuild-every-call behaviour. */ +static uintptr_t zend_compute_call_default_cache_key(const zend_function *fbc) +{ + if (!ZEND_USER_CODE(fbc->common.type)) { + return 0; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) { + return 0; + } + for (uint32_t i = 0; i < params->count; i++) { + const zend_type *def = ¶ms->parameters[i].default_type; + if (ZEND_TYPE_IS_SET(*def) && ZEND_TYPE_HAS_TYPE_PARAMETER(*def)) { + /* default is `= T` of an outer param -> caller-dependent. */ + return 0; + } + } + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; +} + ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( zend_execute_data *call, const zend_type *args_box, void **cache_slot) { + /* Concrete call site (no type-parameter refs in the turbofish, or an + * all-default non-turbofish call): the resolved table is invariant across + * every call here, recorded by the CONCRETE sentinel in slot[1]. Once it is + * cached, return it directly — skip recomputing the binding fingerprint key + * (which would only re-derive the same CONCRETE sentinel). */ + if (cache_slot && cache_slot[0] + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + return (zend_type_arg_table *) cache_slot[0]; + } if (!args_box) { - return zend_build_generic_call_type_args(call, NULL); + /* Non-turbofish generic call. The table is built from the callee's + * defaults (and any value inference). When inference won't fire and no + * default is caller-dependent, the result is invariant at this site — + * cache it the same way the turbofish path does so repeated calls (e.g. + * an internal `add_node($g, $x)`) stop rebuilding+freeing a table whose + * contents never change. */ + uintptr_t nkey = (cache_slot && zend_call_is_cacheable_against_args(call->func, NULL)) + ? zend_compute_call_default_cache_key(call->func) : 0; + if (nkey && cache_slot[0] && (uintptr_t)cache_slot[1] == nkey) { + 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]) { + cache_slot[0] = nt; + cache_slot[1] = (void *)nkey; + nt->persisted = true; + } + return nt; } zend_execute_data *caller = EG(current_execute_data); uintptr_t key = zend_compute_call_cache_key(args_box, caller); @@ -755,6 +809,251 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args( return t; } +/* Resolve (and cache per call site) the concrete monomorph for a turbofish CALL + * with concrete args in `args_box`; *out_type_args gets the invariant type-arg + * table for body T-ref resolution. NULL when the site can't be monomorphized. */ +static zend_type_arg_table *zend_build_concrete_call_type_args( + const zend_function *fbc, const zend_type *args_box); + +ZEND_API zend_function *zend_get_or_synthesize_call_monomorph( + zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, + zend_type_arg_table **out_type_args) +{ + zend_function *base = call->func; + + 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]; + return (zend_function *) cache_slot[0]; + } + + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return NULL; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + + /* Enforce arity + bounds as the erased VERIFY path does (first call only). */ + zend_check_generic_call_arguments(base, arity, args_box); + if (UNEXPECTED(EG(exception))) { + return NULL; + } + + zend_function *mono = zend_synthesize_function_monomorph(base, nwa->args, nwa->count); + if (!mono) { + return NULL; + } + + zend_type_arg_table *table = zend_build_concrete_call_type_args(base, args_box); + if (table) { + table->persisted = true; + } + + if (cache_slot) { + cache_slot[0] = mono; + cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; + cache_slot[3] = (void *) base; + cache_slot[4] = table; + } + *out_type_args = table; + return mono; +} + +/* Monomorphize a non-turbofish generic call once its per-frame type-arg table + * `resolved` is known concrete and invariant, swapping call->func to a plain + * monomorph to skip the per-call value check. NULL keeps the erased path. */ +ZEND_API zend_function *zend_try_monomorph_resolved_call( + zend_execute_data *call, zend_type_arg_table *resolved, void **cache_slot, + zend_type_arg_table **out_type_args) +{ + zend_function *base = call->func; + + 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]; + return (zend_function *) cache_slot[0]; + } + + if (!resolved || resolved->count == 0 + || !ZEND_USER_CODE(base->type) + || !base->op_array.generic_parameters) { + return NULL; + } + + /* All bindings must be concrete (free of T-refs). */ + ALLOCA_FLAG(use_heap) + zend_type *args = do_alloca(sizeof(zend_type) * resolved->count, use_heap); + bool ok = true; + for (uint32_t i = 0; i < resolved->count; i++) { + const zend_type *bt = zend_type_arg_entry_type(&resolved->entries[i]); + if (!bt || !ZEND_TYPE_IS_SET(*bt) || zend_type_contains_type_parameter(*bt)) { + ok = false; + break; + } + args[i] = *bt; + } + + zend_function *mono = ok + ? zend_synthesize_function_monomorph(base, args, resolved->count) + : NULL; + free_alloca(args, use_heap); + if (!mono) { + return NULL; + } + + if (cache_slot) { + cache_slot[0] = mono; + cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; + cache_slot[3] = (void *) base; + cache_slot[4] = resolved; + } + *out_type_args = resolved; + return mono; +} + +ZEND_API zend_class_entry *zend_get_called_scope(const zend_execute_data *ex); + +/* Cache key for a `new C::<...>` site's resolved monomorph. The monomorph is a + * pure function of (base ce, resolved type args). The args are a + * compile-time-constant side table for the opline; only TYPE_PARAMETER refs add + * runtime variability, resolved against the executing frame: + * - FUNCTION_LIKE ref -> the frame's type_args entry, keyed by its interned + * canonical-name pointer (stable per binding); + * - CLASS_LIKE ref -> the called scope's generic_type_args, fully + * determined by the called_scope ce pointer. + * The key is seeded with the base ce pointer so a cached monomorph for one base + * can never satisfy a different base (e.g. `new $dynamic::`). Returns 0 + * ("uncacheable") only when a needed binding is unavailable (error paths). */ +static uintptr_t zend_compute_new_mono_cache_key( + const zend_class_entry *base, const zend_type_named_with_args *nwa, + const zend_execute_data *ex) +{ + uintptr_t key = (uintptr_t) base * 0x9E3779B97F4A7C15ULL; + bool has_tref = false; + for (uint32_t i = 0; i < nwa->count; i++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[i])) { + continue; + } + has_tref = true; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[i]); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + if (!ex || !ex->type_args || ref->index >= ex->type_args->count) { + return 0; + } + zend_string *bound_name = ex->type_args->entries[ref->index].name; + if (!bound_name) { + return 0; + } + key = key * 0x100000001B3ULL + (uintptr_t) bound_name + i + 1; + } else { + zend_class_entry *cs = ex ? zend_get_called_scope(ex) : NULL; + if (!cs) { + return 0; + } + key = key * 0x100000001B3ULL + (uintptr_t) cs + i + 1; + } + } + /* No type-parameter refs: the monomorph is frame-independent. Return the + * CONCRETE sentinel so the caller can take a key-free fast path (validating + * only that the cached monomorph's base matches, which guards a dynamic + * `new $cls::<...>` whose resolved base varies between calls). */ + if (!has_tref) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } + if (key == 0) { + key = (uintptr_t) -1; + } + return key; +} + +static zend_always_inline void zend_generic_new_swap_ce( + zval *new_obj, zend_execute_data *call, zend_class_entry *ce, zend_class_entry *mono) +{ + if (mono && mono != ce) { + Z_OBJ_P(new_obj)->ce = mono; + if (mono->constructor && call->func == ce->constructor) { + call->func = mono->constructor; + } + } +} + +/* `new C::<...>` resolution with a call-site monomorph cache. Without the cache + * every instantiation rebuilds the canonical class name (smart_str + interning), + * lowercases it, and hashes EG(class_table) — all to rediscover the same + * monomorph. On a cache hit we skip the arity/bound check and the synthesis + * lookup entirely: the cached ce is swapped into the new object (and the pending + * constructor call) directly. On a miss we run the full path and, when the site + * is cacheable, stash the resolved monomorph keyed by the binding fingerprint. + * + * `do_checks` mirrors the VERIFY vs INSTALL split: VERIFY runs the runtime + * arity+bound check on a miss; INSTALL (statically pre-validated) does not. The + * cached `mono` lives in EG(class_table) for the whole request and the cache + * slot is request-local runtime-cache memory, so the two share a lifetime. */ +ZEND_API void zend_apply_generic_new( + zval *new_obj, zend_execute_data *call, const zend_type *args_box, + uint32_t arity, void **cache_slot, bool do_checks) +{ + zend_class_entry *ce = Z_OBJCE_P(new_obj); + const zend_type_named_with_args *nwa = + (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) + ? ZEND_TYPE_NAMED_WITH_ARGS(*args_box) : NULL; + + if (nwa && ce->generic_parameters && cache_slot) { + /* Concrete-args fast path: the monomorph is frame-independent, so a + * cached one whose base matches reuses directly — no key recomputation. + * The base check (`->parent == ce`: a monomorph's parent is its base) + * keeps a dynamic `new $cls::<...>` correct when $cls varies. */ + if (cache_slot[0] + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE + && ((zend_class_entry *) cache_slot[0])->parent == ce) { + zend_generic_new_swap_ce(new_obj, call, ce, (zend_class_entry *) cache_slot[0]); + return; + } + uintptr_t key = zend_compute_new_mono_cache_key(ce, nwa, EG(current_execute_data)); + /* CONCRETE keys are handled by the fast path above; reusing here without + * the base check would mis-serve a dynamic base, so require a real + * (binding-fingerprint) key for the generic hit. */ + if (key && key != ZEND_TURBOFISH_CACHE_KEY_CONCRETE + && cache_slot[0] && (uintptr_t) cache_slot[1] == key) { + zend_generic_new_swap_ce(new_obj, call, ce, (zend_class_entry *) cache_slot[0]); + return; + } + if (do_checks) { + zend_check_generic_new_arguments(ce, arity, args_box); + if (EG(exception)) { + return; + } + } + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); + if (!mono || EG(exception)) { + return; + } + if (key) { + cache_slot[0] = mono; + cache_slot[1] = (void *) key; + } + zend_generic_new_swap_ce(new_obj, call, ce, mono); + return; + } + + /* No turbofish args, or ce isn't generic, or no cache slot: preserve the + * original semantics (the arity check still fires to reject turbofish args + * passed to a non-generic class). */ + if (do_checks) { + zend_check_generic_new_arguments(ce, arity, args_box); + if (EG(exception)) { + return; + } + } + if (nwa && ce->generic_parameters) { + zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); + if (!EG(exception)) { + zend_generic_new_swap_ce(new_obj, call, ce, mono); + } + } +} + /* Slots left NULL mean "fall back to the parameter's bound". Order of resolution * for each slot: explicit turbofish arg → parameter's declared default → * value-directed inference from any argument whose pre-erasure type is a @@ -935,6 +1234,141 @@ static bool zend_check_pre_erasure_type_value( * and sets EG(exception) on the first failure. Only bare T at the top level is * substituted in this pass — composite shapes (array, Box, ?T) fall back * to the erased bound check at RECV. */ +/* Verify one value parameter whose pre-erasure type is a direct FUNCTION_LIKE + * T-ref (parameter position `arg_idx`, type-parameter index `tp_index`) against + * the per-call binding. Shared by both the plan-driven fast path and the hash + * fallback so the substitution / variadic-sweep / error semantics stay + * identical. Returns false (with EG(exception) set) on the first violation. */ +static zend_always_inline bool zend_verify_one_generic_param( + zend_execute_data *call, const zend_function *fbc, + const zend_type_named_with_args *nwa, + uint32_t arg_idx, uint32_t tp_index, uint32_t num_args, bool strict) +{ + zend_type substituted; + if (nwa) { + if (tp_index >= nwa->count) return true; + substituted = nwa->args[tp_index]; + } else { + /* No turbofish args_box for this call — resolve directly from the + * captured T-table (e.g., a closure with no turbofish at its own + * call site, whose body references the outer frame's T). */ + if (tp_index >= call->type_args->count) return true; + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[tp_index]); + if (!resolved) return true; + substituted = *resolved; + } + if (!ZEND_TYPE_IS_SET(substituted)) return true; + + /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. + * `id::($x)` inside `nested`), the caller's binding has already been + * resolved into call->type_args by zend_build_generic_call_type_args. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { + if (!call->type_args || tp_index >= call->type_args->count) { + return true; + } + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[tp_index]); + if (!resolved) { + return true; + } + substituted = *resolved; + } + + /* When the pre-erasure parameter slot is variadic (T ...$xs), the single + * key covers every value supplied at runtime. Sweep from arg_idx through + * num_args-1 so each variadic element gets the same reified T-check. */ + bool is_variadic_slot = (fbc->common.fn_flags & ZEND_ACC_VARIADIC) + && arg_idx == fbc->common.num_args; + uint32_t sweep_end = is_variadic_slot ? num_args : arg_idx + 1; + + for (uint32_t aidx = arg_idx; aidx < sweep_end; aidx++) { + zval *arg = ZEND_CALL_ARG(call, aidx + 1); + zval *target = arg; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + + /* Pre-erasure shapes don't match the canonical PHP type layout + * (scalars sit as CODE-only list members instead of bits on the + * outer mask), so walk them directly. */ + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + const zend_arg_info *ai; + if (is_variadic_slot) { + ai = &fbc->common.arg_info[fbc->common.num_args]; + } else { + ai = (aidx < fbc->common.num_args) + ? &fbc->common.arg_info[aidx] : NULL; + } + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + aidx + 1, + (ai && ai->name) ? " ($" : "", + (ai && ai->name) ? ZSTR_VAL(ai->name) : "", + (ai && ai->name) ? ")" : "", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } + } + return true; +} + +/* Count the direct FUNCTION_LIKE T-ref value parameters; the persister and the + * runtime builder share it so both size the plan identically. */ +ZEND_API uint32_t zend_count_generic_value_checks(const HashTable *parameters) +{ + uint32_t cnt = 0; + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(parameters, arg_idx, pe_type_ptr) { + (void) arg_idx; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr)->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + cnt++; + } ZEND_HASH_FOREACH_END(); + return cnt; +} + +/* Fill a plan buffer (sized for zend_count_generic_value_checks() entries): one + * entry per direct FUNCTION_LIKE T-ref value parameter, in hash order so the + * "first failing argument" diagnostic matches the legacy iteration. */ +ZEND_API void zend_fill_generic_value_check_plan( + zend_generic_value_check_plan *plan, const HashTable *parameters) +{ + zend_ulong arg_idx; + zend_type *pe_type_ptr; + uint32_t w = 0; + ZEND_HASH_FOREACH_NUM_KEY_PTR(parameters, arg_idx, pe_type_ptr) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + plan->checks[w].arg_idx = (uint32_t) arg_idx; + plan->checks[w].tp_index = ref->index; + w++; + } ZEND_HASH_FOREACH_END(); + plan->count = w; +} + +/* Build the packed value-check plan from a callee's `parameters` table into + * request-local memory. */ +static zend_generic_value_check_plan *zend_build_generic_value_check_plan( + const HashTable *parameters) +{ + uint32_t cnt = zend_count_generic_value_checks(parameters); + zend_generic_value_check_plan *plan = emalloc( + offsetof(zend_generic_value_check_plan, checks) + cnt * sizeof(zend_generic_value_check)); + zend_fill_generic_value_check_plan(plan, parameters); + return plan; +} + ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box) { const zend_type_named_with_args *nwa = NULL; @@ -952,8 +1386,35 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ || !fbc->op_array.generic_types->parameters) { return true; } - const HashTable *pre = fbc->op_array.generic_types->parameters; + uint32_t num_args = ZEND_CALL_NUM_ARGS(call); + /* Loop-invariant: strictness is a property of the calling frame. */ + bool strict = EG(current_execute_data) + && EG(current_execute_data)->func + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + + zend_generic_type_table *gt = fbc->op_array.generic_types; + zend_generic_value_check_plan *plan = gt->value_check_plan; + if (!plan && !gt->persisted) { + plan = zend_build_generic_value_check_plan(fbc->op_array.generic_types->parameters); + gt->value_check_plan = plan; + } + + if (plan) { + for (uint32_t p = 0; p < plan->count; p++) { + uint32_t arg_idx = plan->checks[p].arg_idx; + if (arg_idx >= num_args) continue; + if (!zend_verify_one_generic_param(call, fbc, nwa, + arg_idx, plan->checks[p].tp_index, num_args, strict)) { + return false; + } + } + return true; + } + + /* Fallback for opcache-persisted tables (read-only SHM, no plan cached): + * iterate the parameters hash directly. */ + const HashTable *pre = gt->parameters; zend_ulong arg_idx; zend_type *pe_type_ptr; ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { @@ -961,89 +1422,9 @@ ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_ if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; - zend_type substituted; - if (nwa) { - if (ref->index >= nwa->count) continue; - substituted = nwa->args[ref->index]; - } else { - /* No turbofish args_box for this call — resolve directly from the - * captured T-table (e.g., a closure with no turbofish at its own - * call site, whose body references the outer frame's T). */ - if (ref->index >= call->type_args->count) continue; - const zend_type *resolved = zend_type_arg_entry_type( - &call->type_args->entries[ref->index]); - if (!resolved) continue; - substituted = *resolved; - } - if (!ZEND_TYPE_IS_SET(substituted)) continue; - - /* If the turbofish arg is itself a forwarded TYPE_PARAMETER (e.g. - * `id::($x)` inside `nested`), the caller's binding has - * already been resolved into call->type_args by - * zend_build_generic_call_type_args. Substitute the resolved - * zend_type in so downstream class/scalar/composite checks run - * against the actual bound type rather than the literal T-ref. */ - if (ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { - if (!call->type_args || ref->index >= call->type_args->count) { - continue; - } - const zend_type *resolved = zend_type_arg_entry_type( - &call->type_args->entries[ref->index]); - if (!resolved) { - /* No binding available; the erased parameter type (the - * parameter's bound) already accepted the value. */ - continue; - } - substituted = *resolved; - } - - bool strict = EG(current_execute_data) - && EG(current_execute_data)->func - && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); - - /* When the pre-erasure parameter slot is variadic (T ...$xs), the - * single key in `pre` covers every value supplied at runtime. Sweep - * from arg_idx through num_args-1 so each variadic element gets the - * same reified T-check, not just the first. */ - bool is_variadic_slot = (fbc->common.fn_flags & ZEND_ACC_VARIADIC) - && arg_idx == fbc->common.num_args; - uint32_t sweep_end = is_variadic_slot ? num_args : (uint32_t) arg_idx + 1; - - for (uint32_t aidx = (uint32_t) arg_idx; aidx < sweep_end; aidx++) { - zval *arg = ZEND_CALL_ARG(call, aidx + 1); - zval *target = arg; - const zend_reference *zref = NULL; - if (Z_ISREF_P(target)) { - zref = Z_REF_P(target); - target = Z_REFVAL_P(target); - } - - /* Pre-erasure shapes don't match the canonical PHP type layout - * (scalars sit as CODE-only list members instead of bits on the - * outer mask), so walk them directly. */ - bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); - if (!ok) { - zend_string *expected = zend_type_to_string(substituted); - const zend_arg_info *ai; - if (is_variadic_slot) { - ai = &fbc->common.arg_info[fbc->common.num_args]; - } else { - ai = (aidx < fbc->common.num_args) - ? &fbc->common.arg_info[aidx] : NULL; - } - zend_throw_error(zend_ce_type_error, - "%s%s%s(): Argument #%u%s%s%s must be of type %s, %s given", - fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", - fbc->common.scope ? "::" : "", - fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", - aidx + 1, - (ai && ai->name) ? " ($" : "", - (ai && ai->name) ? ZSTR_VAL(ai->name) : "", - (ai && ai->name) ? ")" : "", - ZSTR_VAL(expected), zend_zval_value_name(target)); - zend_string_release(expected); - return false; - } + if (!zend_verify_one_generic_param(call, fbc, nwa, + (uint32_t) arg_idx, ref->index, num_args, strict)) { + return false; } } ZEND_HASH_FOREACH_END(); return true; @@ -1193,6 +1574,8 @@ ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint3 #define ZEND_VERIFY_ARITY_KIND_CALL 0 #define ZEND_VERIFY_ARITY_KIND_NEW 1 +/* Site dispatches directly to a monomorph by name; emit no VERIFY/INSTALL. */ +#define ZEND_VERIFY_ARITY_KIND_NONE 2 static zend_type zend_compile_turbofish_args_type(zend_ast *turbofish_ast) { @@ -1263,6 +1646,84 @@ static bool zend_can_install_call_args_statically( params, ZEND_TYPE_NAMED_WITH_ARGS(*args_box), arity); } +/* Compile-time equivalent of zend_build_generic_call_type_args for a concrete + * call site: each entry borrows its type_ref into the args_box NWA (i=passed). NULL when any arg still contains a T-ref. */ +static zend_type_arg_table *zend_build_concrete_call_type_args( + const zend_function *fbc, const zend_type *args_box) +{ + 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) { + return NULL; + } + + uint32_t passed = 0; + const zend_type *passed_args = NULL; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + passed = nwa->count; + passed_args = nwa->args; + } + + zend_type_arg_table *table = zend_type_arg_table_alloc(params->count); + for (uint32_t i = 0; i < params->count; i++) { + const zend_type *src = NULL; + if (i < passed) { + src = &passed_args[i]; + } else if (ZEND_TYPE_IS_SET(params->parameters[i].default_type)) { + src = ¶ms->parameters[i].default_type; + } + if (!src || !ZEND_TYPE_IS_SET(*src)) { + continue; + } + if (zend_type_contains_type_parameter(*src)) { + zend_type_arg_table_destroy(table); + return NULL; + } + table->entries[i].name = zend_type_arg_canonical_name(*src); + table->entries[i].type_ref = src; + } + + return table; +} + +/* 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( + 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)) { + return false; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) { + return false; + } + } + if (!zend_call_is_cacheable_against_args(fbc, args_box)) { + return false; + } + zend_type_arg_table *ct = zend_build_concrete_call_type_args(fbc, args_box); + if (!ct) { + return false; + } + zend_generic_type_table *gtt = CG(active_op_array)->generic_types; + zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(gtt->turbofish_args, args_id); + ZEND_ASSERT(entry != NULL); + entry->concrete_table = ct; + entry->concrete_skip_value_check = + fbc->op_array.generic_types + && fbc->op_array.generic_types->parameters + && zend_count_generic_value_checks( + fbc->op_array.generic_types->parameters) == 0; + return true; +} + static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc) { uint32_t arity = 0; @@ -1300,6 +1761,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) { + /* 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_op *opline = get_next_op(); @@ -1316,13 +1782,22 @@ static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t opline->op1.num = 0; } opline->result_type = IS_UNUSED; - /* When there's a turbofish, reserve a 2-slot inline cache in the caller - * op_array's runtime cache: slot[0] = cached zend_type_arg_table*, - * slot[1] = caller-binding fingerprint (cache key). The runtime cache is - * per-process and writable even when the op_array's side tables are - * persisted to opcache SHM, so this cache survives opcache without the - * SHM-write gating that previously disabled it. */ - opline->result.num = args_id ? zend_alloc_cache_slots(2) : 0; + /* Reserve a 5-slot inline cache in the caller op_array's runtime cache: + * slot[0] = cached zend_type_arg_table* (CALL) / monomorph ce* (NEW), + * slot[1] = cache key (caller-binding fingerprint) / state sentinel, + * slot[2] = cached zend_turbofish_args_entry* (skips the per-call lookup), + * slot[3] = promotion/monomorph memo (resolved callee guard), + * slot[4] = monomorph type-arg table. + * The runtime cache is per-process and writable even when the op_array's + * side tables are persisted to opcache SHM. */ + opline->result.num = (args_id || kind == ZEND_VERIFY_ARITY_KIND_CALL) + ? zend_alloc_cache_slots(5) : 0; + + /* Mark so destroy_op_array scans opcodes for a CALL-form generic-args slot + * that may own a zend_type_arg_table; non-generic op_arrays skip the scan. */ + if (opline->op1_type == IS_UNUSED && opline->result.num) { + CG(active_op_array)->fn_flags2 |= ZEND_ACC2_HAS_GENERIC_CALL_OPS; + } } static zend_generic_parameter_list *zend_compile_generic_type_parameter_list( @@ -6495,8 +6970,111 @@ static uint32_t zend_compile_frameless_icall(znode *result, const zend_ast_list return zend_compile_frameless_icall_ex(result, args, fbc, frameless_function_info, type); } +/* For a turbofish call with only concrete args, return the lowercased mangled + * monomorph name (`base`) used as the by-name dispatch key, or NULL to + * keep the runtime VERIFY path. */ +static zend_string *zend_build_concrete_turbofish_monomorph_name( + zend_string *base_name, zend_ast *turbofish_ast) +{ + if (!turbofish_ast) { + return NULL; + } + zend_ast_list *list = zend_ast_get_list(turbofish_ast); + uint32_t arity = list->children; + if (arity == 0 || arity > ZEND_GENERIC_MAX_PARAMS) { + return NULL; + } + + /* Bail on any T-ref or class-name arg: the mangled name is lowercased and the + * synthesize-by-name path reparses it, which would lose class-name case. Only + * case-insensitive scalar args round-trip losslessly. */ + zend_type args[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + uint32_t built = 0; + for (uint32_t i = 0; i < arity; i++) { + args[i] = zend_compile_pre_erasure_typename(list->child[i]); + built++; + if (zend_type_contains_type_parameter(args[i]) + || ZEND_TYPE_HAS_NAME(args[i]) + || ZEND_TYPE_HAS_LITERAL_NAME(args[i]) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(args[i]) + || ZEND_TYPE_HAS_LIST(args[i])) { + ok = false; + break; + } + } + + zend_string *result = NULL; + if (ok) { + zend_string *canonical = + zend_generic_canonical_class_name(base_name, args, arity); + result = zend_string_tolower(canonical); + zend_string_release(canonical); + } + + for (uint32_t i = 0; i < built; i++) { + zend_type_release(args[i], /* persistent */ false); + } + return result; +} + +/* Emit the literal triple for an INIT_NS_FCALL_BY_NAME targeting the mangled + * monomorph name (mirrors zend_add_ns_func_name_literal). -1 when not concrete. */ +static int zend_add_ns_monomorph_name_literal(zend_string *full_name, zend_ast *turbofish_ast) +{ + zend_string *mangled_lc_full = + zend_build_concrete_turbofish_monomorph_name(full_name, turbofish_ast); + if (!mangled_lc_full) { + return -1; + } + + /* slot[0]: original-case mangled full name (only used in diagnostics). */ + zend_string *orig = zend_string_dup(mangled_lc_full, 0); + int ret = zend_add_literal_string(&orig); + + /* slot[1]: lowercased mangled full name (primary lookup key). */ + zend_string *lc_full = zend_string_copy(mangled_lc_full); + zend_add_literal_string(&lc_full); + + /* slot[2]: lowercased mangled unqualified name (global fallback key). */ + const char *uq; + size_t uq_len; + if (zend_get_unqualified_name(full_name, &uq, &uq_len)) { + zend_string *base_uq = zend_string_init(uq, uq_len, 0); + zend_string *mangled_lc_uq = + zend_build_concrete_turbofish_monomorph_name(base_uq, turbofish_ast); + zend_string_release(base_uq); + if (mangled_lc_uq) { + zend_add_literal_string(&mangled_lc_uq); + } else { + zend_string *dup = zend_string_copy(mangled_lc_full); + zend_add_literal_string(&dup); + } + } + + zend_string_release(mangled_lc_full); + return ret; +} + static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast *args_ast, zend_ast *turbofish_ast, uint32_t lineno, uint32_t type) /* {{{ */ { + /* Concrete turbofish ns call -> by-name dispatch to the monomorph, no VERIFY. + * Skip callable-convert (Closure creation needs the base generic function). */ + if (turbofish_ast != NULL + && args_ast->kind != ZEND_AST_CALLABLE_CONVERT) { + int mono_constants = + zend_add_ns_monomorph_name_literal(Z_STR(name_node->u.constant), turbofish_ast); + if (mono_constants != -1) { + zend_op *opline = get_next_op(); + opline->opcode = ZEND_INIT_NS_FCALL_BY_NAME; + opline->op2_type = IS_CONST; + opline->op2.constant = mono_constants; + opline->result.num = zend_alloc_cache_slot(); + zend_compile_call_common(result, args_ast, NULL, lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_NONE, NULL); + return; + } + } + int name_constants = zend_add_ns_func_name_literal(Z_STR(name_node->u.constant)); /* Find frameless function with same name. */ @@ -7139,6 +7717,28 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) return; } + /* Concrete turbofish call to a known generic function -> by-name dispatch + * to the monomorph (synthesized on first INIT_FCALL_BY_NAME miss), no VERIFY. */ + if (turbofish_ast != NULL + && !is_callable_convert + && ZEND_USER_CODE(fbc->common.type) + && fbc->op_array.generic_parameters) { + zend_string *mono_lc = + zend_build_concrete_turbofish_monomorph_name(lcname, turbofish_ast); + if (mono_lc) { + zend_string_release_ex(lcname, 0); + zval_ptr_dtor(&name_node.u.constant); + zend_op *bn_opline = get_next_op(); + bn_opline->opcode = ZEND_INIT_FCALL_BY_NAME; + bn_opline->op2_type = IS_CONST; + /* zend_add_func_name_literal consumes the reference. */ + bn_opline->op2.constant = zend_add_func_name_literal(mono_lc); + bn_opline->result.num = zend_alloc_cache_slot(); + zend_compile_call_common(result, args_ast, NULL, ast->lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_NONE, NULL); + return; + } + } + if (turbofish_ast == NULL && !is_callable_convert && zend_try_compile_special_func(result, lcname, diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 8e48292212b6..b224ff6dd7ec 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -155,6 +155,22 @@ typedef struct _zend_generic_parameter_list { #define ZEND_GENERIC_PARAMETER_LIST_SIZE(count) \ (sizeof(zend_generic_parameter_list) + ((count) - 1) * sizeof(zend_generic_parameter)) +/* Packed, callee-static list of the direct FUNCTION_LIKE type-parameter value + * parameters that zend_verify_generic_arg_types must runtime-check. Derived once + * from `parameters`, it replaces a per-call HashTable iteration (+ per-entry + * TYPE_PARAMETER/origin filtering) with a tight array walk. Built lazily on the + * first verify; left NULL on opcache-persisted tables (which are read-only), in + * which case verify falls back to iterating `parameters` directly. */ +typedef struct _zend_generic_value_check { + uint32_t arg_idx; /* parameter position (0-based) */ + uint32_t tp_index; /* type-parameter index to substitute (ref->index) */ +} zend_generic_value_check; + +typedef struct _zend_generic_value_check_plan { + uint32_t count; + zend_generic_value_check checks[1]; /* flexible array */ +} zend_generic_value_check_plan; + typedef struct _zend_generic_type_table { zend_type *return_type; /* function/method return; NULL if equal to erased */ zend_type *extends; /* class extends; NULL if equal */ @@ -165,6 +181,8 @@ typedef struct _zend_generic_type_table { HashTable *trait_uses; /* trait-use index -> zend_type * */ HashTable *turbofish_args; /* opline->extended_value -> zend_type * (NAMED_WITH_ARGS holding the call-site type arguments); index is stable across optimizer reorderings */ bool persisted; /* set by opcache when the table lives in SHM/file-cache memory; suppresses destruction */ + zend_generic_value_check_plan *value_check_plan; /* lazily built; request-local; NULL on persisted tables */ + struct _zend_type_arg_table *monomorph_type_args; /* on a monomorph op_array: invariant concrete type-args installed onto the call frame by dispatch; NULL otherwise */ } zend_generic_type_table; /* Compile-time linked stack of in-scope generic type parameters. */ @@ -181,6 +199,8 @@ ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t ZEND_API void zend_generic_parameter_list_destroy(zend_generic_parameter_list *list); ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void); ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table); +ZEND_API uint32_t zend_count_generic_value_checks(const HashTable *parameters); +ZEND_API void zend_fill_generic_value_check_plan(zend_generic_value_check_plan *plan, const HashTable *parameters); ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type); ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type); ZEND_API zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce); @@ -196,6 +216,7 @@ ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box); ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box); +ZEND_API void zend_apply_generic_new(zval *new_obj, zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, bool do_checks); ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id); ZEND_API struct _zend_turbofish_args_entry *zend_generic_get_turbofish_call_entry(const zend_op_array *caller_op_array, uint32_t args_id); @@ -252,13 +273,58 @@ static zend_always_inline const zend_type *zend_type_arg_entry_type(const zend_t * writable even when the entry itself is persisted to opcache SHM. */ typedef struct _zend_turbofish_args_entry { zend_type args_box; + struct _zend_type_arg_table *concrete_table; /* precomputed read-only table for a concrete INSTALL site; NULL when the handler must rebuild at runtime */ + bool concrete_skip_value_check; /* callee has no direct T-ref value params, so the value check is a no-op */ } zend_turbofish_args_entry; +/* Resolve a generic call site's turbofish args_box, memoizing the (static per + * site) turbofish_args lookup in cache_slot[2] so the VERIFY/INSTALL handlers + * skip the hash lookup after the first call. Returns NULL when the site has no + * turbofish (args_id == 0 / no entry). */ +static zend_always_inline const zend_type *zend_generic_get_or_cache_args_box( + const zend_op_array *op_array, uint32_t args_id, void **cache_slot) +{ + zend_turbofish_args_entry *entry; + if (cache_slot && cache_slot[2]) { + entry = (zend_turbofish_args_entry *) cache_slot[2]; + } else { + entry = zend_generic_get_turbofish_call_entry(op_array, args_id); + if (cache_slot && entry) { + cache_slot[2] = entry; + } + } + return entry ? &entry->args_box : NULL; +} + +/* Like zend_generic_get_or_cache_args_box, but returns the whole entry. */ +static zend_always_inline zend_turbofish_args_entry *zend_generic_get_or_cache_args_entry( + const zend_op_array *op_array, uint32_t args_id, void **cache_slot) +{ + zend_turbofish_args_entry *entry; + if (cache_slot && cache_slot[2]) { + entry = (zend_turbofish_args_entry *) cache_slot[2]; + } else { + entry = zend_generic_get_turbofish_call_entry(op_array, args_id); + if (cache_slot && entry) { + cache_slot[2] = entry; + } + } + return entry; +} + /* Cache key sentinel for concrete-arg call sites (no T-refs in the args). * Cache key 0 means empty; CONCRETE means "args fully resolved at compile * time, table is invariant across calls." */ #define ZEND_TURBOFISH_CACHE_KEY_CONCRETE ((uintptr_t)1) +/* Runtime-promoted site: cache_slot[0] = invariant table, cache_slot[3] = + * memoized callee (low bit = value check is a no-op). */ +#define ZEND_TURBOFISH_CACHE_KEY_PROMOTED ((uintptr_t)2) + +/* Monomorphized site: cache_slot[0] = monomorph func, [3] = base func guard, + * [4] = shared invariant type-arg table. */ +#define ZEND_TURBOFISH_CACHE_KEY_MONOMORPH ((uintptr_t)3) + 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 zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); @@ -268,6 +334,8 @@ ZEND_API zend_type_arg_table *zend_build_or_get_cached_type_args(zend_execute_da ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type); ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type); ZEND_API bool zend_verify_generic_arg_types(zend_execute_data *call, const zend_type *args_box); +ZEND_API zend_function *zend_get_or_synthesize_call_monomorph(zend_execute_data *call, const zend_type *args_box, uint32_t arity, void **cache_slot, zend_type_arg_table **out_type_args); +ZEND_API zend_function *zend_try_monomorph_resolved_call(zend_execute_data *call, zend_type_arg_table *resolved, void **cache_slot, zend_type_arg_table **out_type_args); ZEND_API bool zend_verify_generic_return_type(zend_execute_data *call, zval *retval_ptr); typedef union _zend_parser_stack_elem { @@ -574,6 +642,12 @@ typedef struct _zend_oparray_context { /* | | | */ /* Function forbids dynamic calls | | | */ #define ZEND_ACC2_FORBID_DYN_CALLS (1 << 0) /* | X | | */ +/* | | | */ +/* op_array has generic CALL opcodes (scanned at teardown)| | | */ +#define ZEND_ACC2_HAS_GENERIC_CALL_OPS (1 << 1) /* | X | | */ +/* | | | */ +/* synthesized monomorph carrying concrete type-args | | | */ +#define ZEND_ACC2_MONOMORPH_TYPE_ARGS (1 << 2) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 0d120d0113c7..89a83e3c678f 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4486,6 +4486,19 @@ static zend_never_inline void ZEND_FASTCALL init_func_run_time_cache(zend_op_arr } /* }}} */ +/* Synthesize a monomorph from a mangled name (`base`) on by-name dispatch + * miss; returns NULL when the name isn't a synthesizable generic shape. */ +ZEND_API zend_function * ZEND_FASTCALL zend_resolve_monomorph_by_name(zend_string *lc_name) /* {{{ */ +{ + zend_function *fbc = zend_try_synthesize_function_monomorph_by_name(lc_name); + if (fbc) { + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache_i(&fbc->op_array); + } + } + return fbc; +} /* }}} */ + ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name) /* {{{ */ { zval *zv = zend_hash_find(EG(function_table), name); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index fad7301033b1..ff9aff51b9d9 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -348,6 +348,10 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, const zend_closure *closure = (const zend_closure *) ZEND_CLOSURE_OBJECT(func); call->type_args = closure->captured_type_args; + } 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. */ + call->type_args = func->op_array.generic_types->monomorph_type_args; } else { call->type_args = NULL; } @@ -499,6 +503,7 @@ ZEND_API zend_class_entry *zend_fetch_class_with_scope(zend_string *class_name, ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, uint32_t fetch_type); ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name); +ZEND_API zend_function * ZEND_FASTCALL zend_resolve_monomorph_by_name(zend_string *lc_name); ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function_str(const char *name, size_t len); ZEND_API void ZEND_FASTCALL zend_init_func_run_time_cache(zend_op_array *op_array); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 9647dd9281dc..00520d2d3774 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -892,12 +892,15 @@ static bool zend_get_inheritance_binding( * * This is what makes property types like `T|null` reify correctly. Without the * recursive walk, a T living inside a union stays literal at runtime and the - * property type check rejects valid assignments with "of type T". */ -static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) + * property type check rejects valid assignments with "of type T". + * + * `origin` selects which type-parameter refs to substitute: CLASS_LIKE for the + * class monomorphizer, FUNCTION_LIKE for the function monomorphizer. */ +static zend_type zend_substitute_leaf_type_param_origin(zend_type t, const zend_type *args, uint32_t arity, uint8_t origin) { if (ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); - if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index >= arity) { + if (ref->origin != origin || ref->index >= arity) { return t; } @@ -917,7 +920,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a const zend_type_named_with_args *src_nwa = ZEND_TYPE_NAMED_WITH_ARGS(t); bool needs_rebuild = false; for (uint32_t i = 0; i < src_nwa->count; i++) { - zend_type probe = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + zend_type probe = zend_substitute_leaf_type_param_origin(src_nwa->args[i], args, arity, origin); if (memcmp(&probe, &src_nwa->args[i], sizeof(zend_type)) != 0) { needs_rebuild = true; break; @@ -931,7 +934,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a zend_type *new_args = (zend_type *) do_alloca(sizeof(zend_type) * src_nwa->count, use_heap); bool all_concrete = true; for (uint32_t i = 0; i < src_nwa->count; i++) { - new_args[i] = zend_substitute_leaf_type_param(src_nwa->args[i], args, arity); + new_args[i] = zend_substitute_leaf_type_param_origin(src_nwa->args[i], args, arity, origin); if (zend_type_contains_type_parameter(new_args[i])) { all_concrete = false; } @@ -976,14 +979,14 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a const zend_type *elem = &src_list->types[i]; if (ZEND_TYPE_HAS_TYPE_PARAMETER(*elem)) { const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*elem); - if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE && ref->index < arity) { + if (ref->origin == origin && ref->index < arity) { needs_rebuild = true; break; } - } else if (ZEND_TYPE_HAS_LIST(*elem)) { - /* Nested list (DNF: intersection inside union, etc.) — recurse to + } else if (ZEND_TYPE_HAS_LIST(*elem) || ZEND_TYPE_HAS_NAMED_WITH_ARGS(*elem)) { + /* Nested list (DNF) or named-with-args (`I`) — recurse to * see if there's a T-ref buried in there. */ - zend_type probe = zend_substitute_leaf_type_param(*elem, args, arity); + zend_type probe = zend_substitute_leaf_type_param_origin(*elem, args, arity, origin); if (memcmp(&probe, elem, sizeof(zend_type)) != 0) { needs_rebuild = true; break; @@ -1005,7 +1008,7 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a uint32_t out_count = 0; for (uint32_t i = 0; i < src_list->num_types; i++) { - zend_type substituted = zend_substitute_leaf_type_param(src_list->types[i], args, arity); + zend_type substituted = zend_substitute_leaf_type_param_origin(src_list->types[i], args, arity, origin); /* Keep complex elements (named types, intersection sublists, unresolved * T-refs) in the list; their scalar contribution is also OR'd into the @@ -1052,6 +1055,16 @@ static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *a return result; } +static zend_type zend_substitute_leaf_type_param(zend_type t, const zend_type *args, uint32_t arity) +{ + return zend_substitute_leaf_type_param_origin(t, args, arity, ZEND_GENERIC_ORIGIN_CLASS_LIKE); +} + +ZEND_API zend_type zend_substitute_function_type_param(zend_type t, const zend_type *args, uint32_t arity) +{ + return zend_substitute_leaf_type_param_origin(t, args, arity, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); +} + static bool zend_get_trait_use_binding( const zend_class_entry *ce, uint32_t trait_index, const zend_type **out_args, uint32_t *out_arity) @@ -7608,3 +7621,247 @@ ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( efree(args); return mono; } + +/* Function monomorphization: synthesize a concrete op_array for a generic + * function. The clone shares the base's refcounted body buffers (refcount==NULL + * so destroy_op_array never frees them) and only its arg_info differs. */ + +/* 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) +{ + uint32_t num_args = base->num_args; + bool has_return = (base->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + bool variadic = (base->fn_flags & ZEND_ACC_VARIADIC) != 0; + uint32_t total = num_args + (has_return ? 1 : 0) + (variadic ? 1 : 0); + if (total == 0 || !base->arg_info) { + return NULL; + } + + 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); + memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); + + const HashTable *pre_params = base->generic_types ? base->generic_types->parameters : NULL; + const zend_type *pre_return = base->generic_types ? base->generic_types->return_type : NULL; + + for (uint32_t slot = 0; slot < total; slot++) { + /* slot 0 is the return type when has_return; UINT32_MAX marks it. */ + uint32_t param_index = has_return ? (slot == 0 ? UINT32_MAX : slot - 1) : slot; + + const zend_type *pre = NULL; + if (param_index == UINT32_MAX) { + pre = pre_return; + } else if (pre_params) { + zval *zv = zend_hash_index_find(pre_params, param_index); + pre = zv ? (const zend_type *) Z_PTR_P(zv) : NULL; + } + + /* Only specialize a BARE FUNCTION_LIKE type-parameter leaf (`T $x`). + * Composite generic types keep the base's erased arg_info: the erased model + * represents generic instances by their plain class, so folding to a + * monomorph name would make RECV reject the plain instances the body emits. */ + bool is_bare_leaf = pre && ZEND_TYPE_IS_SET(*pre) + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre) + && ZEND_TYPE_TYPE_PARAMETER(*pre)->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE; + if (is_bare_leaf) { + zend_type sub = zend_substitute_function_type_param(*pre, args, arity); + zend_type_copy_ctor(&sub, /* use_arena */ true, /* persistent */ false); + new_block[slot].type = sub; + } else { + zend_type_copy_ctor(&new_block[slot].type, /* use_arena */ true, /* persistent */ false); + } + if (new_block[slot].name) { + zend_string_addref(new_block[slot].name); + } + if (new_block[slot].doc_comment) { + zend_string_addref(new_block[slot].doc_comment); + } + } + + return new_block + (has_return ? 1 : 0); +} + +ZEND_API zend_function *zend_synthesize_function_monomorph( + zend_function *base, const zend_type *args, uint32_t arity) +{ + 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; + } + + /* Fill trailing defaults so the args array covers every parameter. */ + 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; + } + + /* A remaining type parameter means this isn't a concrete instantiation. */ + 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(EG(function_table), lc_name); + if (existing) { + 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_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(mono, &base->op_array, sizeof(zend_op_array)); + + /* Invariant concrete type-arg table, shared across all calls to this monomorph + * so the body's own T-refs resolve under by-name dispatch. Arena-allocated and + * marked persisted so the refcount==NULL monomorph teardown never frees it. */ + 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->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); + mono_targs->entries[i].owned_type = owned; + zend_string *cname = zend_type_arg_canonical_name(args[i]); + mono_targs->entries[i].name = cname; + } + } + } + + { + zend_generic_type_table *gt = zend_arena_alloc(&CG(arena), sizeof(zend_generic_type_table)); + memset(gt, 0, sizeof(*gt)); + if (base->op_array.generic_types && base->op_array.generic_types->turbofish_args) { + gt->turbofish_args = base->op_array.generic_types->turbofish_args; + } + gt->persisted = true; + gt->monomorph_type_args = mono_targs; + mono->generic_types = gt; + } + mono->fn_flags2 |= ZEND_ACC2_MONOMORPH_TYPE_ARGS; + + /* refcount==NULL: destroy_op_array won't free the shared body buffers. */ + mono->refcount = NULL; + mono->fn_flags &= ~(ZEND_ACC_IMMUTABLE | ZEND_ACC_HEAP_RT_CACHE | ZEND_ACC_PRELOADED); + /* TRAIT_CLONE forces RECV onto the slow path that checks the substituted + * arg_info (the shared RECV opcodes carry the base's erased type mask). */ + mono->fn_flags |= ZEND_ACC_TRAIT_CLONE; + + /* Keep the base name so TypeError messages match the erased path; the mangled + * name is only the EG(function_table) key. */ + mono->function_name = zend_string_copy(base->op_array.function_name); + if (new_arg_info) { + mono->arg_info = new_arg_info; + } + + ZEND_MAP_PTR_INIT(mono->run_time_cache, NULL); + ZEND_MAP_PTR_INIT(mono->static_variables_ptr, NULL); + + /* Allocate the runtime cache now: the call swaps to this op_array before + * DO_FCALL, whose hot path reads RUN_TIME_CACHE without lazy allocation. */ + zend_init_func_run_time_cache(mono); + + zend_function *mono_fn = (zend_function *) mono; + if (!zend_hash_add_ptr(EG(function_table), lc_name, mono_fn)) { + existing = zend_hash_find_ptr(EG(function_table), lc_name); + zend_string_release(display_name); + zend_string_release(lc_name); + zend_string_release(mono->function_name); + return existing; + } + + zend_string_release(display_name); + zend_string_release(lc_name); + return mono_fn; +} + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name) +{ + size_t lt_pos, args_len; + if (!zend_mp_split_name(lc_name, <_pos, &args_len)) return NULL; + + zend_string *base_lc = zend_string_init(ZSTR_VAL(lc_name), lt_pos, 0); + zend_function *base = zend_hash_find_ptr(EG(function_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(lc_name) + lt_pos + 1, + .end = ZSTR_VAL(lc_name) + ZSTR_LEN(lc_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; + } + + /* The by-name call skipped ZEND_VERIFY_GENERIC_ARGUMENTS, so enforce arity and + * bounds here (once); the monomorph's RECV opcodes cover per-argument checks. */ + zend_type args_box = ZEND_TYPE_INIT_NONE(0); + zend_type_named_with_args *nwa = + emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count)); + nwa->name = NULL; + nwa->name_attr = 0; + nwa->count = count; + for (uint32_t i = 0; i < count; i++) nwa->args[i] = args[i]; + ZEND_TYPE_SET_PTR(args_box, nwa); + ZEND_TYPE_FULL_MASK(args_box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + zend_check_generic_call_arguments(base, count, &args_box); + efree(nwa); + if (UNEXPECTED(EG(exception))) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_function *mono = zend_synthesize_function_monomorph(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 5cc846048559..86f1460bbba0 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -109,6 +109,16 @@ static zend_always_inline bool zend_class_is_monomorph(const zend_class_entry *c ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( zend_string *name, uint32_t flags); +ZEND_API zend_type zend_substitute_function_type_param(zend_type t, const zend_type *args, uint32_t arity); + +/* Synthesize (or return the cached) concrete specialization of generic function + * `base` for the given type args, registered in EG(function_table) as + * `base`. Returns NULL when args are not concrete or base isn't generic. */ +ZEND_API zend_function *zend_synthesize_function_monomorph( + zend_function *base, const zend_type *args, uint32_t arity); + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name); + void zend_verify_abstract_class(zend_class_entry *ce); void zend_build_properties_info_table(zend_class_entry *ce); ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_entry *parent_ce, zend_string *lcname, zval *delayed_early_binding); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index b92bea88f6b6..8c1dcd7c0fc0 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -189,6 +189,10 @@ static void zend_generic_type_table_value_dtor(zval *zv) { static void zend_turbofish_args_entry_dtor(zval *zv) { zend_turbofish_args_entry *entry = Z_PTR_P(zv); zend_type_release(entry->args_box, /* persistent */ false); + /* Free the heap concrete table; SHM-relocated tables have persisted set. */ + if (entry->concrete_table && !entry->concrete_table->persisted) { + zend_type_arg_table_destroy(entry->concrete_table); + } efree(entry); } @@ -234,6 +238,9 @@ ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table) { zend_hash_destroy(table->turbofish_args); FREE_HASHTABLE(table->turbofish_args); } + if (table->value_check_plan) { + efree(table->value_check_plan); + } efree(table); } @@ -312,6 +319,8 @@ ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type) { zend_turbofish_args_entry *entry = emalloc(sizeof(*entry)); entry->args_box = type; + entry->concrete_table = NULL; + entry->concrete_skip_value_check = false; zend_hash_index_update_ptr(zend_generic_type_table_ensure_turbofish(&t->turbofish_args), op_num, entry); } @@ -862,20 +871,40 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; - /* VERIFY/INSTALL_GENERIC_ARGS may have stashed a zend_type_arg_table* in - * their cache slot, marked persisted so per-frame teardown leaves it - * alone. The cache slot owns that allocation; release the contained - * tables for every op_array, regardless of whether the cache buffer - * itself is heap- or arena-allocated. */ - if (op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { + /* VERIFY/INSTALL_GENERIC_ARGS on a CALL site (op1_type == IS_UNUSED) may + * have stashed a zend_type_arg_table* in their cache slot, marked persisted + * so per-frame teardown leaves it alone. The cache slot owns that + * allocation; release the contained tables for every op_array, regardless + * of whether the cache buffer itself is heap- or arena-allocated. + * + * 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. */ + if ((op_array->fn_flags2 & ZEND_ACC2_HAS_GENERIC_CALL_OPS) + && 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) { 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_VERIFY_GENERIC_ARGUMENTS || op->opcode == ZEND_INSTALL_GENERIC_ARGS) + && op->op1_type == IS_UNUSED && op->result.num) { void **cache_slot = (void **) (cache_buf + op->result.num); + /* Monomorphized site: slot[0] is a function* owned by + * EG(function_table); free only slot[4]'s type_arg table. */ + if ((uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *mt = (zend_type_arg_table *) cache_slot[4]; + if (mt) { + mt->persisted = false; + zend_type_arg_table_destroy(mt); + cache_slot[4] = NULL; + } + cache_slot[0] = NULL; + cache_slot[1] = NULL; + continue; + } zend_type_arg_table *t = (zend_type_arg_table *) cache_slot[0]; if (t) { t->persisted = false; diff --git a/Zend/zend_operators.h b/Zend/zend_operators.h index 2dc6dbfbfc6d..83cfe447dcea 100644 --- a/Zend/zend_operators.h +++ b/Zend/zend_operators.h @@ -81,7 +81,16 @@ ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *ins static zend_always_inline bool instanceof_function( const zend_class_entry *instance_ce, const zend_class_entry *ce) { - return instance_ce == ce || instanceof_function_slow(instance_ce, ce); + /* `instance_ce->parent == ce` is a cheap second-level fast path for the + * direct-subclass case: it always implies `instance_ce instanceof ce` + * (parent is a class, never an interface). It is the common shape for a + * generic monomorph checked against its erased base (e.g. a + * `DirectedGraph` value flowing into a `DirectedGraph`-typed + * parameter), which would otherwise pay a full slow-path call on every + * boundary check. */ + return instance_ce == ce + || instance_ce->parent == ce + || instanceof_function_slow(instance_ce, ce); } ZEND_API bool zend_string_only_has_ascii_alphanumeric(const zend_string *str); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 3b5fa7c087c6..1a3a5e317231 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3903,7 +3903,17 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + CACHE_PTR(opline->result.num, fbc); + goto ZEND_VM_C_LABEL(fcall_by_name_push); } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -3911,6 +3921,7 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) } CACHE_PTR(opline->result.num, fbc); } +ZEND_VM_C_LABEL(fcall_by_name_push): call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4059,7 +4070,21 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + CACHE_PTR(opline->result.num, fbc); + goto ZEND_VM_C_LABEL(ns_fcall_by_name_push); } } fbc = Z_FUNC_P(func); @@ -4069,6 +4094,7 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) CACHE_PTR(opline->result.num, fbc); } +ZEND_VM_C_LABEL(ns_fcall_by_name_push): call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -9075,57 +9101,135 @@ ZEND_VM_HANDLER(212, ZEND_VERIFY_GENERIC_ARGUMENTS, TMP|UNUSED, UNUSED) USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (OP1_TYPE == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (OP1_TYPE == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); /* Monomorphize: synthesize (or look up) Box and swap both the * object's class entry and the pending constructor call. The monomorph * shares Box's property layout, so swapping ce is safe; swapping * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -9160,38 +9264,57 @@ ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + zend_turbofish_args_entry *tf_entry = + (OP1_TYPE == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (OP1_TYPE == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (OP1_TYPE == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); - } else { - zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); } + call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 83377dd06a13..3598c62060af 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4161,7 +4161,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto fcall_by_name_push; } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -4169,6 +4179,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I } CACHE_PTR(opline->result.num, fbc); } +fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -4246,7 +4257,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_by_name_push; } } fbc = Z_FUNC_P(func); @@ -4256,6 +4281,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } +ns_fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -22254,57 +22280,135 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); /* Monomorphize: synthesize (or look up) Box and swap both the * object's class entry and the pending constructor call. The monomorph * shares Box's property layout, so swapping ce is safe; swapping * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -22339,38 +22443,57 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); - } else { - zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); } + call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -37883,57 +38006,135 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERI USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); /* Monomorphize: synthesize (or look up) Box and swap both the * object's class entry and the pending constructor call. The monomorph * shares Box's property layout, so swapping ce is safe; swapping * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -37968,38 +38169,57 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENER { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); - } else { - zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); } + call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -57642,7 +57862,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(function_name+1)); + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto fcall_by_name_push; } fbc = Z_FUNC_P(func); if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { @@ -57650,6 +57880,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F } CACHE_PTR(opline->result.num, fbc); } +fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -57727,7 +57958,21 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Mangled monomorph name: synthesize on first reference (qualified + * then unqualified; may throw). */ + SAVE_OPLINE(); + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 1)); + if (fbc == NULL && !EG(exception)) { + fbc = zend_resolve_monomorph_by_name(Z_STR_P(func_name + 2)); + } + if (UNEXPECTED(EG(exception) != NULL)) { + HANDLE_EXCEPTION(); + } + if (UNEXPECTED(fbc == NULL)) { + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_by_name_push; } } fbc = Z_FUNC_P(func); @@ -57737,6 +57982,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N CACHE_PTR(opline->result.num, fbc); } +ns_fcall_by_name_push: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -75533,57 +75779,135 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); /* Monomorphize: synthesize (or look up) Box and swap both the * object's class entry and the pending constructor call. The monomorph * shares Box's property layout, so swapping ce is safe; swapping * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -75618,38 +75942,57 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + zend_turbofish_args_entry *tf_entry = + (IS_TMP_VAR == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_TMP_VAR == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_TMP_VAR == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); - } else { - zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); } + call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); @@ -91162,57 +91505,135 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARG USE_OPLINE zend_execute_data *call = EX(call); uint32_t arity = opline->op2.num; - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + /* Skip the entry lookup for speculative sites (args_id == 0). */ + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED && opline->extended_value) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - /* Speculative emission for dispatchable calls: when there's no - * turbofish AND the resolved callee turns out to be non-generic, - * there's nothing to verify and no table to build. With turbofish - * present the arity check still needs to fire (the user supplied - * type args to a non-generic callee — explicit "too many" error). */ + /* Erased fast path: non-generic speculative site, nothing to verify. */ if (args_box == NULL && (!ZEND_USER_CODE(call->func->type) || !call->func->op_array.generic_parameters)) { ZEND_VM_NEXT_OPCODE(); } - zend_check_generic_call_arguments(call->func, arity, args_box); - if (!EG(exception)) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Concrete turbofish args: dispatch to a synthesized plain monomorph and + * skip generic verification (the monomorph's RECV does concrete checks). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, arity, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + /* Runtime-promoted site: install the cached invariant table after a func + * guard (cache_slot[3], bit1 = value check is a no-op). */ + if (cache_slot && cache_slot[3] + && ((uintptr_t) cache_slot[3] & ~(uintptr_t)3) == (uintptr_t) call->func) { + call->type_args = (zend_type_arg_table *) cache_slot[0]; + if (((uintptr_t) cache_slot[3] & 2) == 0) { + zend_verify_generic_arg_types(call, args_box); + if (UNEXPECTED(EG(exception))) { + goto generic_verify_check_exception; + } + } + ZEND_VM_NEXT_OPCODE(); + } + /* Inner/inference call already monomorphized: swap func + reinstall table. */ + if (cache_slot && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_MONOMORPH) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, NULL, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete turbofish VERIFY: install the precomputed SHM table after + * the arity/bound check (memoized in cache_slot[0] per resolved func). + * The persisted guard excludes the no-opcache heap-table case. */ + bool checked = (cache_slot && cache_slot[0] == (void *) call->func); + if (!checked) { + zend_check_generic_call_arguments(call->func, arity, args_box); + } + if (EXPECTED(!EG(exception))) { + if (!checked && cache_slot) { + cache_slot[0] = (void *) call->func; + } + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } + } else { + zend_check_generic_call_arguments(call->func, arity, args_box); + if (!EG(exception)) { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); + } + call->type_args = t; + } + /* If the resolved table is invariant (CONCRETE sentinel), try to + * monomorphize so later calls take the mono fast path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + zend_type_arg_table *ma = NULL; + zend_function *mono = zend_try_monomorph_resolved_call(call, t, cache_slot, &ma); + if (mono) { + call->func = mono; + call->type_args = ma; + ZEND_VM_NEXT_OPCODE(); + } + } + zend_verify_generic_arg_types(call, args_box); + /* Promote the invariant site: record the resolved callee in + * cache_slot[3] so later calls take the minimal install path. */ + if (EXPECTED(!EG(exception)) && t + && cache_slot && cache_slot[0] == (void *) t + && (uintptr_t) cache_slot[1] == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + /* Tag bits (func is >=8-aligned): bit0 = PROMOTED, bit1 = value + * check is a no-op. slot[1] stays CONCRETE so the build cache + * keeps returning the table on a func-guard miss. */ + uintptr_t fn = (uintptr_t) call->func | 1u; + if (ZEND_USER_CODE(call->func->type) + && call->func->op_array.generic_types + && call->func->op_array.generic_types->parameters + && zend_count_generic_value_checks( + call->func->op_array.generic_types->parameters) == 0) { + fn |= 2; /* value check is a no-op for this callee */ + } + cache_slot[3] = (void *) fn; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); } } else { zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - zend_check_generic_new_arguments(ce, arity, args_box); /* Monomorphize: synthesize (or look up) Box and swap both the * object's class entry and the pending constructor call. The monomorph * shares Box's property layout, so swapping ce is safe; swapping * call->func ensures the constructor's RECV opcodes verify against the - * monomorph's substituted arg_info. */ - if (!EG(exception) && args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } - } - } - } + * monomorph's substituted arg_info. The call-site inline cache (incl. the + * concrete-args fast path) lives in zend_apply_generic_new. */ + zend_apply_generic_new(new_obj, call, args_box, arity, cache_slot, /* do_checks */ true); } +generic_verify_check_exception: if (UNEXPECTED(EG(exception))) { /* Args have already been pushed by the SEND opcodes preceding the * VERIFY emission for call kind; release them so refcounted values @@ -91247,38 +91668,57 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_AR { USE_OPLINE zend_execute_data *call = EX(call); - zend_turbofish_args_entry *call_entry = zend_generic_get_turbofish_call_entry(&EX(func)->op_array, opline->extended_value); - const zend_type *args_box = call_entry ? &call_entry->args_box : NULL; void **cache_slot = opline->result.num ? CACHE_ADDR(opline->result.num) : NULL; + zend_turbofish_args_entry *tf_entry = + (IS_UNUSED == IS_UNUSED) + ? zend_generic_get_or_cache_args_entry(&EX(func)->op_array, opline->extended_value, cache_slot) + : NULL; + const zend_type *args_box = (IS_UNUSED == IS_UNUSED) + ? (tf_entry ? &tf_entry->args_box : NULL) + : zend_generic_get_or_cache_args_box(&EX(func)->op_array, opline->extended_value, cache_slot); SAVE_OPLINE(); if (IS_UNUSED == IS_UNUSED) { - zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); - if (t) { - if (call->type_args) { - zend_type_arg_table_destroy(call->type_args); + /* Dispatch to a synthesized plain monomorph (see ZEND_VERIFY_GENERIC_ARGUMENTS). */ + { + zend_type_arg_table *mono_args = NULL; + zend_function *mono = zend_get_or_synthesize_call_monomorph(call, args_box, opline->op2.num, cache_slot, &mono_args); + if (mono) { + call->func = mono; + call->type_args = mono_args; + ZEND_VM_NEXT_OPCODE(); + } + if (UNEXPECTED(EG(exception))) { + goto generic_install_check_exception; } - call->type_args = t; } - zend_verify_generic_arg_types(call, args_box); - } else { - zval *new_obj = EX_VAR(opline->op1.var); - zend_class_entry *ce = Z_OBJCE_P(new_obj); - if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { - const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); - if (ce->generic_parameters) { - zend_class_entry *mono = zend_synthesize_monomorph_resolved(ce, nwa->args, nwa->count); - if (mono && mono != ce) { - Z_OBJ_P(new_obj)->ce = mono; - if (mono->constructor && call->func == ce->constructor) { - call->func = mono->constructor; - } + if (tf_entry && tf_entry->concrete_table && tf_entry->concrete_table->persisted) { + /* Concrete INSTALL: install the precomputed SHM table directly. The + * persisted guard excludes the no-opcache heap-table case (whose table + * is owned by the turbofish entry and must not be freed at teardown). */ + call->type_args = tf_entry->concrete_table; + if (!tf_entry->concrete_skip_value_check) { + zend_verify_generic_arg_types(call, args_box); + } + } else { + zend_type_arg_table *t = zend_build_or_get_cached_type_args(call, args_box, cache_slot); + if (t) { + if (call->type_args) { + zend_type_arg_table_destroy(call->type_args); } + call->type_args = t; } + zend_verify_generic_arg_types(call, args_box); } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* Statically pre-validated: skip the runtime arity/bound check, but + * still cache the resolved monomorph at the call site. */ + zend_apply_generic_new(new_obj, call, args_box, opline->op2.num, cache_slot, /* do_checks */ false); } +generic_install_check_exception: if (UNEXPECTED(EG(exception))) { zend_vm_stack_free_args(call); diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 4827a4dcd0a3..a6cd2379c2a1 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -545,6 +545,8 @@ static void zend_file_cache_serialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); UNSERIALIZE_PTR(entry); zend_file_cache_serialize_type(&entry->args_box, script, info, buf); + /* concrete_table is a non-portable SHM address; the load path rebuilds it. */ + entry->concrete_table = NULL; } static void zend_file_cache_serialize_generic_type_table_ht( @@ -1627,6 +1629,8 @@ static void zend_file_cache_unserialize_turbofish_args_entry( zend_turbofish_args_entry *entry = Z_PTR_P(zv); zend_file_cache_unserialize_type( &entry->args_box, zend_file_cache_generic_unserialize_scope, script, buf); + entry->concrete_table = NULL; + entry->concrete_skip_value_check = false; } static void zend_file_cache_unserialize_generic_type_table_ht( @@ -1705,6 +1709,9 @@ static void zend_file_cache_unserialize_generic_type_table( if (table->turbofish_args) { zend_file_cache_unserialize_turbofish_args_ht(&table->turbofish_args, scope, script, buf); } + + /* value_check_plan is not file-serialized; the verify path rebuilds it. */ + table->value_check_plan = NULL; } /* Mirror of zend_file_cache_serialize_type_arg_table. Rebuilds `type_ref` diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 754509c2d108..5b68782c6ebb 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -488,6 +488,47 @@ static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) return ptr; } +/* Relocate a turbofish entry's concrete_table into SHM, rebinding each type_ref + * against the just-persisted copy->args_box NWA. Bails to the runtime rebuild + * path (copy->concrete_table = NULL) for any populated default slot. */ +static void zend_persist_concrete_call_table(zend_turbofish_args_entry *copy) +{ + zend_type_arg_table *src = copy->concrete_table; + if (!src) { + return; + } + + const zend_type_named_with_args *new_nwa = + ZEND_TYPE_HAS_NAMED_WITH_ARGS(copy->args_box) + ? ZEND_TYPE_NAMED_WITH_ARGS(copy->args_box) : NULL; + uint32_t passed = new_nwa ? new_nwa->count : 0; + + for (uint32_t i = 0; i < src->count; i++) { + if (i >= passed && (src->entries[i].name || src->entries[i].type_ref)) { + zend_type_arg_table_destroy(src); + copy->concrete_table = NULL; + return; + } + } + + for (uint32_t i = 0; i < src->count; i++) { + if (src->entries[i].name) { + zend_accel_store_interned_string(src->entries[i].name); + } + if (ZEND_TYPE_IS_SET(src->entries[i].owned_type)) { + zend_persist_type(&src->entries[i].owned_type); + } + src->entries[i].type_ref = + (new_nwa && src->entries[i].name && i < new_nwa->count) + ? &new_nwa->args[i] : NULL; + } + + zend_type_arg_table *persisted = zend_shared_memdup_put_free( + src, ZEND_TYPE_ARG_TABLE_SIZE(src->count)); + persisted->persisted = true; + copy->concrete_table = persisted; +} + /* Persist the turbofish_args HT. Each entry is a zend_turbofish_args_entry * which now stores only args_box — the per-call-site runtime cache that * pairs with it lives in the caller op_array's runtime cache slot, not @@ -501,6 +542,7 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR_P(v); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); + zend_persist_concrete_call_table(copy); Z_PTR_P(v) = copy; } ZEND_HASH_FOREACH_END(); } else { @@ -512,6 +554,7 @@ static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) zend_turbofish_args_entry *entry = Z_PTR(p->val); zend_turbofish_args_entry *copy = zend_shared_memdup_put_free(entry, sizeof(*entry)); zend_persist_type(©->args_box); + zend_persist_concrete_call_table(copy); Z_PTR(p->val) = copy; } ZEND_HASH_FOREACH_END(); } @@ -617,6 +660,19 @@ static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_typ persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); } + /* 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) { + uint32_t cnt = zend_count_generic_value_checks(persisted->parameters); + size_t sz = offsetof(zend_generic_value_check_plan, checks) + + cnt * sizeof(zend_generic_value_check); + zend_generic_value_check_plan *tmp = emalloc(sz); + zend_fill_generic_value_check_plan(tmp, persisted->parameters); + persisted->value_check_plan = zend_shared_memdup_free(tmp, sz); + } else { + persisted->value_check_plan = NULL; + } + return persisted; } diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 7a099207ea8b..be1deda11ca7 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -290,6 +290,34 @@ static void zend_persist_generic_type_table_ht_calc(HashTable *ht) ADD_SIZE(sizeof(HashTable)); } +/* Mirror zend_persist_concrete_call_table's SHM reservation; the eligibility + * predicate must stay byte-identical to persist or SHM is corrupted. */ +static void zend_persist_concrete_call_table_calc(zend_turbofish_args_entry *entry) +{ + zend_type_arg_table *src = entry->concrete_table; + if (!src) { + return; + } + const zend_type_named_with_args *nwa = + ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box) + ? ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box) : NULL; + uint32_t passed = nwa ? nwa->count : 0; + for (uint32_t i = 0; i < src->count; i++) { + if (i >= passed && (src->entries[i].name || src->entries[i].type_ref)) { + return; /* NULL-fallback: persist reserves nothing for this site. */ + } + } + ADD_SIZE(ZEND_TYPE_ARG_TABLE_SIZE(src->count)); + for (uint32_t i = 0; i < src->count; i++) { + if (src->entries[i].name) { + ADD_INTERNED_STRING(src->entries[i].name); + } + if (ZEND_TYPE_IS_SET(src->entries[i].owned_type)) { + zend_persist_type_calc(&src->entries[i].owned_type); + } + } +} + /* The turbofish_args HT entries are zend_turbofish_args_entry — same as a * boxed type for size accounting purposes, but the entry struct itself is * what we shared-memdup, not a bare zend_type. */ @@ -300,7 +328,9 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) zval *v; ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { ADD_SIZE(sizeof(zend_turbofish_args_entry)); - zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR_P(v))->args_box); + zend_turbofish_args_entry *e = (zend_turbofish_args_entry *) Z_PTR_P(v); + zend_persist_type_calc(&e->args_box); + zend_persist_concrete_call_table_calc(e); } ZEND_HASH_FOREACH_END(); } else { Bucket *p; @@ -309,7 +339,9 @@ static void zend_persist_turbofish_args_ht_calc(HashTable *ht) ADD_INTERNED_STRING(p->key); } ADD_SIZE(sizeof(zend_turbofish_args_entry)); - zend_persist_type_calc(&((zend_turbofish_args_entry *) Z_PTR(p->val))->args_box); + zend_turbofish_args_entry *e = (zend_turbofish_args_entry *) Z_PTR(p->val); + zend_persist_type_calc(&e->args_box); + zend_persist_concrete_call_table_calc(e); } ZEND_HASH_FOREACH_END(); } ADD_SIZE(sizeof(HashTable)); @@ -355,6 +387,13 @@ static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) if (table->turbofish_args) { zend_persist_turbofish_args_ht_calc(table->turbofish_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); + ADD_SIZE(offsetof(zend_generic_value_check_plan, checks) + + cnt * sizeof(zend_generic_value_check)); + } } static void zend_persist_type_arg_table_calc(zend_type_arg_table *table) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 67b91295e5ab..9c4d82affb43 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -9053,6 +9053,9 @@ static void reflection_collect_interface_bindings( if (!intermediate || intermediate == ancestor) { continue; } + if (zend_class_is_monomorph(intermediate)) { + continue; + } if (generic_implements && zend_hash_index_exists(generic_implements, i - parent_count)) { continue; diff --git a/ext/reflection/tests/generics/named_type_get_name_erased.phpt b/ext/reflection/tests/generics/named_type_get_name_erased.phpt index cd89077b1547..91a12296d97c 100644 --- a/ext/reflection/tests/generics/named_type_get_name_erased.phpt +++ b/ext/reflection/tests/generics/named_type_get_name_erased.phpt @@ -1,5 +1,5 @@ --TEST-- -Reflection: ReflectionNamedType::getName() returns the erased name +Reflection: ReflectionNamedType::getName() returns the reified name with type arguments --FILE-- {} @@ -9,5 +9,5 @@ echo $r->getParameters()[0]->getType()->getName(), "\n"; echo $r->getReturnType()->getName(), "\n"; ?> --EXPECT-- -Box -Box +Box +Box diff --git a/ext/reflection/tests/generics/nested_args.phpt b/ext/reflection/tests/generics/nested_args.phpt index ad7c352a46bd..ba10c52ccfb1 100644 --- a/ext/reflection/tests/generics/nested_args.phpt +++ b/ext/reflection/tests/generics/nested_args.phpt @@ -12,6 +12,6 @@ $inmost = $inner->getGenericArguments()[0]; echo $inmost->getName(), "\n"; ?> --EXPECT-- -Box +Box> Box int diff --git a/ext/reflection/tests/generics/property_type_args.phpt b/ext/reflection/tests/generics/property_type_args.phpt index 786200f43bfa..d201813592c9 100644 --- a/ext/reflection/tests/generics/property_type_args.phpt +++ b/ext/reflection/tests/generics/property_type_args.phpt @@ -12,6 +12,6 @@ echo count($rt->getGenericArguments()), "\n"; echo $rt->getGenericArguments()[0]->getName(), "\n"; ?> --EXPECT-- -Box +Box 1 int