diff --git a/NEWS b/NEWS index 2bbe43032ece..dc887cba05cf 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,14 @@ PHP NEWS ?? ??? ????, PHP 8.6.0alpha1 - Core: + . Added generics: type parameters on classes, interfaces, traits, functions, + methods, closures, and arrow functions, with optional bounds, defaults, + and variance markers; turbofish syntax (`f::()`) at call sites; and + type arguments on named types (`Box`, `array`, `iterable`, + `self`, `static`, `parent`). Type parameters erase to their bound + at runtime; type arguments are discarded. Pre-erasure metadata is preserved + for Reflection so static-analysis tools can consume generics without + re-parsing source. (azjezz) . Added first-class callable cache to share instances for the duration of the request. (ilutov) . It is now possible to use reference assign on WeakMap without the key diff --git a/UPGRADING b/UPGRADING index bdadc6efbefc..77ff2d445804 100644 --- a/UPGRADING +++ b/UPGRADING @@ -177,6 +177,45 @@ PHP 8.6 UPGRADE NOTES ======================================== - Core: + . Added support for runtime-bound-checked generics. Classes, interfaces, + traits, functions, methods, closures, and arrow functions can now declare + type parameters with optional bounds (`T : Foo`), defaults (`T = int`), + and variance markers (`+T`, `-T`): + + class Box { + public T $value; + public function get(): T { return $this->value; } + } + + function id(T $x): T { return $x; } + + Call sites accept turbofish type arguments (`Box::::new()`, + `id::(7)`); use sites accept type arguments on named types + (`Box`, `array`, `iterable`, `self`, + `static`, `parent`). Recursive bounds (`T : Comparable`) + are supported. Anonymous classes cannot declare type parameters. + + Generics are reified along two axes. Class-like generics are + monomorphised: each distinct argument tuple (`Box`, + `Box`) synthesises a separate class entry whose canonical + name is spelled out (`"Box"`), registered in the class table, + and visible through `get_class()`, `var_dump`, and + `instanceof Box`. Function- and method-level generics are + reified per call frame: each call carries its own type-argument + table, with no persistent `id` function entry (avoiding + unbounded growth). Closures and generators preserve the table so + it survives suspension. + + Ordinary parameter, return, and property type-checks run against + each parameter's declared *bound* (or `mixed` when unbounded, or + when the bound is invalid in the target position - e.g. `callable` + on a property). The bound is the answer to "what can a caller + actually pass at this slot." Reified type arguments are kept on a + side table and consulted at well-defined runtime points: monomorph + synthesis, turbofish argument validation, deferred lookups for + `instanceof Box` / `catch (Box $e)`, bare `T`-ref resolution + (`instanceof T`, `catch (T $e)`, `new T()`, `T::method()`), + inheritance linking, and Reflection. . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. . It is now possible to define the `__debugInfo()` magic method on enums. @@ -315,6 +354,27 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/isreadable-iswriteable . Added ReflectionParameter::getDocComment(). RFC: https://wiki.php.net/rfc/parameter-doccomments + . Added ReflectionFunctionAbstract::isGeneric() and + ReflectionFunctionAbstract::getGenericParameters() (covers + ReflectionFunction, ReflectionMethod, closures, and arrow functions). + . Added ReflectionClass::isGeneric() and + ReflectionClass::getGenericParameters(). + . Added ReflectionClass::getGenericArgumentsForParentClass(), + ReflectionClass::getGenericArgumentsForParentInterface(string $name), + and ReflectionClass::getGenericArgumentsForUsedTrait(string $name) for + inspecting the type arguments supplied at a class's own extends / + implements / use sites. Returns null when no type arguments were + specified for that ancestor at this class's clause site (consumers + enumerate ancestors via the existing getParentClass() / getInterfaces() + / getTraits() APIs). + . Added ReflectionNamedType::hasGenericArguments() and + ReflectionNamedType::getGenericArguments(). The arguments are returned + as ReflectionType instances in source order (reified form); + ReflectionNamedType::getName() returns the bound's name - i.e. the + type a caller can actually pass at the slot - so it stays useful for + runtime checks. Use getGenericArguments() to recover the reified + shape, and look for ReflectionTypeParameterReference inside the + returned list when a slot is itself a type-parameter reference. - Intl: . `grapheme_strrev()` returns strrev for grapheme cluster unit. @@ -346,6 +406,15 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Openssl\Psk +- Reflection: + . ReflectionGenericTypeParameter (final, instances obtained via + ReflectionClass::getGenericParameters() and + ReflectionFunctionAbstract::getGenericParameters()). + . ReflectionTypeParameterReference (extends ReflectionType, appears only + inside reified type expressions: bounds, defaults, and the elements of + ReflectionNamedType::getGenericArguments()). + . enum ReflectionGenericVariance { Invariant; Covariant; Contravariant }. + - Standard: . enum SortDirection RFC: https://wiki.php.net/rfc/sort_direction_enum diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 1f24fb82fcf9..3fcbd2cdbc3f 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -117,6 +117,137 @@ PHP 8.6 INTERNALS UPGRADE NOTES . Added ZEND_CONTAINER_OF(). . The OPENBASEDIR_CHECKPATH() compatibility macro has been removed, instead use php_check_open_basedir() directly. + . Added support for reified generic type parameters. The main + additions: + . New types in zend_compile.h: zend_generic_parameter, + zend_generic_parameter_list, zend_generic_type_table, and + zend_generic_scope_entry. Allocate / destroy via + zend_generic_parameter_list_alloc(), + zend_generic_parameter_list_destroy(), + zend_generic_type_table_alloc(), and + zend_generic_type_table_destroy(). + . zend_op_array and zend_class_entry both gained an optional + `generic_parameters` (declared parameter list) and an optional + `generic_types` side table holding the reified forms of return + types, parameter types, property types, class-constant types, + the extends type, implements list, and trait-use list. The + runtime arg_info / property / class-constant slots continue to + hold the bound view (the parameter's declared bound, with T-refs + resolved); the reified side table is consulted at monomorph + synthesis, ZEND_VERIFY_GENERIC_ARGUMENTS, the new deferred + class-fetch path, inheritance linking, and Reflection. + . New AST kinds: ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, + ZEND_AST_GENERIC_TYPE_PARAMETER, ZEND_AST_GENERIC_NAMED_TYPE, + ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, ZEND_AST_TURBOFISH. + . zend_ast_decl::child[] grew from 5 to 6 entries; the new slot + carries an optional generic-parameter-list AST. + . The child-count groups of ZEND_AST_CALL, ZEND_AST_NEW, + ZEND_AST_METHOD_CALL, ZEND_AST_NULLSAFE_METHOD_CALL, and + ZEND_AST_STATIC_CALL each gained one optional child holding the + call-site turbofish type-argument list. Code that walks these + nodes by hard-coded child count must be updated. + . zend_ast_export handles the new generic AST kinds. + . Two new bits on zend_type's type_mask: + _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) and + _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31), with payload structs + zend_type_parameter_ref { zend_string *name; uint32_t index; + uint8_t origin; } and zend_type_named_with_args { zend_string + *name; uint32_t name_attr; uint32_t count; zend_type args[]; }. + These bits only ever appear in reified forms held by the side + table; runtime arg_info / property / class-constant types never + carry them. Helpers: ZEND_TYPE_HAS_TYPE_PARAMETER(), + ZEND_TYPE_TYPE_PARAMETER(), ZEND_TYPE_HAS_NAMED_WITH_ARGS(), + ZEND_TYPE_NAMED_WITH_ARGS(). + . Class-level reification (monomorphs). Each distinct argument + tuple at a generic class, interface, or trait synthesises a + separate zend_class_entry registered in EG(class_table) under a + canonical name ("Box", "Map>"). The + canonical encoding uses `<...>` in the class name, which is + otherwise invalid in user-declared classes; the bit is detected + with zend_class_name_is_monomorph() / zend_class_is_monomorph(). + New API: + - zend_synthesize_monomorph(base, args, arity): synthesise (or + return the cached) class entry; idempotent on canonical + tuples. + - zend_get_defaults_monomorph(base): the monomorph built from + the parameters' declared defaults; NULL when any parameter + has no default. Used by ZEND_NEW for `new static()` and + dynamic `new $name()`. + - zend_try_synthesize_monomorph_by_name(name, flags): parses a + canonical-shaped name and synthesises on demand. Hooked into + zend_lookup_class_ex so unserialize, dynamic new, and + class_exists() all materialise monomorphs transparently. + - zend_type_to_canonical_string(type), + zend_generic_canonical_class_name(base_name, args, arity), + zend_type_contains_type_parameter(type) — the canonical-name + builder is permutation-stable on union/intersection order so + equivalent type-arg lists hash to the same entry. + - zend_generic_get_or_create_class_table(ce) — exposed so + extensions can populate the side table during synthesis. + - ZEND_ACC_GENERIC_ALL_DEFAULTS (1u << 31) on ce_flags — hot + path bit for ZEND_NEW to skip the per-parameter default + scan. + . Function- and method-level reification (per call frame). Each + generic call installs a fresh table mapping parameter index to + bound class name: + typedef struct _zend_type_arg_table { + uint32_t count; + zend_string *names[1]; + } zend_type_arg_table; + Allocate / destroy via zend_type_arg_table_alloc(count) and + zend_type_arg_table_destroy(t). zend_execute_data gained a + `type_args` field initialised to NULL by + zend_vm_init_call_frame() and released by + zend_vm_stack_free_call_frame_ex(). The table survives closure + and generator suspension. There is no persistent function-level + monomorph — the binding lives on the frame. + Population API: + - zend_type_arg_canonical_name(type): canonical-name for a + single type-arg slot, including scalars (`"int"`, + `"?string"`). Returns NULL only for unset types. + - zend_build_generic_call_type_args(call, args_box): builds + the table from a turbofish NAMED_WITH_ARGS carrier (or from + declared defaults for unsupplied slots). + - zend_verify_generic_arg_types(call, args_box): validates + each arg against the corresponding parameter's bound. + - zend_resolve_generic_type_param(index, fetch_type): runtime + T-ref resolver. Walks EX(type_args) for function-level T + and the called scope -> lexical scope chain for class-level + T (the binding lives on the monomorph that is the direct + child of the lexical scope on cur->generic_type_args). + zend_generic_parameter_list gained an `inferable_mask` (bit i + set when some value-parameter's reified type is exactly param i + at a top-level position) so inference can short-circuit. + . Three new class-fetch sub-types on the class-fetch enum, + packed into op.num via zend_pack_*_fetch helpers: + - ZEND_FETCH_CLASS_TYPE_PARAM (7) — function/method- + level bare T-ref (`instanceof T`, `catch (T $e)`, + `new T()`, `T::method()`). + - ZEND_FETCH_CLASS_TYPE_PARAM_CLASS (8) — class-level bare + T-ref; resolves via the called-scope walk. + - ZEND_FETCH_CLASS_GENERIC_DEFERRED (9) — generic named type + whose args contain T-refs (`instanceof Box`, + `catch (Box $e)`). The args_id sits in the high bits of + op.num and points into the op_array's + generic_types->turbofish_args side table. + All three are detected with zend_fetch_is_type_param() (the + first two) or by the sub-type directly; packed with + zend_pack_type_param_fetch(idx, flags, class_level) and + zend_pack_generic_deferred_fetch(args_id, flags); unpacked with + zend_unpack_type_param_index(fetch_type). Routed through + zend_fetch_class, which dispatches to + zend_resolve_generic_type_param() or + zend_resolve_deferred_generic_class(). + . New compiler-globals fields: CG(type_arg_depth) (right-angle + split state used by the zendlex wrapper), CG(token_residual) + (single-token pushback slot), and CG(generic_scope) (linked + stack of in-scope type parameters). + . New T_TURBOFISH lexer token (literal `::<`). The zendlex wrapper + splits T_SR (`>>`), T_IS_GREATER_OR_EQUAL (`>=`), and T_SR_EQUAL + (`>>=`) into separate `>` tokens whenever CG(type_arg_depth) is + non-zero, with a single-token pushback slot. + . Module API bumped to 20260506; extension API bumped to + 420260506. All extensions must be recompiled. ======================== 2. Build system changes diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index cf74dd8fc147..49bcf7b13196 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -674,6 +674,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx cache_size += sizeof(void *); class_slot[opline->op2.constant] = opline->extended_value; } + } else if (opline->opcode == ZEND_INSTANCEOF + && opline->op2_type == IS_UNUSED) { + /* instanceof T / instanceof Box: 3-slot PIC keyed on + * (type_args generation, called scope, resolved ce). */ + opline->extended_value = cache_size; + cache_size += 3 * sizeof(void *); } break; case ZEND_NEW: @@ -688,6 +694,18 @@ 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) + * 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 += 5 * sizeof(void *); + } + break; case ZEND_CATCH: if (opline->op1_type == IS_CONST) { // op1 class @@ -698,6 +716,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx cache_size += sizeof(void *); class_slot[opline->op1.constant] = opline->extended_value & ~ZEND_LAST_CATCH; } + } else { + ZEND_ASSERT(opline->op1_type == IS_UNUSED); + /* T-ref / deferred-turbofish catch: 3-slot PIC keyed on + * (type_args generation, called scope, resolved ce). */ + opline->extended_value = cache_size | (opline->extended_value & ZEND_LAST_CATCH); + cache_size += 3 * sizeof(void *); } break; case ZEND_BIND_GLOBAL: diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 77dc322fbdec..fde6642af247 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -27,6 +27,7 @@ #include "zend_call_graph.h" #include "zend_inference.h" #include "zend_dump.h" +#include "zend_inheritance.h" #ifndef ZEND_DEBUG_DFA # define ZEND_DEBUG_DFA ZEND_DEBUG @@ -297,6 +298,13 @@ static inline bool can_elide_return_type_check( if (use_type & MAY_BE_REF) { return false; } + /* When the declared return type contains a generic parameter, the + * compile-time arg_info is the erased type (typically mixed) but the + * effective type on a monomorph is the substituted one — eliding the + * VERIFY_RETURN_TYPE here would skip the per-monomorph runtime check. */ + if (op_array->generic_types && op_array->generic_types->return_type) { + return false; + } if (use_type & MAY_BE_UNDEF) { use_type &= ~MAY_BE_UNDEF; @@ -396,6 +404,601 @@ static bool variable_defined_or_used_in_range(zend_ssa *ssa, int var, int start, return false; } +/* SSA arg types reflect the call-site value, not the declared type, so inferring T from them is sound. */ +static bool zend_dfa_send_concrete_type( + const zend_op_array *op_array, const zend_ssa *ssa, + const zend_op *send, zend_type *out) +{ + /* By-ref / unusual sends: the callee could observe a reference; skip. */ + if (send->opcode == ZEND_SEND_REF + || send->opcode == ZEND_SEND_VAR_NO_REF + || send->opcode == ZEND_SEND_VAR_NO_REF_EX + || send->opcode == ZEND_SEND_USER) { + return false; + } + + if (send->op1_type == IS_CONST) { + const zval *zv = CT_CONSTANT_EX(op_array, send->op1.constant); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } + } + + int var = ssa->ops[send - op_array->opcodes].op1_use; + if (var < 0) { + return false; + } + const zend_ssa_var_info *info = &ssa->var_info[var]; + if (info->type & (MAY_BE_UNDEF | MAY_BE_REF)) { + return false; + } + uint32_t pure = info->type & MAY_BE_ANY; + switch (pure) { + case MAY_BE_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case MAY_BE_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case MAY_BE_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + } + /* Exact class only: !is_instanceof matches what runtime inference binds T to. */ + if (pure == MAY_BE_OBJECT && info->ce && !info->is_instanceof) { + *out = (zend_type) ZEND_TYPE_INIT_CLASS(zend_string_copy(info->ce->name), 0, 0); + return true; + } + return false; +} + +static zend_op *zend_dfa_find_call_verify(zend_op_array *op_array, const zend_call_info *call_info) +{ + if (!call_info->caller_call_opline || !call_info->caller_init_opline) { + return NULL; + } + zend_op *p = call_info->caller_call_opline; + while (p > call_info->caller_init_opline) { + p--; + if (p->opcode == ZEND_NOP || p->opcode == ZEND_EXT_NOP + || p->opcode == ZEND_EXT_FCALL_BEGIN) { + continue; + } + /* VERIFY extended_value 0 = speculative non-turbofish site, != 0 = turbofish; INSTALL always turbofish. */ + if ((p->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS + || p->opcode == ZEND_INSTALL_GENERIC_ARGS) + && p->op1_type == IS_UNUSED) { + return p; + } + return NULL; + } + return NULL; +} + +static bool zend_dfa_try_direct_dispatch(zend_op_array *op_array, + const zend_call_info *ci, zend_op *site, const zend_function *fbc) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args) { + return false; + } + zend_op *init = ci->caller_init_opline; + if (!init || (init->opcode != ZEND_INIT_FCALL + && init->opcode != ZEND_INIT_FCALL_BY_NAME + && init->opcode != ZEND_INIT_NS_FCALL_BY_NAME)) { + return false; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, site->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + return false; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) { + return false; + } + } + + zend_string *mangled = zend_generic_canonical_class_name( + fbc->common.function_name, nwa->args, nwa->count); + zend_string *lc = zend_string_tolower(mangled); + zend_string_hash_val(mangled); + zend_string_hash_val(lc); + zval zv; + ZVAL_STR(&zv, mangled); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* orig name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc name at lit+1 */ + + init->opcode = ZEND_INIT_FCALL_BY_NAME; + init->op1_type = IS_UNUSED; + init->op1.num = 0; + init->op2_type = IS_CONST; + init->op2.constant = lit; + /* result.num keeps the existing 1-slot function cache. */ + MAKE_NOP(site); + return true; +} + +/* On a final class the called scope is always one of its own monomorphs, so static ≡ the own-params turbofish. */ +static uint32_t zend_dfa_selfize_generic_new(zend_op_array *op_array) +{ + zend_class_entry *scope = op_array->scope; + if (!scope || !scope->generic_parameters + || !(scope->ce_flags & ZEND_ACC_FINAL) + || !op_array->generic_types || !op_array->generic_types->turbofish_args) { + return 0; + } + uint32_t pcount = scope->generic_parameters->count; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; + } + zend_string *cname = Z_STR(op_array->literals[newop->op1.constant]); + if (!zend_string_equals_ci(cname, scope->name)) { + continue; + } + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count != pcount) { + continue; + } + bool identity = true; + for (uint32_t a = 0; a < nwa->count; a++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(nwa->args[a]) + || (ZEND_TYPE_FULL_MASK(nwa->args[a]) & MAY_BE_NULL)) { + identity = false; + break; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(nwa->args[a]); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE || ref->index != a) { + identity = false; + break; + } + } + if (!identity) { + continue; + } + newop->op1_type = IS_UNUSED; + newop->op1.num = ZEND_FETCH_CLASS_STATIC | ZEND_FETCH_CLASS_EXCEPTION; + newop->op2_type = IS_UNUSED; + newop->op2.num = 0; + MAKE_NOP(verify); + changed++; + } + return changed; +} + +static uint32_t zend_dfa_optimize_generic_calls(zend_op_array *op_array, zend_ssa *ssa) +{ + const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); + uint32_t changed = 0; + + if (!func_info || !func_info->callee_info) { + return 0; + } + + for (const zend_call_info *ci = func_info->callee_info; ci; ci = ci->next_callee) { + const zend_function *fbc = ci->callee_func; + if (!fbc || ci->is_prototype || ci->named_args || ci->send_unpack) { + continue; + } + if (!ZEND_USER_CODE(fbc->common.type)) { + continue; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0) { + continue; + } + + zend_op *verify = zend_dfa_find_call_verify(op_array, ci); + if (!verify) { + continue; + } + + /* Turbofish site needs no value inference, so runs even for callees with no inferable params. */ + if (verify->extended_value != 0) { + if (zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc)) { + changed++; + continue; + } + uint8_t opcode = zend_generic_try_install_resolved_turbofish( + op_array, fbc, verify->extended_value, verify->op2.num); + if (opcode != ZEND_NOP) { + verify->opcode = opcode; + changed++; + } + continue; + } + + if (params->inferable_mask == 0 + || !fbc->op_array.generic_types || !fbc->op_array.generic_types->parameters) { + continue; + } + + uint32_t total = params->count; + uint32_t required = 0; + while (required < total + && !ZEND_TYPE_IS_SET(params->parameters[required].default_type)) { + required++; + } + if (total > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + + /* Bare top-level T only, matching zend_build_generic_call_type_args. */ + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + HashTable *pre = fbc->op_array.generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe)) continue; + if (ZEND_TYPE_FULL_MASK(*pe) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < ci->num_args) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0 || !ci->arg_info[pos].opline) { + break; + } + zend_type entry; + if (!zend_dfa_send_concrete_type(op_array, ssa, ci->arg_info[pos].opline, &entry)) { + break; + } + inferred[arity++] = entry; + } + /* arity 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE; stay generic. */ + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + continue; + } + + uint32_t args_id = 0; + uint8_t opcode = zend_generic_install_inferred_call( + op_array, fbc, inferred, arity, &args_id); + if (opcode == ZEND_NOP) { + continue; + } + verify->opcode = opcode; + verify->op2.num = arity; + verify->extended_value = args_id; + zend_dfa_try_direct_dispatch(op_array, ci, verify, fbc); + changed++; + } + + return changed; +} + +static uint32_t zend_aot_rewrite_new_sites(zend_op_array *op_array) +{ + if (!op_array->generic_types || !op_array->generic_types->turbofish_args + || !op_array->generic_types->monomorph_type_args) { + return 0; + } + const zend_type_arg_table *binds = op_array->generic_types->monomorph_type_args; + if (binds->count > ZEND_GENERIC_MAX_PARAMS) { + return 0; + } + zend_type bindv[ZEND_GENERIC_MAX_PARAMS]; + uint32_t bindc = binds->count; + for (uint32_t i = 0; i < bindc; i++) { + const zend_type *t = zend_type_arg_entry_type(&binds->entries[i]); + bindv[i] = t ? *t : (zend_type) ZEND_TYPE_INIT_NONE(0); + } + + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *verify = &op_array->opcodes[i]; + if (verify->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS + || verify->op1_type == IS_UNUSED + || verify->extended_value == 0) { + continue; + } + zend_op *newop = NULL; + for (uint32_t j = i; j > 0; j--) { + zend_op *p = &op_array->opcodes[j - 1]; + if (p->opcode == ZEND_NEW + && p->result_type == IS_TMP_VAR + && p->result.var == verify->op1.var) { + newop = p; + break; + } + } + if (!newop || newop->op1_type != IS_CONST) { + continue; /* self/static/dynamic base: leave the runtime VERIFY. */ + } + + zend_turbofish_args_entry *entry = zend_hash_index_find_ptr( + op_array->generic_types->turbofish_args, verify->extended_value); + if (!entry || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(entry->args_box)) { + continue; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(entry->args_box); + if (nwa->count == 0 || nwa->count > ZEND_GENERIC_MAX_PARAMS) { + continue; + } + zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + for (uint32_t a = 0; a < nwa->count; a++) { + zend_type r = bindc + ? zend_substitute_function_type_param(nwa->args[a], bindv, bindc) + : nwa->args[a]; + if (zend_type_contains_type_parameter(r)) { + ok = false; + break; + } + resolved[a] = r; + } + if (!ok) { + continue; + } + + zend_string *base_name = Z_STR(op_array->literals[newop->op1.constant]); + zend_string *canonical = zend_generic_canonical_class_name( + base_name, resolved, nwa->count); + zend_string *lc = zend_string_tolower(canonical); + zend_string_hash_val(canonical); + zend_string_hash_val(lc); + + zval zv; + ZVAL_STR(&zv, canonical); + uint32_t lit = zend_optimizer_add_literal(op_array, &zv); /* full name */ + ZVAL_STR(&zv, lc); + zend_optimizer_add_literal(op_array, &zv); /* lc at lit+1 */ + + newop->op1.constant = lit; /* keep op2.num cache slot (caches mono ce) */ + MAKE_NOP(verify); + changed++; + } + return changed; +} + +/* Per-monomorph ONCE: re-running zend_optimize_script would corrupt SSA. */ +static void zend_aot_optimize_monomorph(zend_op_array *op_array, zend_script *script, zend_long opt_level) +{ + if (op_array->last_try_catch) { + /* dfa bails on try/catch; do the SSA-free class-`new` rewrite, leave the rest generic. */ + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_redo_pass_two(op_array); + return; + } + zend_optimizer_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.arena = zend_arena_create(64 * 1024); + ctx.script = script; + ctx.optimization_level = opt_level; + + zend_func_info *func_info = zend_arena_calloc(&ctx.arena, 1, sizeof(zend_func_info)); + ZEND_SET_FUNC_INFO(op_array, func_info); + + zend_revert_pass_two(op_array); + zend_aot_rewrite_new_sites(op_array); + zend_analyze_calls(&ctx.arena, script, 0, op_array, func_info); + func_info->call_map = zend_build_call_map(&ctx.arena, func_info, op_array); + if (zend_dfa_analyze_op_array(op_array, &ctx, &func_info->ssa) == SUCCESS) { + zend_dfa_optimize_op_array(op_array, &ctx, &func_info->ssa, func_info->call_map); + } + zend_redo_pass_two(op_array); + + ZEND_SET_FUNC_INFO(op_array, NULL); + zend_arena_destroy(ctx.arena); +} + +/* Returns the case-preserved (+0) name, not the lc one: arg_info needs the original case. */ +static zend_string *zend_aot_mangled_call_name(const zend_op_array *op_array, const zend_op *op) +{ + if (op->op2_type != IS_CONST) { + return NULL; + } + zend_string *display; + if (op->opcode == ZEND_INIT_FCALL_BY_NAME || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { + display = Z_STR_P(RT_CONSTANT(op, op->op2)); + } else { + return NULL; + } + return memchr(ZSTR_VAL(display), '<', ZSTR_LEN(display)) ? display : NULL; +} + +/* Synthesizes each direct-dispatch callee; its own calls become the next round's work (fixpoint). */ +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level) +{ + /* Collect names first: synthesis rehashes function_table mid-iteration. */ + zend_string *wanted[4096]; + uint32_t want_count = 0; + zend_op_array *op_array; + /* wanted[] holds display names; table is keyed by lc, so lowercase only for the existence check. */ + #define ZEND_AOT_SCAN_OPS(oa) do { \ + zend_op_array *zoa = (oa); \ + if (zoa->type == ZEND_USER_FUNCTION && zoa->opcodes) { \ + for (uint32_t _i = 0; _i < zoa->last && want_count < (sizeof(wanted)/sizeof(wanted[0])); _i++) { \ + zend_string *_disp = zend_aot_mangled_call_name(zoa, &zoa->opcodes[_i]); \ + if (_disp) { \ + zend_string *_lc = zend_string_tolower(_disp); \ + if (!zend_hash_exists(&script->function_table, _lc)) { \ + wanted[want_count++] = _disp; \ + } \ + zend_string_release(_lc); \ + } \ + } \ + } \ + } while (0) + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + ZEND_AOT_SCAN_OPS(op_array); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + ZEND_AOT_SCAN_OPS(&m->op_array); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + #undef ZEND_AOT_SCAN_OPS + + zend_op_array *new_monos[4096]; + uint32_t new_count = 0; + for (uint32_t i = 0; i < want_count; i++) { + zend_string *lc = zend_string_tolower(wanted[i]); + bool exists = zend_hash_exists(&script->function_table, lc); + zend_string_release(lc); + if (exists) { + continue; /* synthesized as a duplicate request earlier this round. */ + } + zend_function *mono = zend_synthesize_specialized_monomorph_by_name( + &script->function_table, wanted[i]); + if (mono && new_count < (sizeof(new_monos)/sizeof(new_monos[0]))) { + new_monos[new_count++] = &mono->op_array; + } + } + + for (uint32_t i = 0; i < new_count; i++) { + zend_aot_optimize_monomorph(new_monos[i], script, opt_level); + } + return new_count; +} + +/* Upgrade INIT_FCALL_BY_NAME -> INIT_FCALL (+ DO_UCALL): safe since AOT persists the callee. */ +static zend_always_inline bool zend_aot_is_call_open(uint8_t opcode) +{ + switch (opcode) { + case ZEND_INIT_FCALL: + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + return true; + default: + return false; + } +} + +static uint32_t zend_aot_upgrade_op_array_to_ucall(zend_op_array *op_array, zend_script *script) +{ + if (!op_array->opcodes) { + return 0; + } + /* Nesting stack matches each call-open to its closing DO_FCALL*; fbc resolved at INIT for the SENDs. */ + struct { uint32_t init_i; zend_function *fbc; } stack[512]; + uint32_t sp = 0; + uint32_t changed = 0; + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *op = &op_array->opcodes[i]; + if (zend_aot_is_call_open(op->opcode)) { + if (sp >= sizeof(stack)/sizeof(stack[0])) { + return changed; /* pathological nesting — stop, stay correct. */ + } + /* Both by-name forms keep the registered lc name at the +1 literal. */ + zend_function *fbc = NULL; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->op2_type == IS_CONST) { + zend_string *lc = Z_STR_P(RT_CONSTANT(op, op->op2) + 1); + if (memchr(ZSTR_VAL(lc), '<', ZSTR_LEN(lc))) { + zend_function *f = zend_hash_find_ptr(&script->function_table, lc); + if (f && f->type == ZEND_USER_FUNCTION) { + fbc = f; + } + } + } + stack[sp].init_i = i; + stack[sp].fbc = fbc; + sp++; + continue; + } + if (op->opcode == ZEND_CALLABLE_CONVERT) { + if (sp > 0) sp--; /* first-class callable closes its frame, no DO. */ + continue; + } + if (sp > 0 && stack[sp - 1].fbc + && op->opcode == ZEND_SEND_VAR_EX + && op->op2_type != IS_CONST + && !ARG_SHOULD_BE_SENT_BY_REF(stack[sp - 1].fbc, op->op2.num)) { + op->opcode = ZEND_SEND_VAR; + zend_vm_set_opcode_handler(op); + } + bool is_do = op->opcode == ZEND_DO_FCALL || op->opcode == ZEND_DO_FCALL_BY_NAME + || op->opcode == ZEND_DO_ICALL || op->opcode == ZEND_DO_UCALL; + if (!is_do || sp == 0) { + continue; + } + zend_function *fbc = stack[sp - 1].fbc; + zend_op *init = &op_array->opcodes[stack[sp - 1].init_i]; + sp--; + if (!fbc) { + continue; /* not a resolved monomorph dispatch site. */ + } + init->opcode = ZEND_INIT_FCALL; + init->op1_type = IS_UNUSED; + init->op1.num = zend_vm_calc_used_stack(init->extended_value, fbc); + /* INIT_FCALL reads op2 directly: advance one zval to the +1 lc literal it resolves. */ + init->op2.constant += sizeof(zval); + zend_vm_set_opcode_handler(init); + + /* Skip deprecated/nodiscard (as zend_get_call_op does) so warnings still fire. */ + if (op->opcode == ZEND_DO_FCALL_BY_NAME + && !(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED | ZEND_ACC_NODISCARD))) { + op->opcode = ZEND_DO_UCALL; + zend_vm_set_opcode_handler(op); + } + changed++; + } + return changed; +} + +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script) +{ + uint32_t changed = 0; + zend_op_array *op_array; + ZEND_HASH_MAP_FOREACH_PTR(&script->function_table, op_array) { + changed += zend_aot_upgrade_op_array_to_ucall(op_array, script); + } ZEND_HASH_FOREACH_END(); + zend_class_entry *ce; + ZEND_HASH_MAP_FOREACH_PTR(&script->class_table, ce) { + zend_function *m; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, m) { + if (m->type == ZEND_USER_FUNCTION) { + changed += zend_aot_upgrade_op_array_to_ucall(&m->op_array, script); + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + return changed; +} + static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa) { const zend_func_info *func_info = ZEND_FUNC_INFO(op_array); @@ -1063,6 +1666,10 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx if (zend_dfa_optimize_calls(op_array, ssa)) { remove_nops = 1; } + zend_dfa_optimize_generic_calls(op_array, ssa); + if (zend_dfa_selfize_generic_new(op_array)) { + remove_nops = 1; + } } if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_8) { zend_dump_op_array(op_array, ZEND_DUMP_SSA, "after sccp pass", ssa); diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index 69c371207ddc..9995473f48ab 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -74,6 +74,112 @@ static void zend_delete_call_instructions(const zend_op_array *op_array, zend_op } } +/* The compiler emits a speculative VERIFY_GENERIC_ARGUMENTS at call sites + * where the callee can't be resolved at compile time; the runtime handler + * short-circuits when the resolved callee turns out to be non-generic. Once + * the optimizer has resolved the callee here, that VERIFY (and any INSTALL, + * defensively) is provably dead — NOP it out so downstream passes (inline, + * DCE, JIT) don't have to model the extra opcode. + * + * For NEW the VERIFY also performs the class-level generic-arg arity/bound + * check against the *class*, not the constructor; in that case the + * constructor's func is irrelevant and we have to consult the class entry + * passed alongside it. */ +static void zend_nop_dead_generic_verify( + zend_op *do_opline, const zend_function *func, const zend_class_entry *new_ce) +{ + if (new_ce && new_ce->generic_parameters) { + return; /* `new GenericClass::<...>` VERIFY does class-arity check */ + } + if (func && ZEND_USER_CODE(func->common.type) && func->op_array.generic_parameters) { + return; /* genuinely generic — leave the verify alone */ + } + if (!func && !new_ce) return; + zend_op *opline = do_opline - 1; + int call = 0; + while (1) { + switch (opline->opcode) { + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_FCALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + /* Hitting any call-setup at call==0 means we've reached the + * matching INIT/NEW for this DO_FCALL — no further VERIFY + * to scan; bail out before walking off the opcode array. */ + if (call == 0) return; + call--; + break; + case ZEND_DO_FCALL: + case ZEND_DO_ICALL: + case ZEND_DO_UCALL: + case ZEND_DO_FCALL_BY_NAME: + call++; + break; + case ZEND_VERIFY_GENERIC_ARGUMENTS: + case ZEND_INSTALL_GENERIC_ARGS: + if (call == 0) { + /* Even when the callee is non-generic, VERIFY's arity + * check needs to fire if turbofish args were supplied + * (`f::()` on a non-generic `f` must error). The + * compiler stores the turbofish args_id in extended_value + * — non-zero means "user passed turbofish here." */ + if (opline->extended_value != 0) return; + MAKE_NOP(opline); + } + break; + } + opline--; + } +} + +/* Returns true if a VERIFY_GENERIC_ARGUMENTS sits between this call's INIT and + * DO opcodes; such a call cannot be inlined because the verify opcode reads + * EX(call), which goes away once the frame is dropped. */ +static bool zend_call_has_generic_arguments_check(zend_op *opline) +{ + int call = 0; + while (1) { + switch (opline->opcode) { + case ZEND_INIT_FCALL_BY_NAME: + case ZEND_INIT_NS_FCALL_BY_NAME: + case ZEND_INIT_STATIC_METHOD_CALL: + case ZEND_INIT_METHOD_CALL: + case ZEND_INIT_FCALL: + case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: + case ZEND_NEW: + case ZEND_INIT_DYNAMIC_CALL: + case ZEND_INIT_USER_CALL: + /* Same termination guard as zend_nop_dead_generic_verify + * — NEW also acts as the call setup for the constructor's + * DO_FCALL in `new Foo()` (no separate INIT). */ + if (call == 0) { + return false; + } + call--; + break; + case ZEND_DO_FCALL: + case ZEND_DO_ICALL: + case ZEND_DO_UCALL: + case ZEND_DO_FCALL_BY_NAME: + call++; + break; + case ZEND_VERIFY_GENERIC_ARGUMENTS: + case ZEND_INSTALL_GENERIC_ARGS: + if (call == 0) { + return true; + } + break; + } + + opline--; + } +} + static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, zend_op *opline, const zend_function *func) { const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; @@ -97,6 +203,15 @@ static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, return; } + if (zend_call_has_generic_arguments_check(opline - 1)) { + /* The verify opcode must run; inlining would orphan it. The + * scan is cheap and handles both the turbofish path (where + * generic_types->turbofish_args is populated) and the + * speculative emit for unknown-at-compile-time callees + * (which has no turbofish args at all). */ + return; + } + for (i = 0; i < num_args; i++) { /* Don't inline functions with by-reference arguments. This would require * correct handling of INDIRECT arguments. */ @@ -192,6 +307,24 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) case ZEND_DO_FCALL_BY_NAME: case ZEND_CALLABLE_CONVERT: call--; + /* INIT_DYNAMIC_CALL / INIT_USER_CALL don't get their callee + * resolved in the upper switch (no specialized INIT rewrite is + * possible), but we can still resolve for VERIFY-elision purposes + * — get_called_func now infers a callee when the CV holds a + * known constant or NEW result. Use the result for VERIFY + * elision only; don't promote the INIT/DO opcodes or attempt + * inlining, since the call site must stay dynamic-shaped. */ + if (!call_stack[call].func && call_stack[call].opline + && (call_stack[call].opline->opcode == ZEND_INIT_DYNAMIC_CALL + || call_stack[call].opline->opcode == ZEND_INIT_USER_CALL) + && opline->opcode != ZEND_CALLABLE_CONVERT) { + bool is_prototype = false; + const zend_function *func = zend_optimizer_get_called_func( + ctx->script, op_array, call_stack[call].opline, &is_prototype); + if (func) { + zend_nop_dead_generic_verify(opline, func, NULL); + } + } if (call_stack[call].func && call_stack[call].opline) { zend_op *fcall = call_stack[call].opline; @@ -227,6 +360,22 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) opline->opcode = zend_get_call_op(fcall, call_stack[call].func, !RESULT_UNUSED(opline)); } + /* Strip the speculative generic-arg verify now that we + * know the callee — needs to happen before the inline + * attempt below, which bails out when a verify still + * sits between INIT and DO. For NEW, pass the class + * entry separately so the helper can spot a generic + * class with a non-generic constructor (the VERIFY's + * arity check is against the class, not the ctor). */ + if (opline->opcode != ZEND_CALLABLE_CONVERT) { + const zend_class_entry *new_ce = NULL; + if (fcall->opcode == ZEND_NEW) { + new_ce = zend_optimizer_get_class_entry_from_op1( + ctx->script, op_array, fcall); + } + zend_nop_dead_generic_verify(opline, call_stack[call].func, new_ce); + } + if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level) && call_stack[call].try_inline && opline->opcode != ZEND_CALLABLE_CONVERT) { diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index d10b4d83fc3e..26d6f1f425bb 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -902,6 +902,87 @@ const zend_class_constant *zend_fetch_class_const_info( return const_info; } +/* Returns the unique opcode in this op_array that writes to the given CV, or + * NULL if there are zero or multiple writers, or if any operation could mutate + * the CV through a reference. This is sound enough to use as a single-value + * assumption for CV-based callsite resolution: if we conclude a CV holds X at + * the call, eliding the runtime VERIFY is safe because any other actual value + * at runtime would cause the call itself to fail before VERIFY ever ran. */ +static const zend_op *zend_find_unique_cv_writer( + const zend_op_array *op_array, uint32_t cv_var) +{ + const zend_op *end = op_array->opcodes + op_array->last; + const zend_op *unique = NULL; + + for (const zend_op *op = op_array->opcodes; op < end; op++) { + if (op->result_type == IS_CV && op->result.var == cv_var) { + if (op->opcode == ZEND_QM_ASSIGN) { + if (unique) return NULL; + unique = op; + continue; + } + return NULL; + } + + switch (op->opcode) { + case ZEND_ASSIGN: + if (op->op1_type == IS_CV && op->op1.var == cv_var) { + if (unique) return NULL; + unique = op; + } + break; + case ZEND_ASSIGN_REF: + case ZEND_BIND_GLOBAL: + case ZEND_BIND_INIT_STATIC_OR_JMP: + case ZEND_BIND_STATIC: + case ZEND_SEND_REF: + case ZEND_SEND_VAR_NO_REF: + case ZEND_SEND_VAR_NO_REF_EX: + case ZEND_SEND_USER: + case ZEND_SEND_UNPACK: + case ZEND_SEND_ARRAY: + case ZEND_FE_FETCH_RW: + case ZEND_FE_RESET_RW: + case ZEND_ASSIGN_OP: + case ZEND_ASSIGN_DIM: + case ZEND_ASSIGN_OBJ: + case ZEND_ASSIGN_DIM_OP: + case ZEND_ASSIGN_OBJ_OP: + case ZEND_ASSIGN_OBJ_REF: + case ZEND_ASSIGN_STATIC_PROP_OP: + case ZEND_PRE_INC: + case ZEND_PRE_DEC: + case ZEND_POST_INC: + case ZEND_POST_DEC: + case ZEND_UNSET_CV: + case ZEND_FETCH_W: + case ZEND_FETCH_REF: + case ZEND_FETCH_FUNC_ARG: + if (op->op1_type == IS_CV && op->op1.var == cv_var) { + return NULL; + } + break; + } + } + + return unique; +} + +/* Walks backward from `from` to find the most recent opcode whose result port + * wrote to the given TMP/VAR slot. Used to chain `ASSIGN CV = TMP`-style + * patterns through to the value-producing opcode (e.g. NEW). */ +static const zend_op *zend_find_recent_tmp_writer( + const zend_op_array *op_array, const zend_op *from, uint32_t tmp_var) +{ + for (const zend_op *op = from - 1; op >= op_array->opcodes; op--) { + if ((op->result_type == IS_TMP_VAR || op->result_type == IS_VAR) + && op->result.var == tmp_var) { + return op; + } + } + return NULL; +} + zend_function *zend_optimizer_get_called_func( const zend_script *script, const zend_op_array *op_array, zend_op *opline, bool *is_prototype) { @@ -979,6 +1060,65 @@ zend_function *zend_optimizer_get_called_func( } return fbc; } + } else if (opline->op1_type == IS_CV + && opline->op2_type == IS_CONST && Z_TYPE_P(CRT_CONSTANT(opline->op2)) == IS_STRING) { + /* `$o->method()` where $o has a unique ASSIGN whose source is a + * NEW result. The instance class is then known exactly (not just + * a base class), so the resolved method isn't a prototype. */ + const zend_op *cv_writer = zend_find_unique_cv_writer(op_array, opline->op1.var); + if (cv_writer && cv_writer->opcode == ZEND_ASSIGN + && (cv_writer->op2_type == IS_TMP_VAR || cv_writer->op2_type == IS_VAR)) { + const zend_op *src = zend_find_recent_tmp_writer( + op_array, cv_writer, cv_writer->op2.var); + if (src && src->opcode == ZEND_NEW) { + const zend_class_entry *ce = zend_optimizer_get_class_entry_from_op1( + script, op_array, src); + if (ce && ce->type == ZEND_USER_CLASS) { + zend_string *method_name = Z_STR_P(CRT_CONSTANT(opline->op2) + 1); + zend_function *fbc = zend_hash_find_ptr(&ce->function_table, method_name); + if (fbc) { + uint32_t flags = fbc->common.fn_flags; + if (flags & ZEND_ACC_PRIVATE) { + if (fbc->common.scope != op_array->scope) return NULL; + } else if (flags & ZEND_ACC_PROTECTED) { + if (!op_array->scope + || !instanceof_function(op_array->scope, fbc->common.scope)) { + return NULL; + } + } + return fbc; + } + } + } + } + } + break; + case ZEND_INIT_DYNAMIC_CALL: + /* `$f(...)` where $f's unique writer is `ASSIGN $f = "name"`. The + * literal string isn't normalized at compile time the way an + * INIT_FCALL_BY_NAME literal is, so lowercase it for the lookup. */ + if (opline->op2_type == IS_CV) { + const zend_op *cv_writer = zend_find_unique_cv_writer(op_array, opline->op2.var); + if (cv_writer && cv_writer->opcode == ZEND_ASSIGN + && cv_writer->op2_type == IS_CONST) { + const zval *val = CRT_CONSTANT_EX(op_array, cv_writer, cv_writer->op2); + if (Z_TYPE_P(val) == IS_STRING && Z_STRLEN_P(val) > 0 + && Z_STRVAL_P(val)[0] != '\\') { + zend_string *lc_name = zend_string_tolower(Z_STR_P(val)); + zend_function *func; + zval *func_zv; + zend_function *result = NULL; + if (script && (func = zend_hash_find_ptr(&script->function_table, lc_name)) != NULL) { + result = func; + } else if ((func_zv = zend_hash_find(EG(function_table), lc_name)) != NULL) { + if (!zend_optimizer_ignore_function(func_zv, op_array->filename)) { + result = Z_PTR_P(func_zv); + } + } + zend_string_release_ex(lc_name, 0); + if (result) return result; + } + } } break; case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: { @@ -1166,7 +1306,7 @@ static void zend_optimize(zend_op_array *op_array, } } -static void zend_revert_pass_two(zend_op_array *op_array) +void zend_revert_pass_two(zend_op_array *op_array) { zend_op *opline; @@ -1196,7 +1336,7 @@ static void zend_revert_pass_two(zend_op_array *op_array) op_array->fn_flags &= ~ZEND_ACC_DONE_PASS_TWO; } -static void zend_redo_pass_two(zend_op_array *op_array) +void zend_redo_pass_two(zend_op_array *op_array) { zend_op *opline, *end; #if ZEND_USE_ABS_JMP_ADDR && !ZEND_USE_ABS_CONST_ADDR @@ -1734,12 +1874,45 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l uint32_t fn_flags2 = op_array->fn_flags2; zend_function *prototype = op_array->prototype; HashTable *ht = op_array->static_variables; + zend_arg_info *arg_info = op_array->arg_info; + bool arg_info_substituted = (arg_info != orig_op_array->arg_info); *op_array = *orig_op_array; op_array->fn_flags = fn_flags; op_array->fn_flags2 = fn_flags2; op_array->prototype = prototype; op_array->static_variables = ht; + if (arg_info_substituted) { + op_array->arg_info = arg_info; + } + } + } + } ZEND_HASH_FOREACH_END(); + + zend_property_info *prop; + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, name, prop) { + if (!(prop->flags & ZEND_ACC_GENERIC_CLONE) || !prop->hooks) { + continue; + } + + const zend_property_info *parent_prop = zend_hash_find_ptr(&prop->ce->properties_info, name); + if (!parent_prop || !parent_prop->hooks) { + continue; + } + + for (uint32_t hi = 0; hi < ZEND_PROPERTY_HOOK_COUNT; hi++) { + zend_function *clone_hook = prop->hooks[hi]; + zend_function *parent_hook = parent_prop->hooks[hi]; + if (!clone_hook || !parent_hook || clone_hook == parent_hook) { + continue; + } + + zend_arg_info *arg_info = clone_hook->op_array.arg_info; + bool arg_info_substituted = (arg_info != parent_hook->op_array.arg_info); + + clone_hook->op_array = parent_hook->op_array; + if (arg_info_substituted) { + clone_hook->op_array.arg_info = arg_info; } } } ZEND_HASH_FOREACH_END(); diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index d2847c92869c..6d3ac2423e5d 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -92,6 +92,8 @@ typedef void (*zend_optimizer_pass_t)(zend_script *, void *context); BEGIN_EXTERN_C() ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_level, zend_long debug_level); +ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level); +ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script); ZEND_API int zend_optimizer_register_pass(zend_optimizer_pass_t pass); ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); diff --git a/Zend/Optimizer/zend_optimizer_internal.h b/Zend/Optimizer/zend_optimizer_internal.h index d01df56260bc..9592183c503f 100644 --- a/Zend/Optimizer/zend_optimizer_internal.h +++ b/Zend/Optimizer/zend_optimizer_internal.h @@ -111,6 +111,8 @@ void zend_optimizer_pass3(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_cfg(zend_op_array *op_array, zend_optimizer_ctx *ctx); void zend_optimize_dfa(zend_op_array *op_array, zend_optimizer_ctx *ctx); +void zend_revert_pass_two(zend_op_array *op_array); +void zend_redo_pass_two(zend_op_array *op_array); zend_result zend_dfa_analyze_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa); void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa, zend_call_info **call_map); void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx); diff --git a/Zend/tests/exit/exit_statements.phpt b/Zend/tests/exit/exit_statements.phpt index 6b3147156709..c64b6bedda94 100644 --- a/Zend/tests/exit/exit_statements.phpt +++ b/Zend/tests/exit/exit_statements.phpt @@ -17,7 +17,9 @@ TEMPLATE; $php = getenv('TEST_PHP_EXECUTABLE_ESCAPED'); -$command = $php . ' ' . escapeshellarg(FILE_PATH); +// Use -n so the spawned PHP doesn't inherit host php.ini extension +// configuration (otherwise missing extension warnings can pollute output). +$command = $php . ' -n ' . escapeshellarg(FILE_PATH); foreach (['exit', 'die'] as $value) { echo 'Using ', $value, ' as value:', PHP_EOL; diff --git a/Zend/tests/generics/declaration/default_satisfies_bound.phpt b/Zend/tests/generics/declaration/default_satisfies_bound.phpt new file mode 100644 index 000000000000..6aebf72326c5 --- /dev/null +++ b/Zend/tests/generics/declaration/default_satisfies_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Declaration: type-parameter default that satisfies its bound is accepted +--FILE-- + {} +function f(): void {} +trait Tr {} +interface I {} + +new Box; +f(); +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/declaration/default_violates_bound_class.phpt b/Zend/tests/generics/declaration/default_violates_bound_class.phpt new file mode 100644 index 000000000000..17c79f43bd7d --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_class.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: class type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_function.phpt b/Zend/tests/generics/declaration/default_violates_bound_function.phpt new file mode 100644 index 000000000000..ad83c63395ec --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_function.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: function type-parameter default that does not satisfy its bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_interface.phpt b/Zend/tests/generics/declaration/default_violates_bound_interface.phpt new file mode 100644 index 000000000000..0339c260e029 --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_interface.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: interface type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_violates_bound_trait.phpt b/Zend/tests/generics/declaration/default_violates_bound_trait.phpt new file mode 100644 index 000000000000..656fa275618d --- /dev/null +++ b/Zend/tests/generics/declaration/default_violates_bound_trait.phpt @@ -0,0 +1,9 @@ +--TEST-- +Declaration: trait type-parameter default that does not satisfy its bound is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d diff --git a/Zend/tests/generics/declaration/default_with_intersection_bound.phpt b/Zend/tests/generics/declaration/default_with_intersection_bound.phpt new file mode 100644 index 000000000000..a493c0003646 --- /dev/null +++ b/Zend/tests/generics/declaration/default_with_intersection_bound.phpt @@ -0,0 +1,8 @@ +--TEST-- +Declaration: default checked against intersection bound when types are concrete +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Traversable&Countable in %s on line %d diff --git a/Zend/tests/generics/declaration/default_with_union_bound.phpt b/Zend/tests/generics/declaration/default_with_union_bound.phpt new file mode 100644 index 000000000000..5477170bca2d --- /dev/null +++ b/Zend/tests/generics/declaration/default_with_union_bound.phpt @@ -0,0 +1,8 @@ +--TEST-- +Declaration: default checked against union bound when types are concrete +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Default float for type parameter T does not satisfy its bound string|int in %s on line %d diff --git a/Zend/tests/generics/declaration/error_renders_default_args.phpt b/Zend/tests/generics/declaration/error_renders_default_args.phpt new file mode 100644 index 000000000000..e92783bbcfc3 --- /dev/null +++ b/Zend/tests/generics/declaration/error_renders_default_args.phpt @@ -0,0 +1,9 @@ +--TEST-- +Bound error message: declaration-time default-vs-bound error renders NAMED_WITH_ARGS in the bound +--FILE-- + {} +class Box = int> {} +?> +--EXPECTF-- +Fatal error: Default int for type parameter T does not satisfy its bound Comparable in %s on line %d diff --git a/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt b/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt new file mode 100644 index 000000000000..14c7181fa8ce --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_dnf_bound.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generics: declaring a class with a DNF-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} + public function get(): T {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt b/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt new file mode 100644 index 000000000000..19c8197a8612 --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_intersection_bound.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: declaring a class with an intersection-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/no_leak_union_bound.phpt b/Zend/tests/generics/declaration/no_leak_union_bound.phpt new file mode 100644 index 000000000000..de18838ff1eb --- /dev/null +++ b/Zend/tests/generics/declaration/no_leak_union_bound.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: declaring a class with a union-typed bound does not leak memory +--FILE-- + { + public function take(T $x): void {} +} + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt b/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt new file mode 100644 index 000000000000..9a5625588b06 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/chain_forward.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: chain of forward references through three parameters +--FILE-- + {} +class Foo {} +function f, U: Box, V: Foo>(T $a, U $b, V $c): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt b/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt new file mode 100644 index 000000000000..239f16563044 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/default_backward_ref_allowed.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: backward reference in a default is allowed +--FILE-- + {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt new file mode 100644 index 000000000000..206ae5157b64 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/default_forward_ref_rejected.phpt @@ -0,0 +1,9 @@ +--TEST-- +Recursive bounds: forward reference in a default is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter U referenced before declaration in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt new file mode 100644 index 000000000000..f6d669013f1a --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/direct_self_ref_still_rejected.phpt @@ -0,0 +1,8 @@ +--TEST-- +Recursive bounds: direct self-reference in a bound (not inside a type argument) is still rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt new file mode 100644 index 000000000000..f3b2c515d3f6 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/extends_with_mutual_bounds.phpt @@ -0,0 +1,17 @@ +--TEST-- +Recursive bounds: a child class can extend a parent that uses mutual bounds +--FILE-- + {} +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +class StrictPair, B: Box> extends Pair {} + +$p = new StrictPair::, Box>(new Box(), new Box()); +var_dump($p instanceof Pair, $p instanceof StrictPair); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt new file mode 100644 index 000000000000..51d7330fb619 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_bound_with_class_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Recursive bounds: forward reference where the target has a non-generic class bound +--FILE-- +(T $x): U { return $x; } +var_dump(f(new Foo())); +try { + f(new Bar()); +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECTF-- +object(Foo)#%d (0) { +} +type error diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt new file mode 100644 index 000000000000..86f7feb081f0 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_in_generic_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: forward reference nested inside generic type argument +--FILE-- + {} +function g, T>(U $x): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt b/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt new file mode 100644 index 000000000000..7438e337627b --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/forward_simple.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: simple forward reference in bound +--FILE-- +(U $x): T { return $x; } +var_dump(f(new Foo())); +?> +--EXPECTF-- +object(Foo)#%d (0) { +} diff --git a/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt b/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt new file mode 100644 index 000000000000..af67dbf89f66 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/four_way_cycle.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: four-parameter cycle A -> B -> C -> D -> A +--FILE-- + {} +class Quad, B: Box, C: Box, D: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt b/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt new file mode 100644 index 000000000000..4dcbd2c34460 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/hammer_combined.phpt @@ -0,0 +1,34 @@ +--TEST-- +Recursive bounds: combined exercise — class, interface, generic methods, multi-cycle +--FILE-- + {} +class IntBox implements Box {} +class StrBox implements Box {} + +interface Pair, U: Box> { + public function left(): Box; + public function right(): Box; +} + +class IntStrPair implements Pair { + public function __construct(private Box $l, private Box $r) {} + public function left(): Box { return $this->l; } + public function right(): Box { return $this->r; } +} + +class Container { + public function swap, B: Box>(Pair $p): array { + return [$p->right(), $p->left()]; + } +} + +$p = new IntStrPair(new IntBox(), new StrBox()); +$c = new Container::(); +[$first, $second] = $c->swap($p); + +var_dump($first instanceof StrBox, $second instanceof IntBox); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt b/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt new file mode 100644 index 000000000000..9d767f9f7c73 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/interface_with_mutual_bounds.phpt @@ -0,0 +1,25 @@ +--TEST-- +Recursive bounds: an interface can declare mutual bounds +--FILE-- + {} +interface PairLike, U: Box> { + public function left(): Box; + public function right(): Box; +} + +class IntBox implements Box {} +class StrBox implements Box {} + +class Pair implements PairLike { + public function __construct(private Box $l, private Box $r) {} + public function left(): Box { return $this->l; } + public function right(): Box { return $this->r; } +} + +$p = new Pair(new IntBox(), new StrBox()); +var_dump($p->left() instanceof IntBox, $p->right() instanceof StrBox); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt new file mode 100644 index 000000000000..631c51afc2e5 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_class.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: class with mutually recursive type parameters +--FILE-- + {} +class Pair, U: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt new file mode 100644 index 000000000000..28377df49805 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: function with mutually recursive type parameters +--FILE-- + {} +function f, U: Box>(T $x, U $y): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt new file mode 100644 index 000000000000..6d6c3dea0a3c --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_intersection_bound.phpt @@ -0,0 +1,11 @@ +--TEST-- +Recursive bounds: mutual references inside intersection bounds +--FILE-- + {} +interface Tagged {} +class Pair&Tagged, U: Box&Tagged> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt new file mode 100644 index 000000000000..cbe70255fc6e --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_method.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: method with mutually recursive type parameters +--FILE-- + {} +class C { + public function f, U: Box>(T $x, U $y): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt new file mode 100644 index 000000000000..ae95eb6e79c9 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/mutual_union_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: mutual references inside union bounds +--FILE-- + {} +class Pair|null, U: Box|null> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt b/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt new file mode 100644 index 000000000000..d084c1274ad9 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/nested_mutual_in_outer.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: outer-scope class param appears inside mutually recursive method bounds +--FILE-- + {} +class Carrier> { + public function pair, U: Box>(T $a, U $b, O $c): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt b/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt new file mode 100644 index 000000000000..6638805dc595 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/outer_scope_param_in_recursive_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: an enclosing class's type parameter can appear inside a recursive method bound +--FILE-- + {} +class C { + public function f, U: Box>(T $a, U $b, O $c): void {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt b/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt new file mode 100644 index 000000000000..1d3b690a63ff --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/redeclare_still_rejected.phpt @@ -0,0 +1,8 @@ +--TEST-- +Recursive bounds: duplicate parameter names are still rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt new file mode 100644 index 000000000000..53c310f5f629 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/runtime_instantiation.phpt @@ -0,0 +1,20 @@ +--TEST-- +Recursive bounds: a class with mutually recursive bounds can be instantiated and used +--FILE-- + { + public function __construct(public mixed $value) {} +} + +final class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +$bx = new Box(1); +$by = new Box("hi"); +$p = new Pair::, Box>($bx, $by); +var_dump($p->left->value, $p->right->value); +?> +--EXPECT-- +int(1) +string(2) "hi" diff --git a/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt new file mode 100644 index 000000000000..ee81760af212 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/self_ref_through_generic_arg_allowed.phpt @@ -0,0 +1,17 @@ +--TEST-- +Recursive bounds: F-bounded polymorphism (T appears as a type argument inside its own bound) +--FILE-- +` — T isn't its own bound, it's a type +// *argument* to the bound. This is the pattern PHP supports for self-referential +// constraints (analogous to Java `>`). +class Box {} +class Foo> {} + +// A direct self-bound (`T: T`) would be rejected; verify the F-bounded form +// is in fact distinct by reflecting it. +$bound = (new ReflectionClass('Foo'))->getGenericParameters()[0]; +echo $bound, "\n"; +?> +--EXPECT-- +T : Box diff --git a/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt b/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt new file mode 100644 index 000000000000..fa89a02ab6b6 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/triple_cycle.phpt @@ -0,0 +1,10 @@ +--TEST-- +Recursive bounds: three-way cycle T -> U -> V -> T +--FILE-- + {} +class Cycle, U: Box, V: Box> {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt b/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt new file mode 100644 index 000000000000..22145614882e --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/turbofish_satisfied.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: turbofish argument satisfying the head class is accepted +--FILE-- + {} +class Pair, U: Box> {} + +$p = new Pair::, Box>(); +var_dump($p instanceof Pair); +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt b/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt new file mode 100644 index 000000000000..dd8c130d9792 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/turbofish_violation.phpt @@ -0,0 +1,15 @@ +--TEST-- +Recursive bounds: turbofish argument still must satisfy each parameter's bound +--FILE-- + {} +class Pair, U: Box> {} + +try { + $p = new Pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Type argument 1 to new Pair does not satisfy the bound Box on parameter T, int given diff --git a/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt b/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt new file mode 100644 index 000000000000..fc3189b3f784 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/with_variance_covariant_violation.phpt @@ -0,0 +1,9 @@ +--TEST-- +Recursive bounds: covariant marker in bound position is still rejected (bounds are invariant) +--FILE-- + {} +class Pair<+T: Box, +U: Box> {} +?> +--EXPECTF-- +Fatal error: Type parameter %s declared covariant (+%s) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt b/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt new file mode 100644 index 000000000000..9235476a5d82 --- /dev/null +++ b/Zend/tests/generics/declaration/recursive_bounds/with_variance_invariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Recursive bounds: invariant class params with mutual bounds are accepted +--FILE-- + {} +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..c8ab23de97aa --- /dev/null +++ b/Zend/tests/generics/declaration/variance/arrow_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on an arrow function in parameter position is rejected +--FILE-- +(T $x): T => $x; +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt new file mode 100644 index 000000000000..4b3c255ce18a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_forwarded.phpt @@ -0,0 +1,20 @@ +--TEST-- +Variance: covariant T forwarded into an `implements` arg whose target autoloads later is accepted +--FILE-- + 'interface AUI_Sequence<+T> {}', + 'AUI_ArraySequence' => 'final class AUI_ArraySequence<+T> implements AUI_Sequence {}', + ]; + + if (isset($sources[$class])) { + eval($sources[$class]); + } +}); + +class_exists('AUI_ArraySequence'); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt new file mode 100644 index 000000000000..2f32f90a6cfc --- /dev/null +++ b/Zend/tests/generics/declaration/variance/autoload_unresolved_implements_violation.phpt @@ -0,0 +1,19 @@ +--TEST-- +Variance: covariant T forwarded into a contravariant `implements` arg is rejected at link time when the target autoloads later +--FILE-- + 'interface AUI2_Sink<-U> {}', + 'AUI2_Bad' => 'final class AUI2_Bad<+T> implements AUI2_Sink {}', + ]; + + if (isset($sources[$class])) { + eval($sources[$class]); + } +}); + +class_exists('AUI2_Bad'); +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..2edff154047b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/closure_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on a closure in parameter position is rejected +--FILE-- +(T $x): void {}; +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_happy_path.phpt b/Zend/tests/generics/declaration/variance/function_happy_path.phpt new file mode 100644 index 000000000000..5640a800ccb7 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_happy_path.phpt @@ -0,0 +1,29 @@ +--TEST-- +Variance: function-origin type parameters checked uniformly with class-origin +--FILE-- +(T $x): int { return 0; } +function make<+T>(): T { return null; } +function transform<-I, +O>(I $input): O { return null; } + +$closure_take = function <-T>(T $x): int { return 0; }; +$closure_make = function <+T>(): T { return null; }; +$arrow_take = fn<-T>(T $x): int => 0; +$arrow_make = fn<+T>(): T => null; + +class A<+T = mixed> { + public function map<-A_, +B>(A_ $a): B { return null; } +} + +take(42); +make(); +transform(1); +$closure_take(7); +$closure_make(); +$arrow_take(7); +$arrow_make(); +(new A)->map(42); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/function_violation_composed.phpt b/Zend/tests/generics/declaration/variance/function_violation_composed.phpt new file mode 100644 index 000000000000..7b789e03a84a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_composed.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: function-origin +T composed through Consumer<-U> in return is rejected +--FILE-- + { public function consume(U $x): void; } + +function f<+T>(): Consumer {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt b/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt new file mode 100644 index 000000000000..2777162dfe41 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_contravariant_in_return.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: -T on a function in return position (covariant) is rejected +--FILE-- +(): T {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt new file mode 100644 index 000000000000..8cbbac94eff8 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_covariant_in_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Variance: +T on a function in parameter position (contravariant) is rejected +--FILE-- +(T $x): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt b/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt new file mode 100644 index 000000000000..38bd9ad201c4 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/function_violation_own_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a function appearing in its own bound (invariant position) is rejected +--FILE-- + {} + +function f<+T : Box>(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/happy_path.phpt b/Zend/tests/generics/declaration/variance/happy_path.phpt new file mode 100644 index 000000000000..6dc0ed560b11 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/happy_path.phpt @@ -0,0 +1,87 @@ +--TEST-- +Variance: full happy-path showing every legal combination +--FILE-- + { public function produce(): T; } +interface Consumer<-T> { public function consume(T $x): void; } +interface Inv { public function rw(U $x): U; } + +class CoProducer<+T> { + public readonly T $tag; + public function __construct(T $tag) { $this->tag = $tag; } + public function get(): T {} + public function makeProducer(): Producer {} + public function makeConsumerOfConsumers(): Consumer> {} +} + +class ReadOnlyHooked<+T> { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { get => $this->backing; } +} + +class CoConsumer<-T> { + public function take(T $x): void {} + public function takeMany(T ...$xs): void {} + public function makeConsumer(): Consumer {} + public function consumerOfProducers(Producer $p): void {} +} + +class Holder { + public T $val; + public function __construct(T $v) { $this->val = $v; } + public function get(): T { return $this->val; } + public function set(T $v): void { $this->val = $v; } + public T $hooked { + get => $this->val; + set(T $v) { $this->val = $v; } + } +} + +interface Source<+T> { public function fetch(): T; } +class StringSource implements Source { + public function fetch(): string { return "ok"; } +} + +class CovariantChain<+T> implements Source { + public function __construct(public readonly T $value) {} + public function fetch(): T { return $this->value; } +} + +class MethodLevel<+T> { + public function map(U $key): T {} +} + +trait TraitProducer<+U> { + public function produceU(): U {} +} +class UsesProducerTrait { + use TraitProducer; + public function produceU(): int { return 7; } +} + +$h = new Holder::(42); +$h->set(7); +echo $h->get(), "\n"; + +$rh = new ReadOnlyHooked::("hi"); +echo $rh->val, "\n"; + +$cp = new CoProducer::(new Dog()); +echo get_class($cp->tag), "\n"; + +echo (new UsesProducerTrait)->produceU(), "\n"; +echo (new StringSource)->fetch(), "\n"; + +echo "all variance combinations OK\n"; +?> +--EXPECT-- +7 +hi +Dog +7 +ok +all variance combinations OK diff --git a/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt b/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt new file mode 100644 index 000000000000..2d053516ebb8 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/method_function_origin_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a method's own type parameter in parameter position is rejected +--FILE-- +(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt new file mode 100644 index 000000000000..3e1b9bc8c0fa --- /dev/null +++ b/Zend/tests/generics/declaration/variance/mixed_class_T_and_function_T.phpt @@ -0,0 +1,13 @@ +--TEST-- +Variance: class-T and function-T variance markers are checked independently in the same method +--FILE-- + { + public function map<-A, +B>(A $a): B { return null; } +} + +(new C)->map(42); +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt new file mode 100644 index 000000000000..de3a72a07366 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/parent_keyword_ok.phpt @@ -0,0 +1,16 @@ +--TEST-- +Variance: parent from a generic child of a generic parent +--FILE-- + { + public function __construct(public T $value) {} +} +final readonly class B<+T> extends A { + public function asParent(): parent { return $this; } +} + +$b = new B::(7); +echo $b->asParent()->value, "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt b/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt new file mode 100644 index 000000000000..47c5217e614f --- /dev/null +++ b/Zend/tests/generics/declaration/variance/parent_keyword_violation.phpt @@ -0,0 +1,13 @@ +--TEST-- +Variance: parent in a contravariant parameter position is rejected for +T +--FILE-- + { + public function __construct(public T $value) {} +} +class B<+T> extends A { + public function take(parent $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/rfc_example.phpt b/Zend/tests/generics/declaration/variance/rfc_example.phpt new file mode 100644 index 000000000000..e65c030b9369 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/rfc_example.phpt @@ -0,0 +1,45 @@ +--TEST-- +RFC example: covariant Pair/Box with self-referential return type, zip, map, identity +--FILE-- + { + public function __construct( + public L $left, + public R $right, + ) {} + + public function swap(): Pair { + return new Pair($this->right, $this->left); + } +} + +final readonly class Box<+T = mixed> { + public function __construct( + public T $value, + ) {} + + public function map(callable $fn): Box { + return new Box(($fn)($this->value)); + } + + public function zip(O $value): Box> { + return new Box(new Pair($this->value, $value)); + } +} + +function identity(T $value): T { + return $value; +} + +$greeting = new Box::("hello, world"); +$paired = $greeting->zip::(42); +$swapped = $paired->value->swap(); +$result = identity($swapped); + +var_dump($result->left); +var_dump($result->right); +?> +--EXPECT-- +int(42) +string(12) "hello, world" diff --git a/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt new file mode 100644 index 000000000000..62825c96bb2e --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_ok.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: self in a covariant return position +--FILE-- + { + public function __construct(public T $value) {} + public function clone_(): self { return new self($this->value); } +} + +$c = new C::(42); +echo $c->clone_()->value, "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt b/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt new file mode 100644 index 000000000000..7c579f8f4986 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_through_invariant_class_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T cannot appear at an invariant slot of self<...> when the class param is invariant +--FILE-- + { + public function get(): self {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt b/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt new file mode 100644 index 000000000000..cc90c2ad7d0c --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_keyword_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: self in a contravariant parameter position is rejected for +T +--FILE-- + { + public function take(self $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt new file mode 100644 index 000000000000..288c8794079e --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_ref_class_name_ok.phpt @@ -0,0 +1,18 @@ +--TEST-- +Variance: a generic class referencing itself by name in a covariant return position +--FILE-- + { + public function __construct(public L $left, public R $right) {} + public function swap(): Pair { + return new Pair::($this->right, $this->left); + } +} + +$p = new Pair::(7, "hi"); +$s = $p->swap(); +var_dump($s->left, $s->right); +?> +--EXPECT-- +string(2) "hi" +int(7) diff --git a/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt b/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt new file mode 100644 index 000000000000..505d5773102d --- /dev/null +++ b/Zend/tests/generics/declaration/variance/self_ref_class_name_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: self-referenced class name in a violating position is still rejected +--FILE-- + { + public function take(C $other): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt b/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt new file mode 100644 index 000000000000..0fac59480454 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/static_keyword_ok.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: static in a covariant return position +--FILE-- + { + public function __construct(public T $value) {} + public function dup(): static { return new static($this->value); } +} + +$c = new C::("ok"); +echo $c->dup()->value, "\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt b/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt new file mode 100644 index 000000000000..f1998ace645a --- /dev/null +++ b/Zend/tests/generics/declaration/variance/static_method_function_origin_violation.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T on a static method's own type parameter in parameter position is rejected +--FILE-- +(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt new file mode 100644 index 000000000000..48809790c6ff --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_get_hook.phpt @@ -0,0 +1,15 @@ +--TEST-- +Variance: -T in a hooked property with set hook (invariant position) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + get => $this->backing; + set(T $v) { $this->backing = $v; } + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt new file mode 100644 index 000000000000..4f5e19309fa3 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_readonly_property.phpt @@ -0,0 +1,11 @@ +--TEST-- +Variance: -T in a readonly property (covariant position) is rejected +--FILE-- + { + public readonly T $val; + public function __construct(T $v) { $this->val = $v; } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt new file mode 100644 index 000000000000..e70dd1668a48 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_return.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: -T in method return (covariant position) is rejected +--FILE-- + { + public function get(): T {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt new file mode 100644 index 000000000000..4089e855215d --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_in_rw_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: -T in a read/write typed property (invariant position) is rejected +--FILE-- + { + public T $val; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt new file mode 100644 index 000000000000..d546fc82ec56 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_contravariant_through_covariant_arg_in_return.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: -T composed through Producer<+U> in return (+ × + = +) is rejected +--FILE-- + { public function produce(): U; } + +class A<-T> { + public function makeProducer(): Producer {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared contravariant (-T) cannot appear in covariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt new file mode 100644 index 000000000000..d325cc358f94 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_byref_get_hook.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: +T in a hooked property with by-ref get (effectively invariant) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + &get => $this->backing; + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt new file mode 100644 index 000000000000..917bdf2d3fbb --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_default.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in a sibling type parameter's default (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T, U = Container> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt new file mode 100644 index 000000000000..137641603c97 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_own_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T appearing in its own bound (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T : Box> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt new file mode 100644 index 000000000000..1beeba25176b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_param.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in method parameter (contravariant position) is rejected +--FILE-- + { + public function take(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt new file mode 100644 index 000000000000..b03d1aff1fec --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_rw_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T in a read/write typed property (invariant position) is rejected +--FILE-- + { + public T $val; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt new file mode 100644 index 000000000000..28267341e6b5 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_set_hook.phpt @@ -0,0 +1,15 @@ +--TEST-- +Variance: +T in a hooked property's set hook (contravariant position) is rejected +--FILE-- + { + private T $backing; + public function __construct(T $v) { $this->backing = $v; } + public T $val { + get => $this->backing; + set(T $v) { $this->backing = $v; } + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt new file mode 100644 index 000000000000..10880152c60f --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_in_sibling_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T mentioned in a sibling type parameter's bound (invariant position) is rejected +--FILE-- + { public function get(): U; } + +class A<+T, U : Container> {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt new file mode 100644 index 000000000000..ddd4f6825393 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_through_contravariant_arg_in_return.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T composed through Consumer<-U> in return (+ × - = -) is rejected +--FILE-- + { public function consume(U $x): void; } + +class A<+T> { + public function makeConsumer(): Consumer {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt new file mode 100644 index 000000000000..10005ea1449c --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_covariant_through_invariant_arg.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T composed through an invariant generic's slot (invariant) is rejected +--FILE-- + { public function rw(U $x): U; } + +class A<+T> { + public function get(): Inv {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt new file mode 100644 index 000000000000..d912f81535d3 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_extends_contravariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to a contravariant parent's parameter is rejected +--FILE-- + {} + +class A<+T> extends Cons {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt new file mode 100644 index 000000000000..ac7524f9e28b --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_extends_invariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to an invariant parent's parameter is rejected +--FILE-- + {} + +class A<+T> extends Inv {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt new file mode 100644 index 000000000000..f4e63a3f78e9 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_implements_invariant_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Variance: +T as the type-arg to an invariant interface's parameter is rejected +--FILE-- + {} + +class A<+T> implements Inv {} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt b/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt new file mode 100644 index 000000000000..32b84d702a93 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_line_points_at_method.phpt @@ -0,0 +1,16 @@ +--TEST-- +Variance: a violation inside a method signature reports the method's line, not the class header +--FILE-- + { + public function ok(): T { + throw new RuntimeException(); + } + + public function bad(O $default): T|O { + return $default; + } +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line 7 diff --git a/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt b/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt new file mode 100644 index 000000000000..d1f12650b1cc --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_method_T_bound_uses_class_T.phpt @@ -0,0 +1,12 @@ +--TEST-- +Variance: +T appearing inside a method-level type parameter's bound (invariant) is rejected +--FILE-- + {} + +class A<+T> { + public function map>(W $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt b/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt new file mode 100644 index 000000000000..a5b8618683f7 --- /dev/null +++ b/Zend/tests/generics/declaration/variance/violation_use_trait_invariant_arg.phpt @@ -0,0 +1,14 @@ +--TEST-- +Variance: +T as the type-arg to an invariant trait's parameter is rejected +--FILE-- + { + public function rw(U $x): U { return $x; } +} + +class A<+T> { + use InvTrait; +} +?> +--EXPECTF-- +Fatal error: Type parameter T declared covariant (+T) cannot appear in invariant position in %s on line %d diff --git a/Zend/tests/generics/erasure/arrow_fn_erasure.phpt b/Zend/tests/generics/erasure/arrow_fn_erasure.phpt new file mode 100644 index 000000000000..d14f301d4cd4 --- /dev/null +++ b/Zend/tests/generics/erasure/arrow_fn_erasure.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: arrow function with type parameters erases to bound +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +?> +--EXPECT-- +int diff --git a/Zend/tests/generics/erasure/bound_object.phpt b/Zend/tests/generics/erasure/bound_object.phpt new file mode 100644 index 000000000000..815dd1a5e92c --- /dev/null +++ b/Zend/tests/generics/erasure/bound_object.phpt @@ -0,0 +1,12 @@ +--TEST-- +Erasure: T : object erases to object +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/erasure/bound_to_class.phpt b/Zend/tests/generics/erasure/bound_to_class.phpt new file mode 100644 index 000000000000..7b1cfa50414e --- /dev/null +++ b/Zend/tests/generics/erasure/bound_to_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: T : Foo erases to Foo +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Foo +Foo diff --git a/Zend/tests/generics/erasure/closure_erasure.phpt b/Zend/tests/generics/erasure/closure_erasure.phpt new file mode 100644 index 000000000000..6a533558b3aa --- /dev/null +++ b/Zend/tests/generics/erasure/closure_erasure.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: closure with type parameters erases to bound +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($f); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/erasure/composite_bound.phpt b/Zend/tests/generics/erasure/composite_bound.phpt new file mode 100644 index 000000000000..5e38afba9a81 --- /dev/null +++ b/Zend/tests/generics/erasure/composite_bound.phpt @@ -0,0 +1,11 @@ +--TEST-- +Erasure: composite bound erased +--FILE-- +(T $x): T { return $x; } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo get_class($rt), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/erasure/erased_signature_match.phpt b/Zend/tests/generics/erasure/erased_signature_match.phpt new file mode 100644 index 000000000000..64406feb72a6 --- /dev/null +++ b/Zend/tests/generics/erasure/erased_signature_match.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: generic signature matches hand-erased equivalent +--FILE-- + { + public function get(): T { return new Foo; } +} + +class Erased { + public function get(): Foo { return new Foo; } +} + +$rg = (new ReflectionClass('Generic'))->getMethod('get')->getReturnType()->__toString(); +$re = (new ReflectionClass('Erased'))->getMethod('get')->getReturnType()->__toString(); +var_dump($rg === $re); +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/generics/erasure/extends_args_discarded.phpt b/Zend/tests/generics/erasure/extends_args_discarded.phpt new file mode 100644 index 000000000000..1c46d8103850 --- /dev/null +++ b/Zend/tests/generics/erasure/extends_args_discarded.phpt @@ -0,0 +1,13 @@ +--TEST-- +Inheritance: `extends Base` makes the direct parent the synthesized monomorph +--FILE-- + {} +class Derived extends Base {} +echo (new ReflectionClass('Derived'))->getParentClass()->getName(), "\n"; +$d = new Derived; +var_dump($d instanceof Base); +?> +--EXPECT-- +Base +bool(true) diff --git a/Zend/tests/generics/erasure/generic_class_param.phpt b/Zend/tests/generics/erasure/generic_class_param.phpt new file mode 100644 index 000000000000..bde3ae6281c5 --- /dev/null +++ b/Zend/tests/generics/erasure/generic_class_param.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: generic class parameter type erased to bound +--FILE-- + { + public function set(T $x): void {} +} +$rm = (new ReflectionClass('Box'))->getMethod('set'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/erasure/get_class_returns_erased.phpt b/Zend/tests/generics/erasure/get_class_returns_erased.phpt new file mode 100644 index 000000000000..2a85a21c3e31 --- /dev/null +++ b/Zend/tests/generics/erasure/get_class_returns_erased.phpt @@ -0,0 +1,10 @@ +--TEST-- +Monomorphization: get_class returns the canonical monomorph name +--FILE-- + {} +$b = new Box::; +echo get_class($b), "\n"; +?> +--EXPECT-- +Box diff --git a/Zend/tests/generics/erasure/instanceof_args_discarded.phpt b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt new file mode 100644 index 000000000000..d2a501d96193 --- /dev/null +++ b/Zend/tests/generics/erasure/instanceof_args_discarded.phpt @@ -0,0 +1,10 @@ +--TEST-- +Erasure: instanceof on a non-generic class with type arguments is a compile-time error +--FILE-- +); +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class C in %s on line %d diff --git a/Zend/tests/generics/erasure/intersection_t_bound_class.phpt b/Zend/tests/generics/erasure/intersection_t_bound_class.phpt new file mode 100644 index 000000000000..20421c878257 --- /dev/null +++ b/Zend/tests/generics/erasure/intersection_t_bound_class.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: T in intersection position with class-like bound erases cleanly +--FILE-- +(T & Foo $x): T & Foo { return $x; } + +$r = new ReflectionFunction('pickIface'); +echo "param: ", $r->getParameters()[0]->getType(), "\n"; +echo "return: ", $r->getReturnType(), "\n"; + +class BarFoo implements Bar, Foo {} +var_dump(pickIface(new BarFoo) instanceof BarFoo); +?> +--EXPECT-- +param: Bar&Foo +return: Bar&Foo +bool(true) diff --git a/Zend/tests/generics/erasure/method_bound.phpt b/Zend/tests/generics/erasure/method_bound.phpt new file mode 100644 index 000000000000..4562b258efad --- /dev/null +++ b/Zend/tests/generics/erasure/method_bound.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: method-level type parameter erased +--FILE-- +(U $x): U { return $x; } +} +$rt = (new ReflectionClass('C'))->getMethod('m')->getReturnType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +Stringable2 diff --git a/Zend/tests/generics/erasure/method_inheritance_erased.phpt b/Zend/tests/generics/erasure/method_inheritance_erased.phpt new file mode 100644 index 000000000000..a94f25177bba --- /dev/null +++ b/Zend/tests/generics/erasure/method_inheritance_erased.phpt @@ -0,0 +1,19 @@ +--TEST-- +Erasure: child overriding generic parent method works on erased signature +--FILE-- +(T $x): T { return $x; } +} +class Child extends Parent2 { + public function f(U $x): U { return $x; } +} +$pm = (new ReflectionClass('Parent2'))->getMethod('f'); +$cm = (new ReflectionClass('Child'))->getMethod('f'); +echo $pm->getParameters()[0]->getType()->getName(), "\n"; +echo $cm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Animal +Animal diff --git a/Zend/tests/generics/erasure/named_args_stripped.phpt b/Zend/tests/generics/erasure/named_args_stripped.phpt new file mode 100644 index 000000000000..066e6680ad72 --- /dev/null +++ b/Zend/tests/generics/erasure/named_args_stripped.phpt @@ -0,0 +1,9 @@ +--TEST-- +Erasure: type arguments on a non-generic class in a parameter/return type is a compile-time error +--FILE-- + $x): Container { return $x; } +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Container in %s on line %d diff --git a/Zend/tests/generics/erasure/property_type_erased.phpt b/Zend/tests/generics/erasure/property_type_erased.phpt new file mode 100644 index 000000000000..9b1e93e3d5a5 --- /dev/null +++ b/Zend/tests/generics/erasure/property_type_erased.phpt @@ -0,0 +1,16 @@ +--TEST-- +Erasure: generic property type erased to bound +--FILE-- + { + public T $value; +} +$rp = (new ReflectionClass('Box'))->getProperty('value'); +$rt = $rp->getType(); +echo get_class($rt), "\n"; +echo $rt->getName(), "\n"; +?> +--EXPECT-- +ReflectionNamedType +Foo diff --git a/Zend/tests/generics/erasure/runtime_bound_check.phpt b/Zend/tests/generics/erasure/runtime_bound_check.phpt new file mode 100644 index 000000000000..dc2b563701ec --- /dev/null +++ b/Zend/tests/generics/erasure/runtime_bound_check.phpt @@ -0,0 +1,21 @@ +--TEST-- +Erasure: bound is enforced at runtime +--FILE-- +(T $x): T { return $x; } + +echo get_class(f(new Dog)), "\n"; + +try { + f("not an animal"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "caught\n"; +} +?> +--EXPECT-- +Dog +caught diff --git a/Zend/tests/generics/erasure/runtime_unbounded.phpt b/Zend/tests/generics/erasure/runtime_unbounded.phpt new file mode 100644 index 000000000000..e86b52e60926 --- /dev/null +++ b/Zend/tests/generics/erasure/runtime_unbounded.phpt @@ -0,0 +1,20 @@ +--TEST-- +Erasure: unbounded T accepts anything (mixed) +--FILE-- +(T $x): T { return $x; } +var_dump(id(42)); +var_dump(id("hello")); +var_dump(id(null)); +var_dump(id([1, 2])); +?> +--EXPECT-- +int(42) +string(5) "hello" +NULL +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} diff --git a/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt new file mode 100644 index 000000000000..91f3959bce95 --- /dev/null +++ b/Zend/tests/generics/erasure/t_in_nullable_and_union.phpt @@ -0,0 +1,72 @@ +--TEST-- +Erasure: T inside nullable and union types collapses redundant nulls/mixed +--FILE-- +(?T $x): ?T { return $x; } +function nullableBounded(?T $x): ?T { return $x; } +function unionNullUnbounded(null|T $x): null|T { return $x; } +function unionMultiUnbounded(int|bool|T $x): int|bool|T { return $x; } +function unionNullBounded(null|T $x): null|T { return $x; } +function unionMultiBounded(int|T $x): int|T { return $x; } + +class Holder { + public function __construct(public ?T $value) {} + public function set(?T $v): void { $this->value = $v; } + public function get(): ?T { return $this->value; } +} + +var_dump(nullableUnbounded(42)); +var_dump(nullableUnbounded(null)); +var_dump(nullableUnbounded("hi")); +var_dump(nullableBounded(7)); +var_dump(nullableBounded(null)); +var_dump(unionNullUnbounded(null)); +var_dump(unionNullUnbounded("x")); +var_dump(unionMultiUnbounded(1)); +var_dump(unionMultiUnbounded(true)); +var_dump(unionMultiUnbounded(new stdClass)); +var_dump(unionNullBounded(null)); +var_dump(unionNullBounded(7)); +var_dump(unionMultiBounded(1)); + +echo "nullableUnbounded: ", (new ReflectionFunction('nullableUnbounded'))->getReturnType(), "\n"; +echo "nullableBounded: ", (new ReflectionFunction('nullableBounded'))->getReturnType(), "\n"; +echo "unionNullUnbounded: ", (new ReflectionFunction('unionNullUnbounded'))->getReturnType(), "\n"; +echo "unionMultiUnbounded: ", (new ReflectionFunction('unionMultiUnbounded'))->getReturnType(), "\n"; +echo "unionNullBounded: ", (new ReflectionFunction('unionNullBounded'))->getReturnType(), "\n"; +echo "unionMultiBounded: ", (new ReflectionFunction('unionMultiBounded'))->getReturnType(), "\n"; + +$rc = new ReflectionClass(Holder::class); +echo "Holder property: ", $rc->getProperty('value')->getType(), "\n"; +echo "Holder set param: ", $rc->getMethod('set')->getParameters()[0]->getType(), "\n"; +echo "Holder get return: ", $rc->getMethod('get')->getReturnType(), "\n"; + +$h = new Holder(1); +$h->set(null); +var_dump($h->get()); +?> +--EXPECT-- +int(42) +NULL +string(2) "hi" +int(7) +NULL +NULL +string(1) "x" +int(1) +bool(true) +object(stdClass)#1 (0) { +} +NULL +int(7) +int(1) +nullableUnbounded: mixed +nullableBounded: ?int +unionNullUnbounded: mixed +unionMultiUnbounded: mixed +unionNullBounded: ?int +unionMultiBounded: Stringable|int +Holder property: mixed +Holder set param: mixed +Holder get return: mixed +NULL diff --git a/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt b/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt new file mode 100644 index 000000000000..a41ecae4ace3 --- /dev/null +++ b/Zend/tests/generics/erasure/turbofish_no_runtime_effect.phpt @@ -0,0 +1,13 @@ +--TEST-- +Erasure: turbofish (with matching arity) has zero runtime effect +--FILE-- +($x) { return $x * 2; } +var_dump(f(5)); +var_dump(f::(5)); +var_dump(f::(5)); +?> +--EXPECT-- +int(10) +int(10) +int(10) diff --git a/Zend/tests/generics/erasure/unbounded_to_mixed.phpt b/Zend/tests/generics/erasure/unbounded_to_mixed.phpt new file mode 100644 index 000000000000..15ff253c3173 --- /dev/null +++ b/Zend/tests/generics/erasure/unbounded_to_mixed.phpt @@ -0,0 +1,12 @@ +--TEST-- +Erasure: unbounded T erases to mixed +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +echo $r->getParameters()[0]->getType()->__toString(), "\n"; +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +mixed +mixed diff --git a/Zend/tests/generics/erasure/var_dump_class_name.phpt b/Zend/tests/generics/erasure/var_dump_class_name.phpt new file mode 100644 index 000000000000..5852b3910887 --- /dev/null +++ b/Zend/tests/generics/erasure/var_dump_class_name.phpt @@ -0,0 +1,14 @@ +--TEST-- +Monomorphization: var_dump shows the canonical monomorph name +--FILE-- + { + public int $x = 1; +} +var_dump(new Box::); +?> +--EXPECT-- +object(Box)#1 (1) { + ["x"]=> + int(1) +} diff --git a/Zend/tests/generics/errors/anonymous_class_no_params.phpt b/Zend/tests/generics/errors/anonymous_class_no_params.phpt new file mode 100644 index 000000000000..6aca672cb9b2 --- /dev/null +++ b/Zend/tests/generics/errors/anonymous_class_no_params.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: anonymous classes cannot declare type parameters +--FILE-- + {};'); +} catch (ParseError $e) { + echo "parse error: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +parse error: %s diff --git a/Zend/tests/generics/errors/closure_shadows_function.phpt b/Zend/tests/generics/errors/closure_shadows_function.phpt new file mode 100644 index 000000000000..29c6c8397605 --- /dev/null +++ b/Zend/tests/generics/errors/closure_shadows_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: closure type parameter shadows enclosing function type parameter +--FILE-- +(): void { + $cl = function (): void {}; +} +?> +--EXPECTF-- +Fatal error: Type parameter T shadows enclosing type parameter in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_in_method.phpt b/Zend/tests/generics/errors/duplicate_in_method.phpt new file mode 100644 index 000000000000..78fd038e233c --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_in_method.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter inside a method's own list +--FILE-- +(): void {} } +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_type_param.phpt b/Zend/tests/generics/errors/duplicate_type_param.phpt new file mode 100644 index 000000000000..77c148794173 --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_type_param.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter name +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter T in %s on line %d diff --git a/Zend/tests/generics/errors/duplicate_type_param_function.phpt b/Zend/tests/generics/errors/duplicate_type_param_function.phpt new file mode 100644 index 000000000000..f9ad4e8b270b --- /dev/null +++ b/Zend/tests/generics/errors/duplicate_type_param_function.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: duplicate type parameter name on function +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Cannot redeclare type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/empty_arg_list.phpt b/Zend/tests/generics/errors/empty_arg_list.phpt new file mode 100644 index 000000000000..6ac10dcff2db --- /dev/null +++ b/Zend/tests/generics/errors/empty_arg_list.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: empty type argument list +--FILE-- + $x): void {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/empty_param_list.phpt b/Zend/tests/generics/errors/empty_param_list.phpt new file mode 100644 index 000000000000..918533b25f26 --- /dev/null +++ b/Zend/tests/generics/errors/empty_param_list.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: empty type parameter list +--FILE-- + {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/forward_ref_at_self.phpt b/Zend/tests/generics/errors/forward_ref_at_self.phpt new file mode 100644 index 000000000000..b4dd1601bd0a --- /dev/null +++ b/Zend/tests/generics/errors/forward_ref_at_self.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: bound `T : T` (top-level self-reference) is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/forward_reference_in_default.phpt b/Zend/tests/generics/errors/forward_reference_in_default.phpt new file mode 100644 index 000000000000..915c0b4d09dd --- /dev/null +++ b/Zend/tests/generics/errors/forward_reference_in_default.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: forward reference in default of earlier parameter +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T referenced before declaration in %s on line %d diff --git a/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt new file mode 100644 index 000000000000..a0deb98dca56 --- /dev/null +++ b/Zend/tests/generics/errors/intersection_t_bound_scalar.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: T in intersection position with scalar bound +--FILE-- +(): T & Foo {} +?> +--EXPECTF-- +Fatal error: Type parameter T with bound string cannot be part of an intersection type; bound it to a class or interface (e.g. T: SomeInterface) in %s on line %d diff --git a/Zend/tests/generics/errors/intersection_t_unbounded.phpt b/Zend/tests/generics/errors/intersection_t_unbounded.phpt new file mode 100644 index 000000000000..6fe0e50df2c7 --- /dev/null +++ b/Zend/tests/generics/errors/intersection_t_unbounded.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: T in intersection position with unbounded T +--FILE-- +(): T & Foo {} +?> +--EXPECTF-- +Fatal error: Type parameter T with bound mixed cannot be part of an intersection type; bound it to a class or interface (e.g. T: SomeInterface) in %s on line %d diff --git a/Zend/tests/generics/errors/method_shadows_class_param.phpt b/Zend/tests/generics/errors/method_shadows_class_param.phpt new file mode 100644 index 000000000000..6c8b3cfd7935 --- /dev/null +++ b/Zend/tests/generics/errors/method_shadows_class_param.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: method type parameter shadows class type parameter +--FILE-- + { + public function f(): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T shadows enclosing type parameter in %s on line %d diff --git a/Zend/tests/generics/errors/missing_close_angle.phpt b/Zend/tests/generics/errors/missing_close_angle.phpt new file mode 100644 index 000000000000..0ee4bae3581f --- /dev/null +++ b/Zend/tests/generics/errors/missing_close_angle.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: missing closing angle in type parameter list +--FILE-- + +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/missing_param_after_comma.phpt b/Zend/tests/generics/errors/missing_param_after_comma.phpt new file mode 100644 index 000000000000..df223a838c29 --- /dev/null +++ b/Zend/tests/generics/errors/missing_param_after_comma.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: missing parameter after trailing comma plus comma +--FILE-- + {}'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/naked_new_without_defaults.phpt b/Zend/tests/generics/errors/naked_new_without_defaults.phpt new file mode 100644 index 000000000000..290ebd8f38bd --- /dev/null +++ b/Zend/tests/generics/errors/naked_new_without_defaults.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: naked `new GenericClass()` is rejected if any type parameter has no default +--FILE-- + {} +new Box(); +?> +--EXPECTF-- +Fatal error: Cannot instantiate generic class Box without type arguments; type parameter T has no default in %s on line %d diff --git a/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt b/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt new file mode 100644 index 000000000000..101a3f0c6f85 --- /dev/null +++ b/Zend/tests/generics/errors/optional_only_at_tail_ok.phpt @@ -0,0 +1,15 @@ +--TEST-- +Errors: optional type parameters at the tail are accepted +--FILE-- + {} +class B {} +class C {} +class D {} +class E {} +function f(): void {} +function g(): void {} +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/errors/recursive_bound_ok.phpt b/Zend/tests/generics/errors/recursive_bound_ok.phpt new file mode 100644 index 000000000000..2b05466e6096 --- /dev/null +++ b/Zend/tests/generics/errors/recursive_bound_ok.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: recursive bound (T : Comparable) is allowed (positive test) +--FILE-- + {} +function f>(T $x): T { return $x; } +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/errors/required_after_optional.phpt b/Zend/tests/generics/errors/required_after_optional.phpt new file mode 100644 index 000000000000..65a48dad1157 --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter cannot follow an optional one +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/required_after_optional_function.phpt b/Zend/tests/generics/errors/required_after_optional_function.phpt new file mode 100644 index 000000000000..3bfefa731931 --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional_function.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter cannot follow an optional one +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter U in %s on line %d diff --git a/Zend/tests/generics/errors/required_after_optional_three_params.phpt b/Zend/tests/generics/errors/required_after_optional_three_params.phpt new file mode 100644 index 000000000000..a4a5ad579ace --- /dev/null +++ b/Zend/tests/generics/errors/required_after_optional_three_params.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: required type parameter following two optional ones names the first optional +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Optional type parameter T cannot be declared before required type parameter V in %s on line %d diff --git a/Zend/tests/generics/errors/runtime_bound_violation.phpt b/Zend/tests/generics/errors/runtime_bound_violation.phpt new file mode 100644 index 000000000000..c6589429f414 --- /dev/null +++ b/Zend/tests/generics/errors/runtime_bound_violation.phpt @@ -0,0 +1,16 @@ +--TEST-- +Errors: runtime type error when bound is violated +--FILE-- +(T $x): T { return $x; } + +try { + f(42); + echo "no error\n"; +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECT-- +type error diff --git a/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt b/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt new file mode 100644 index 000000000000..23084a77f8dd --- /dev/null +++ b/Zend/tests/generics/errors/runtime_unbound_accepts_anything.phpt @@ -0,0 +1,19 @@ +--TEST-- +Errors: runtime accepts anything for unbounded T (mixed) +--FILE-- +(T $x): T { return $x; } + +var_dump(f(42)); +var_dump(f("foo")); +var_dump(f(null)); +var_dump(f(new stdClass)); +echo "no errors\n"; +?> +--EXPECT-- +int(42) +string(3) "foo" +NULL +object(stdClass)#1 (0) { +} +no errors diff --git a/Zend/tests/generics/errors/self_ref_in_default.phpt b/Zend/tests/generics/errors/self_ref_in_default.phpt new file mode 100644 index 000000000000..d852a85d83a5 --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_default.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: top-level self-reference in default is rejected +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/self_ref_in_intersection.phpt b/Zend/tests/generics/errors/self_ref_in_intersection.phpt new file mode 100644 index 000000000000..3a6970bf3a4a --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_intersection.phpt @@ -0,0 +1,9 @@ +--TEST-- +Errors: top-level self-reference inside an intersection bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/self_ref_in_union.phpt b/Zend/tests/generics/errors/self_ref_in_union.phpt new file mode 100644 index 000000000000..e40264a7b61d --- /dev/null +++ b/Zend/tests/generics/errors/self_ref_in_union.phpt @@ -0,0 +1,8 @@ +--TEST-- +Errors: top-level self-reference inside a union bound is rejected +--FILE-- +(): void {} +?> +--EXPECTF-- +Fatal error: Type parameter T cannot reference itself in its own bound or default outside of a generic type argument in %s on line %d diff --git a/Zend/tests/generics/errors/turbofish_no_call_target.phpt b/Zend/tests/generics/errors/turbofish_no_call_target.phpt new file mode 100644 index 000000000000..fd3c7fb8be6c --- /dev/null +++ b/Zend/tests/generics/errors/turbofish_no_call_target.phpt @@ -0,0 +1,12 @@ +--TEST-- +Errors: bare turbofish without function call +--FILE-- +;'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/turbofish_with_space.phpt b/Zend/tests/generics/errors/turbofish_with_space.phpt new file mode 100644 index 000000000000..a7460772979a --- /dev/null +++ b/Zend/tests/generics/errors/turbofish_with_space.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: whitespace between :: and < is not turbofish +--FILE-- +::f();'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_args_in_class_const.phpt b/Zend/tests/generics/errors/type_args_in_class_const.phpt new file mode 100644 index 000000000000..8077a89c3e38 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_in_class_const.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: type arguments not allowed in expression position (Foo::class) +--FILE-- +::class;'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_args_in_static_call.phpt b/Zend/tests/generics/errors/type_args_in_static_call.phpt new file mode 100644 index 000000000000..509cd35991a2 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_in_static_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Errors: type arguments not allowed before :: (use ::<...> turbofish instead) +--FILE-- +::bar();'); +} catch (ParseError $e) { + echo "parse error\n"; +} +?> +--EXPECT-- +parse error diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt new file mode 100644 index 000000000000..48bbd541521a --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_catch.phpt @@ -0,0 +1,12 @@ +--TEST-- +Type arguments on a non-generic class in `catch` are a compile-time error +--FILE-- + $e) { +} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class MyErr in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt new file mode 100644 index 000000000000..ec535974e86f --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_implements.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic interface in `implements` are a compile-time error +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt new file mode 100644 index 000000000000..fe06732eed73 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_instanceof.phpt @@ -0,0 +1,10 @@ +--TEST-- +Type arguments on a non-generic class in `instanceof` are a compile-time error +--FILE-- +); +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt new file mode 100644 index 000000000000..2490ce2181cc --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_new.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in `new` (turbofish) are a compile-time error +--FILE-- +(); +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt new file mode 100644 index 000000000000..d23d51ad3c49 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_param.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in a parameter type are a compile-time error +--FILE-- + $x): void {} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt new file mode 100644 index 000000000000..49e1e7356012 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_property.phpt @@ -0,0 +1,11 @@ +--TEST-- +Type arguments on a non-generic class in a property type are a compile-time error +--FILE-- + $x; +} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt b/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt new file mode 100644 index 000000000000..7a4b5d3d19b0 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_non_generic_return.phpt @@ -0,0 +1,9 @@ +--TEST-- +Type arguments on a non-generic class in a return type are a compile-time error +--FILE-- + { return new Plain; } +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_static_new.phpt b/Zend/tests/generics/errors/type_args_on_static_new.phpt new file mode 100644 index 000000000000..2cfb0d4ae4a0 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_static_new.phpt @@ -0,0 +1,18 @@ +--TEST-- +Type arguments on `new static::<...>` are a compile-time error (static is the runtime type) +--FILE-- + { + public function __construct(private array $e = []) {} + public function dup(): static { return new static::($this->e); } +} +(new C::([1, 2]))->dup(); +?> +--EXPECTF-- +Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d diff --git a/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt b/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt new file mode 100644 index 000000000000..6c47140ea418 --- /dev/null +++ b/Zend/tests/generics/errors/type_args_on_static_new_subclass.phpt @@ -0,0 +1,17 @@ +--TEST-- +`new static::<...>` is rejected even when the runtime type is a non-generic subclass of a monomorph +--FILE-- + +// but is itself non-generic — it has no type parameters to apply `` to. The +// rejection is the same compile-time scoping error: use `new static()`. +class Box { + public function __construct(private mixed $v = null) {} + public function make(): object { return new static::($this->v); } +} +class IntBox extends Box {} + +(new IntBox(5))->make(); +?> +--EXPECTF-- +Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_catch.phpt b/Zend/tests/generics/errors/type_param_in_catch.phpt new file mode 100644 index 000000000000..7d46d8f88380 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_catch.phpt @@ -0,0 +1,17 @@ +--TEST-- +catch (T $e) silently does not match when T has no usable binding +--FILE-- +(): void { + // T has no class bound and no caller-supplied binding. catch (T $e) + // is therefore "catch nothing"; the thrown exception propagates out. + try { throw new Exception("boom"); } catch (T $e) { echo "caught\n"; } +} +try { + f(); +} catch (Throwable $e) { + echo "outer: ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +outer: boom diff --git a/Zend/tests/generics/errors/type_param_in_class_const.phpt b/Zend/tests/generics/errors/type_param_in_class_const.phpt new file mode 100644 index 000000000000..fc8c6410544a --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_class_const.phpt @@ -0,0 +1,11 @@ +--TEST-- +Errors: bare function-level type parameter used in `T::class` errors at runtime when nothing pins it +--FILE-- +(): string { + return T::class; +} +f(); +?> +--EXPECTF-- +Fatal error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_instanceof.phpt b/Zend/tests/generics/errors/type_param_in_instanceof.phpt new file mode 100644 index 000000000000..45644edbe8d4 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_instanceof.phpt @@ -0,0 +1,15 @@ +--TEST-- +Errors: bare function-level type parameter used in `instanceof T` errors at runtime when nothing pins it +--FILE-- +($x): bool { + return $x instanceof T; +} +f(new stdClass()); +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f(Object(stdClass)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_new_expression.phpt b/Zend/tests/generics/errors/type_param_in_new_expression.phpt new file mode 100644 index 000000000000..bd7a6bfc8d8b --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_new_expression.phpt @@ -0,0 +1,15 @@ +--TEST-- +Errors: bare function-level type parameter used in `new T()` errors at runtime when nothing pins it +--FILE-- +(): void { + new T(); +} +f(); +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f() +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt b/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt new file mode 100644 index 000000000000..ef408a982579 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_new_turbofish.phpt @@ -0,0 +1,40 @@ +--TEST-- +Errors: a forwarded TYPE_PARAMETER ref in a `new C::()` turbofish errors at the synth site when nothing pins T and its bound is not a class +--FILE-- +(...)` instead of +// being the class itself. Both must produce the same diagnostic, fired at the +// `new` site — not silently produce a broken-refs monomorph that crashes far +// from the cause when a method is later called on it. +final readonly class Box { + public function __construct(public array $items = []) {} +} + +function makeBox(): Box { + return new Box::([]); +} + +try { + makeBox(); + echo "no error??\n"; +} catch (Error $e) { + echo "ok: " . $e->getMessage() . "\n"; +} + +// A class-bound on the outer T gives a fallback target. +class Base {} +function makeBoxFromBound(): Box { + return new Box::([]); +} +$b = makeBoxFromBound(); +var_dump($b::class); + +// Turbofish supplied: no error, T resolves to the supplied type. +$b = makeBox::(); +var_dump($b::class); +?> +--EXPECT-- +ok: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class +string(9) "Box" +string(8) "Box" diff --git a/Zend/tests/generics/errors/type_param_in_static_call.phpt b/Zend/tests/generics/errors/type_param_in_static_call.phpt new file mode 100644 index 000000000000..66cd5ae9cab7 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_call.phpt @@ -0,0 +1,15 @@ +--TEST-- +Errors: bare function-level type parameter used as a static-call target errors at runtime when nothing pins it +--FILE-- +(): void { + T::foo(); +} +f(); +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class in %s:%d +Stack trace: +#0 %s(%d): f() +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method.phpt b/Zend/tests/generics/errors/type_param_in_static_method.phpt new file mode 100644 index 000000000000..aa67becd88c9 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a static method signature +--FILE-- + { + public static function foo(T $a): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt b/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt new file mode 100644 index 000000000000..ebb09bf3ddb4 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_bound.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a bound on a static method's own type parameter +--FILE-- + { + public static function foo(U $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_default.phpt b/Zend/tests/generics/errors/type_param_in_static_method_default.phpt new file mode 100644 index 000000000000..a2d5edbb94fa --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_default.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a default for a static method's own type parameter +--FILE-- + { + public static function foo(U $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt b/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt new file mode 100644 index 000000000000..bff9674b4951 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_in_trait.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: trait type parameter cannot be used in a static method signature +--FILE-- + { + public static function make(T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Bag and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt b/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt new file mode 100644 index 000000000000..afe7ee4a36c1 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_intersection.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot appear in an intersection type on a static method +--FILE-- + { + public static function foo(T&Countable $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt b/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt new file mode 100644 index 000000000000..2c467f43333d --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_nested.phpt @@ -0,0 +1,14 @@ +--TEST-- +Errors: class type parameter cannot be nested inside a generic type in a static method signature +--FILE-- + { + public function __construct(public null|U $value) {} +} + +class Example { + public static function unwrap(Optional $value): mixed { return $value->value; } +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Example and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt b/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt new file mode 100644 index 000000000000..a021dd2ee9e4 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_nullable.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot appear in a nullable type on a static method +--FILE-- + { + public static function foo(?T $x): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_return.phpt b/Zend/tests/generics/errors/type_param_in_static_method_return.phpt new file mode 100644 index 000000000000..bbcd3b326094 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_return.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used as a static method return type +--FILE-- + { + public static function foo(): T {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt b/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt new file mode 100644 index 000000000000..ea2297380515 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_method_variadic.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a variadic static method parameter +--FILE-- + { + public static function foo(T ...$xs): void {} +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property.phpt b/Zend/tests/generics/errors/type_param_in_static_property.phpt new file mode 100644 index 000000000000..55429fe1de8c --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: class type parameter cannot be used in a static property type +--FILE-- + { + public static null|T $a = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt b/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt new file mode 100644 index 000000000000..ba1ba26c80c3 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property_in_trait.phpt @@ -0,0 +1,10 @@ +--TEST-- +Errors: trait type parameter cannot be used as a static property type +--FILE-- + { + public static null|T $slot = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to Bag and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt b/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt new file mode 100644 index 000000000000..a0bfbc78dd30 --- /dev/null +++ b/Zend/tests/generics/errors/type_param_in_static_property_nested.phpt @@ -0,0 +1,14 @@ +--TEST-- +Errors: class type parameter cannot be nested inside a generic type on a static property +--FILE-- + { + public function __construct(public U $value) {} +} + +class A { + public static ?Container $box = null; +} +?> +--EXPECTF-- +Fatal error: Type parameter T is bound to A and cannot be used in static context in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt new file mode 100644 index 000000000000..5e7e4c88737d --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends without args, parent has required type parameter, is a compile error +--FILE-- + {} +class Bad extends Box {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to extends Box in Bad, 0 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt new file mode 100644 index 000000000000..79b2bfa12def --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_non_generic.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends a non-generic class with type arguments is a compile error +--FILE-- + {} +?> +--EXPECTF-- +Fatal error: Type arguments are not allowed on non-generic class Plain in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt b/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt new file mode 100644 index 000000000000..ed07eb160fbf --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_over_default_max.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends Pair with 3 args is a compile error +--FILE-- + {} +class Bad extends Pair {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Pair in Bad, 3 passed and at most 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_too_few.phpt b/Zend/tests/generics/inheritance/arity/extends_too_few.phpt new file mode 100644 index 000000000000..ca15febd2da7 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_too_few.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends with too few type arguments is a compile error +--FILE-- + {} +class Bad extends Pair {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to extends Pair in Bad, 1 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/extends_too_many.phpt b/Zend/tests/generics/inheritance/arity/extends_too_many.phpt new file mode 100644 index 000000000000..d8e7cdc124ba --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/extends_too_many.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: extends with too many type arguments is a compile error +--FILE-- + {} +class Bad extends Box {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Box in Bad, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt new file mode 100644 index 000000000000..49ef6bdcffb2 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/implements_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: implements without args, interface has required type parameter, is a compile error +--FILE-- + {} +class Bad implements I {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to implements I in Bad, 0 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/implements_too_few.phpt b/Zend/tests/generics/inheritance/arity/implements_too_few.phpt new file mode 100644 index 000000000000..1902c7df1a99 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/implements_too_few.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: implements with too few type arguments is a compile error +--FILE-- + {} +class Bad implements Map {} +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to implements Map in Bad, 1 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt b/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt new file mode 100644 index 000000000000..1ebd7e7586e1 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/interface_extends_arity.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: interface extends another interface with wrong arity is a compile error +--FILE-- + {} +interface IExt extends J {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends J in IExt, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt b/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt new file mode 100644 index 000000000000..3c0019575e98 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/no_args_with_defaults.phpt @@ -0,0 +1,16 @@ +--TEST-- +Inheritance arity: no args is OK when all type parameters have defaults +--FILE-- + {} +interface I {} +trait Holder { public T $v; } + +class A extends Box {} +class B implements I {} +class C { use Holder; } + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt b/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt new file mode 100644 index 000000000000..dbaed739f0cf --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/use_trait_no_args_required.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: use Trait without args, trait has required type parameter, is a compile error +--FILE-- + { public T $val; } +class Bad { use Holder; } +?> +--EXPECTF-- +Fatal error: Too few generic type arguments to use Holder in Bad, 0 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt b/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt new file mode 100644 index 000000000000..1c9f085a4074 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/use_trait_too_many.phpt @@ -0,0 +1,9 @@ +--TEST-- +Inheritance arity: use trait with too many type arguments is a compile error +--FILE-- + { public T $v; } +class Bad { use Holder; } +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to use Holder in Bad, 2 passed and exactly 1 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/arity/with_defaults.phpt b/Zend/tests/generics/inheritance/arity/with_defaults.phpt new file mode 100644 index 000000000000..7420fbe70c36 --- /dev/null +++ b/Zend/tests/generics/inheritance/arity/with_defaults.phpt @@ -0,0 +1,19 @@ +--TEST-- +Inheritance arity: defaults extend the accepted range +--FILE-- + {} +interface Maybe {} +trait Slot {} + +// Required-only arity +class P1 extends Pair {} +// Including the optional +class P2 extends Pair {} +// Trait with default — works without args +class T1 { use Slot; } + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt new file mode 100644 index 000000000000..260e17395710 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_pass.phpt @@ -0,0 +1,18 @@ +--TEST-- +Bound check: forwarded T-ref where child's bound is at least as tight as parent's +--FILE-- + {} +interface B extends A {} +interface C extends A {} + +class IntBox {} +class IntSubBox extends IntBox {} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt new file mode 100644 index 000000000000..714911b933fa --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_too_loose.phpt @@ -0,0 +1,12 @@ +--TEST-- +Bound check: forwarded T-ref where child's bound is looser than parent's is rejected +--FILE-- + {} +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Dog on parameter T, Y given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt b/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt new file mode 100644 index 000000000000..be93b443b778 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/bound_on_bound_unbounded_arg.phpt @@ -0,0 +1,10 @@ +--TEST-- +Bound check: forwarded T-ref where child has no bound is rejected when parent requires one +--FILE-- + {} +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Animal on parameter T, W given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt b/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt new file mode 100644 index 000000000000..dd539770f094 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/error_renders_args.phpt @@ -0,0 +1,12 @@ +--TEST-- +Bound error message: link-time bound violation renders NAMED_WITH_ARGS with its type arguments +--FILE-- + {} + +interface Bag> {} + +class IntBag implements Bag {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to implements Bag in IntBag does not satisfy the bound Comparable on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt b/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt new file mode 100644 index 000000000000..aa1517ccc37f --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/extends_concrete_satisfies.phpt @@ -0,0 +1,13 @@ +--TEST-- +Bound check: class extends with a concrete arg satisfying the parent bound +--FILE-- + {} + +class DogBox extends Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/bounds/extends_violates.phpt b/Zend/tests/generics/inheritance/bounds/extends_violates.phpt new file mode 100644 index 000000000000..568a47a0f779 --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/extends_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: class extends with a concrete arg violating the parent bound +--FILE-- + {} + +class IntBox extends Box {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends Box in IntBox does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/implements_violates.phpt b/Zend/tests/generics/inheritance/bounds/implements_violates.phpt new file mode 100644 index 000000000000..ce909c2fa42f --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/implements_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: implements with a concrete arg violating the interface bound +--FILE-- + {} + +class IntBag implements Bag {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to implements Bag in IntBag does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt b/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt new file mode 100644 index 000000000000..34910ac0040d --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/interface_extends_interface_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: interface extends another generic interface with violating arg +--FILE-- + {} + +interface B extends A {} +?> +--EXPECTF-- +Fatal error: Type argument 1 to extends A in B does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt b/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt new file mode 100644 index 000000000000..9381fbcc40fd --- /dev/null +++ b/Zend/tests/generics/inheritance/bounds/use_trait_violates.phpt @@ -0,0 +1,11 @@ +--TEST-- +Bound check: use trait with a concrete arg violating the trait bound +--FILE-- + { public T $val; } + +class IntHolder { use Holder; } +?> +--EXPECTF-- +Fatal error: Type argument 1 to use Holder in IntHolder does not satisfy the bound Animal on parameter T, int given in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt b/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt new file mode 100644 index 000000000000..3f520348eb81 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/conflicting_parent_and_interface.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond: parent and direct interface providing different bindings for the same invariant ancestor (class case is deferred to per-path LSP) +--FILE-- + {} +class P implements Box {} + +class C extends P implements Box {} +$c = new C(); +var_dump($c instanceof Box, $c instanceof P); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt b/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt new file mode 100644 index 000000000000..219ae6c215a9 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/conflicting_via_two_interfaces.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond: class implementing two interfaces that bind the same invariant ancestor (accepted, LSP per path validates any actual methods) +--FILE-- + {} +interface ABox extends Box {} +interface BBox extends Box {} + +class C implements ABox, BBox {} +$c = new C(); +var_dump($c instanceof Box, $c instanceof ABox, $c instanceof BBox); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt b/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt new file mode 100644 index 000000000000..bc413328885c --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/forwarding_t_consistent.phpt @@ -0,0 +1,12 @@ +--TEST-- +Diamond: generic class forwarding the same T into two paths is accepted +--FILE-- + {} +interface Wrapper extends Box {} + +class C implements Wrapper, Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt new file mode 100644 index 000000000000..f0da6a4ebf9e --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/forwarding_t_vs_concrete.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond: a generic class forwarding T conflicts with a concrete arg on another path (accepted for the class form, LSP catches actual signature mismatches) +--FILE-- + {} +interface Wrapper extends Box {} + +class C implements Wrapper, Box {} +$c = new C::(); +var_dump($c instanceof Box, $c instanceof Wrapper); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt new file mode 100644 index 000000000000..00cefe61d12e --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_covariant_missing.phpt @@ -0,0 +1,17 @@ +--TEST-- +Diamond + variance: class implementing a covariant interface diamond must return the intersection +--FILE-- + { public function get(): X; } +interface Flex extends Getter, Getter {} + +class Bad implements Flex { + public function get(): JustFoo { return new JustFoo(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::get(): JustFoo must be compatible with %s::get(): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt new file mode 100644 index 000000000000..cbd41974f4dd --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_class_impl_missing.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond + variance: class implementing an interface diamond must satisfy the merged signature +--FILE-- + { public function set(X $v): void; } +interface Flex extends Setter, Setter {} + +class Bad implements Flex { + public function set(int $v): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::set(int $v): void must be compatible with %s::set(string|int $v): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt new file mode 100644 index 000000000000..4405f1b4a691 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance.phpt @@ -0,0 +1,121 @@ +--TEST-- +Diamond + variance: an interface can itself diamond-inherit a generic ancestor +--FILE-- + { public function set(X $v): void; } +interface I1IntSink extends I1Setter {} +interface I1StrSink extends I1Setter {} +interface I1Flex extends I1IntSink, I1StrSink {} + +final class I1Impl implements I1Flex { + public function set(int|string $v): void { echo "I1: ", var_export($v, true), "\n"; } +} + +interface I2Setter<-X> { public function set(X $v): void; } +interface I2Flex extends I2Setter, I2Setter {} + +final class I2Impl implements I2Flex { + public function set(int|string $v): void { echo "I2: ", var_export($v, true), "\n"; } +} + +interface I3Setter<-X> { public function set(X $v): void; } +interface I3Flex extends I3Setter, I3Setter, I3Setter {} + +final class I3Impl implements I3Flex { + public function set(int|string|float $v): void { echo "I3: ", var_export($v, true), "\n"; } +} + +interface I4Getter<+X : object> { public function get(): X; } +interface I4FooSrc extends I4Getter {} +interface I4BarSrc extends I4Getter {} +interface I4Flex extends I4FooSrc, I4BarSrc {} + +final class I4Impl implements I4Flex { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface I5Getter<+X : object> { public function get(): X; } +interface I5Flex extends I5Getter, I5Getter, I5Getter {} + +final class I5Impl implements I5Flex { + public function get(): IFoo&IBar&IBaz { return new FooBarBaz(); } +} + +interface I6IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface I6Flex extends I6IO, I6IO {} + +final class I6Impl implements I6Flex { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "I6.set: ", var_export($v, true), "\n"; } +} + +interface I7Setter<-X> { public function set(X $v): void; } +interface I7L1A extends I7Setter {} +interface I7L2A extends I7L1A {} +interface I7L1B extends I7Setter {} +interface I7L2B extends I7L1B {} +interface I7Flex extends I7L2A, I7L2B {} + +final class I7Impl implements I7Flex { + public function set(int|string $v): void { echo "I7: ", var_export($v, true), "\n"; } +} + +interface I8Setter<-X> { public function set(X $v): void; } +interface I8Flex extends I8Setter, I8Setter {} + +final class I8Impl implements I8Flex { + public function set(mixed $v): void { echo "I8: ", var_export($v, true), "\n"; } +} + +interface I9Getter<+X : object> { public function get(): X; } +interface I9Flex extends I9Getter, I9Getter {} + +final class I9Impl implements I9Flex { + public function get(): FooBar { return new FooBar(); } +} + +(new I1Impl)->set(7); (new I1Impl)->set("hi"); +(new I2Impl)->set(7); (new I2Impl)->set("hi"); +(new I3Impl)->set(7); (new I3Impl)->set("hi"); (new I3Impl)->set(3.14); +var_dump((new I4Impl)->get() instanceof IFoo, (new I4Impl)->get() instanceof IBar); +var_dump((new I5Impl)->get() instanceof IFoo, (new I5Impl)->get() instanceof IBar, (new I5Impl)->get() instanceof IBaz); +$i6 = new I6Impl; var_dump($i6->get() instanceof FooBar); $i6->set(1); $i6->set("x"); +(new I7Impl)->set(7); (new I7Impl)->set("hi"); +(new I8Impl)->set(7); (new I8Impl)->set("hi"); (new I8Impl)->set(3.14); +var_dump((new I9Impl)->get() instanceof FooBar); + +echo "done\n"; +?> +--EXPECT-- +I1: 7 +I1: 'hi' +I2: 7 +I2: 'hi' +I3: 7 +I3: 'hi' +I3: 3.14 +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +I6.set: 1 +I6.set: 'x' +I7: 7 +I7: 'hi' +I8: 7 +I8: 'hi' +I8: 3.14 +bool(true) +done diff --git a/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt new file mode 100644 index 000000000000..e4a76aed950f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/interface_diamond_variance_invariant_rejected.phpt @@ -0,0 +1,10 @@ +--TEST-- +Diamond + variance: invariant T interface diamond merges via use-site variance +--FILE-- + {} +interface FlexBox extends Box, Box {} +echo (new ReflectionClass(FlexBox::class))->getMethods() === [] ? "ok\n" : "unexpected\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt new file mode 100644 index 000000000000..0377755a4911 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_bad_impl_rejected.phpt @@ -0,0 +1,20 @@ +--TEST-- +Diamond + invariant T: bad concrete implementer is rejected by per-path LSP +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +class Bad implements FooTapper, BarTapper { + public function tap(IFoo $object): IFoo { return $object; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::tap(IFoo $object): IFoo must be compatible with Tapper::tap(IBar $object): IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt new file mode 100644 index 000000000000..65176762e366 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_class_diamond_with_use_site_variance.phpt @@ -0,0 +1,34 @@ +--TEST-- +Diamond + invariant T: a concrete class can satisfy multiple bindings via use-site variance (param union, return intersection) +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +final class FooBarTapper implements FooTapper, BarTapper, Tapper { + public function tap(IFoo | IBar $object): IFoo & IBar { + if ($object instanceof IFoo && $object instanceof IBar) { + return $object; + } + return new FooBar(); + } +} + +$t = new FooBarTapper(); +var_dump($t->tap(new FooBar()) instanceof FooBar); +var_dump($t instanceof FooTapper, $t instanceof BarTapper, $t instanceof Tapper); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt new file mode 100644 index 000000000000..c7d735bcdd2a --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_bad_impl_rejected.phpt @@ -0,0 +1,19 @@ +--TEST-- +Diamond + invariant T: class missing a path of the use-site-merged interface contract is rejected +--FILE-- + { + public function tap(T $object): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class Bad implements FlexTapper { + public function tap(IFoo $o): IFoo { return $o; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::tap(IFoo $o): IFoo must be compatible with %s::tap(IFoo|IBar $object): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt new file mode 100644 index 000000000000..95fa0870079c --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_class_impl.phpt @@ -0,0 +1,31 @@ +--TEST-- +Diamond + invariant T: class implementing a use-site-merged interface diamond +--FILE-- + { + public function tap(T $object): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class Concrete implements FlexTapper { + public function tap(IFoo|IBar $o): IFoo&IBar { + if ($o instanceof IFoo && $o instanceof IBar) { + return $o; + } + return new FooBar(); + } +} + +$c = new Concrete(); +var_dump($c->tap(new FooBar()) instanceof FooBar); +var_dump($c instanceof FlexTapper, $c instanceof Tapper); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt new file mode 100644 index 000000000000..ace90e13e289 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_merge.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + invariant T: interface diamond synthesises use-site-merged abstract contract +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +interface FlexTapper extends FooTapper, BarTapper {} + +$r = new ReflectionClass(FlexTapper::class); +$m = $r->getMethod('tap'); +echo "param: ", (string)$m->getParameters()[0]->getType(), "\n"; +echo "return: ", (string)$m->getReturnType(), "\n"; + +interface FlexDirect extends Tapper, Tapper {} +$rd = new ReflectionClass(FlexDirect::class); +$md = $rd->getMethod('tap'); +echo "direct param: ", (string)$md->getParameters()[0]->getType(), "\n"; +echo "direct return: ", (string)$md->getReturnType(), "\n"; +?> +--EXPECT-- +param: IFoo|IBar +return: IFoo&IBar +direct param: IFoo|IBar +direct return: IFoo&IBar diff --git a/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt new file mode 100644 index 000000000000..61476796c8a7 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_interface_diamond_still_rejected.phpt @@ -0,0 +1,10 @@ +--TEST-- +Diamond + invariant T: arity mismatch is the one remaining rejection at the diamond stage +--FILE-- + {} +interface Alpha extends Multi {} +interface Beta extends Multi {} +?> +--EXPECTF-- +Fatal error: Too many generic type arguments to extends Multi in Beta, 3 passed and exactly 2 expected in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt b/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt new file mode 100644 index 000000000000..184f75bf62f5 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/invariant_tapper_class_diamond.phpt @@ -0,0 +1,52 @@ +--TEST-- +Diamond + invariant T: Tapper with T in both parameter and return positions, concrete class satisfies all paths via use-site variance +--FILE-- + { + public function tap(T $object): T; +} + +interface FooTapper extends Tapper {} +interface BarTapper extends Tapper {} + +class FooBarTapper implements FooTapper, BarTapper, Tapper { + public function tap(IFoo | IBar $object): IFoo & IBar { + if ($object instanceof IFoo) { + if ($object instanceof IBar) { + return $object; + } + + return new FooBar(); + } else { + return new FooBar(); + } + } +} + +$tapper = new FooBarTapper; +$a = $tapper->tap(new Foo()); +$b = $tapper->tap(new Bar()); +$c = $tapper->tap(new FooBar()); + +assert($a instanceof IFoo); +assert($a instanceof IBar); + +assert($b instanceof IFoo); +assert($b instanceof IBar); + +assert($c instanceof IFoo); +assert($c instanceof IBar); + +echo "ok\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt b/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt new file mode 100644 index 000000000000..189699e23766 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/matching_args_via_two_paths.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond: same args via two paths is accepted +--FILE-- + {} +interface ABox extends Box {} +interface BBox extends Box {} + +class C implements ABox, BBox {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt b/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt new file mode 100644 index 000000000000..ad7533afba24 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_intersection_merge.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + nullable: covariant intersection allows null only when every path does +--FILE-- + { public function get(): ?X; } + +interface Flex extends Getter, Getter {} +$r = new ReflectionClass(Flex::class); +echo "merged: ", (string)$r->getMethod('get')->getReturnType(), "\n"; + +class Good1 implements Flex { + public function get(): (IFoo&IBar)|null { return null; } +} + +class Good2 implements Flex { + public function get(): IFoo&IBar { return new FooBar(); } +} + +var_dump((new Good1())->get(), (new Good2())->get() instanceof FooBar); +?> +--EXPECT-- +merged: IFoo&IBar|null +NULL +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt b/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt new file mode 100644 index 000000000000..b16a59c7ee28 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_intersection_missing_branch_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond + nullable intersection: implementer missing one branch is rejected +--FILE-- + { public function get(): ?X; } +interface Flex extends Getter, Getter {} + +class Bad implements Flex { + public function get(): ?IFoo { return null; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::get(): ?IFoo must be compatible with Getter::get(): IFoo&IBar|null in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt b/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt new file mode 100644 index 000000000000..157df44803ae --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_union_merge.phpt @@ -0,0 +1,18 @@ +--TEST-- +Diamond + nullable: contravariant union propagates null from any operand +--FILE-- + { public function set(X $v): void; } + +interface Flex extends Setter, Setter {} +$r = new ReflectionClass(Flex::class); +echo "merged: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; + +class Good implements Flex { + public function set(int|string|null $v): void {} +} +echo "good ok\n"; +?> +--EXPECT-- +merged: string|int|null +good ok diff --git a/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt b/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt new file mode 100644 index 000000000000..958b48a633c7 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/nullable_union_missing_null_rejected.phpt @@ -0,0 +1,13 @@ +--TEST-- +Diamond + nullable union: implementer missing null in param is rejected +--FILE-- + { public function set(X $v): void; } +interface Flex extends Setter, Setter {} + +class Bad implements Flex { + public function set(int|string $v): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::set(string|int $v): void must be compatible with Setter::set(string|int|null $v): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt b/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt new file mode 100644 index 000000000000..94f575d47011 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/parent_supplies_interface_matches.phpt @@ -0,0 +1,12 @@ +--TEST-- +Diamond: parent's transitive binding matches a direct interface binding +--FILE-- + {} +class P implements Box {} + +class C extends P implements Box {} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt new file mode 100644 index 000000000000..ddef3637f995 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + property hook (get-only): covariant T merges return type as intersection +--FILE-- + { + public T $x { get; } +} +interface IFoo {} +interface IBar {} +class FooBar implements IFoo, IBar {} + +interface FlexX extends HasX, HasX {} + +$r = new ReflectionClass(FlexX::class); +$prop = $r->getProperty('x'); +$hooks = $prop->getHooks(); +foreach ($hooks as $kind => $hook) { + if ($hook->hasReturnType()) { + echo "hook $kind return: ", (string)$hook->getReturnType(), "\n"; + } +} + +class C implements FlexX { + public IFoo&IBar $x { get => new FooBar(); } +} +$c = new C; +var_dump($c->x instanceof IFoo, $c->x instanceof IBar); +?> +--EXPECT-- +hook get return: IFoo&IBar +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt new file mode 100644 index 000000000000..81cb4736a929 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/property_hook_get_only_diamond_bad.phpt @@ -0,0 +1,18 @@ +--TEST-- +Diamond + property hook (get-only): impl missing one branch of the merged intersection is rejected +--FILE-- + { + public T $x { get; } +} +interface IFoo {} +interface IBar {} + +interface FlexX extends HasX, HasX {} + +class Bad implements FlexX { + public IFoo $x { get => new class implements IFoo {}; } +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::$x::get(): IFoo must be compatible with HasX::$x::get(): IFoo&IBar in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt b/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt new file mode 100644 index 000000000000..11b81a469813 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/reflection_merged_signatures.phpt @@ -0,0 +1,50 @@ +--TEST-- +Reflection: merged diamond signatures are visible through ReflectionMethod / ReflectionType +--FILE-- + { + public function tap(T $o): T; +} + +interface FlexTapper extends Tapper, Tapper {} + +class C implements FlexTapper { + public function tap(IFoo|IBar $o): IFoo&IBar { + if ($o instanceof IFoo && $o instanceof IBar) { + return $o; + } + return new FooBar(); + } +} + +$flex = (new ReflectionClass(FlexTapper::class))->getMethod('tap'); +echo "FlexTapper::tap return = ", (string)$flex->getReturnType(), "\n"; +echo "FlexTapper::tap param = ", (string)$flex->getParameters()[0]->getType(), "\n"; +echo "FlexTapper::tap return is intersection: ", + ($flex->getReturnType() instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; +echo "FlexTapper::tap param is union: ", + ($flex->getParameters()[0]->getType() instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +$c = (new ReflectionClass(C::class))->getMethod('tap'); +echo "C::tap return = ", (string)$c->getReturnType(), "\n"; +echo "C::tap param = ", (string)$c->getParameters()[0]->getType(), "\n"; +echo "C::tap prototype declaring class = ", $c->getPrototype()->getDeclaringClass()->getName(), "\n"; + +$base = (new ReflectionClass(Tapper::class))->getMethod('tap'); +echo "Tapper::tap return = ", (string)$base->getReturnType(), "\n"; +echo "Tapper::tap param = ", (string)$base->getParameters()[0]->getType(), "\n"; +?> +--EXPECT-- +FlexTapper::tap return = IFoo&IBar +FlexTapper::tap param = IFoo|IBar +FlexTapper::tap return is intersection: yes +FlexTapper::tap param is union: yes +C::tap return = IFoo&IBar +C::tap param = IFoo|IBar +C::tap prototype declaring class = Tapper +Tapper::tap return = object +Tapper::tap param = object diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt new file mode 100644 index 000000000000..35a6ea166b58 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_class_diamond_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Diamond + invariant T: a class double-implementing with scalar bindings is caught by per-path LSP +--FILE-- + { + public function get(): T; +} +interface IntBox extends Box {} +interface StrBox extends Box {} + +class C implements IntBox, StrBox { + public function get() { return 0; } +} +?> +--EXPECTF-- +Fatal error: Declaration of C::get() must be compatible with Box::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt new file mode 100644 index 000000000000..b166332cdd6f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_covariant_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + covariant +T: scalar bindings in a covariant return position are rejected +--FILE-- + { + public function make(): T; +} +interface IntProducer extends Producer {} +interface StrProducer extends Producer {} + +interface FlexProducer extends IntProducer, StrProducer {} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Producer::make() in FlexProducer would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt new file mode 100644 index 000000000000..ae752a8bd4b6 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_invariant_use_site_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + invariant T: scalar bindings at a use-site covariant return are rejected +--FILE-- + { + public function get(): T; +} +interface IntBox extends Box {} +interface StrBox extends Box {} + +interface FlexBox extends IntBox, StrBox {} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Box::get() in FlexBox would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt b/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt new file mode 100644 index 000000000000..0fc3de93172d --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_intersection_trait_rejected.phpt @@ -0,0 +1,14 @@ +--TEST-- +Diamond + trait-use: scalar bindings at a covariant return position are rejected +--FILE-- + { + public function make(): T { throw new Error('stub'); } +} + +class C { + use Producer, Producer; +} +?> +--EXPECTF-- +Fatal error: Diamond inheritance of Producer::make() in C would require return type int&string, which is uninhabited; constrain the type parameter with an object bound in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt b/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt new file mode 100644 index 000000000000..91e8210c4e68 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/scalar_union_param_accepted.phpt @@ -0,0 +1,17 @@ +--TEST-- +Diamond + contravariant -T: scalar bindings at a parameter position merge as a union (no rejection) +--FILE-- + { + public function sink(T $v): void; +} +interface IntSink extends Sink {} +interface StrSink extends Sink {} + +interface FlexSink extends IntSink, StrSink {} + +$r = new ReflectionClass(FlexSink::class); +echo (string)$r->getMethod('sink')->getParameters()[0]->getType(), "\n"; +?> +--EXPECT-- +string|int diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt new file mode 100644 index 000000000000..cef8ac7736fd --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_compiles.phpt @@ -0,0 +1,26 @@ +--TEST-- +Diamond + trait-use: multiple uses of the same generic trait with different bindings merge use-site-variantly at the reflection level +--FILE-- + { + public function set(X $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use Setter, Setter; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('set')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; +echo "is union: ", ($type instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +$c = new C(); +$c->set(1); +$c->set("hello"); +?> +--EXPECT-- +param: string|int +is union: yes +1 +'hello' diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt new file mode 100644 index 000000000000..f0f7645e0d28 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_covariant.phpt @@ -0,0 +1,29 @@ +--TEST-- +Diamond + trait-use: covariant return merges as intersection +--FILE-- + { + public function get(): X { return new FooBar(); } +} + +class C { + use HasGetter, HasGetter; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('get')->getReturnType(); +echo "return: ", (string)$type, "\n"; +echo "is intersection: ", ($type instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; + +$c = new C(); +var_dump($c->get() instanceof IFoo, $c->get() instanceof IBar); +?> +--EXPECT-- +return: IFoo&IBar +is intersection: yes +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt new file mode 100644 index 000000000000..dfc0f7b12403 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_invariant_use_site.phpt @@ -0,0 +1,32 @@ +--TEST-- +Diamond + trait-use: invariant T (no marker) with T in both positions merges use-site +--FILE-- + { + public function pipe(T $x): T { return $x; } +} + +interface IFoo {} +interface IBar {} +class FooBar implements IFoo, IBar {} + +class C { + use Pipe, Pipe; +} + +$r = new ReflectionClass(C::class); +$m = $r->getMethod('pipe'); +echo "param: ", (string)$m->getParameters()[0]->getType(), "\n"; +echo "return: ", (string)$m->getReturnType(), "\n"; +echo "param is union: ", ($m->getParameters()[0]->getType() instanceof ReflectionUnionType ? "yes" : "no"), "\n"; +echo "return is intersection: ", ($m->getReturnType() instanceof ReflectionIntersectionType ? "yes" : "no"), "\n"; + +$c = new C(); +var_dump($c->pipe(new FooBar()) instanceof FooBar); +?> +--EXPECT-- +param: IFoo|IBar +return: IFoo&IBar +param is union: yes +return is intersection: yes +bool(true) diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt new file mode 100644 index 000000000000..57e6dc64b11f --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_mixed_methods.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + trait-use: only T-touching methods are merged; non-generic methods are unaffected +--FILE-- + { + public function set(X $v): void { echo "set ", var_export($v, true), "\n"; } + public function ping(): string { return "pong"; } +} + +class C { + use Box, Box; +} + +$r = new ReflectionClass(C::class); +echo "set param: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; +echo "ping return: ", (string)$r->getMethod('ping')->getReturnType(), "\n"; + +$c = new C; +$c->set(1); +$c->set("hi"); +echo $c->ping(), "\n"; +?> +--EXPECT-- +set param: string|int +ping return: string +set 1 +set 'hi' +pong diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt new file mode 100644 index 000000000000..319c10de75ba --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_multi_param_mixed.phpt @@ -0,0 +1,35 @@ +--TEST-- +Diamond + trait-use: multi-param mixed +T / -U, declared variance drives merge polarity +--FILE-- + { + public function get(): T { throw new Error('stub'); } + public function set(U $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use IO, IO; + + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "set: ", var_export($v, true), "\n"; } +} + +$r = new ReflectionClass(C::class); +echo "get return: ", (string)$r->getMethod('get')->getReturnType(), "\n"; +echo "set param: ", (string)$r->getMethod('set')->getParameters()[0]->getType(), "\n"; + +$c = new C(); +var_dump($c->get() instanceof FooBar); +$c->set(7); +$c->set("hi"); +?> +--EXPECT-- +get return: IFoo&IBar +set param: string|int +bool(true) +set: 7 +set: 'hi' diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt new file mode 100644 index 000000000000..cd7ef2f08aab --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_three_uses.phpt @@ -0,0 +1,26 @@ +--TEST-- +Diamond + trait-use: three uses of the same generic trait fold pairwise into one merged contract +--FILE-- + { + public function sink(X $v): void { echo var_export($v, true), "\n"; } +} + +class C { + use Sink, Sink, Sink; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('sink')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; + +$c = new C; +$c->sink(1); +$c->sink("x"); +$c->sink(3.14); +?> +--EXPECT-- +param: string|int|float +1 +'x' +3.14 diff --git a/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt b/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt new file mode 100644 index 000000000000..df7fde63aa83 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/trait_diamond_variadic.phpt @@ -0,0 +1,28 @@ +--TEST-- +Diamond + trait-use: variadic contravariant param merges as union +--FILE-- + { + public function sink(X ...$xs): void { + foreach ($xs as $x) echo var_export($x, true), "\n"; + } +} + +class C { + use Sink, Sink; +} + +$r = new ReflectionClass(C::class); +$type = $r->getMethod('sink')->getParameters()[0]->getType(); +echo "param: ", (string)$type, "\n"; +echo "is union: ", ($type instanceof ReflectionUnionType ? "yes" : "no"), "\n"; + +(new C)->sink(1, "x", 2, "y"); +?> +--EXPECT-- +param: string|int +is union: yes +1 +'x' +2 +'y' diff --git a/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt b/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt new file mode 100644 index 000000000000..64bdeaab7a66 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variadic_bad_impl_rejected.phpt @@ -0,0 +1,11 @@ +--TEST-- +Diamond + variadic: bad impl rejected by LSP with correctly substituted error message +--FILE-- + { public function sink(X ...$xs): void; } +class Bad implements Sink, Sink { + public function sink(int ...$xs): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of Bad::sink(int ...$xs): void must be compatible with Sink::sink(string ...$xs): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt b/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt new file mode 100644 index 000000000000..882b6c2c2093 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variadic_contravariant_diamond.phpt @@ -0,0 +1,39 @@ +--TEST-- +Diamond + variadic: contravariant T variadic merges as union, exercised at runtime +--FILE-- + { public function sink(X ...$xs): void; } + +class A implements Sink, Sink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "A: ", var_export($x, true), "\n"; + } +} + +interface IntSink extends Sink {} +interface StrSink extends Sink {} +class B implements IntSink, StrSink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "B: ", var_export($x, true), "\n"; + } +} + +interface FlexSink extends IntSink, StrSink {} +class C implements FlexSink { + public function sink(int|string ...$xs): void { + foreach ($xs as $x) echo "C: ", var_export($x, true), "\n"; + } +} + +(new A)->sink(1, "x", 2); +(new B)->sink("y", 3); +(new C)->sink(4, "z"); +?> +--EXPECT-- +A: 1 +A: 'x' +A: 2 +B: 'y' +B: 3 +C: 4 +C: 'z' diff --git a/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt b/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt new file mode 100644 index 000000000000..3c71f2e55417 --- /dev/null +++ b/Zend/tests/generics/inheritance/diamond/variance_positive_all_paths.phpt @@ -0,0 +1,234 @@ +--TEST-- +Diamond + variance: all positive paths: contravariant union, covariant intersection +--FILE-- + { + public function set(X $v): void; +} + +final class A1Impl implements A1Setter, A1Setter { + public function set(int|string $v): void { echo "A1: ", var_export($v, true), "\n"; } +} + +interface A2Setter<-X> { + public function set(X $v): void; +} + +final class A2Impl implements A2Setter, A2Setter, A2Setter { + public function set(int|string|float $v): void { echo "A2: ", var_export($v, true), "\n"; } +} + +interface A3Setter<-X> { + public function set(X $v): void; +} + +interface A3IntSink extends A3Setter {} +interface A3StrSink extends A3Setter {} +final class A3Impl implements A3IntSink, A3StrSink { + public function set(int|string $v): void { echo "A3: ", var_export($v, true), "\n"; } +} + +interface A5Setter<-X> { + public function set(X $v): void; +} +interface A5IntL1 extends A5Setter {} +interface A5IntL2 extends A5IntL1 {} +interface A5StrL1 extends A5Setter {} +interface A5StrL2 extends A5StrL1 {} +final class A5Impl implements A5IntL2, A5StrL2 { + public function set(int|string $v): void { echo "A5: ", var_export($v, true), "\n"; } +} + +class A6Apple {} +class A6Orange {} +interface A6Setter<-X> { + public function set(X $v): void; +} +final class A6Impl implements A6Setter, A6Setter { + public function set(A6Apple|A6Orange $v): void { echo "A6: ", $v::class, "\n"; } +} + +interface A7Sink<-X> { + public function setOne(X $v): void; + public function setTwo(X $other): void; +} +final class A7Impl implements A7Sink, A7Sink { + public function setOne(int|string $v): void { echo "A7.setOne: ", var_export($v, true), "\n"; } + public function setTwo(int|string $other): void { echo "A7.setTwo: ", var_export($other, true), "\n"; } +} + +interface A8Setter<-X> { + public function set(X $v): void; +} +final class A8Impl implements A8Setter, A8Setter { + public function set(mixed $v): void { echo "A8: ", var_export($v, true), "\n"; } +} + + +interface B1Getter<+X : object> { + public function get(): X; +} +final class B1Impl implements B1Getter, B1Getter { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B2Getter<+X : object> { + public function get(): X; +} +final class B2Impl implements B2Getter, B2Getter, B2Getter { + public function get(): IFoo&IBar&IBaz { return new FooBarBaz(); } +} + +interface B3Getter<+X : object> { + public function get(): X; +} +interface B3FooSrc extends B3Getter {} +interface B3BarSrc extends B3Getter {} +final class B3Impl implements B3FooSrc, B3BarSrc { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B4Getter<+X : object> { + public function get(): X; +} +abstract class B4ParentSrc implements B4Getter { + public abstract function get(): IFoo; +} +final class B4Impl extends B4ParentSrc implements B4Getter { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B5Getter<+X : object> { + public function get(): X; +} +interface B5FooL1 extends B5Getter {} +interface B5FooL2 extends B5FooL1 {} +interface B5BarL1 extends B5Getter {} +interface B5BarL2 extends B5BarL1 {} +final class B5Impl implements B5FooL2, B5BarL2 { + public function get(): IFoo&IBar { return new FooBar(); } +} + +interface B6Getter<+X : object> { + public function get(): X; +} +final class B6Impl implements B6Getter, B6Getter { + public function get(): FooBar { return new FooBar(); } +} + + +interface C1IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +final class C1Impl implements C1IO, C1IO { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C1.set: ", var_export($v, true), "\n"; } +} + +interface C2IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface C2FooInt extends C2IO {} +interface C2BarStr extends C2IO {} +final class C2Impl implements C2FooInt, C2BarStr { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C2.set: ", var_export($v, true), "\n"; } +} + +interface C3IO<+X : object, -Y> { + public function get(): X; + public function set(Y $v): void; +} +interface C3FooIntL1 extends C3IO {} +interface C3FooIntL2 extends C3FooIntL1 {} +interface C3BarStrL1 extends C3IO {} +interface C3BarStrL2 extends C3BarStrL1 {} +final class C3Impl implements C3FooIntL2, C3BarStrL2 { + public function get(): IFoo&IBar { return new FooBar(); } + public function set(int|string $v): void { echo "C3.set: ", var_export($v, true), "\n"; } +} + + +interface D1Setter<-X> { + public function set(X $v): void; +} +interface D1Wrap<-Y> extends D1Setter {} +final class D1Impl implements D1Wrap, D1Setter { + public function set(int|string $v): void { echo "D1: ", var_export($v, true), "\n"; } +} + + +$a1 = new A1Impl(); $a1->set(7); $a1->set("hi"); +$a2 = new A2Impl(); $a2->set(7); $a2->set("hi"); $a2->set(3.14); +$a3 = new A3Impl(); $a3->set(7); $a3->set("hi"); +$a5 = new A5Impl(); $a5->set(7); $a5->set("hi"); +$a6 = new A6Impl(); $a6->set(new A6Apple()); $a6->set(new A6Orange()); +$a7 = new A7Impl(); $a7->setOne(7); $a7->setTwo("hi"); +$a8 = new A8Impl(); $a8->set(7); $a8->set("hi"); $a8->set(3.14); + +$b1 = new B1Impl(); var_dump($b1->get() instanceof IFoo, $b1->get() instanceof IBar); +$b2 = new B2Impl(); var_dump($b2->get() instanceof IFoo, $b2->get() instanceof IBar, $b2->get() instanceof IBaz); +$b3 = new B3Impl(); var_dump($b3->get() instanceof IFoo, $b3->get() instanceof IBar); +$b4 = new B4Impl(); var_dump($b4->get() instanceof IFoo, $b4->get() instanceof IBar); +$b5 = new B5Impl(); var_dump($b5->get() instanceof IFoo, $b5->get() instanceof IBar); +$b6 = new B6Impl(); var_dump($b6->get() instanceof FooBar); + +$c1 = new C1Impl(); var_dump($c1->get() instanceof FooBar); $c1->set(7); $c1->set("hi"); +$c2 = new C2Impl(); var_dump($c2->get() instanceof FooBar); $c2->set(7); $c2->set("hi"); +$c3 = new C3Impl(); var_dump($c3->get() instanceof FooBar); $c3->set(7); $c3->set("hi"); + +$d1 = new D1Impl(); $d1->set(7); $d1->set("hi"); + +echo "done\n"; +?> +--EXPECT-- +A1: 7 +A1: 'hi' +A2: 7 +A2: 'hi' +A2: 3.14 +A3: 7 +A3: 'hi' +A5: 7 +A5: 'hi' +A6: A6Apple +A6: A6Orange +A7.setOne: 7 +A7.setTwo: 'hi' +A8: 7 +A8: 'hi' +A8: 3.14 +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +C1.set: 7 +C1.set: 'hi' +bool(true) +C2.set: 7 +C2.set: 'hi' +bool(true) +C3.set: 7 +C3.set: 'hi' +D1: 7 +D1: 'hi' +done diff --git a/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt b/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt new file mode 100644 index 000000000000..710f785f0ed8 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/instanceof_monomorph_parent.phpt @@ -0,0 +1,46 @@ +--TEST-- +Extends-with-args: subclass's direct parent is the canonical monomorph +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$i = new IntBox(42); +$s = new StrBox("hi"); + +// instanceof at the AST level discards type arguments, so we verify the +// monomorph parent via reflection and string class comparisons instead. +var_dump($i instanceof Box); +var_dump($i instanceof IntBox); + +$rcInt = new ReflectionClass(IntBox::class); +$rcStr = new ReflectionClass(StrBox::class); +var_dump($rcInt->getParentClass()->getName()); +var_dump($rcStr->getParentClass()->getName()); + +// Each subclass's direct parent is its own distinct monomorph. +var_dump($rcInt->getParentClass()->getName() !== $rcStr->getParentClass()->getName()); + +// The grandparent is the bare base, shared. +var_dump($rcInt->getParentClass()->getParentClass()->getName()); +var_dump($rcInt->getParentClass()->getParentClass()->getName() + === $rcStr->getParentClass()->getParentClass()->getName()); + +// And the canonical monomorphs are registered with their canonical names. +var_dump(class_exists("Box", false)); +var_dump(class_exists("Box", false)); +?> +--EXPECT-- +bool(true) +bool(true) +string(8) "Box" +string(11) "Box" +bool(true) +string(3) "Box" +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt b/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt new file mode 100644 index 000000000000..8b694703ac69 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/property_hook_on_inherited_T.phpt @@ -0,0 +1,39 @@ +--TEST-- +Extends-with-args: property hook on inherited T-typed property survives monomorph link +--FILE-- + { + public T $value { + get => $this->value; + set(T $v) { $this->value = $v; } + } + public function __construct(T $v) { $this->value = $v; } +} + +class IntBox extends Box {} + +$b = new IntBox(42); +echo "type: ", (new ReflectionProperty($b, "value"))->getType(), "\n"; +echo "value: ", $b->value, "\n"; + +// Set hook with the substituted type accepts an int. +$b->value = 99; +echo "after set: ", $b->value, "\n"; + +// And the inherited typed property is monomorph-substituted to int. +$rc = new ReflectionClass($b); +echo "parent: ", $rc->getParentClass()->getName(), "\n"; + +$baseRC = new ReflectionClass(Box::class); +echo "base property type: ", $baseRC->getProperty('value')->getType(), "\n"; + +$monoRC = new ReflectionClass("Box"); +echo "mono property type: ", $monoRC->getProperty('value')->getType(), "\n"; +?> +--EXPECT-- +type: int +value: 42 +after set: 99 +parent: Box +base property type: mixed +mono property type: int diff --git a/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt b/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt new file mode 100644 index 000000000000..0930b9b017cb --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/recursive_bounds_extends.phpt @@ -0,0 +1,37 @@ +--TEST-- +Extends-with-args: subclass with recursive-bound type parameters extending a generic parent +--FILE-- + {} + +class Pair, U: Box> { + public function __construct(public Box $left, public Box $right) {} +} + +// Subclass binds Pair's parameters to two concrete monomorphs. +class StrictPair, B: Box> extends Pair {} + +// Two further subclasses, both passing concrete args to StrictPair. +class IntFirst extends StrictPair, Box> {} +class StrFirst extends StrictPair, Box> {} + +$p1 = new IntFirst(new Box(), new Box()); +$p2 = new StrFirst(new Box(), new Box()); + +echo (new ReflectionClass($p1))->getParentClass()->getName(), "\n"; +echo (new ReflectionClass($p2))->getParentClass()->getName(), "\n"; + +// Distinct StrictPair monos. +var_dump((new ReflectionClass($p1))->getParentClass()->getName() + !== (new ReflectionClass($p2))->getParentClass()->getName()); + +// Both transitively extend Pair (bare base, two hops up). +var_dump($p1 instanceof Pair); +var_dump($p2 instanceof Pair); +?> +--EXPECT-- +StrictPair,Box> +StrictPair,Box> +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt b/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt new file mode 100644 index 000000000000..b2657361e619 --- /dev/null +++ b/Zend/tests/generics/inheritance/extends_args/variance_through_synthesis.phpt @@ -0,0 +1,38 @@ +--TEST-- +Variance enforcement at synthesis: extends-with-args composes through the monomorph +--FILE-- + { + public function get(): T { return new Animal(); } +} + +class DogBox extends Box {} +$d = new DogBox(); +var_dump($d instanceof Box); +var_dump((new ReflectionClass($d))->getParentClass()->getName()); + +// Two synthesized monos extending the same base both succeed; their own +// concrete signatures honour covariance. +class AnimalBox extends Box {} +$rcAnimalMono = new ReflectionClass("Box"); +echo "Box::get returns: ", $rcAnimalMono->getMethod('get')->getReturnType(), "\n"; +$rcDogMono = new ReflectionClass("Box"); +echo "Box::get returns: ", $rcDogMono->getMethod('get')->getReturnType(), "\n"; + +// Bound violation at extends-with-args fires the standard error. +try { + eval('class IntBox extends Box {}'); +} catch (\Throwable $e) { + echo "bound violation: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +bool(true) +string(8) "Box" +Box::get returns: Animal +Box::get returns: Dog +%aType argument 1 to extends Box in IntBox does not satisfy the bound Animal on parameter T, int given%a diff --git a/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt b/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt new file mode 100644 index 000000000000..7af42f043164 --- /dev/null +++ b/Zend/tests/generics/inheritance/implements_args/implements_monomorph.phpt @@ -0,0 +1,61 @@ +--TEST-- +Implements-with-args: implementing class's direct interface is the canonical monomorph +--FILE-- + { + public function next(): T; +} + +class IntIter implements Iter { + public function __construct(public int $v) {} + public function next(): int { return ++$this->v; } +} + +class StrIter implements Iter { + public function next(): string { return "a"; } +} + +$i = new IntIter(0); + +// The monomorph is recorded as the directly-implemented interface; the bare +// base shows up transitively. +$rcInt = new ReflectionClass(IntIter::class); +$names = $rcInt->getInterfaceNames(); +sort($names); +var_dump($names); + +$rcStr = new ReflectionClass(StrIter::class); +$names = $rcStr->getInterfaceNames(); +sort($names); +var_dump($names); + +// instanceof against the bare base works transitively. +var_dump($i instanceof Iter); + +// The monomorph and its base are registered. +var_dump(interface_exists("Iter", false)); +var_dump(interface_exists("Iter", false)); +var_dump(interface_exists("Iter", false)); + +// The monomorph itself reports the correct kind. +$rcMono = new ReflectionClass("Iter"); +var_dump($rcMono->isInterface()); +?> +--EXPECT-- +array(2) { + [0]=> + string(4) "Iter" + [1]=> + string(9) "Iter" +} +array(2) { + [0]=> + string(4) "Iter" + [1]=> + string(12) "Iter" +} +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt b/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt new file mode 100644 index 000000000000..674b98e12375 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/abstract_class_chain.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: abstract generic class extended by another abstract class, then concretized +--FILE-- + { + abstract public function process(T $x): T; +} + +abstract class Middle extends Base { + public function double(U $x): U { return $this->process($this->process($x)); } +} + +class IntLeaf extends Middle { + public function process(int $x): int { return $x + 1; } +} + +echo (new IntLeaf)->double(10), "\n"; +?> +--EXPECT-- +12 diff --git a/Zend/tests/generics/inheritance/lsp/abstract_method.phpt b/Zend/tests/generics/inheritance/lsp/abstract_method.phpt new file mode 100644 index 000000000000..27773e88f331 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/abstract_method.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: substitution applies to abstract methods inherited from a generic ancestor +--FILE-- + { + abstract public function unwrap(): T; +} + +class IntContainer extends Container { + public function __construct(private int $v) {} + public function unwrap(): int { return $this->v; } +} + +echo (new IntContainer(11))->unwrap(), "\n"; +?> +--EXPECT-- +11 diff --git a/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt b/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt new file mode 100644 index 000000000000..2b96fb813731 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/basic_param_and_return.phpt @@ -0,0 +1,29 @@ +--TEST-- +Parametric LSP: child overrides param and return with the bound argument +--FILE-- + { + public function put(T $value): void; + public function get(): T; +} + +class IntBag implements Bag { + private int $v = 0; + public function put(int $value): void { $this->v = $value; } + public function get(): int { return $this->v; } +} + +$b = new IntBag; +$b->put(42); +echo $b->get(), "\n"; + +try { + /** @phpstan-ignore-next-line */ + $b->put("no"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +42 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/bounded_param.phpt b/Zend/tests/generics/inheritance/lsp/bounded_param.phpt new file mode 100644 index 000000000000..858387ac74b7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/bounded_param.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: substitution still composes with a bounded type parameter +--FILE-- + { + public function add(T $x): void; +} + +class DogBag implements Bag { + public function add(Dog $x): void { echo "added ", $x->name(), "\n"; } +} + +(new DogBag)->add(new Dog); +?> +--EXPECT-- +added dog diff --git a/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt b/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt new file mode 100644 index 000000000000..adbf9729176b --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/by_reference_substituted.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: by-reference parameter substituted with the bound argument +--FILE-- + { + public function fill(T &$slot): void; +} + +class IntFiller implements Filler { + public function fill(int &$slot): void { $slot = 99; } +} + +$x = 0; +(new IntFiller)->fill($x); +echo $x, "\n"; +?> +--EXPECT-- +99 diff --git a/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt b/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt new file mode 100644 index 000000000000..e7420179aeb9 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/contravariant_param.phpt @@ -0,0 +1,21 @@ +--TEST-- +Parametric LSP: contravariant parameter — child accepts a supertype of the bound argument +--FILE-- + { + public function eat(T $x): void; +} + +class DogEater implements Eater { + public function eat(Animal $x): void { echo "ate ", get_class($x), "\n"; } +} + +(new DogEater)->eat(new Dog); +(new DogEater)->eat(new Animal); +?> +--EXPECT-- +ate Dog +ate Animal diff --git a/Zend/tests/generics/inheritance/lsp/covariant_return.phpt b/Zend/tests/generics/inheritance/lsp/covariant_return.phpt new file mode 100644 index 000000000000..0985b93de5a2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/covariant_return.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: covariant return — child returns a strict subtype of the bound argument +--FILE-- + { + public function produce(): T; +} + +class DogProducer implements Producer { + public function produce(): Dog { return new Dog; } +} + +echo get_class((new DogProducer)->produce()), "\n"; +?> +--EXPECT-- +Dog diff --git a/Zend/tests/generics/inheritance/lsp/default_param.phpt b/Zend/tests/generics/inheritance/lsp/default_param.phpt new file mode 100644 index 000000000000..648b477c78b2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/default_param.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: a default type parameter still allows the child's signature to be erased-compatible +--FILE-- + { + public function set(T $value): void; +} + +class IntHolder implements Holder { + public function set(int $value): void { echo "set $value\n"; } +} + +(new IntHolder)->set(5); +?> +--EXPECT-- +set 5 diff --git a/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt b/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt new file mode 100644 index 000000000000..2ac2872cafc7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/diamond_via_interfaces.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: diamond inheritance via two generic intermediaries with the same target +--FILE-- + { + public function id(T $x): T; +} + +interface LeftPath extends Base {} +interface RightPath extends Base {} + +class IntDiamond implements LeftPath, RightPath { + public function id(int $x): int { return $x; } +} + +echo (new IntDiamond)->id(99), "\n"; +?> +--EXPECT-- +99 diff --git a/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt b/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt new file mode 100644 index 000000000000..d2a7f87fccd2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/extends_and_implements_generic.phpt @@ -0,0 +1,23 @@ +--TEST-- +Parametric LSP: a class extending a generic class while implementing an unrelated generic interface +--FILE-- + { + public function append(T $value): void { echo "append:$value\n"; } +} + +interface Counter { + public function count(): U; +} + +class IntListWithCount extends BaseList implements Counter { + public function count(): int { return 0; } +} + +$l = new IntListWithCount; +$l->append(7); +echo $l->count(), "\n"; +?> +--EXPECT-- +append:7 +0 diff --git a/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt b/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt new file mode 100644 index 000000000000..0705d638d7c2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/generic_class_extends_generic.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: a generic class extending a generic class forwards bindings to subclasses +--FILE-- + { + public function f(X $x): void {} +} + +class B extends A {} + +class C extends B { + public function f(int $x): void { echo "C::f($x)\n"; } +} + +(new C)->f(7); +?> +--EXPECT-- +C::f(7) diff --git a/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt b/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt new file mode 100644 index 000000000000..8aa381658bea --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/impl_with_identity_type_param_binding.phpt @@ -0,0 +1,24 @@ +--TEST-- +Parametric LSP: child implementing `I` with its own TN forwarded to parent's TN +--FILE-- + { + public function getEdgesFrom(TN $from): array; +} + +class DG implements GI { + public function getEdgesFrom(TN $from): array { return []; } +} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt b/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt new file mode 100644 index 000000000000..c8b0fb55d328 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_level_param_bound_subst.phpt @@ -0,0 +1,31 @@ +--TEST-- +Parametric LSP: method-level type-parameter bound that references a class-scope T substitutes when the subclass binds T +--FILE-- + { function set(U $x) }` — the method's +// own U has a bound that's a class-scope T-ref. When `class B extends A` +// supplies T = string, the inheritance check on the inherited (or overridden) +// method must see U's bound substituted from T → string, otherwise the parent +// renders as `set(mixed $x)` (T erased) and the child's `set(U $x)` +// gets rejected as narrower than mixed. + +class A { + public function set(U $x): void {} +} + +class B extends A { + public function set(U $x): void {} +} + +class C { + public function set(U $x): void {} +} + +class D extends C { + public function set(U $x): void {} +} + +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt b/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt new file mode 100644 index 000000000000..ef2fbdc435b8 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_param_separate_scope.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: a method-level generic parameter is not affected by class-level substitution +--FILE-- + { + public function get(): T; + public function map(callable $f): U; +} + +class IntContainer implements Container { + public function get(): int { return 5; } + public function map(callable $f): U { return $f(42); } +} + +echo (new IntContainer)->get(), "\n"; +echo (new IntContainer)->map(fn($x) => "v=$x"), "\n"; +?> +--EXPECT-- +5 +v=42 diff --git a/Zend/tests/generics/inheritance/lsp/method_substituted.phpt b/Zend/tests/generics/inheritance/lsp/method_substituted.phpt new file mode 100644 index 000000000000..ec3fd14778b5 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/method_substituted.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: methods on the parent are substituted in the child +--FILE-- + { + abstract public function make(): T; +} + +class IntFactory extends Factory { + public function make(): int { return 7; } +} + +echo (new IntFactory())->make(), "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/inheritance/lsp/multi_interface.phpt b/Zend/tests/generics/inheritance/lsp/multi_interface.phpt new file mode 100644 index 000000000000..12f6fbff896e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/multi_interface.phpt @@ -0,0 +1,25 @@ +--TEST-- +Parametric LSP: implementing two distinct generic interfaces with different bindings +--FILE-- + { + public function read(): T; +} + +interface Writer { + public function write(U $value): void; +} + +class IntReaderStringWriter implements Reader, Writer { + private string $w = ""; + public function read(): int { return 42; } + public function write(string $value): void { $this->w = $value; echo "wrote: $value\n"; } +} + +$x = new IntReaderStringWriter; +echo $x->read(), "\n"; +$x->write("hello"); +?> +--EXPECT-- +42 +wrote: hello diff --git a/Zend/tests/generics/inheritance/lsp/multi_param.phpt b/Zend/tests/generics/inheritance/lsp/multi_param.phpt new file mode 100644 index 000000000000..36775f44b5e4 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/multi_param.phpt @@ -0,0 +1,23 @@ +--TEST-- +Parametric LSP: multi-parameter generic interface substitutes both parameters +--FILE-- + { + public function set(K $key, V $value): void; + public function lookup(K $key): V; +} + +class StrIntMap implements Map { + private array $items = []; + public function set(string $key, int $value): void { $this->items[$key] = $value; } + public function lookup(string $key): int { return $this->items[$key] ?? -1; } +} + +$m = new StrIntMap; +$m->set("a", 1); +echo $m->lookup("a"), "\n"; +echo $m->lookup("missing"), "\n"; +?> +--EXPECT-- +1 +-1 diff --git a/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt b/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt new file mode 100644 index 000000000000..19dc88618b12 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/non_generic_inherits_unrelated.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: a non-generic class still uses standard LSP when the parent is non-generic +--FILE-- +f(3), "\n"; +?> +--EXPECT-- +8 diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt new file mode 100644 index 000000000000..dad6ab6004df --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_chain_subst.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: ?T return substitutes through a multi-level chain (interface extending interface) +--FILE-- + { + public function next(): ?T; +} + +interface J extends I {} + +class C implements J { + public function next(): ?int { return 7; } +} + +var_dump((new C)->next()); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt new file mode 100644 index 000000000000..37c97e94b636 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_param_subst.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: child param narrower than ?T (substituted to ?int) is rejected +--FILE-- + { + public function take(?T $x): void {} +} + +class B extends A { + public function take(int $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of B::take(int $x): void must be compatible with A::take(?int $x): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt b/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt new file mode 100644 index 000000000000..0760cc0b9a5a --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/nullable_T_return_subst.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: ?T return type substitutes through implements with the nullable bit preserved +--FILE-- + { + public function f(): ?T; +} + +class B implements A { + public function f(): ?int { return 1; } +} + +var_dump((new B)->f()); +?> +--EXPECT-- +int(1) diff --git a/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt b/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt new file mode 100644 index 000000000000..4b89ae236d43 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/optional_arg_substituted.phpt @@ -0,0 +1,27 @@ +--TEST-- +Parametric LSP: optional parameter substituted with the bound argument +--FILE-- + { + public function set(T $value, bool $flag = false): void; +} + +class IntMaybe implements Maybe { + public function set(int $value, bool $flag = false): void { echo "$value/", (int)$flag, "\n"; } +} + +$m = new IntMaybe; +$m->set(5); +$m->set(6, true); + +try { + /** @phpstan-ignore-next-line */ + $m->set("x"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +5/0 +6/1 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/parent_call.phpt b/Zend/tests/generics/inheritance/lsp/parent_call.phpt new file mode 100644 index 000000000000..89dee35e724b --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/parent_call.phpt @@ -0,0 +1,22 @@ +--TEST-- +Parametric LSP: calling parent::method() from substituted child still routes to the erased parent +--FILE-- + { + public function f(T $x): void { + echo "C::f got ", get_debug_type($x), "\n"; + } +} + +class IntC extends C { + public function f(int $x): void { + echo "IntC::f got $x\n"; + parent::f($x); + } +} + +(new IntC)->f(3); +?> +--EXPECT-- +IntC::f got 3 +C::f got int diff --git a/Zend/tests/generics/inheritance/lsp/reordered_params.phpt b/Zend/tests/generics/inheritance/lsp/reordered_params.phpt new file mode 100644 index 000000000000..a47500267117 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/reordered_params.phpt @@ -0,0 +1,32 @@ +--TEST-- +Parametric LSP: child reorders the parent's type parameters +--FILE-- + { + public function first(): A; + public function second(): B; +} + +class IntStrPair implements Pair { + public function __construct(private int $a, private string $b) {} + public function first(): int { return $this->a; } + public function second(): string { return $this->b; } +} + +class FlippedPair implements Pair { + public function __construct(private mixed $x, private mixed $y) {} + public function first(): Y { return $this->y; } + public function second(): X { return $this->x; } +} + +class StrIntFlipped extends FlippedPair {} + +$p = new IntStrPair(1, "hi"); +echo $p->first(), "/", $p->second(), "\n"; + +$f = new StrIntFlipped(7, "world"); +echo $f->first(), "/", $f->second(), "\n"; +?> +--EXPECT-- +1/hi +world/7 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt b/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt new file mode 100644 index 000000000000..99d541d5014d --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_three_level.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: substitution composes across three levels of generic ancestors +--FILE-- + { + public function take(T $x): T; +} +interface M extends L {} +interface N extends M {} + +class IntN implements N { + public function take(int $x): int { return $x + 1; } +} + +echo (new IntN)->take(41), "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt b/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt new file mode 100644 index 000000000000..9f02cefc3f9e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_through_class.phpt @@ -0,0 +1,19 @@ +--TEST-- +Parametric LSP: substitution composes through a generic abstract class and a concrete leaf +--FILE-- + { + public function area(): T; +} + +abstract class Polygon implements Shape {} + +class IntSquare extends Polygon { + public function __construct(private int $side) {} + public function area(): int { return $this->side * $this->side; } +} + +echo (new IntSquare(4))->area(), "\n"; +?> +--EXPECT-- +16 diff --git a/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt b/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt new file mode 100644 index 000000000000..0a78e360a6e2 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/transitive_two_level.phpt @@ -0,0 +1,20 @@ +--TEST-- +Parametric LSP: substitution composes through a generic intermediate +--FILE-- + { + public function consume(T $value): void; +} + +interface Service extends Consumer {} + +class NullService implements Service { + public function consume(null $value): void { + echo "consumed null\n"; + } +} + +(new NullService)->consume(null); +?> +--EXPECT-- +consumed null diff --git a/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt b/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt new file mode 100644 index 000000000000..a11fba5996cd --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/union_of_type_params_narrower_return.phpt @@ -0,0 +1,25 @@ +--TEST-- +Parametric LSP: child narrowing Tl|Tr return down to a single Tl is accepted +--FILE-- + { + abstract public function get(): T; +} + +class LeftOnly extends Base { + public function __construct(private Tl $value) {} + public function get(): Tl { return $this->value; } +} + +$a = new LeftOnly::("hi"); +var_dump($a->get()); + +$b = new LeftOnly::(42); +var_dump($b->get()); +?> +--EXPECT-- +string(2) "hi" +int(42) diff --git a/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt b/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt new file mode 100644 index 000000000000..bb561a56edf1 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/union_of_type_params_return_subst.phpt @@ -0,0 +1,49 @@ +--TEST-- +Parametric LSP: child returning union of two type parameters (Tl|Tr) satisfies parent's T when parent is bound to Tl|Tr +--FILE-- + with overrides returning Tl|Tr. + +abstract class Base { + abstract public function get(): T; +} + +class Pair extends Base { + public function __construct(private Tl|Tr $value) {} + public function get(): Tl|Tr { return $this->value; } +} + +$p = new Pair::("hi"); +var_dump($p->get()); + +$q = new Pair::(42); +var_dump($q->get()); + +// Also exercise the interface-implements path, which is what the Psl +// failure actually hit (UnionType extends Type which implements TypeInterface). +interface I<+T> { + public function read(mixed $v): T; +} + +abstract class IBase implements I { + abstract public function read(mixed $v): T; +} + +class Union extends IBase { + public function read(mixed $v): Tl|Tr { + if ($v instanceof Tl || $v instanceof Tr) return $v; + throw new TypeError("nope"); + } +} + +echo "loaded\n"; +?> +--EXPECT-- +string(2) "hi" +int(42) +loaded diff --git a/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt b/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt new file mode 100644 index 000000000000..abc2f86c8db7 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/variadic_substituted.phpt @@ -0,0 +1,24 @@ +--TEST-- +Parametric LSP: variadic parameter substituted with the bound argument +--FILE-- + { + public function acceptAll(T ...$items): int; +} + +class IntAcceptor implements Acceptor { + public function acceptAll(int ...$items): int { return array_sum($items); } +} + +echo (new IntAcceptor)->acceptAll(1, 2, 3, 4), "\n"; + +try { + /** @phpstan-ignore-next-line */ + (new IntAcceptor)->acceptAll(1, "no"); +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +10 +TypeError diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt new file mode 100644 index 000000000000..e9a493b12132 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_sibling_of_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Parametric LSP: child of `extends A` cannot override return type T with a sibling of the bound (Cat) +--FILE-- + { + public function get(): T {} +} + +class DogChild extends A { + public function get(): Cat { return new Cat(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of DogChild::get(): Cat must be compatible with A::get(): Dog in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt new file mode 100644 index 000000000000..7dc31ae3607e --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_transitive.phpt @@ -0,0 +1,16 @@ +--TEST-- +Parametric LSP: violation propagates through a two-level extends chain +--FILE-- + { + abstract public function get(): T; +} + +abstract class B extends A {} + +class C extends B { + public function get(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of C::get(): bool must be compatible with A::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt b/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt new file mode 100644 index 000000000000..035ec8bd0538 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_extends_return_unbounded.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: child of `extends A` cannot override return type T with an unrelated type +--FILE-- + { + public function get(): T {} +} + +class IntChild extends A { + public function get(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of IntChild::get(): bool must be compatible with A::get(): int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt b/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt new file mode 100644 index 000000000000..93db9b0e7e96 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_nullable_T_message.phpt @@ -0,0 +1,14 @@ +--TEST-- +Parametric LSP: error message for a ?T return violation shows the substituted nullable form +--FILE-- + { + public function f(): ?T {} +} + +class B extends A { + public function f(): bool { return true; } +} +?> +--EXPECTF-- +Fatal error: Declaration of B::f(): bool must be compatible with A::f(): ?int in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt b/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt new file mode 100644 index 000000000000..832a8c92bee0 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_param_too_specific.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child param more specific than the bound argument is rejected (contravariance) +--FILE-- + { + public function eat(T $x): void; +} + +class TooSpecificEater implements Eater { + public function eat(Dog $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of TooSpecificEater::eat(Dog $x): void must be compatible with Eater::eat(Animal $x): void in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt b/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt new file mode 100644 index 000000000000..bfd442d0c484 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_return_too_general.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child return more general than the bound argument is rejected (covariance) +--FILE-- + { + public function produce(): T; +} + +class TooGeneralProducer implements Producer { + public function produce(): Animal { return new Animal; } +} +?> +--EXPECTF-- +Fatal error: Declaration of TooGeneralProducer::produce(): Animal must be compatible with Producer::produce(): Dog in %s on line %d diff --git a/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt b/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt new file mode 100644 index 000000000000..0e83ce757c60 --- /dev/null +++ b/Zend/tests/generics/inheritance/lsp/violation_union_of_t_refs_widened.phpt @@ -0,0 +1,17 @@ +--TEST-- +Parametric LSP: child returning Tl|Tr|string (a widening of the parent's substituted Tl|Tr) is rejected +--FILE-- + { + abstract public function get(): T; +} + +class BadPair extends Base { + public function get(): Tl|Tr|string { throw new Exception(); } +} +?> +--EXPECTF-- +Fatal error: Declaration of BadPair::get(): Tl|Tr|string must be compatible with Base::get(): Tl|Tr in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt b/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt new file mode 100644 index 000000000000..9b08820032dc --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_concrete_unchanged.phpt @@ -0,0 +1,16 @@ +--TEST-- +Inherited methods: concretely typed parameters and returns are unaffected by substitution +--FILE-- + { + public function bare(int $x): string { return (string)$x; } +} + +class IntHelper extends Helper {} + +$rm = (new ReflectionClass('IntHelper'))->getMethod('bare'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int -> string diff --git a/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt b/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt new file mode 100644 index 000000000000..f01ef9796d5d --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_from_interface.phpt @@ -0,0 +1,15 @@ +--TEST-- +Inherited methods: abstract method inherited from a generic interface is substituted on the implementing class +--FILE-- + { + public function unwrap(): T; +} + +abstract class IntBoxImpl implements Box {} + +$rm = (new ReflectionClass('IntBoxImpl'))->getMethod('unwrap'); +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int diff --git a/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt b/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt new file mode 100644 index 000000000000..9ceac319adf7 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_method_with_named_with_args_return.phpt @@ -0,0 +1,20 @@ +--TEST-- +Inherited methods: substituted clone keeps the erased class-name view when the return type wraps a method-level type parameter +--FILE-- + { + private mixed $value; + public function __construct(mixed $val) { $this->value = $val; } + public static function create(O $value): Box { return new self($value); } +} + +class StringBox extends Box {} + +$b = StringBox::create("hello"); +var_dump($b instanceof Box); +$rm = (new ReflectionMethod(StringBox::class, 'create')); +echo "return type: ", (string)$rm->getReturnType(), "\n"; +?> +--EXPECT-- +bool(true) +return type: Box diff --git a/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt b/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt new file mode 100644 index 000000000000..c64ffc005ad9 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_signature_substituted.phpt @@ -0,0 +1,21 @@ +--TEST-- +Inherited methods: param and return types are substituted on a class extending a generic parent +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBase extends Base {} +class StrBase extends Base {} + +foreach (['IntBase', 'StrBase'] as $cn) { + $rm = (new ReflectionClass($cn))->getMethod('take'); + echo "$cn::take: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +} +?> +--EXPECT-- +IntBase::take: int -> int +StrBase::take: string -> string diff --git a/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt b/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt new file mode 100644 index 000000000000..42d27b7d7ffb --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_signature_transitive.phpt @@ -0,0 +1,17 @@ +--TEST-- +Inherited methods: substitution composes through a generic intermediate class +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class Mid extends Base {} +class Leaf extends Mid {} + +$rm = (new ReflectionClass('Leaf'))->getMethod('take'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +float -> float diff --git a/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt b/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt new file mode 100644 index 000000000000..c2dbc918e450 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/inherited_variadic_substituted.phpt @@ -0,0 +1,15 @@ +--TEST-- +Inherited methods: variadic parameter typed T is substituted on the child class +--FILE-- + { + public function all(T ...$xs): int { return count($xs); } +} + +class IntBase extends Base {} + +$rm = (new ReflectionClass('IntBase'))->getMethod('all'); +echo "param: ", $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +param: int diff --git a/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt b/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt new file mode 100644 index 000000000000..d468988629b6 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_check_transitive.phpt @@ -0,0 +1,24 @@ +--TEST-- +Inherited methods: runtime check fires across a multi-level generic chain +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class Mid extends Base {} + +class Leaf extends Mid {} + +$l = new Leaf(); +var_dump($l->take(42)); + +try { + $l->take("nope"); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +rejected: Base::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt new file mode 100644 index 000000000000..a1561e6ec91f --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_unbounded.phpt @@ -0,0 +1,38 @@ +--TEST-- +Inherited methods: substituted unbounded T parameter is checked at runtime on the child +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$ib = new IntBox(); +$sb = new StrBox(); + +// Happy paths +var_dump($ib->take(42)); +var_dump($sb->take("hi")); + +// Cross-type rejections +try { + $ib->take("not an int"); +} catch (TypeError $e) { + echo "IntBox rejects string: ", $e->getMessage(), "\n"; +} + +try { + $sb->take(123); +} catch (TypeError $e) { + echo "StrBox rejects int: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +IntBox rejects string: Box::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d +StrBox rejects int: Box::take(): Argument #1 ($x) must be of type string, int given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt new file mode 100644 index 000000000000..75858902e2ba --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_variadic.phpt @@ -0,0 +1,29 @@ +--TEST-- +Inherited methods: substituted variadic T parameter is checked at runtime on the child +--FILE-- + { + public function takeMany(T ...$xs): array { return $xs; } +} + +class IntBox extends Box {} + +$ib = new IntBox(); +var_dump($ib->takeMany(1, 2, 3)); + +try { + $ib->takeMany(1, "two", 3); +} catch (TypeError $e) { + echo "rejects mixed: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +rejects mixed: Box::takeMany(): Argument #2 must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt new file mode 100644 index 000000000000..b1357e3118d5 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_weak_mode.phpt @@ -0,0 +1,30 @@ +--TEST-- +Inherited methods: substituted T parameter honors weak-mode coercion like any other type +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +// Numeric strings coerce to int under weak mode +var_dump((new IntBox())->take("42")); + +// Scalars coerce to string under weak mode +var_dump((new StrBox())->take(123)); + +// Non-coercible still throws +try { + (new IntBox())->take("not a number"); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(3) "123" +rejected: Box::take(): Argument #1 ($x) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt b/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt new file mode 100644 index 000000000000..0b87d4c936a2 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_param_check_with_default.phpt @@ -0,0 +1,28 @@ +--TEST-- +Inherited methods: substituted T parameter with a default still rejects bad explicit args +--FILE-- + { + public function take(T $x = null): mixed { return $x; } +} + +class IntBox extends Box {} + +$ib = new IntBox(); + +// Default and matching values pass +var_dump($ib->take()); +var_dump($ib->take(7)); +var_dump($ib->take(null)); + +try { + $ib->take("nope"); +} catch (TypeError $e) { + echo "rejects string: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +NULL +int(7) +NULL +rejects string: Box::take(): Argument #1 ($x) must be of type ?int, string given, called in %s on line %d diff --git a/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt b/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt new file mode 100644 index 000000000000..be3b761fd686 --- /dev/null +++ b/Zend/tests/generics/inheritance/method/runtime_return_check_unbounded.phpt @@ -0,0 +1,42 @@ +--TEST-- +Inherited methods: substituted unbounded T return type is checked at runtime on the child +--FILE-- + { + public mixed $payload = null; + public function get(): T { return $this->payload; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$ib = new IntBox(); +$ib->payload = 42; +var_dump($ib->get()); + +$sb = new StrBox(); +$sb->payload = "hi"; +var_dump($sb->get()); + +// Now return values that violate the substituted type +$ib->payload = "not an int"; +try { + $ib->get(); +} catch (TypeError $e) { + echo "IntBox return rejects string: ", $e->getMessage(), "\n"; +} + +$sb->payload = 123; +try { + $sb->get(); +} catch (TypeError $e) { + echo "StrBox return rejects int: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +IntBox return rejects string: Box::get(): Return value must be of type int, string returned +StrBox return rejects int: Box::get(): Return value must be of type string, int returned diff --git a/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt b/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt new file mode 100644 index 000000000000..1f685d3f0b00 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_concrete_arg.phpt @@ -0,0 +1,27 @@ +--TEST-- +Property substitution: class extends generic parent with concrete arg substitutes property type +--FILE-- + { + public T $val; +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$ib = new IntBox; +$ib->val = 42; +echo $ib->val, "\n"; + +try { + $ib->val = []; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +int +42 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt b/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt new file mode 100644 index 000000000000..74b04b7f8e72 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_concrete_property_unchanged.phpt @@ -0,0 +1,17 @@ +--TEST-- +Property substitution: parent property already typed with a concrete type is unchanged +--FILE-- + { + public int $count = 0; + public T $val; +} + +class StrBox extends Box {} + +$rcCount = (new ReflectionClass('StrBox'))->getProperty('count'); +$rcVal = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcCount->getType()->getName(), "/", $rcVal->getType()->getName(), "\n"; +?> +--EXPECT-- +int/string diff --git a/Zend/tests/generics/inheritance/property/extends_default_arg.phpt b/Zend/tests/generics/inheritance/property/extends_default_arg.phpt new file mode 100644 index 000000000000..2e809d906fc3 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_default_arg.phpt @@ -0,0 +1,23 @@ +--TEST-- +Property substitution: extends without args uses parent's parameter default +--FILE-- + { + public T $val; +} + +class Bag extends Box {} + +$rp = (new ReflectionClass('Bag'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$b = new Bag; +$b->val = 7; +echo $b->val, "\n"; + +try { $b->val = "x"; } catch (TypeError) { echo "TypeError\n"; } +?> +--EXPECT-- +int +7 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt b/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt new file mode 100644 index 000000000000..7c71f594849a --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_and_set_hook.phpt @@ -0,0 +1,27 @@ +--TEST-- +Property substitution: get and set hook signatures are both substituted on a class extending a generic parent +--FILE-- + { + public T $val { + get => $this->val; + set => $this->val = $value; + } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} +class StrBox extends Box {} + +foreach (['IntBox', 'StrBox'] as $cn) { + $rp = (new ReflectionClass($cn))->getProperty('val'); + $g = $rp->getHook(PropertyHookType::Get); + $s = $rp->getHook(PropertyHookType::Set); + echo "$cn: prop=", $rp->getType()->getName(), + " get=", $g->getReturnType()->getName(), + " set=", $s->getParameters()[0]->getType()->getName(), "\n"; +} +?> +--EXPECT-- +IntBox: prop=int get=int set=int +StrBox: prop=string get=string set=string diff --git a/Zend/tests/generics/inheritance/property/extends_get_hook.phpt b/Zend/tests/generics/inheritance/property/extends_get_hook.phpt new file mode 100644 index 000000000000..3e8f46d8bbba --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_hook.phpt @@ -0,0 +1,24 @@ +--TEST-- +Property substitution: get hook return type is substituted on a class extending a generic parent +--FILE-- + { + public T $val { get => $this->val; } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$get = $rp->getHook(PropertyHookType::Get); +echo "get return: ", $get->getReturnType()->getName(), "\n"; + +$ib = new IntBox(42); +echo "value: ", $ib->val, "\n"; +?> +--EXPECT-- +prop type: int +get return: int +value: 42 diff --git a/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt b/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt new file mode 100644 index 000000000000..98b021c591fb --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_get_hook_transitive.phpt @@ -0,0 +1,25 @@ +--TEST-- +Property substitution: get hook return type substitutes through a generic intermediate class +--FILE-- + { + public T $val { get => $this->val; } + public function __construct(T $v) { $this->val = $v; } +} + +class Mid extends Box {} +class Leaf extends Mid {} + +$rp = (new ReflectionClass('Leaf'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$get = $rp->getHook(PropertyHookType::Get); +echo "get return: ", $get->getReturnType()->getName(), "\n"; + +$leaf = new Leaf(3.14); +echo "value: ", $leaf->val, "\n"; +?> +--EXPECT-- +prop type: float +get return: float +value: 3.14 diff --git a/Zend/tests/generics/inheritance/property/extends_set_hook.phpt b/Zend/tests/generics/inheritance/property/extends_set_hook.phpt new file mode 100644 index 000000000000..a06399be4801 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_set_hook.phpt @@ -0,0 +1,32 @@ +--TEST-- +Property substitution: set hook value parameter type is substituted on a class extending a generic parent +--FILE-- + { + public T $val { set => $this->val = $value; } + public function __construct(T $v) { $this->val = $v; } +} + +class IntBox extends Box {} + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "prop type: ", $rp->getType()->getName(), "\n"; + +$set = $rp->getHook(PropertyHookType::Set); +echo "set param: ", $set->getParameters()[0]->getType()->getName(), "\n"; + +$ib = new IntBox(5); +$ib->val = 10; +echo $ib->val, "\n"; + +try { + $ib->val = "no"; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +prop type: int +set param: int +10 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_transitive.phpt b/Zend/tests/generics/inheritance/property/extends_transitive.phpt new file mode 100644 index 000000000000..7f9298efd8e4 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_transitive.phpt @@ -0,0 +1,24 @@ +--TEST-- +Property substitution: substitution composes through a generic intermediate class +--FILE-- + { + public T $val; +} + +class Mid extends Box {} +class Leaf extends Mid {} + +$rp = (new ReflectionClass('Leaf'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$leaf = new Leaf; +$leaf->val = 3.14; +echo $leaf->val, "\n"; + +try { $leaf->val = "x"; } catch (TypeError) { echo "TypeError\n"; } +?> +--EXPECT-- +float +3.14 +TypeError diff --git a/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt b/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt new file mode 100644 index 000000000000..cea7bcfdd921 --- /dev/null +++ b/Zend/tests/generics/inheritance/property/extends_two_classes_distinct_types.phpt @@ -0,0 +1,25 @@ +--TEST-- +Property substitution: two classes extending the same generic parent get distinct property types +--FILE-- + { + public T $val; +} + +class IntBox extends Box {} +class StrBox extends Box {} + +$rcInt = (new ReflectionClass('IntBox'))->getProperty('val'); +$rcStr = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcInt->getType()->getName(), "/", $rcStr->getType()->getName(), "\n"; + +$ib = new IntBox; $ib->val = 1; +$sb = new StrBox; $sb->val = "x"; + +try { $ib->val = "no"; } catch (TypeError) { echo "TypeError IntBox\n"; } +try { $sb->val = []; } catch (TypeError) { echo "TypeError StrBox\n"; } +?> +--EXPECT-- +int/string +TypeError IntBox +TypeError StrBox diff --git a/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt b/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt new file mode 100644 index 000000000000..ac66243bdf06 --- /dev/null +++ b/Zend/tests/generics/inheritance/self_and_static_with_generics.phpt @@ -0,0 +1,36 @@ +--TEST-- +Inheritance: self / static in a generic ancestor compose with parametric LSP +--FILE-- + { + abstract public function build(): T; + public static function make(): static { return new static; } +} + +class IntBuilder extends Builder { + public function build(): int { return 42; } +} + +interface Box { + public function unwrap(): T; + public function self_clone(): self; +} + +class IntBox implements Box { + public function __construct(private int $v) {} + public function unwrap(): int { return $this->v; } + public function self_clone(): self { return new self($this->v); } +} + +$ib = IntBuilder::make(); +echo get_class($ib), ":", $ib->build(), "\n"; + +$b = new IntBox(7); +echo $b->unwrap(), "\n"; +$c = $b->self_clone(); +echo get_class($c), ":", $c->unwrap(), "\n"; +?> +--EXPECT-- +IntBuilder:42 +7 +IntBox:7 diff --git a/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt b/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt new file mode 100644 index 000000000000..a81e43d381ae --- /dev/null +++ b/Zend/tests/generics/inheritance/trait_use_args/trait_monomorph.phpt @@ -0,0 +1,64 @@ +--TEST-- +Trait-use-with-args: each `use Foo;` binds to the canonical monomorph trait +--FILE-- + { + public T $value; + public function get(): T { return $this->value; } + public function set(T $v): void { $this->value = $v; } +} + +class IntHolder { + use Box; +} + +class StrHolder { + use Box; +} + +$i = new IntHolder(); +$i->set(42); +var_dump($i->get()); + +$s = new StrHolder(); +$s->set("hi"); +var_dump($s->get()); + +// Substituted property type is visible per use-site. +$rcI = new ReflectionClass(IntHolder::class); +var_dump($rcI->getTraitNames()); +echo "IntHolder::value type: ", $rcI->getProperty('value')->getType(), "\n"; + +$rcS = new ReflectionClass(StrHolder::class); +var_dump($rcS->getTraitNames()); +echo "StrHolder::value type: ", $rcS->getProperty('value')->getType(), "\n"; + +// The trait monomorphs are registered as traits. +var_dump(trait_exists("Box", false)); +var_dump(trait_exists("Box", false)); +var_dump((new ReflectionClass("Box"))->isTrait()); + +// Wrong-type set throws TypeError on the substituted signature. +try { + $i->set("not int"); +} catch (TypeError $e) { + echo "ok: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +array(1) { + [0]=> + string(8) "Box" +} +IntHolder::value type: int +array(1) { + [0]=> + string(11) "Box" +} +StrHolder::value type: string +bool(true) +bool(true) +bool(true) +ok: %sset()%sint%s diff --git a/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt b/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt new file mode 100644 index 000000000000..bcfc3cf2cf14 --- /dev/null +++ b/Zend/tests/generics/limit/arg_list_return_type_128_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generics: 127 type arguments on a function return type is OK; 128 is a compile error +--FILE-- + {}"); +echo "OK\n"; + +$N = implode(",", array_fill(0, 128, "int")); +eval("function f128(): Box<{$N}> {}"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot specify more than 127 generic type arguments (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/limit/param_list_class_128_rejected.phpt b/Zend/tests/generics/limit/param_list_class_128_rejected.phpt new file mode 100644 index 000000000000..2897d63b7d17 --- /dev/null +++ b/Zend/tests/generics/limit/param_list_class_128_rejected.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generics: 127 type parameters on a class is OK; 128 is a compile error +--FILE-- + "T{$i}", range(0, 126))); +eval("class C127<{$names127}> {}"); +echo "OK\n"; + +$names128 = implode(",", array_map(fn($i) => "T{$i}", range(0, 127))); +eval("class C128<{$names128}> {}"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot declare more than 127 generic type parameters (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt b/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt new file mode 100644 index 000000000000..7ea201aada64 --- /dev/null +++ b/Zend/tests/generics/limit/turbofish_function_call_128_rejected.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generics: 127 type arguments on a function call turbofish is OK; 128 is a compile error +--FILE-- +(); }"); +echo "OK\n"; + +$N = implode(",", array_fill(0, 128, "int")); +eval("function g128() { f::<{$N}>(); }"); +?> +--EXPECTF-- +OK + +Fatal error: Cannot specify more than 127 generic type arguments (got 128) in %s : eval()'d code on line %d diff --git a/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt b/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt new file mode 100644 index 000000000000..87bb2d4aaca4 --- /dev/null +++ b/Zend/tests/generics/reflection/namespace_relative_extends_implements_uses.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reflection: namespace-relative `extends`/`implements`/`use` keep their generic arguments in the side table +--FILE-- + {} +interface Iface<+T> {} +trait Mixin<+T> {} + +class FqnExt extends \NSRelExtImpUse\Base {} +class NsExt extends namespace\Base {} +class RelExt extends Base {} + +class FqnImpl implements \NSRelExtImpUse\Iface {} +class NsImpl implements namespace\Iface {} +class RelImpl implements Iface {} + +class FqnUse { use \NSRelExtImpUse\Mixin; } +class NsUse { use namespace\Mixin; } +class RelUse { use Mixin; } + +$singles = [ + 'extends FQN' => (new \ReflectionClass(FqnExt::class))->getGenericArgumentsForParentClass(), + 'extends ns' => (new \ReflectionClass(NsExt::class))->getGenericArgumentsForParentClass(), + 'extends rel' => (new \ReflectionClass(RelExt::class))->getGenericArgumentsForParentClass(), + 'use FQN' => (new \ReflectionClass(FqnUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), + 'use ns' => (new \ReflectionClass(NsUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), + 'use rel' => (new \ReflectionClass(RelUse::class))->getGenericArgumentsForUsedTrait(Mixin::class), +]; + +foreach ($singles as $label => $args) { + $rendered = array_map(static fn(\ReflectionNamedType $t): string => $t->getName(), $args); + printf("%-12s count=%d args=[%s]\n", $label, count($args), implode(', ', $rendered)); +} + +$plural = [ + 'impl FQN' => (new \ReflectionClass(FqnImpl::class))->getGenericArgumentsForParentInterface(Iface::class), + 'impl ns' => (new \ReflectionClass(NsImpl::class))->getGenericArgumentsForParentInterface(Iface::class), + 'impl rel' => (new \ReflectionClass(RelImpl::class))->getGenericArgumentsForParentInterface(Iface::class), +]; + +foreach ($plural as $label => $bindings) { + $rendered = array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static fn(\ReflectionNamedType $t): string => $t->getName(), + $b, + )) . ']', + $bindings, + ); + printf("%-12s bindings=%d sets=[%s]\n", $label, count($bindings), implode(', ', $rendered)); +} +?> +--EXPECT-- +extends FQN count=1 args=[int] +extends ns count=1 args=[int] +extends rel count=1 args=[int] +use FQN count=1 args=[int] +use ns count=1 args=[int] +use rel count=1 args=[int] +impl FQN bindings=1 sets=[[int]] +impl ns bindings=1 sets=[[int]] +impl rel bindings=1 sets=[[int]] diff --git a/Zend/tests/generics/reification/T_keyed_access_forms.phpt b/Zend/tests/generics/reification/T_keyed_access_forms.phpt new file mode 100644 index 000000000000..87a961137ab2 --- /dev/null +++ b/Zend/tests/generics/reification/T_keyed_access_forms.phpt @@ -0,0 +1,54 @@ +--TEST-- +Reification: every T-keyed expression form resolves through the frame's T-table +--FILE-- +($x): array { + return [ + "new T()" => get_class(new T()), + "T::NAME" => T::NAME, + "T::class" => T::class, + "T::make()" => get_class(T::make()), + "instanceof"=> $x instanceof T, + ]; +} + +var_dump(show::(new Foo())); +var_dump(show::(new Foo())); +?> +--EXPECT-- +array(5) { + ["new T()"]=> + string(3) "Foo" + ["T::NAME"]=> + string(3) "Foo" + ["T::class"]=> + string(3) "Foo" + ["T::make()"]=> + string(3) "Foo" + ["instanceof"]=> + bool(true) +} +array(5) { + ["new T()"]=> + string(3) "Bar" + ["T::NAME"]=> + string(3) "Bar" + ["T::class"]=> + string(3) "Bar" + ["T::make()"]=> + string(3) "Bar" + ["instanceof"]=> + bool(false) +} diff --git a/Zend/tests/generics/reification/bound_fallback.phpt b/Zend/tests/generics/reification/bound_fallback.phpt new file mode 100644 index 000000000000..3021dd02ac12 --- /dev/null +++ b/Zend/tests/generics/reification/bound_fallback.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: when no binding is supplied, the parameter's class bound is the fallback +--FILE-- +(): T { + // No turbofish, no default, no inferable arg. T falls back to its bound. + return new T(); +} + +var_dump(makeDefault()->kind); + +// Inference still wins over the bound. +function makeFromArg(T $hint): T { + return new T(); +} +var_dump(makeFromArg(new Derived())->kind); + +// Turbofish still wins over the bound. +var_dump(makeDefault::()->kind); +?> +--EXPECT-- +string(4) "base" +string(7) "derived" +string(7) "derived" diff --git a/Zend/tests/generics/reification/catch_with_args.phpt b/Zend/tests/generics/reification/catch_with_args.phpt new file mode 100644 index 000000000000..aa97c7e02395 --- /dev/null +++ b/Zend/tests/generics/reification/catch_with_args.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: catch with concrete type arguments matches the corresponding monomorph +--FILE-- + extends Exception {} + +// Throw a synthesized monomorph of Bag and catch it via the canonical name. +try { + throw new Bag::("payload"); +} catch (Bag $e) { + echo "caught Bag: ", $e->getMessage(), "\n"; +} + +// Wrong arg in the catch — the canonical names differ, so no match. +try { + try { + throw new Bag::("payload"); + } catch (Bag $e) { + echo "inner caught\n"; + } +} catch (Bag $e) { + echo "outer caught Bag (parent): ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught Bag: payload +outer caught Bag (parent): payload diff --git a/Zend/tests/generics/reification/catch_with_t_ref.phpt b/Zend/tests/generics/reification/catch_with_t_ref.phpt new file mode 100644 index 000000000000..e72cf3af9438 --- /dev/null +++ b/Zend/tests/generics/reification/catch_with_t_ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: catch (Box $e) matches only when the thrown exception's monomorph args agree with T +--FILE-- + extends Exception {} + +function tryCatch(Exception $e): string { + try { + throw $e; + } catch (BoxedError $caught) { + return "matched BoxedError"; + } catch (Exception $other) { + return "fell through"; + } +} + +echo tryCatch::(new BoxedError::('payload')), "\n"; // matched +echo tryCatch::(new BoxedError::('payload')), "\n"; // fell through: different mono +echo tryCatch::(new Exception('plain')), "\n"; // fell through: not a Box* +echo tryCatch::(new BoxedError::('s')), "\n"; // matched +?> +--EXPECT-- +matched BoxedError +fell through +fell through +matched BoxedError diff --git a/Zend/tests/generics/reification/class_level_T_in_catch.phpt b/Zend/tests/generics/reification/class_level_T_in_catch.phpt new file mode 100644 index 000000000000..d0f7c5513f15 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_in_catch.phpt @@ -0,0 +1,29 @@ +--TEST-- +Reification: catch (T $e) inside a method body matches against the class-level T binding +--FILE-- + { + public function trap(callable $fn): string { + try { + $fn(); + return "no-throw"; + } catch (T $e) { + return "caught " . $e::class . ": " . $e->getMessage(); + } + } +} + +$h = new Handler::(); +echo $h->trap(fn() => throw new MyExc("hi")), "\n"; // T = MyExc → caught +try { + $h->trap(fn() => throw new OtherExc("bye")); // T = MyExc → rethrows +} catch (Throwable $e) { + echo "outer ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught MyExc: hi +outer OtherExc: bye diff --git a/Zend/tests/generics/reification/class_level_T_in_method_body.phpt b/Zend/tests/generics/reification/class_level_T_in_method_body.phpt new file mode 100644 index 000000000000..bba1bc1d0f47 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_in_method_body.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: class-level T resolves at runtime inside instance method bodies +--FILE-- + { + public function summary(object $x): array { + return [ + "new T()" => get_class(new T()), + "T::KIND" => T::KIND, + "T::class" => T::class, + "T::make()" => get_class(T::make()), + "instanceof" => $x instanceof T, + ]; + } +} + +var_dump((new Box::())->summary(new Foo())); +var_dump((new Box::())->summary(new Foo())); +?> +--EXPECT-- +array(5) { + ["new T()"]=> + string(3) "Foo" + ["T::KIND"]=> + string(3) "foo" + ["T::class"]=> + string(3) "Foo" + ["T::make()"]=> + string(3) "Foo" + ["instanceof"]=> + bool(true) +} +array(5) { + ["new T()"]=> + string(3) "Bar" + ["T::KIND"]=> + string(3) "bar" + ["T::class"]=> + string(3) "Bar" + ["T::make()"]=> + string(3) "Bar" + ["instanceof"]=> + bool(false) +} diff --git a/Zend/tests/generics/reification/class_level_T_inherited.phpt b/Zend/tests/generics/reification/class_level_T_inherited.phpt new file mode 100644 index 000000000000..0eab27084809 --- /dev/null +++ b/Zend/tests/generics/reification/class_level_T_inherited.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: a subclass `extends Box` carries the parent's T binding through inherited method bodies +--FILE-- + { + public function make(): T { + return new T(); + } +} + +class FooBox extends Box {} + +// FooBox::make is inherited from Box; the binding T=Foo on FooBox's parent +// must be visible to the inherited body via the called scope. +$fb = new FooBox(); +var_dump(get_class($fb->make())); +?> +--EXPECT-- +string(3) "Foo" diff --git a/Zend/tests/generics/reification/closure_bind_preserves_T.phpt b/Zend/tests/generics/reification/closure_bind_preserves_T.phpt new file mode 100644 index 000000000000..ce3381902407 --- /dev/null +++ b/Zend/tests/generics/reification/closure_bind_preserves_T.phpt @@ -0,0 +1,53 @@ +--TEST-- +Reification: Closure::bind / Closure::bindTo carry the captured T-table to the rebound closure +--FILE-- +(): Closure { + return function (T $x): T { return $x; }; +} + +$f = maker::(); + +// Closure::bindTo to a new instance of A (compatible) — same scope. +$rb = $f->bindTo(new A); +var_dump(get_class($rb(new A))); // accepts A + +try { + $rb(new B); // wrong concrete class +} catch (TypeError $e) { + echo "bindTo wrong: ", $e->getMessage(), "\n"; +} + +// Closure::bind static form — also propagates. +$rb2 = Closure::bind($f, new A); +var_dump(get_class($rb2(new A))); + +try { + $rb2(new B); +} catch (TypeError $e) { + echo "bind wrong: ", $e->getMessage(), "\n"; +} + +// bindTo(null) — strip the $this — still keeps the T-table. +$rb3 = $f->bindTo(null); +var_dump(get_class($rb3(new A))); +try { + $rb3(new B); +} catch (TypeError $e) { + echo "bindTo(null) wrong: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +bindTo wrong: %s must be of type A, B given%S +string(1) "A" +bind wrong: %s must be of type A, B given%S +string(1) "A" +bindTo(null) wrong: %s must be of type A, B given%S diff --git a/Zend/tests/generics/reification/closure_call_preserves_T.phpt b/Zend/tests/generics/reification/closure_call_preserves_T.phpt new file mode 100644 index 000000000000..b94ab23d3682 --- /dev/null +++ b/Zend/tests/generics/reification/closure_call_preserves_T.phpt @@ -0,0 +1,57 @@ +--TEST-- +Reification: Closure::call propagates the captured T-table into the fake closure that actually runs the body +--FILE-- +(): Closure { + return function (T $x): string { + $this->store = $x; + return get_class($x); + }; +} + +$f = maker::(); +$h = new Holder; + +// Positive: A and its subclass B both satisfy T = A. +var_dump($f->call($h, new A)); +var_dump($f->call($h, new B)); + +// Negative: a value that doesn't satisfy T fires with the resolved T. +class Unrelated {} +try { + $f->call($h, new Unrelated); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} + +// Variadic + call: every element checked against the captured T. +function vmaker(): Closure { + return function (T ...$xs): int { return count($xs); }; +} + +$vf = vmaker::(); +var_dump($vf->call($h, new A, new B, new A)); + +try { + $vf->call($h, new A, new Unrelated, new A); +} catch (TypeError $e) { + echo "rejected2: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +string(1) "B" +rejected: %s must be of type A, Unrelated given%S +int(3) +rejected2: %s must be of type A, Unrelated given%S diff --git a/Zend/tests/generics/reification/closure_captures_T.phpt b/Zend/tests/generics/reification/closure_captures_T.phpt new file mode 100644 index 000000000000..d53c5a2fc5a1 --- /dev/null +++ b/Zend/tests/generics/reification/closure_captures_T.phpt @@ -0,0 +1,44 @@ +--TEST-- +Reification: a closure created inside a generic frame captures that frame's T-table, so the closure's body resolves T to the binding in effect at creation time +--FILE-- +(): Closure { + return fn() => T::class; +} +echo nameOf::()(), "\n"; +echo nameOf::()(), "\n"; + +// A closure with a T-typed parameter rejects values that don't satisfy the +// CAPTURED binding, not just the parameter's erased bound. +function id(): Closure { + return function(T $x): T { return $x; }; +} +$fooId = id::(); +$f = $fooId(new Foo); +var_dump(get_class($f)); + +try { + $fooId(new Bar); +} catch (TypeError $e) { + echo "rejected wrong class: ", $e->getMessage(), "\n"; +} + +// Scalars are rejected against the resolved T (Foo) — the captured binding +// is more specific than the parameter's erased object bound. +try { + $fooId(42); +} catch (TypeError $e) { + echo "rejected scalar: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Foo +Bar +string(3) "Foo" +rejected wrong class: %s must be of type Foo, Bar given%S +rejected scalar: %s must be of type Foo, int given%S diff --git a/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt b/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt new file mode 100644 index 000000000000..063b4ddc9dce --- /dev/null +++ b/Zend/tests/generics/reification/closure_from_callable_preserves_T.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: Closure::fromCallable on a generic closure preserves the captured T-table (returns the same closure) +--FILE-- +(): Closure { + return function (T $x): string { return get_class($x); }; +} + +$f = maker::(); +$wrapped = Closure::fromCallable($f); + +// Positive: behaves like the original — T is still A. +var_dump($wrapped(new A)); + +// Negative: wrong concrete class still fires with the resolved T. +try { + $wrapped(new B); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(1) "A" +rejected: %s must be of type A, B given%S diff --git a/Zend/tests/generics/reification/composite_return_reification.phpt b/Zend/tests/generics/reification/composite_return_reification.phpt new file mode 100644 index 000000000000..c6e48ee04c36 --- /dev/null +++ b/Zend/tests/generics/reification/composite_return_reification.phpt @@ -0,0 +1,67 @@ +--TEST-- +Reification: composite return/param types (T|Other, A|B, T&B) are reified and enforced, with null/member folding +--FILE-- +(mixed $x): T|Other { return $x; } +var_dump(ret::(new Foo)::class); // Foo member +var_dump(ret::(new Other)::class); // Other member +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// When T resolves to the other member the union folds (Other|Other -> Other). +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// A union *binding* flattens into the union position (Foo|Other)|Other -> Foo|Other. +var_dump(ret::(new Foo)::class); +try { ret::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// A union of two type parameters. +function two(mixed $x): X|Y { return $x; } +var_dump(two::(new Other)::class); +try { two::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// Composite parameter types are enforced too. +function par(T|Other $x): string { return $x::class; } +var_dump(par::(new Foo)); +try { par::(42); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// Parameters fold/flatten through the same path as returns: T=Other folds the +// duplicate, and a union binding flattens. The reified param type is observable. +par::(new Other); +var_dump((string) (new ReflectionFunction('par'))->getParameters()[0]->getType()); +par::(new Foo); +var_dump((string) (new ReflectionFunction('par'))->getParameters()[0]->getType()); + +// Intersection return: T bound to an interface, T&B reifies to A&B. +function inter(mixed $x): T&B { return $x; } +var_dump(inter::(new AB)::class); +try { inter::(new Foo); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } + +// DNF: an intersection binding spliced into a union stays nested ((A&B)|Other). +function dnf(mixed $x): T|Other { return $x; } +var_dump(dnf::(new AB)::class); +var_dump(dnf::(new Other)::class); +try { dnf::(new Foo); } catch (TypeError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECTF-- +string(3) "Foo" +string(5) "Other" +ret(): Return value must be of type Foo|Other, int returned +ret(): Return value must be of type Other, int returned +string(3) "Foo" +ret(): Return value must be of type Foo|Other, int returned +string(5) "Other" +two(): Return value must be of type Foo|Other, int returned +string(3) "Foo" +par(): Argument #1 ($x) must be of type Foo|Other, int given, called in %s on line %d +string(5) "Other" +string(9) "Foo|Other" +string(2) "AB" +inter(): Return value must be of type A&B, Foo returned +string(2) "AB" +string(5) "Other" +dnf(): Return value must be of type (A&B)|Other, Foo returned diff --git a/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt new file mode 100644 index 000000000000..08015a300682 --- /dev/null +++ b/Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt @@ -0,0 +1,87 @@ +--TEST-- +Reification: forwarded T-ref in turbofish (id::(...) inside nested) resolves the value-arg check through the caller's bindings +--FILE-- +(T $x): T { return $x; } + +// Same-type forwarding: U binds to Dog at the outer call, the inner +// id::($x) forwards U into id's T, and id should accept Dog as Dog — +// not as a literal "U" type. +function nested(U $x): U { + return id::($x); +} +var_dump(nested::(new Dog)); + +// Mismatch caught against the *resolved* class, not the forwarded T-ref. +// The error message must name the bound class ("Dog"), not the parameter +// letter ("U"). +function pass_through(mixed $x): U { + return id::($x); +} +try { + pass_through::(new Cat); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Forwarded scalar binding: U = int, the value 42 satisfies id's T = int. +function nested_scalar(U $x): U { + return id::($x); +} +var_dump(nested_scalar::(42)); + +// Forwarded scalar mismatch caught against the resolved scalar type: +// pass_through_scalar(mixed $x) doesn't constrain $x at its own +// boundary, so the string "hello" reaches the inner id::(...) call. +// id's T is resolved to int via U, and the value check fires with the +// resolved type ("int"), not the literal T-ref letter. +function pass_through_scalar(mixed $x): U { + return id::($x); +} +try { + pass_through_scalar::("hello"); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Composite forwarded bindings: a union arg threaded through a forwarding +// wrapper still names the union ("int|string") in the error, not the +// T-ref letter. Same with intersection. +try { + pass_through_scalar::([1, 2]); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} + +// Intersection forwarded through a wrapper: positive case (a value +// implementing both interfaces) passes; negative reports the resolved +// intersection ("Fooable&Barable"), not the T-ref letter. +interface Fooable {} +interface Barable {} +class Both implements Fooable, Barable {} +class FooOnly implements Fooable {} + +function pass_through_obj(object $x): U { + return id::($x); +} +var_dump(pass_through_obj::(new Both)); +try { + pass_through_obj::(new FooOnly); +} catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +object(Dog)#%d (0) { +} +caught: id(): Argument #1 ($x) must be of type Dog, Cat given +int(42) +caught: id(): Argument #1 ($x) must be of type int, string given +caught: id(): Argument #1 ($x) must be of type string|int, array given +object(Both)#%d (0) { +} +caught: id(): Argument #1 ($x) must be of type Fooable&Barable, FooOnly given diff --git a/Zend/tests/generics/reification/func_get_type_args.phpt b/Zend/tests/generics/reification/func_get_type_args.phpt new file mode 100644 index 000000000000..0730b21c6775 --- /dev/null +++ b/Zend/tests/generics/reification/func_get_type_args.phpt @@ -0,0 +1,34 @@ +--TEST-- +Reification: func_get_type_args() reflects turbofish, defaults, and inference +--FILE-- +(T $x): array { + return func_get_type_args(); +} + +var_dump(inspect::(new Foo())); // explicit turbofish for T and U +var_dump(inspect::(new Bar())); // U falls back to its default Foo +var_dump(inspect(new Bar())); // T inferred from arg, U default +?> +--EXPECT-- +array(2) { + ["T"]=> + string(3) "Foo" + ["U"]=> + string(3) "Bar" +} +array(2) { + ["T"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} +array(2) { + ["T"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} diff --git a/Zend/tests/generics/reification/function_level_T_in_catch.phpt b/Zend/tests/generics/reification/function_level_T_in_catch.phpt new file mode 100644 index 000000000000..2e9f3a43be55 --- /dev/null +++ b/Zend/tests/generics/reification/function_level_T_in_catch.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: catch (T $e) inside a function body matches against the function-level T binding +--FILE-- +(callable $fn): string { + try { + $fn(); + return "no-throw"; + } catch (T $e) { + return "caught " . $e::class . ": " . $e->getMessage(); + } +} + +echo trap::(fn() => throw new MyExc("a")), "\n"; // matches +try { + trap::(fn() => throw new OtherExc("b")); // doesn't match +} catch (Throwable $e) { + echo "outer ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +caught MyExc: a +outer OtherExc: b diff --git a/Zend/tests/generics/reification/generator_preserves_T.phpt b/Zend/tests/generics/reification/generator_preserves_T.phpt new file mode 100644 index 000000000000..2c4d4c3e43d9 --- /dev/null +++ b/Zend/tests/generics/reification/generator_preserves_T.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a generator's T-table is preserved across yield/suspend and resume +--FILE-- +(): Generator { + yield T::class; + yield T::class; + yield T::class; +} + +$g = gen::(); +foreach ($g as $v) echo $v, "\n"; + +$g = gen::(); +foreach ($g as $v) echo $v, "\n"; +?> +--EXPECT-- +Foo +Foo +Foo +Bar +Bar +Bar diff --git a/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt new file mode 100644 index 000000000000..f72df5890e73 --- /dev/null +++ b/Zend/tests/generics/reification/inference_all_defaulted_no_crash.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a non-turbofish call to an all-defaulted generic callee with non-literal args infers nothing and stays generic (must not build an empty type-arg box) +--FILE-- + crash). +function neighbors(mixed $graph, TNode $node): array { + return [$node]; +} + +function caller(mixed $g, mixed $n): array { + return neighbors($g, $n); +} + +var_dump(caller('graph', 'x')); +var_dump(caller('graph', 42)); +echo "ok\n"; +?> +--EXPECT-- +array(1) { + [0]=> + string(1) "x" +} +array(1) { + [0]=> + int(42) +} +ok diff --git a/Zend/tests/generics/reification/inference_basic.phpt b/Zend/tests/generics/reification/inference_basic.phpt new file mode 100644 index 000000000000..9c05c291b024 --- /dev/null +++ b/Zend/tests/generics/reification/inference_basic.phpt @@ -0,0 +1,24 @@ +--TEST-- +Reification: T is inferred from an argument whose declared type is exactly T; the substituted parameter type is enforced when turbofish disagrees with the value +--FILE-- +(T $x): string { + return T::class; +} + +echo kind(new Foo()), "\n"; +echo kind(new Bar()), "\n"; + +try { + kind::(new Bar()); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Foo +Bar +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/inference_beats_default.phpt b/Zend/tests/generics/reification/inference_beats_default.phpt new file mode 100644 index 000000000000..fc2c6a872a32 --- /dev/null +++ b/Zend/tests/generics/reification/inference_beats_default.phpt @@ -0,0 +1,76 @@ +--TEST-- +Reification: value-directed inference wins over a type parameter's declared default +--FILE-- + { + public function __construct(public T $value) {} +} + +function foo(A $a): array { + return func_get_type_args(); +} + +// Inferred from the argument, not the `mixed` default. +var_dump(foo(new Foo())); +var_dump(foo(new Bar())); + +// Explicit turbofish still wins over inference: A is pinned to Bar even though +// the argument's runtime class is Sub (which satisfies the Bar binding). +var_dump(foo::(new Sub())); + +// The default is still used when nothing pins the slot: U is not a value +// parameter, so it can't be inferred and falls back to its default. +function pick(A $a): array { + return func_get_type_args(); +} +var_dump(pick(new Bar())); + +// Partial turbofish: A is given explicitly, B is inferred from its argument +// even though B has a default — the monomorph fast path must defer to the +// inference path here. +function two(A $a, B $b): array { + return func_get_type_args(); +} +var_dump(two::(new Bar(), new Foo())); + +// Return type Box reifies to the inferred A, not Box. The forwarded +// turbofish `new Box::` resolves A against the (now inferred) binding. +function wrap(A $a): Box { + return new Box::($a); +} +$b = wrap(new Foo()); +var_dump($b::class); +?> +--EXPECT-- +array(1) { + ["A"]=> + string(3) "Foo" +} +array(1) { + ["A"]=> + string(3) "Bar" +} +array(1) { + ["A"]=> + string(3) "Bar" +} +array(2) { + ["A"]=> + string(3) "Bar" + ["U"]=> + string(3) "Foo" +} +array(2) { + ["A"]=> + string(3) "Bar" + ["B"]=> + string(3) "Foo" +} +string(8) "Box" diff --git a/Zend/tests/generics/reification/inference_literal_arg.phpt b/Zend/tests/generics/reification/inference_literal_arg.phpt new file mode 100644 index 000000000000..14829462fb20 --- /dev/null +++ b/Zend/tests/generics/reification/inference_literal_arg.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a scalar literal argument monomorphizes a non-turbofish generic call at compile time (no opcache needed); the reified return type enforces the inferred T +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function lit_int() { return retStr(42); } +function lit_float() { return retStr(3.14); } +function lit_string() { return retInt("hi"); } + +foreach (['lit_int', 'lit_float', 'lit_string'] as $fn) { + try { + $fn(); + echo "$fn: NO error (not monomorphized!)\n"; + } catch (TypeError $e) { + echo "$fn: ", $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +lit_int: retStr(): Return value must be of type int, string returned +lit_float: retStr(): Return value must be of type float, string returned +lit_string: retInt(): Return value must be of type string, int returned diff --git a/Zend/tests/generics/reification/inference_optimizer_ssa.phpt b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt new file mode 100644 index 000000000000..716957ccd5bf --- /dev/null +++ b/Zend/tests/generics/reification/inference_optimizer_ssa.phpt @@ -0,0 +1,45 @@ +--TEST-- +Reification: the opcache DFA optimizer monomorphizes non-turbofish generic calls from each argument's SSA-inferred type; reassignment binds the actual type, not the declared one, and unknown types stay generic +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--FILE-- +(T $x): T { return "s"; } +function retInt(T $x): T { return 7; } + +function from_int(int $i) { return retStr($i); } +function from_float(float $f) { return retStr($f); } +function local_literal() { $v = 1.5; return retStr($v); } + +// Reassignment must bind T from the SSA type (string), not the declared int. +function reassigned(int $val) { $val = 'x'; return retInt($val); } + +function from_unknown(array $a) { $v = $a[0]; return retInt($v); } + +function check(string $name, callable $fn): void { + try { + $r = $fn(); + echo "$name: ", var_export($r, true), "\n"; + } catch (TypeError $e) { + echo "$name: TypeError\n"; + } +} + +check('from_int', fn() => from_int(1)); +check('from_float', fn() => from_float(1.5)); +check('local_literal', fn() => local_literal()); +check('reassigned', fn() => reassigned(1)); +check('from_unknown', fn() => from_unknown([42])); +?> +--EXPECT-- +from_int: TypeError +from_float: TypeError +local_literal: TypeError +reassigned: TypeError +from_unknown: 7 diff --git a/Zend/tests/generics/reification/inference_scalar.phpt b/Zend/tests/generics/reification/inference_scalar.phpt new file mode 100644 index 000000000000..db2bc65ee52b --- /dev/null +++ b/Zend/tests/generics/reification/inference_scalar.phpt @@ -0,0 +1,22 @@ +--TEST-- +Reification: T is inferred from scalar/array arguments, not just objects +--FILE-- +(T $a): array { return func_get_type_args(); } + +var_dump(tparams(1)["T"]); +var_dump(tparams("foo")["T"]); +var_dump(tparams(1.5)["T"]); +var_dump(tparams(true)["T"]); +var_dump(tparams(false)["T"]); +var_dump(tparams([1, 2])["T"]); +var_dump(tparams(new stdClass())["T"]); +?> +--EXPECT-- +string(3) "int" +string(6) "string" +string(5) "float" +string(4) "bool" +string(4) "bool" +string(5) "array" +string(8) "stdClass" diff --git a/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt b/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt new file mode 100644 index 000000000000..67d3f4cba0f1 --- /dev/null +++ b/Zend/tests/generics/reification/inference_scalar_return_enforced.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: a :T return type reified from an inferred scalar T is enforced +--FILE-- +(T $a): T { return "not the same type"; } + +echo "calling bad(1) — T inferred as int, returning a string must fail\n"; +try { + var_dump(bad(1)); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} + +// Returning a matching type is fine. +function good(T $a): T { return $a; } +var_dump(good(42)); +var_dump(good("ok")); +?> +--EXPECT-- +calling bad(1) — T inferred as int, returning a string must fail +TypeError: bad(): Return value must be of type int, string returned +int(42) +string(2) "ok" diff --git a/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt b/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt new file mode 100644 index 000000000000..9d71095bbc65 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_distinguishes_monos.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reification: instanceof distinguishes monos by their canonical argument tuple +--FILE-- + {} + +$bd = new Box::(); +$ba = new Box::(); + +// Each mono extends the bare class but is otherwise distinct. +var_dump($bd instanceof Box); // true: parent-name chain +var_dump($bd instanceof Box); // true: same canonical mono +var_dump($bd instanceof Box); // false: invariant distinction +var_dump($ba instanceof Box); // false +var_dump($ba instanceof Box); // true +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) diff --git a/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt b/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt new file mode 100644 index 000000000000..eff113d3e711 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_nested_t_ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reification: nested T-refs (instanceof Outer>) substitute through and resolve the outermost monomorph +--FILE-- + {} +class Outer {} + +class Holder { + public function isOuterOfBox(mixed $x): bool { + return $x instanceof Outer>; + } +} + +$intHolder = new Holder::(); +$strHolder = new Holder::(); + +var_dump($intHolder->isOuterOfBox(new Outer::>)); // true +var_dump($intHolder->isOuterOfBox(new Outer::>)); // false +var_dump($strHolder->isOuterOfBox(new Outer::>)); // true +var_dump($strHolder->isOuterOfBox(new Outer::)); // false: outer mono name differs +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/Zend/tests/generics/reification/instanceof_variance.phpt b/Zend/tests/generics/reification/instanceof_variance.phpt new file mode 100644 index 000000000000..6be079f63d10 --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_variance.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reification: instanceof respects variance markers on the generic base +--FILE-- + {} // invariant +class Producer<+T : object> {} // covariant +class Consumer<-T : object> {} // contravariant + +$dogBox = new Box::(); +$dogProd = new Producer::(); +$animalCons = new Consumer::(); + +// Invariant: only same-arg passes. +var_dump($dogBox instanceof Box); // true (identical) +var_dump($dogBox instanceof Box); // false +var_dump($dogBox instanceof Box); // false + +// Covariant: Dog <: Animal, so Producer <: Producer. +var_dump($dogProd instanceof Producer); // true +var_dump($dogProd instanceof Producer); // false +var_dump($dogProd instanceof Producer); // true + +// Contravariant: Animal :> Dog, so Consumer <: Consumer. +var_dump($animalCons instanceof Consumer); // true +var_dump($animalCons instanceof Consumer); // true +var_dump($animalCons instanceof Consumer);// true + +// Reverse contravariant fails. +$dogCons = new Consumer::(); +var_dump($dogCons instanceof Consumer); // false +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt new file mode 100644 index 000000000000..21a3bda926df --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_with_t_ref_class_level.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reification: instanceof Box with class-level T resolves against the called scope's monomorph args +--FILE-- + {} + +class Outer { + public function isBox(mixed $x): bool { + return $x instanceof Box; + } +} + +$animalOuter = new Outer::(); +$dogOuter = new Outer::(); +$intOuter = new Outer::(); + +class Animal {} +class Dog extends Animal {} + +var_dump($animalOuter->isBox(new Box::)); // true +var_dump($animalOuter->isBox(new Box::)); // false: nominal monos +var_dump($dogOuter->isBox(new Box::)); // true +var_dump($dogOuter->isBox(new Box::)); // false +var_dump($intOuter->isBox(new Box::)); // true +var_dump($intOuter->isBox(new Box::)); // false +var_dump($animalOuter->isBox(42)); // false: not an object +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) diff --git a/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt b/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt new file mode 100644 index 000000000000..8e9707cf117e --- /dev/null +++ b/Zend/tests/generics/reification/instanceof_with_t_ref_function_level.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reification: instanceof Box with function-level T resolves against the current frame's turbofish bindings +--FILE-- + {} + +function isBoxOf(mixed $x): bool { + return $x instanceof Box; +} + +var_dump(isBoxOf::(new Box::)); // true +var_dump(isBoxOf::(new Box::)); // false +var_dump(isBoxOf::(new Box::));// true +var_dump(isBoxOf::(42)); // false: not an object +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/Zend/tests/generics/reification/interface_dispatched_method.phpt b/Zend/tests/generics/reification/interface_dispatched_method.phpt new file mode 100644 index 000000000000..3fc6ce4efa46 --- /dev/null +++ b/Zend/tests/generics/reification/interface_dispatched_method.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a generic method called via an interface-typed variable still resolves T +--FILE-- +(): U; +} + +class Factory implements Maker { + public function build(): U { + return new U(); + } +} + +class Foo {} +class Bar {} + +function callIt(Maker $m, string $which): object { + return $which === "foo" ? $m->build::() : $m->build::(); +} + +var_dump(get_class(callIt(new Factory(), "foo"))); +var_dump(get_class(callIt(new Factory(), "bar"))); +?> +--EXPECT-- +string(3) "Foo" +string(3) "Bar" diff --git a/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt b/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt new file mode 100644 index 000000000000..89de420c1222 --- /dev/null +++ b/Zend/tests/generics/reification/jit_monomorph_no_crosstalk.phpt @@ -0,0 +1,51 @@ +--TEST-- +JIT: turbofish monomorphs sharing a base op_array must not alias each other's type checks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +--FILE-- +(T $x): T { return $x; } +function nestedGen(U $x): U { return idGen::($x); } + +function call_scalar(int $n): void { for ($i = 0; $i < $n; $i++) { nestedGen::($i); } } +function call_class(int $n): void { $f = new Foo(); for ($i = 0; $i < $n; $i++) { nestedGen::($f); } } +function call_union(int $n): void { for ($i = 0; $i < $n; $i++) { nestedGen::($i); } } +function call_inter(int $n): void { $fb = new FooBar(); for ($i = 0; $i < $n; $i++) { nestedGen::($fb); } } + +/* Each concrete turbofish call synthesizes a monomorph of nestedGen with its + * own substituted arg_info, but all monomorphs share the base's opcode buffer. + * The tracing JIT keys compiled code on opline addresses and bakes arg_info as + * a constant; before the fix it compiled one binding's trace and reused it for + * the others, so the intersection call below was checked against the earlier + * union binding (int|string) and wrongly threw. Run the bindings in the order + * that made nestedGen hot under the scalar/class bindings first. */ +bench('call_scalar', 100000); +bench('call_class', 100000); +bench('call_union', 20000); +bench('call_inter', 20000); + +$fb = new FooBar(); +var_dump(nestedGen::($fb) === $fb); +var_dump(nestedGen::(7)); +echo "done\n"; +?> +--EXPECT-- +bool(true) +int(7) +done diff --git a/Zend/tests/generics/reification/method_level_T.phpt b/Zend/tests/generics/reification/method_level_T.phpt new file mode 100644 index 000000000000..49c15ffbdffe --- /dev/null +++ b/Zend/tests/generics/reification/method_level_T.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reification: a generic method on a non-generic class can use T inside its body +--FILE-- +(): T { + return new T(); + } +} + +$f = new Factory(); +var_dump($f->build::()->tag); +var_dump($f->build::()->tag); +?> +--EXPECT-- +string(3) "foo" +string(3) "bar" diff --git a/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt b/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt new file mode 100644 index 000000000000..8595207d03ee --- /dev/null +++ b/Zend/tests/generics/reification/method_monomorph_no_cross_class_collision.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: same-named generic methods in different classes get distinct monomorphs +--FILE-- + +// and F::pick collide and the second class's call dispatches to the +// first class's body. +class E { public function pick($x) { return 'E::pick'; } } +class F { public function pick($x) { return 'F::pick'; } } + +var_dump((new E)->pick(1)); +var_dump((new F)->pick(1)); + +// Order-independent: resolving F first must not poison E either. +class G { public function take($x) { return 'G::take'; } } +class H { public function take($x) { return 'H::take'; } } + +var_dump((new H)->take(1)); +var_dump((new G)->take(1)); + +// Same method called twice on the same class still reuses one monomorph. +var_dump((new E)->pick(2)); + +// Turbofish form must also stay class-distinct. +var_dump((new E)->pick::(1)); +var_dump((new F)->pick::(1)); +?> +--EXPECT-- +string(7) "E::pick" +string(7) "F::pick" +string(7) "H::take" +string(7) "G::take" +string(7) "E::pick" +string(7) "E::pick" +string(7) "F::pick" diff --git a/Zend/tests/generics/reification/monomorph_implements_substitution.phpt b/Zend/tests/generics/reification/monomorph_implements_substitution.phpt new file mode 100644 index 000000000000..f55fd47bd82f --- /dev/null +++ b/Zend/tests/generics/reification/monomorph_implements_substitution.phpt @@ -0,0 +1,41 @@ +--TEST-- +Reification: a monomorph of `B implements I` should report `is_a I` true +--FILE-- + implements I` is monomorphized as `B`, +// the implements binding has to be substituted so that `B` is also +// known to implement `I`. Otherwise `instanceof I` / +// `is_a(..., 'I')` returns false even though the type relationship +// holds, and property writes typed `I` reject `B` values. + +interface I<+T> {} +class B implements I {} + +// 1) Direct monomorph +$b = new B::(); +var_dump(is_a($b, 'I')); +var_dump(is_a($b, 'B')); +$mono = 'B'; +var_dump($b instanceof $mono); + +// 2) Non-generic subclass of a monomorph +class StrB extends B {} +$s = new StrB(); +var_dump(is_a($s, 'I')); +var_dump(is_a($s, 'B')); + +// 3) Assignment to a property typed I accepts a B +class Holder { + public I $val; +} +$h = new Holder::(); +$h->val = new B::(); +var_dump($h->val::class); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(9) "B" diff --git a/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt b/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt new file mode 100644 index 000000000000..a41442ee15b4 --- /dev/null +++ b/Zend/tests/generics/reification/multiple_inference_targets_agree.phpt @@ -0,0 +1,24 @@ +--TEST-- +Reification: T appearing in multiple arg positions — the first hit wins (subsequent objects of compatible types don't update) +--FILE-- +(T $a, T $b): string { + return T::class; +} + +// Both args are Foo — T inferred as Foo +var_dump(pair(new Foo(), new Foo())); + +// First arg pins T to Bar; second is also a Bar, fine +var_dump(pair(new Bar(), new Bar())); + +// First arg Foo, second Bar — T pinned to Foo (first wins) +var_dump(pair(new Foo(), new Bar())); +?> +--EXPECT-- +string(3) "Foo" +string(3) "Bar" +string(3) "Foo" diff --git a/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt b/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt new file mode 100644 index 000000000000..a0a64c2d4a7d --- /dev/null +++ b/Zend/tests/generics/reification/naked_new_runtime_declared_class.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: naked `new C()` synthesizes the defaults monomorph even when C is declared at runtime (trait / parameterized implements) +--FILE-- + { + use AnyTrait; + public array $items = []; + public function __construct($i = []) { $this->items = (array) $i; } + public function copy(): static { return new static($this->items); } +} + +$a = new C([1, 2]); +echo $a::class, "\n"; // C, not bare C +echo $a->copy()::class, "\n"; // C +var_dump($a::class === $a->copy()::class); +var_dump((new C([1, 2])) == (new C([1, 2]))->copy()); + +// Parameterized implements also defers declaration to runtime. +interface Iface {} +class D implements Iface { + public function __construct(public int $n = 0) {} + public function dup(): static { return new static($this->n); } +} +$d = new D(7); +echo $d::class, "\n"; // D +var_dump($d::class === $d->dup()::class); +var_dump($d instanceof Iface); + +// Control: a class with neither trait nor parameterized implements is +// early-bound; the compile-time rewrite already produces the monomorph. +class E { + public function __construct(public array $items = []) {} + public function copy(): static { return new static($this->items); } +} +$e = new E([1]); +echo $e::class, "\n"; // E +var_dump($e::class === $e->copy()::class); +?> +--EXPECT-- +C +C +bool(true) +bool(true) +D +bool(true) +bool(true) +E +bool(true) diff --git a/Zend/tests/generics/reification/named_with_args_t_in_property.phpt b/Zend/tests/generics/reification/named_with_args_t_in_property.phpt new file mode 100644 index 000000000000..7c06d2f76ce3 --- /dev/null +++ b/Zend/tests/generics/reification/named_with_args_t_in_property.phpt @@ -0,0 +1,56 @@ +--TEST-- +Reification: a property typed `I` (T inside a generic class type arg) substitutes T → binding for the monomorph +--FILE-- + $val` on a generic class. When the class is +// monomorphized (e.g. `new Box::()`), the property type should +// substitute T → string and the runtime check should accept values that +// satisfy `I`. The bug was that `zend_substitute_leaf_type_param` +// didn't recurse into NAMED_WITH_ARGS payloads, so the property stayed +// typed as the unsubstituted `I` and any assignment failed with +// "Cannot assign ... of type I". + +interface I<+T> {} + +class StrImpl implements I {} +class IntImpl implements I {} + +class Box { + public I $val; +} + +// Direct monomorph use. +$b = new Box::(); +$b->val = new StrImpl(); +var_dump($b->val::class); + +// Wrong-arg implementation rejected. +try { + $b->val = new IntImpl(); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Inherited subclass case: same substitution applies when the property is +// touched on an instance of a non-generic child. Use `mixed` on the ctor +// so the bad value reaches the property assignment site, exercising the +// inherited property's substituted type rather than the ctor's own type. +class StringBox extends Box { + public function __construct(mixed $val) { + $this->val = $val; + } +} +$sb = new StringBox(new StrImpl()); +var_dump($sb->val::class); + +try { + new StringBox(new IntImpl()); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +string(7) "StrImpl" +1: Cannot assign IntImpl to property %s::$val of type I +string(7) "StrImpl" +2: Cannot assign IntImpl to property %s::$val of type I diff --git a/Zend/tests/generics/reification/new_static_vs_self.phpt b/Zend/tests/generics/reification/new_static_vs_self.phpt new file mode 100644 index 000000000000..bbd8d32a0f8b --- /dev/null +++ b/Zend/tests/generics/reification/new_static_vs_self.phpt @@ -0,0 +1,45 @@ +--TEST-- +Reification: `new static()` reproduces the exact runtime type; `new self::<...>` re-applies the lexical template +--FILE-- +` remains the way to apply type arguments to the enclosing +// (lexical) class — including re-binding to different args. +class C { + public function __construct(private array $e = []) {} + + public function dupStatic(): static { return new static($this->e); } + public function dupSelf(): static { return new self::($this->e); } + public function swapSelf(): object { return new self::($this->e); } + public function reSelf(): object { return new self::($this->e); } +} + +$o = new C::([1, 2]); +echo "orig: ", $o::class, "\n"; +echo "dupStatic: ", $o->dupStatic()::class, "\n"; +echo "dupSelf: ", $o->dupSelf()::class, "\n"; +echo "swapSelf: ", $o->swapSelf()::class, "\n"; +echo "reSelf: ", $o->reSelf()::class, "\n"; + +// `new static()` through a non-generic subclass of a monomorph reproduces the +// subclass, exactly as plain LSB does. +class Box { + public function __construct(public mixed $v = null) {} + public function copy(): static { return new static($this->v); } +} +class IntBox extends Box {} + +$b = new IntBox(5); +echo "IntBox: ", $b::class, "\n"; +echo "IntBox copy: ", $b->copy()::class, "\n"; +?> +--EXPECT-- +orig: C +dupStatic: C +dupSelf: C +swapSelf: C +reSelf: C +IntBox: IntBox +IntBox copy: IntBox diff --git a/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt b/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt new file mode 100644 index 000000000000..f1d7b6274db4 --- /dev/null +++ b/Zend/tests/generics/reification/new_turbofish_with_class_t_ref.phpt @@ -0,0 +1,48 @@ +--TEST-- +Reification: `new SelfClass::(...)` inside a generic class method resolves T against the instance's monomorph args at synth time +--FILE-- +(...)` +// using its own class-level T's. Runtime synth must walk the called scope +// up to the lexical class's monomorph descendant to resolve the binding. + +final readonly class Bag { + public function __construct(public array $items = []) {} + + public function withItem(T $item): Bag { + $items = $this->items; + $items[] = $item; + // Forward T explicitly — must resolve against the current instance's + // monomorph (Bag stays Bag, not Bag). + return new Bag::($items); + } +} + +$b = new Bag::([1, 2, 3]); +var_dump($b::class); +$b = $b->withItem(4); +var_dump($b::class); +$b = $b->withItem(5); +var_dump($b::class); + +$s = new Bag::(['a']); +var_dump($s::class); +$s = $s->withItem('b'); +var_dump($s::class); + +// Calling withItem on a bare-default Bag still works — forwarding +// resolves T to its (default) mixed binding from the monomorph. +$d = new Bag(); +var_dump($d::class); +$d = $d->withItem(42); +var_dump($d::class); +?> +--EXPECT-- +string(8) "Bag" +string(8) "Bag" +string(8) "Bag" +string(11) "Bag" +string(11) "Bag" +string(10) "Bag" +string(10) "Bag" diff --git a/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt b/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt new file mode 100644 index 000000000000..7bdfb9ac92de --- /dev/null +++ b/Zend/tests/generics/reification/new_turbofish_with_outer_t_ref.phpt @@ -0,0 +1,61 @@ +--TEST-- +Reification: `new Box::(...)` inside `function(...)` resolves T against the caller frame's T-table at synth time, not the literal ref +--FILE-- +" class with un-resolvable +// arg_info and runtime TypeErrors mentioning the literal parameter name). + +final readonly class Box { + public function __construct(public array $items = []) {} + public function get(TNode $key): mixed { return $this->items[$key] ?? null; } +} + +function make(): Box { + return new Box::([]); +} + +function withItem(Box $b, TNode $key, mixed $val): Box { + $items = $b->items; + $items[$key] = $val; + return new Box::($items); +} + +// Concrete turbofish on the outer call — inner `new Box::(...)` +// must resolve to , not . +$b = make::(); +var_dump($b::class); + +$b = withItem::($b, 'a', 1); +$b = withItem::($b, 'b', 2); +var_dump($b::class); + +// Method that's substituted on the monomorph should accept the bound key type. +var_dump($b->get('a')); +var_dump($b->get('b')); + +// Repeat with different turbofish — distinct monomorph, no stale binding leak. +$s = make::(); +var_dump($s::class); +$s = withItem::($s, 0, 'zero'); +var_dump($s::class); +var_dump($s->get(0)); + +// Defaults case: turbofish-less call falls back to declared defaults +// (mixed, mixed), and inner `new Box::(...)` resolves against +// those — producing Box, not Box. +$m = make(); +var_dump($m::class); +?> +--EXPECT-- +string(15) "Box" +string(15) "Box" +int(1) +int(2) +string(15) "Box" +string(15) "Box" +string(4) "zero" +string(16) "Box" diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.inc b/Zend/tests/generics/reification/preload_aot_call_lowering.inc new file mode 100644 index 000000000000..b86db5d7f720 --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.inc @@ -0,0 +1,28 @@ +(T $x): T { return $x; } + +function add(T $a, T $b): T { return $a + $b; } + +// No value params: dispatch must fire from the turbofish alone, nothing inferable from args. +function zero(): string { return "z"; } + +class Runner { + public static function ints(int $n): int { + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = add::($acc, id::($i)); + } + return $acc; + } + + public static function strs(): string { + return id::("hello") . ":" . zero::(); + } + + public static function badType(): void { + add::(1, []); + } +} diff --git a/Zend/tests/generics/reification/preload_aot_call_lowering.phpt b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt new file mode 100644 index 000000000000..f35cd4e034af --- /dev/null +++ b/Zend/tests/generics/reification/preload_aot_call_lowering.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: AOT-lowered preloaded generic calls (INIT_FCALL/DO_UCALL + concrete RECV + by-value SEND) stay correct and type-safe +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_aot_call_lowering.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + 5. +var_dump(Bench\add::("5", 2)); + +try { + Runner::badType(); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +int(10) +string(7) "hello:z" +int(7) +Bench\add(): Argument #2 ($b) must be of type int, array given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.inc b/Zend/tests/generics/reification/preload_class_monomorph_new.inc new file mode 100644 index 000000000000..15ca454caaed --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.inc @@ -0,0 +1,27 @@ + { + public function __construct(public array $items = []) {} + public function with(T $x): Vec { + $n = $this->items; + $n[] = $x; + return new Vec::($n); + } + public function sum(): int { + $s = 0; + foreach ($this->items as $v) { $s += $v; } + return $s; + } + public function size(): int { return count($this->items); } +} + +class Driver { + public static function build(): string { + $v = new Vec::([1, 2]); + $v = $v->with(3); + $v = $v->with(4); + return $v->sum() . ':' . $v->size() . ':' . get_class($v); + } +} diff --git a/Zend/tests/generics/reification/preload_class_monomorph_new.phpt b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt new file mode 100644 index 000000000000..44c9911d2cf2 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_monomorph_new.phpt @@ -0,0 +1,27 @@ +--TEST-- +Reification: a compile-time class monomorph (`new C::`) inside a preloaded method preloads cleanly and behaves correctly +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_monomorph_new.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + SEGV at preload. +echo Driver::build(), "\n"; + +$s = new Vec::(['a']); +$s = $s->with('b'); +echo $s->size(), ':', get_class($s), "\n"; +echo "done\n"; +?> +--EXPECT-- +10:4:Vec +2:Vec +done diff --git a/Zend/tests/generics/reification/preload_class_typed_param.inc b/Zend/tests/generics/reification/preload_class_typed_param.inc new file mode 100644 index 000000000000..d48ba9710738 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.inc @@ -0,0 +1,18 @@ +(T $x): T { return $x; } + +class Keeper { + public static function take(Animal $a): string { + return get_class(keep::($a)); + } + public static function bad(): void { + keep::("not an animal"); + } +} diff --git a/Zend/tests/generics/reification/preload_class_typed_param.phpt b/Zend/tests/generics/reification/preload_class_typed_param.phpt new file mode 100644 index 000000000000..cb1fc27f1505 --- /dev/null +++ b/Zend/tests/generics/reification/preload_class_typed_param.phpt @@ -0,0 +1,35 @@ +--TEST-- +Reification: an AOT-preloaded monomorph of a bare class-typed parameter accepts subclasses and rejects non-instances +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_class_typed_param.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECTF-- +Zoo\Dog +Zoo\Animal +Zoo\keep(): Argument #1 ($x) must be of type Zoo\Animal, string given, called in %s on line %d +done diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.inc b/Zend/tests/generics/reification/preload_generic_call_ops.inc new file mode 100644 index 000000000000..b02304b58327 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.inc @@ -0,0 +1,10 @@ +(T $x): T { + return $x; +} + +function outer(T $x): T { + return inner($x); +} diff --git a/Zend/tests/generics/reification/preload_generic_call_ops.phpt b/Zend/tests/generics/reification/preload_generic_call_ops.phpt new file mode 100644 index 000000000000..871747b8e8d6 --- /dev/null +++ b/Zend/tests/generics/reification/preload_generic_call_ops.phpt @@ -0,0 +1,25 @@ +--TEST-- +Reification: a preloaded generic function that contains a generic call op resolves correctly and shuts down cleanly (smoke test for the destroy_op_array runtime-cache cleanup; the underlying UAF is caught deterministically under valgrind/ASAN) +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.preload={PWD}/preload_generic_call_ops.inc +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +(5)); +var_dump(outer(7)); +var_dump(inner::("ok")); +echo "done\n"; +?> +--EXPECT-- +int(5) +int(7) +string(2) "ok" +done diff --git a/Zend/tests/generics/reification/property_t_reified.phpt b/Zend/tests/generics/reification/property_t_reified.phpt new file mode 100644 index 000000000000..f8da7dfec662 --- /dev/null +++ b/Zend/tests/generics/reification/property_t_reified.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reification: typed properties whose declared type is `T` check the reified binding on assignment +--FILE-- +() — assignments to $value must +// check against int, not the erased "mixed" bound. + +class Box { + public T $value; +} + +$b = new Box::(); +$b->value = 42; +var_dump($b->value); + +try { + $b->value = "not-an-int"; +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Object T case. +class Item {} +class Other {} + +class Holder { + public T $thing; +} + +$h = new Holder::(); +$h->thing = new Item; +var_dump(get_class($h->thing)); + +try { + $h->thing = new Other; +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Nullable T. +class MaybeBox { + public ?T $value = null; +} + +$mb = new MaybeBox::(); +$mb->value = 7; +var_dump($mb->value); +$mb->value = null; +var_dump($mb->value); + +try { + $mb->value = "wrong"; +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +int(42) +1: Cannot assign string to property Box::$value of type int +string(4) "Item" +2: Cannot assign Other to property Holder::$thing of type Item +int(7) +NULL +3: Cannot assign string to property MaybeBox::$value of type ?int diff --git a/Zend/tests/generics/reification/recursion_per_frame_table.phpt b/Zend/tests/generics/reification/recursion_per_frame_table.phpt new file mode 100644 index 000000000000..9bddb9205798 --- /dev/null +++ b/Zend/tests/generics/reification/recursion_per_frame_table.phpt @@ -0,0 +1,43 @@ +--TEST-- +Reification: each call frame has its own T-table; recursion across different bindings does not leak +--FILE-- +(int $depth): array { + if ($depth === 0) { + return ["T" => T::class, "U" => U::class]; + } + // Recurse with T and U swapped; on return our slot must still read A/B. + $inner = pair::($depth - 1); + return [ + "outer_T" => T::class, + "outer_U" => U::class, + "inner" => $inner, + ]; +} + +var_dump(pair::(2)); +?> +--EXPECT-- +array(3) { + ["outer_T"]=> + string(1) "A" + ["outer_U"]=> + string(1) "B" + ["inner"]=> + array(3) { + ["outer_T"]=> + string(1) "B" + ["outer_U"]=> + string(1) "A" + ["inner"]=> + array(2) { + ["T"]=> + string(1) "A" + ["U"]=> + string(1) "B" + } + } +} diff --git a/Zend/tests/generics/reification/return_type_reified.phpt b/Zend/tests/generics/reification/return_type_reified.phpt new file mode 100644 index 000000000000..04f3ba7f0aeb --- /dev/null +++ b/Zend/tests/generics/reification/return_type_reified.phpt @@ -0,0 +1,63 @@ +--TEST-- +Reification: VERIFY_RETURN_TYPE checks the reified T binding, not the erased bound +--FILE-- +(T $x): T { return $x; } +function ret_nonnumeric_string(T $x): T { return "abc"; } +function ret_array(T $x): T { return [1, 2]; } +function maybe(T $x): ?T { return null; } +function lift(T $x): array { return [$x]; } + +// Positive: returning the correct concrete T passes. +var_dump(id::(42)); +var_dump(id::("hi")); + +// Negative: returning a wrong, non-coercible concrete type fires with the +// resolved T, not the erased "mixed". +// - "abc" (non-numeric string) cannot coerce to int even in weak mode. +// - array cannot coerce to string at all. +try { + ret_nonnumeric_string::(1); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} +try { + ret_array::("x"); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// `?T`: nullable return on a T-typed function still accepts null when bound +// to a non-nullable concrete type. The reified check preserves the nullable +// bit from the outer T-ref. +var_dump(maybe::(0)); + +// Non-T return type (`array`) is unaffected by the reified-return path — +// goes through the normal erased check. +var_dump(lift::(7)); + +// Object T: returning the wrong class names the bound class in the error. +class A {} +class B {} +function obj(T $x): T { return new B(); } +try { + obj::(new A()); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(42) +string(2) "hi" +1: ret_nonnumeric_string(): Return value must be of type int, string returned +2: ret_array(): Return value must be of type string, array returned +NULL +array(1) { + [0]=> + int(7) +} +3: obj(): Return value must be of type A, B returned diff --git a/Zend/tests/generics/reification/return_type_reified_strict.phpt b/Zend/tests/generics/reification/return_type_reified_strict.phpt new file mode 100644 index 000000000000..16a9855f3fbe --- /dev/null +++ b/Zend/tests/generics/reification/return_type_reified_strict.phpt @@ -0,0 +1,44 @@ +--TEST-- +Reification: VERIFY_RETURN_TYPE under strict_types catches coercible mismatches the weak path would silently accept +--FILE-- +(T $x): T { return 999; } +function ret_str(T $x): T { return "1.5"; } +function ret_float(T $x): T { return 3.14; } + +// Under strict_types, an int return from a function bound to T=string must +// throw — weak mode would silently coerce to "999". +try { + ret_int::("a"); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Numeric string returned where T=int — weak would coerce; strict throws. +try { + ret_str::(0); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Float returned where T=int — weak would coerce; strict throws. +try { + ret_float::(0); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} + +// Positive: correct concrete types still pass. +var_dump(ret_int::(0)); +var_dump(ret_str::("a")); +var_dump(ret_float::(0.0)); +?> +--EXPECTF-- +1: ret_int(): Return value must be of type string, int returned +2: ret_str(): Return value must be of type int, string returned +3: ret_float(): Return value must be of type int, float returned +int(999) +string(3) "1.5" +float(3.14) diff --git a/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt b/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt new file mode 100644 index 000000000000..ebb7567f28a0 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_chains_through_outer.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reification: turbofish whose arg is an outer T resolves against the caller's T-table +--FILE-- +(): string { + return U::class; +} + +function outer(): string { + // The turbofish :: here doesn't have a literal class name; it names + // the enclosing parameter T. The runtime must look T up in the caller + // frame's T-table and pass that to inner. + return inner::(); +} + +echo outer::(), "\n"; +?> +--EXPECT-- +Foo diff --git a/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt new file mode 100644 index 000000000000..790d0bf63333 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt @@ -0,0 +1,36 @@ +--TEST-- +Reification: an unqualified class name in a turbofish type argument resolves against the current namespace +--FILE-- +(T $x): T { return $x; } + +$dog = identity::(new Dog()); +echo get_class($dog), "\n"; + +$animal = identity::(new Animal()); +echo get_class($animal), "\n"; + +try { + identity::("not an animal"); +} catch (\TypeError $e) { + echo $e->getMessage(), "\n"; +} + +function wrap(T $x): T { return $x; } +$d2 = wrap::(new Dog()); +echo get_class($d2), "\n"; + +echo "done\n"; +?> +--EXPECTF-- +Acme\Dog +Acme\Animal +Acme\identity(): Argument #1 ($x) must be of type Acme\Animal, string given, called in %s on line %d +Acme\Dog +done diff --git a/Zend/tests/generics/reification/turbofish_new_T.phpt b/Zend/tests/generics/reification/turbofish_new_T.phpt new file mode 100644 index 000000000000..b70952227b79 --- /dev/null +++ b/Zend/tests/generics/reification/turbofish_new_T.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reification: explicit turbofish populates the T-table; `new T()` in body resolves to the supplied class +--FILE-- +(): T { + return new T(); +} + +var_dump(make::()->kind); +var_dump(make::()->kind); +?> +--EXPECT-- +string(3) "foo" +string(3) "bar" diff --git a/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt b/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt new file mode 100644 index 000000000000..507a1f2b0ab9 --- /dev/null +++ b/Zend/tests/generics/reification/union_t_null_pre_erasure.phpt @@ -0,0 +1,49 @@ +--TEST-- +Reification: `T|null` and `?T` are the same pre-erasure shape across abstract/interface inheritance +--FILE-- + { + public function get(TK|null $k): ?TV; +} +class C implements I { + public function get(TK|null $k): ?TV { return $k; } +} + +// Same shape via an abstract class. +abstract class AB { + abstract public function get(TK|null $k): ?TV; +} +class CB extends AB { + public function get(TK|null $k): ?TV { return $k; } +} + +// Mixed syntax: proto uses `?TK`, child uses `TK|null` — must still match. +interface J { + public function get(?TK $k): ?TV; +} +class D implements J { + public function get(TK|null $k): ?TV { return $k; } +} + +echo "compiled ok\n"; + +// The reified binding is still enforced at runtime for the union param form. +$c = new C::(); +var_dump($c->get(null)); // null accepted by `TK|null` == `?int` +var_dump($c->get(5)); + +try { + $c->get("not-an-int"); +} catch (TypeError $e) { + echo "TypeError: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +compiled ok +NULL +int(5) +TypeError: C::get(): Argument #1 ($k)%smust be of type ?int, string given%a diff --git a/Zend/tests/generics/reification/union_with_t_in_property.phpt b/Zend/tests/generics/reification/union_with_t_in_property.phpt new file mode 100644 index 000000000000..2d594bbeaf0b --- /dev/null +++ b/Zend/tests/generics/reification/union_with_t_in_property.phpt @@ -0,0 +1,63 @@ +--TEST-- +Reification: a union `T|null` (and other unions containing T) on a property reifies the T-ref the same way `?T` does +--FILE-- + { + public T|null $w; + public function __construct(T|null $w) { $this->w = $w; } +} + +class TInt { + public T|int $w; + public function __construct(T|int $w) { $this->w = $w; } +} + +class Foo {} +class Bar {} +class Baz {} + +class TFoo { + public T|Foo $w; + public function __construct(T|Foo $w) { $this->w = $w; } +} + +// T|null with primitive T accepts T and null and rejects others. +$a = new TNull::(5); +var_dump($a->w); +$a->w = null; +var_dump($a->w); +try { $a->w = "x"; } catch (TypeError $e) { echo "1: ", $e->getMessage(), "\n"; } + +// T|int: T=string accepts strings and ints; under strict_types, a float is +// rejected outright. +$b = new TInt::("hi"); +var_dump($b->w); +$b->w = 7; +var_dump($b->w); +try { $b->w = 1.5; } catch (TypeError $e) { echo "2: ", $e->getMessage(), "\n"; } + +// T|Foo: T=Bar accepts Bar or Foo, rejects an unrelated class. +$c = new TFoo::(new Bar); +var_dump($c->w::class); +$c->w = new Foo; +var_dump($c->w::class); +try { $c->w = new Baz; } catch (TypeError $e) { echo "3: ", $e->getMessage(), "\n"; } +?> +--EXPECT-- +int(5) +NULL +1: Cannot assign string to property TNull::$w of type ?int +string(2) "hi" +int(7) +2: Cannot assign float to property TInt::$w of type string|int +string(3) "Bar" +string(3) "Foo" +3: Cannot assign Baz to property TFoo::$w of type Bar|Foo diff --git a/Zend/tests/generics/reification/variadic_t_in_closure.phpt b/Zend/tests/generics/reification/variadic_t_in_closure.phpt new file mode 100644 index 000000000000..696944ae10ea --- /dev/null +++ b/Zend/tests/generics/reification/variadic_t_in_closure.phpt @@ -0,0 +1,55 @@ +--TEST-- +Reification: variadic T parameter is checked even when T comes from a closure's captured frame, not the closure's own turbofish +--FILE-- +(): Closure { + return function(T ...$xs): int { + return count($xs); + }; +} + +$intMaker = maker::(); +var_dump($intMaker(1, 2, 3)); // positive: all int + +try { + $intMaker(1, "bad", 3); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +try { + $intMaker(1, 2, [4]); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +class Animal {} +class Dog extends Animal {} +class Cat extends Animal {} + +function dogMaker(): Closure { + return function(T ...$xs): int { + return count($xs); + }; +} + +$dm = dogMaker::(); +var_dump($dm(new Dog, new Dog)); + +try { + $dm(new Dog, new Cat); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(3) +1: {closure:maker():%d}(): Argument #2 ($xs) must be of type int, string given +2: {closure:maker():%d}(): Argument #3 ($xs) must be of type int, array given +int(2) +3: {closure:dogMaker():%d}(): Argument #2 ($xs) must be of type Dog, Cat given diff --git a/Zend/tests/generics/reification/variadic_t_reified.phpt b/Zend/tests/generics/reification/variadic_t_reified.phpt new file mode 100644 index 000000000000..9cdd17bcf6fb --- /dev/null +++ b/Zend/tests/generics/reification/variadic_t_reified.phpt @@ -0,0 +1,75 @@ +--TEST-- +Reification: variadic T parameter checks every element against the reified T binding, not just the first +--FILE-- +(T ...$xs): T { + $r = $xs[0]; + for ($i = 1; $i < count($xs); $i++) $r += $xs[$i]; + return $r; +} + +// Positive: all three elements are ints. +var_dump(sum::(1, 2, 3)); + +// Negative: the second element violates T; the error names that element +// (Argument #2) with the resolved T type ("int"). +try { + sum::(1, "abc", 3); +} catch (TypeError $e) { + echo "1: ", $e->getMessage(), "\n"; +} + +// Negative: the third element violates T. +try { + sum::(1, 2, "xyz"); +} catch (TypeError $e) { + echo "2: ", $e->getMessage(), "\n"; +} + +// Variadic with leading non-variadic param: positional 1 ($prefix) is a +// plain string, the variadic slot is `T ...`. The reified sweep starts at +// the variadic position and reports element index correctly. +function concat(string $prefix, T ...$xs): string { + $r = $prefix; + foreach ($xs as $x) $r .= (string) $x; + return $r; +} + +var_dump(concat::(":: ", 1, 2, 3)); + +try { + concat::(":: ", 1, [4, 5], 3); +} catch (TypeError $e) { + echo "3: ", $e->getMessage(), "\n"; +} + +// Object T over a variadic: a value of the wrong class fires with the bound +// class name, not the parameter letter. +class Animal {} +class Dog extends Animal {} +class Cat extends Animal {} + +function herd(T ...$xs): int { + return count($xs); +} + +var_dump(herd::(new Dog, new Dog, new Dog)); + +try { + herd::(new Dog, new Cat, new Dog); +} catch (TypeError $e) { + echo "4: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(6) +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 must be of type int, array given, called in %s on line %d +int(3) +4: herd(): Argument #2 must be of type Dog, Cat given, called in %s on line %d diff --git a/Zend/tests/generics/reification/variance_reified.phpt b/Zend/tests/generics/reification/variance_reified.phpt new file mode 100644 index 000000000000..4f068d72d05b --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified.phpt @@ -0,0 +1,54 @@ +--TEST-- +Reification: positive inheritance-variance cases — child signatures substituted against the reified T compile correctly +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } + public function set(T $x): void {} +} + +// Child matches the substituted parent signature exactly. +class SubInt extends Base { + public function get(): int { return 42; } + public function set(int $x): void {} +} + +var_dump((new SubInt)->get()); +(new SubInt)->set(99); +echo "SubInt ok\n"; + +// Param contravariance: child accepts a WIDER param (mixed accepts int) +// than the parent's substituted int. This is allowed. +class SubParamWide extends Base { + public function set(mixed $x): void {} +} +(new SubParamWide)->set(7); +(new SubParamWide)->set("hello"); +echo "SubParamWide ok\n"; + +// Return-type covariance with class hierarchy: parent returns T = Animal, +// child returns Dog (a subclass of Animal). Allowed. +class Animal {} +class Dog extends Animal {} + +class AnimalSrc { + public function get(): T { throw new Exception('abstract'); } +} + +class DogSrc extends AnimalSrc { + public function get(): Dog { return new Dog; } +} +var_dump(get_class((new DogSrc)->get())); +?> +--EXPECT-- +int(42) +SubInt ok +SubParamWide ok +string(3) "Dog" diff --git a/Zend/tests/generics/reification/variance_reified_violation_param.phpt b/Zend/tests/generics/reification/variance_reified_violation_param.phpt new file mode 100644 index 000000000000..42e42625e700 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child narrowing a generic parent's reified parameter type (int → string) is rejected by variance (contravariance) +--FILE-- + { + public function set(T $x): void {} +} + +// Child accepts NARROWER (string) than parent's reified int — not contravariant. +class SubParam extends Base { + public function set(string $x): void {} +} +?> +--EXPECTF-- +Fatal error: Declaration of SubParam::set(string $x): void must be compatible with Base::set(int $x): void in %s on line %d diff --git a/Zend/tests/generics/reification/variance_reified_violation_return.phpt b/Zend/tests/generics/reification/variance_reified_violation_return.phpt new file mode 100644 index 000000000000..2997cdcaa5e9 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_return.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child overriding a generic parent method with a non-compatible return type fires the variance check against the reified T +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } +} + +// Child says get(): string but parent's reified return is int. +class SubIntBroken extends Base { + public function get(): string { return "x"; } +} +?> +--EXPECTF-- +Fatal error: Declaration of SubIntBroken::get(): string must be compatible with Base::get(): int in %s on line %d diff --git a/Zend/tests/generics/reification/variance_reified_violation_widening.phpt b/Zend/tests/generics/reification/variance_reified_violation_widening.phpt new file mode 100644 index 000000000000..e48d75c1b578 --- /dev/null +++ b/Zend/tests/generics/reification/variance_reified_violation_widening.phpt @@ -0,0 +1,15 @@ +--TEST-- +Reification: child widening a generic parent's reified return type (int → mixed) is rejected by variance +--FILE-- + { + public function get(): T { throw new Exception('abstract'); } +} + +// Child can't widen the parent's reified int return to mixed. +class SubIntWiden extends Base { + public function get(): mixed { return 42; } +} +?> +--EXPECTF-- +Fatal error: Declaration of SubIntWiden::get(): mixed must be compatible with Base::get(): int in %s on line %d diff --git a/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt b/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt new file mode 100644 index 000000000000..ff42a5abfcac --- /dev/null +++ b/Zend/tests/generics/runtime/class_exists_invalid_monomorph_name.phpt @@ -0,0 +1,35 @@ +--TEST-- +class_exists()/interface_exists() return false (never raise) for out-of-bound or non-generic monomorph names +--FILE-- + {} +interface Iface {} + +// Bound violation: int is not an object, so "Box" names no class. A +// passive existence probe must report that as false, not raise (the `new` / +// type-declaration paths still enforce the bound with a fatal). +var_dump(class_exists('Box')); +// Bound satisfied -> the monomorph is synthesized on demand. +var_dump(class_exists('Box')); +// Type arguments on a non-generic class name no existing class either. +var_dump(class_exists('Plain')); +// Plain lookups are unaffected. +var_dump(class_exists('Plain')); +var_dump(class_exists('Box')); +// interface_exists() behaves the same way. +var_dump(interface_exists('Iface')); +var_dump(interface_exists('Iface')); + +echo "done\n"; +?> +--EXPECT-- +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +done diff --git a/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt b/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt new file mode 100644 index 000000000000..952680530dc1 --- /dev/null +++ b/Zend/tests/generics/runtime/dynamic_new_synthesis.phpt @@ -0,0 +1,46 @@ +--TEST-- +Dynamic `new $name()`: bare generic with defaults synthesizes; without defaults throws +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class NoDefault { + public function __construct(public mixed $value) {} +} + +// Dynamic new of a bare generic with defaults → defaults monomorph. +$cls = 'Box'; +$b = new $cls(42); +var_dump($b::class); + +// Dynamic new of a canonical name → that monomorph. +$cls2 = 'Box'; +$b2 = new $cls2("x"); +var_dump($b2::class); + +// Dynamic new with turbofish: the explicit args win. +$cls3 = 'Box'; +$b3 = new $cls3::(1.5); +var_dump($b3::class); + +// Dynamic new of a no-defaults bare generic → Error. +try { + $cls4 = 'NoDefault'; + new $cls4("oops"); +} catch (Error $e) { + echo "err: ", $e->getMessage(), "\n"; +} + +// But turbofish on the same bare name works. +$cls5 = 'NoDefault'; +$b5 = new $cls5::(7); +var_dump($b5::class); +?> +--EXPECT-- +string(8) "Box" +string(11) "Box" +string(10) "Box" +err: Cannot instantiate generic class NoDefault without type arguments via dynamic class name; no defaults declared +string(14) "NoDefault" diff --git a/Zend/tests/generics/runtime/lsb_through_monomorph.phpt b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt new file mode 100644 index 000000000000..259953f81097 --- /dev/null +++ b/Zend/tests/generics/runtime/lsb_through_monomorph.phpt @@ -0,0 +1,62 @@ +--TEST-- +Late static binding from inside a monomorph: static::class / static::$prop / static::FOO +--FILE-- + { + public static int $count = 0; + const TAG = "Box"; + + public static function whoAmI(): string { return static::class; } + public static function bump(): void { static::$count++; } + public static function tag(): string { return static::TAG; } + public static function self_(): static { return new static(0); } + + public function __construct(public mixed $value) {} +} + +class StrBox extends Box { + public static int $count = 100; + const TAG = "StrBox"; +} + +$intCls = "Box"; +$strCls = "Box"; + +// static::class returns the late-bound class. +var_dump($intCls::whoAmI()); +var_dump($strCls::whoAmI()); +var_dump(StrBox::whoAmI()); + +// static::$prop sees the late-bound class's storage. Monomorphs share the +// base's static property (Box and Box are one slot); StrBox +// redeclares $count, so it gets its own — exactly as a subclass does in +// stock PHP. +$intCls::bump(); +$intCls::bump(); +$strCls::bump(); +StrBox::bump(); +var_dump($intCls::$count); +var_dump($strCls::$count); +var_dump(StrBox::$count); + +// static::FOO honours overrides via LSB. +var_dump($intCls::tag()); +var_dump(StrBox::tag()); + +// new static() returns the late-bound monomorph instance. +$a = $intCls::self_(); +$b = StrBox::self_(); +var_dump($a::class); +var_dump($b::class); +?> +--EXPECT-- +string(8) "Box" +string(11) "Box" +string(6) "StrBox" +int(3) +int(3) +int(101) +string(3) "Box" +string(6) "StrBox" +string(8) "Box" +string(6) "StrBox" diff --git a/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt b/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt new file mode 100644 index 000000000000..9eabeadfd3d3 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_canonical_unions.phpt @@ -0,0 +1,44 @@ +--TEST-- +Monomorph canonicalization: union and intersection arguments collapse to the same monomorph regardless of source order +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Union args canonicalize: order doesn't matter. +$a = new Box::(0); +$b = new Box::(0); +var_dump($a::class); +var_dump($b::class); +var_dump($a::class === $b::class); + +// Intersection args canonicalize: order doesn't matter. +$c = new Box::(new class implements Stringable1, Stringable2 { + public function s1(): string { return "s1"; } + public function s2(): string { return "s2"; } +}); +$d = new Box::(new class implements Stringable1, Stringable2 { + public function s1(): string { return "s1"; } + public function s2(): string { return "s2"; } +}); +var_dump($c::class); +var_dump($d::class); +var_dump($c::class === $d::class); + +// Two equivalent canonical forms produce the same monomorph class. +$x = new Box::(1); +$y = new Box::(1); +var_dump($x::class === $y::class); +?> +--EXPECT-- +string(15) "Box" +string(15) "Box" +bool(true) +string(28) "Box" +string(28) "Box" +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_class_exists.phpt b/Zend/tests/generics/runtime/monomorph_class_exists.phpt new file mode 100644 index 000000000000..99bdbf12c32a --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_class_exists.phpt @@ -0,0 +1,25 @@ +--TEST-- +Monomorph lookup: class_exists synthesizes the monomorph on demand +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Before any synthesis, class_exists on a canonical name should synthesize. +var_dump(class_exists("Box")); +var_dump(class_exists("Box")); +var_dump(class_exists("Box")); + +// Once synthesized, subsequent lookups hit the cached entry. +var_dump(class_exists("Box")); + +// Base class still exists. +var_dump(class_exists("Box")); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt b/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt new file mode 100644 index 000000000000..3eda60d6ab58 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_dnf_canonicalization.phpt @@ -0,0 +1,50 @@ +--TEST-- +Monomorph canonicalization: DNF type-args sort to a canonical form (intersection-sorted, union-sorted) +--FILE-- + {} + +// Same canonical class entry regardless of source order. +$names = [ + new Box::<(A&B)|(C&D)>(), + new Box::<(B&A)|(D&C)>(), + new Box::<(C&D)|(A&B)>(), + new Box::<(A&B)|(D&C)>(), + new Box::<(B&A)|(C&D)>(), +]; + +$canonical = $names[0]::class; +echo "canonical: $canonical\n"; +foreach ($names as $i => $b) { + var_dump($b::class === $canonical); +} + +// Three-way union of intersections. +$x = new Box::<(A&B)|(C&D)|(D&E)>(); +$y = new Box::<(D&E)|(B&A)|(D&C)>(); +var_dump($x::class); +var_dump($y::class === $x::class); + +// Mixed scalars and intersections in a union. +$p = new Box::(); +$q = new Box::<(B&A)|string|int>(); +var_dump($p::class); +var_dump($q::class === $p::class); +?> +--EXPECT-- +canonical: Box<(A&B)|(C&D)> +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(22) "Box<(A&B)|(C&D)|(D&E)>" +bool(true) +string(21) "Box<(A&B)|int|string>" +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt b/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt new file mode 100644 index 000000000000..ced054aa90a3 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_dynamic_new.phpt @@ -0,0 +1,34 @@ +--TEST-- +Monomorph lookup: `new $name()` with a canonical name string synthesizes on demand +--FILE-- + { + public function __construct(public mixed $value) {} +} + +// Pre-warm so the class entry exists for the explicit form too. +new Box::(0); + +$name = "Box"; +$obj = new $name(42); +var_dump($obj::class); +var_dump($obj instanceof Box); +var_dump($obj->value); + +// Names not yet synthesized are created on demand. +$other = "Box"; +$obj2 = new $other("hi"); +var_dump($obj2::class); +var_dump($obj2 instanceof Box); + +// Identity: two dynamic news of the same canonical name produce instances of the same class. +$obj3 = new $name(7); +var_dump($obj3::class === $obj::class); +?> +--EXPECT-- +string(8) "Box" +bool(true) +int(42) +string(11) "Box" +bool(true) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_nested_args.phpt b/Zend/tests/generics/runtime/monomorph_nested_args.phpt new file mode 100644 index 000000000000..5510c3a59ddd --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_nested_args.phpt @@ -0,0 +1,37 @@ +--TEST-- +Monomorph nesting: type arguments may themselves be generic, canonicalized recursively +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class Pair { + public function __construct(public mixed $left, public mixed $right) {} +} + +// Nested generic args canonicalize. +$a = new Container::>(new Container::(1)); +var_dump($a::class); +var_dump($a instanceof Container); +var_dump($a->value::class); + +// Mixed nesting and unions. +$b = new Container::>(new Pair::(1, "x")); +var_dump($b::class); + +// Same nested form yields the same monomorph. +$c = new Container::>(new Container::(2)); +var_dump($a::class === $c::class); + +// Deeply nested. +$d = new Container::>>(new Container::>(new Container::(3))); +var_dump($d::class); +?> +--EXPECT-- +string(25) "Container>" +bool(true) +string(14) "Container" +string(27) "Container>" +bool(true) +string(36) "Container>>" diff --git a/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt b/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt new file mode 100644 index 000000000000..842bfaa817c0 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_observables_sweep.phpt @@ -0,0 +1,100 @@ +--TEST-- +Monomorph observables sweep: var_dump/export, exceptions, __toString, WeakMap/WeakRef, FCC, class_parents, class_implements +--FILE-- + { + public function __construct(public T $value) {} + public function __toString(): string { return "Box(" . $this->value . ")"; } +} + +class IntBoxChild extends Box {} + +$b = new Box::(42); + +// var_dump and var_export use the canonical mono name. +var_dump($b); +var_export($b); echo "\n"; + +// __toString shows the substituted T-typed property. +echo "toString: ", $b, "\n"; + +// TypeError when the substituted constructor signature is violated. The error +// message references the property's declaring class (the bare base), not the +// monomorph — that's the standard PHP behaviour for typed-property errors and +// matches where the slot's metadata lives. +try { + new Box::("not int"); +} catch (TypeError $e) { + echo "type-err raised: int property rejects string\n"; +} + +// get_class / ::class / get_parent_class. +var_dump(get_class($b)); +var_dump($b::class); +var_dump(get_parent_class("Box")); + +// class_parents / class_implements walk the mono chain. +$c = new IntBoxChild(1); +$parents = class_parents($c); +ksort($parents); +print_r(array_keys($parents)); +print_r(array_keys(class_implements($c))); + +// WeakMap keyed by a mono instance. +$wm = new WeakMap(); +$wm[$b] = "tag"; +var_dump($wm[$b]); + +// WeakReference also. +$wr = WeakReference::create($b); +var_dump($wr->get() === $b); + +// First-class callable on a mono method. +$f = $b->__toString(...); +var_dump($f()); + +// get_object_vars / get_class_vars / get_class_methods. +print_r(get_object_vars($b)); +print_r(get_class_vars("Box")); +$methods = get_class_methods("Box"); +sort($methods); +print_r($methods); +?> +--EXPECTF-- +object(Box)#%d (1) { + ["value"]=> + int(42) +} +\Box::__set_state(array( + 'value' => 42, +)) +toString: Box(42) +type-err raised: int property rejects string +string(8) "Box" +string(8) "Box" +string(3) "Box" +Array +( + [0] => Box + [1] => Box +) +Array +( + [0] => Stringable +) +string(3) "tag" +bool(true) +string(7) "Box(42)" +Array +( + [value] => 42 +) +Array +( + [value] => %w +) +Array +( + [0] => __construct + [1] => __toString +) diff --git a/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt b/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt new file mode 100644 index 000000000000..c727fe5032c1 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_opcache_file_cache.phpt @@ -0,0 +1,35 @@ +--TEST-- +Opcache file_cache: monomorphs synthesize freshly on each request; cached classes preserve canonical parent_name +--EXTENSIONS-- +opcache +--INI-- +opcache.enable_cli=1 +opcache.file_cache={TMP} +opcache.file_cache_only=1 +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class IntBox extends Box {} + +// Force the same set of synthesis events on every request. +$b = new IntBox(42); +echo "class: ", $b::class, "\n"; +echo "parent: ", (new ReflectionClass($b))->getParentClass()->getName(), "\n"; + +// Defaults monomorph also synthesizes. +$b2 = new Box(7); +echo "defaults: ", $b2::class, "\n"; + +// Dynamic name still hits the lookup hook on the second-load path. +$name = "Box"; +$b3 = new $name("hi"); +echo "dyn: ", $b3::class, "\n"; +?> +--EXPECT-- +class: IntBox +parent: Box +defaults: Box +dyn: Box diff --git a/Zend/tests/generics/runtime/monomorph_reflection.phpt b/Zend/tests/generics/runtime/monomorph_reflection.phpt new file mode 100644 index 000000000000..3605ef461108 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_reflection.phpt @@ -0,0 +1,32 @@ +--TEST-- +Monomorph reflection: ReflectionClass on a synthesized monomorph reports the canonical name and parent +--FILE-- + { + public function __construct(public mixed $value) {} + public function get(): mixed { return $this->value; } +} + +$b = new Box::(42); +$mono = $b::class; + +$rc = new ReflectionClass($mono); +var_dump($rc->getName()); + +$parent = $rc->getParentClass(); +var_dump($parent->getName()); + +// The monomorph carries the same method shape as the base. +var_dump($rc->hasMethod('get')); +var_dump($rc->getMethod('__construct')->getNumberOfParameters()); + +// Reflecting via the base name still works. +$rcBase = new ReflectionClass(Box::class); +var_dump($rcBase->getName()); +?> +--EXPECT-- +string(8) "Box" +string(3) "Box" +bool(true) +int(1) +string(3) "Box" diff --git a/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt b/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt new file mode 100644 index 000000000000..090d1b3ab0d6 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_reflection_surface.phpt @@ -0,0 +1,58 @@ +--TEST-- +Reflection surface for monomorphs: isGeneric, getGenericParameters, newInstance/Args/WithoutConstructor +--FILE-- + { + public function __construct(public mixed $value) {} + public function get(): T { return $this->value; } +} + +class NoDefault {} + +$rcBase = new ReflectionClass(Box::class); +$rcMono = new ReflectionClass("Box"); + +// Generic-template vs concrete-instantiation flags. +var_dump($rcBase->isGeneric()); +var_dump($rcMono->isGeneric()); +var_dump(count($rcBase->getGenericParameters())); +var_dump(count($rcMono->getGenericParameters())); + +// Reflecting the base class and calling newInstance synthesizes the defaults +// monomorph (same contract as `new Box()`). +$bb = $rcBase->newInstance(42); +var_dump($bb::class); + +// Reflecting a monomorph instantiates that monomorph. +$bm = $rcMono->newInstance(7); +var_dump($bm::class); + +// newInstanceArgs honours the same synthesis path. +$ba = $rcMono->newInstanceArgs([99]); +var_dump($ba::class); + +// newInstanceWithoutConstructor too. +$bw = (new ReflectionClass("Box"))->newInstanceWithoutConstructor(); +var_dump($bw::class); + +// And newInstance on a no-defaults generic throws. +try { + (new ReflectionClass(NoDefault::class))->newInstance(); +} catch (Error $e) { + echo "err: ", $e->getMessage(), "\n"; +} + +// Substituted method signature is visible on the monomorph. +echo "Box::get returns: ", $rcMono->getMethod('get')->getReturnType(), "\n"; +?> +--EXPECT-- +bool(true) +bool(false) +int(1) +int(0) +string(8) "Box" +string(8) "Box" +string(8) "Box" +string(11) "Box" +err: Cannot instantiate generic class NoDefault without type arguments; type parameter T has no default +Box::get returns: int diff --git a/Zend/tests/generics/runtime/monomorph_serialize.phpt b/Zend/tests/generics/runtime/monomorph_serialize.phpt new file mode 100644 index 000000000000..f6509147df63 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_serialize.phpt @@ -0,0 +1,29 @@ +--TEST-- +Monomorph serialization: serialize/unserialize round-trips through the canonical class name +--FILE-- + { + public function __construct(public mixed $value) {} +} + +$b = new Box::(42); +$s = serialize($b); + +// The serialized payload encodes the canonical class name. +echo $s, "\n"; + +// Unserialize materializes the monomorph (synthesizing if not yet present). +$b2 = unserialize($s); +var_dump($b2::class); +var_dump($b2 instanceof Box); +var_dump($b2->value); + +// Round-trip identity through the canonical name. +var_dump($b::class === $b2::class); +?> +--EXPECTF-- +O:8:"Box":1:{s:5:"value";i:42;} +string(8) "Box" +bool(true) +int(42) +bool(true) diff --git a/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt b/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt new file mode 100644 index 000000000000..384f5579513f --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_typed_class_constants.phpt @@ -0,0 +1,36 @@ +--TEST-- +Typed class constants: per-monomorph substituted type info +--FILE-- + { + const T DEFAULT_VALUE = 0; + const string TAG = "Box"; // non-generic, shared as-is +} + +$rcBase = new ReflectionClass(Box::class); +echo "Box::DEFAULT_VALUE type=", $rcBase->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; +echo "Box::TAG type=", $rcBase->getReflectionConstant("TAG")->getType(), "\n"; + +$rcInt = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcInt->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; +echo "Box::TAG type=", $rcInt->getReflectionConstant("TAG")->getType(), "\n"; + +$rcStr = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcStr->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; + +$rcUnion = new ReflectionClass("Box"); +echo "Box::DEFAULT_VALUE type=", $rcUnion->getReflectionConstant("DEFAULT_VALUE")->getType(), "\n"; + +// Constant values stay shared (immutable) — substitution only changes the type. +var_dump(Box::DEFAULT_VALUE); +var_dump((new ReflectionClass("Box"))->getReflectionConstant("DEFAULT_VALUE")->getValue()); +?> +--EXPECT-- +Box::DEFAULT_VALUE type=mixed +Box::TAG type=string +Box::DEFAULT_VALUE type=int +Box::TAG type=string +Box::DEFAULT_VALUE type=?string +Box::DEFAULT_VALUE type=int|float +int(0) +int(0) diff --git a/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt b/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt new file mode 100644 index 000000000000..56eee80829e8 --- /dev/null +++ b/Zend/tests/generics/runtime/monomorph_unserialize_fresh_process.phpt @@ -0,0 +1,63 @@ +--TEST-- +Monomorph unserialize: a fresh process can unserialize a canonical generic name even when no instance has been constructed yet +--FILE-- +); unserialize() in a fresh process must +// be able to materialize that monomorph just from the name, without the +// caller priming it by constructing an instance first. + +class Box { + public function __construct(public T $value) {} +} + +// Produce the canonical payload from a primed process. +$payload = serialize(new Box::(42)); + +$tmp = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_'); +file_put_contents($tmp, $payload); + +// Run the unserialize side in a fresh PHP process. The child declares the +// generic template but never constructs an instance, so the monomorph +// Box only exists as a name in the serialized payload. +$script = <<<'PHP' + { + public function __construct(public T $value) {} +} +$s = file_get_contents($argv[1]); +$b = unserialize($s); +$out = "###RESULT###\n"; +$out .= var_export($b instanceof Box, true) . "\n"; +$out .= var_export($b::class, true) . "\n"; +$out .= var_export($b->value, true) . "\n"; +file_put_contents($argv[2], $out); +PHP; + +$scriptFile = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_script_'); +file_put_contents($scriptFile, $script); + +$resultFile = tempnam(sys_get_temp_dir(), 'monomorph_unserialize_result_'); + +$cmd = sprintf( + '%s -n %s %s %s >/dev/null 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg($scriptFile), + escapeshellarg($tmp), + escapeshellarg($resultFile), +); +shell_exec($cmd); + +echo $payload, "\n"; +echo file_get_contents($resultFile); + +@unlink($tmp); +@unlink($scriptFile); +@unlink($resultFile); +?> +--EXPECT-- +O:8:"Box":1:{s:5:"value";i:42;} +###RESULT### +true +'Box' +42 diff --git a/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt b/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt new file mode 100644 index 000000000000..fcbfe40d7ca4 --- /dev/null +++ b/Zend/tests/generics/runtime/naked_new_no_defaults_self_vs_byname.phpt @@ -0,0 +1,42 @@ +--TEST-- +Naked new in a no-defaults generic class: `new self()` binds from the frame, by-name `new C()` is an error +--FILE-- +()`. +final readonly class Box<+T> { + public function __construct(public T $value) {} + public function cloneSelf(): self { return new self($this->value); } + public function cloneStatic(): static { return new static($this->value); } + public function cloneByName(): Box { return new Box($this->value); } +} + +$b = new Box::(42); + +// `new self()` -> the live monomorph, not a bare Box. +$s = $b->cloneSelf(); +var_dump($s::class); +var_dump($s->value); + +// `new static()` -> the called scope's monomorph. +$t = $b->cloneStatic(); +var_dump($t::class); + +// `new Box()` by name (lexical self-reference) -> ambiguous type args -> a +// catchable error at runtime. (An external by-name `new Box()` the compiler can +// see is rejected at compile time instead; see naked_new_without_defaults.phpt.) +try { + $b->cloneByName(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +string(8) "Box" +int(42) +string(8) "Box" +Cannot instantiate generic class Box without type arguments; type parameter T has no default diff --git a/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt b/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt new file mode 100644 index 000000000000..c0bc09c4c1bf --- /dev/null +++ b/Zend/tests/generics/runtime/naked_new_self_static_parent.phpt @@ -0,0 +1,40 @@ +--TEST-- +Naked new self/static/parent inside generic class bodies +--FILE-- + { + public function __construct(public mixed $value) {} + public static function makeSelf(mixed $v): self { return new self($v); } + public static function makeStatic(mixed $v): static { return new static($v); } + public static function makeBare(mixed $v): Box { return new Box($v); } +} + +class IntBox extends Box { + public static function makeStatic(mixed $v): static { return new static($v); } + public function makeParent(): Box { return new parent($this->value); } +} + +class StrBox extends Box { + public function makeParent(): Box { return new parent($this->value); } +} + +// `new self()` → lexical class with defaults applied. +var_dump(Box::makeSelf(1)::class); +// `new Box()` (literal) inside Box's static method also routes through the +// same rewrite (lexical self-reference). +var_dump(Box::makeBare(2)::class); +// `new static()` from Box → defaults mono; from IntBox → IntBox (no params). +var_dump(Box::makeStatic(3)::class); +var_dump(IntBox::makeStatic(4)::class); + +// `new parent()` uses the extends args. +var_dump((new IntBox(5))->makeParent()::class); +var_dump((new StrBox("x"))->makeParent()::class); +?> +--EXPECT-- +string(8) "Box" +string(8) "Box" +string(8) "Box" +string(6) "IntBox" +string(8) "Box" +string(11) "Box" diff --git a/Zend/tests/generics/runtime/static_property_shared.phpt b/Zend/tests/generics/runtime/static_property_shared.phpt new file mode 100644 index 000000000000..3408f806a486 --- /dev/null +++ b/Zend/tests/generics/runtime/static_property_shared.phpt @@ -0,0 +1,51 @@ +--TEST-- +Static properties are shared between a generic base and all of its monomorphs +--FILE-- + { + public static int $count = 0; + public static array $items = []; + public static function bump(): void { static::$count++; } +} + +new Counter::(); +new Counter::(); + +$intCls = "Counter"; +$strCls = "Counter"; + +// Writes through any view land in the same storage. +$intCls::$count = 5; +$strCls::$count = 10; // overwrites the shared slot +var_dump($intCls::$count, $strCls::$count, Counter::$count); + +// Appends accumulate into one array regardless of the view used. +$intCls::$items[] = "via-int"; +$strCls::$items[] = "via-string"; +Counter::$items[] = "via-base"; +var_dump(Counter::$items); + +// LSB `static::` from a monomorph instance reaches the same storage as the base. +$c = new Counter::(); +$c::bump(); // static = Counter -> shared slot (now 11) +Counter::bump(); // base -> same slot (now 12) +var_dump($c::$count, Counter::$count); +?> +--EXPECT-- +int(10) +int(10) +int(10) +array(3) { + [0]=> + string(7) "via-int" + [1]=> + string(10) "via-string" + [2]=> + string(8) "via-base" +} +int(12) +int(12) diff --git a/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt b/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt new file mode 100644 index 000000000000..8ceeac6f49a9 --- /dev/null +++ b/Zend/tests/generics/scoping/closure_returned_visible_in_reflection.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: nested closure has its own ReflectionFunction view +--FILE-- +(): Closure { + return function (T $y): T { return $y; }; +} +$cl = f(); +$r = new ReflectionFunction($cl); +echo $r->getReturnType()->__toString(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/extends_clause.phpt b/Zend/tests/generics/scoping/extends_clause.phpt new file mode 100644 index 000000000000..d9839bf3d48f --- /dev/null +++ b/Zend/tests/generics/scoping/extends_clause.phpt @@ -0,0 +1,11 @@ +--TEST-- +Scoping: T is visible in extends clause +--FILE-- + {} +class Derived extends Base {} +$rc = new ReflectionClass('Derived'); +echo $rc->getParentClass()->getName(), "\n"; +?> +--EXPECT-- +Base diff --git a/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt new file mode 100644 index 000000000000..fcf1302bddf8 --- /dev/null +++ b/Zend/tests/generics/scoping/file_scope_class_does_not_shadow.phpt @@ -0,0 +1,17 @@ +--TEST-- +Scoping: a file-scope class does NOT shadow a generic type parameter +--FILE-- +(): T { + // Inside f, the bare name T refers to the generic parameter, not the + // file-scope class T. With no binding supplied, the resolver falls back + // to the parameter's bound (the file-scope class via \T) — proving the + // parameter name shadowed the class name in the body. + return new T(); +} +$obj = f(); +var_dump($obj->tag); +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt b/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt new file mode 100644 index 000000000000..032fad8b69b4 --- /dev/null +++ b/Zend/tests/generics/scoping/fq_name_bypasses_type_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: a fully qualified name `\T` bypasses the type-parameter rule +--FILE-- +(): int { + $x = new \T(); + return $x->v; +} +echo f(), "\n"; +?> +--EXPECT-- +5 diff --git a/Zend/tests/generics/scoping/implements_clause.phpt b/Zend/tests/generics/scoping/implements_clause.phpt new file mode 100644 index 000000000000..44de9db4b292 --- /dev/null +++ b/Zend/tests/generics/scoping/implements_clause.phpt @@ -0,0 +1,11 @@ +--TEST-- +Scoping: T is visible in implements clause +--FILE-- + {} +class Sink implements IConsumer {} +$rc = new ReflectionClass('Sink'); +echo $rc->getInterfaceNames()[0], "\n"; +?> +--EXPECT-- +IConsumer diff --git a/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt b/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt new file mode 100644 index 000000000000..0261f6a21b33 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadowing_is_case_insensitive.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: inner-class shadowing of a type parameter is case-insensitive +--FILE-- +(): void { + class t { + public int $v = 9; + } + $x = new T(); + echo $x->v, "\n"; +} +f(); +?> +--EXPECT-- +9 diff --git a/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt b/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt new file mode 100644 index 000000000000..a4637ae93909 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadows_in_instanceof.phpt @@ -0,0 +1,14 @@ +--TEST-- +Scoping: inner class shadows the type parameter for `instanceof` +--FILE-- +($x): bool { + class T {} + return $x instanceof T; +} +$called = false; +$result = f(new stdClass); +var_dump($result); +?> +--EXPECT-- +bool(false) diff --git a/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt b/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt new file mode 100644 index 000000000000..813387557be2 --- /dev/null +++ b/Zend/tests/generics/scoping/inner_class_shadows_type_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: a class declared inside a generic body shadows the type parameter +--FILE-- +(): void { + class T { + public int $v = 7; + } + $x = new T(); + echo $x->v, "\n"; +} +f(); +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt b/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt new file mode 100644 index 000000000000..479039281346 --- /dev/null +++ b/Zend/tests/generics/scoping/left_to_right_param_bounds.phpt @@ -0,0 +1,12 @@ +--TEST-- +Scoping: type parameters resolve in subsequent bounds (left to right) +--FILE-- + { + public U $val; +} +$rt = (new ReflectionClass('Box'))->getProperty('val')->getType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/method_introduces_own.phpt b/Zend/tests/generics/scoping/method_introduces_own.phpt new file mode 100644 index 000000000000..5c3ff1c900f2 --- /dev/null +++ b/Zend/tests/generics/scoping/method_introduces_own.phpt @@ -0,0 +1,18 @@ +--TEST-- +Scoping: method introduces its own type parameter +--FILE-- + { + public function morph(T $x, U $y): U { return $y; } +} +$rm = (new ReflectionClass('Box'))->getMethod('morph'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +echo $rm->getParameters()[1]->getType()->getName(), "\n"; +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +A +B +B diff --git a/Zend/tests/generics/scoping/method_param_does_not_leak.phpt b/Zend/tests/generics/scoping/method_param_does_not_leak.phpt new file mode 100644 index 000000000000..ec5bd29b0eb5 --- /dev/null +++ b/Zend/tests/generics/scoping/method_param_does_not_leak.phpt @@ -0,0 +1,20 @@ +--TEST-- +Scoping: method type parameter does not leak to other methods +--FILE-- +(): int { + return 1; + } + public function m2(): T { + return new T; + } +} +$b = new Box; +echo $b->m2()->val, "\n"; +?> +--EXPECT-- +7 diff --git a/Zend/tests/generics/scoping/method_param_type.phpt b/Zend/tests/generics/scoping/method_param_type.phpt new file mode 100644 index 000000000000..3be048c7b36e --- /dev/null +++ b/Zend/tests/generics/scoping/method_param_type.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: T from class scope is visible in method parameter types +--FILE-- + { + public function set(T $x): void {} +} +$rm = (new ReflectionClass('Box'))->getMethod('set'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/scoping/method_return_type.phpt b/Zend/tests/generics/scoping/method_return_type.phpt new file mode 100644 index 000000000000..6951dcc9e65d --- /dev/null +++ b/Zend/tests/generics/scoping/method_return_type.phpt @@ -0,0 +1,13 @@ +--TEST-- +Scoping: T from class scope is visible in method return types +--FILE-- + { + public function get(): T { return new Animal; } +} +$rm = (new ReflectionClass('Container'))->getMethod('get'); +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Animal diff --git a/Zend/tests/generics/scoping/nested_anon_class_captures.phpt b/Zend/tests/generics/scoping/nested_anon_class_captures.phpt new file mode 100644 index 000000000000..b7dd6bd3769f --- /dev/null +++ b/Zend/tests/generics/scoping/nested_anon_class_captures.phpt @@ -0,0 +1,21 @@ +--TEST-- +Scoping: nested anonymous class captures enclosing type parameter +--FILE-- +(T $x): object { + return new class($x) { + public T $val; + public function __construct(T $x) { $this->val = $x; } + public function get(): T { return $this->val; } + }; +} +class Dog implements Animal {} +$obj = makeAnimalBox(new Dog); +$r = new ReflectionClass($obj); +echo $r->getProperty('val')->getType()->getName(), "\n"; +echo $r->getMethod('get')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Animal +Animal diff --git a/Zend/tests/generics/scoping/nested_arrow_captures.phpt b/Zend/tests/generics/scoping/nested_arrow_captures.phpt new file mode 100644 index 000000000000..1dce854b025d --- /dev/null +++ b/Zend/tests/generics/scoping/nested_arrow_captures.phpt @@ -0,0 +1,15 @@ +--TEST-- +Scoping: nested arrow function captures enclosing type parameter +--FILE-- +(): Closure { + return fn(T $y): T => $y; +} +$cl = f(); +$r = new ReflectionFunction($cl); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int +int diff --git a/Zend/tests/generics/scoping/nested_closure_captures.phpt b/Zend/tests/generics/scoping/nested_closure_captures.phpt new file mode 100644 index 000000000000..d0fcf8ec4118 --- /dev/null +++ b/Zend/tests/generics/scoping/nested_closure_captures.phpt @@ -0,0 +1,16 @@ +--TEST-- +Scoping: nested closure captures enclosing function's type parameter +--FILE-- +(T $x): Closure { + return function (T $y): T { return $y; }; +} +class Foo {} +$cl = f(new Foo); +$r = new ReflectionFunction($cl); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/scoping/property_type.phpt b/Zend/tests/generics/scoping/property_type.phpt new file mode 100644 index 000000000000..898ecd1d96b9 --- /dev/null +++ b/Zend/tests/generics/scoping/property_type.phpt @@ -0,0 +1,12 @@ +--TEST-- +Scoping: T from class scope is visible in property types +--FILE-- + { + public T $value; +} +$rt = (new ReflectionClass('Box'))->getProperty('value')->getType(); +echo $rt->getName(), "\n"; +?> +--EXPECT-- +object diff --git a/Zend/tests/generics/scoping/trait_use_clause.phpt b/Zend/tests/generics/scoping/trait_use_clause.phpt new file mode 100644 index 000000000000..6a72ab293df9 --- /dev/null +++ b/Zend/tests/generics/scoping/trait_use_clause.phpt @@ -0,0 +1,33 @@ +--TEST-- +Scoping: a trait `use Holder` flows the using class's T-bound into the trait method's signature +--FILE-- + { + public function tag(X $x): X { return $x; } +} +class Box { + use Holder; +} + +// The trait method's X gets substituted with Box's T, which is bounded to +// object. The erased signature reflects that bound — not the trait's +// original unbounded "mixed". +echo (new ReflectionClass('Box'))->getMethod('tag')->getReturnType()->getName(), "\n"; + +// On a monomorph Box, the trait method's X is substituted to the +// concrete argument and the runtime check enforces it. +$b = new Box::(); +echo get_class($b), "\n"; +var_dump($b->tag(new stdClass) instanceof stdClass); + +try { + $b->tag(42); +} catch (TypeError $e) { + echo "rejected: ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +object +Box +bool(true) +rejected: Box::tag(): Argument #1 ($x) must be of type stdClass, int given%S diff --git a/Zend/tests/generics/syntax/args_dnf.phpt b/Zend/tests/generics/syntax/args_dnf.phpt new file mode 100644 index 000000000000..e0903258189e --- /dev/null +++ b/Zend/tests/generics/syntax/args_dnf.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic syntax: type argument is DNF +--FILE-- + {} +function f(D<(A&B)|C> $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/args_intersection.phpt b/Zend/tests/generics/syntax/args_intersection.phpt new file mode 100644 index 000000000000..9842148e216e --- /dev/null +++ b/Zend/tests/generics/syntax/args_intersection.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: type argument is an intersection +--FILE-- + {} +function f(C $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionIntersectionType diff --git a/Zend/tests/generics/syntax/args_union.phpt b/Zend/tests/generics/syntax/args_union.phpt new file mode 100644 index 000000000000..1e424f8bf363 --- /dev/null +++ b/Zend/tests/generics/syntax/args_union.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type argument is a union +--FILE-- + {} +function f(Container $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$arg = $pt->getGenericArguments()[0]; +echo get_class($arg), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/arrow_fn_decl.phpt b/Zend/tests/generics/syntax/arrow_fn_decl.phpt new file mode 100644 index 000000000000..22790e280438 --- /dev/null +++ b/Zend/tests/generics/syntax/arrow_fn_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: arrow function with type parameters +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/bound_and_default_with_args.phpt b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt new file mode 100644 index 000000000000..832cdada25e4 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_and_default_with_args.phpt @@ -0,0 +1,22 @@ +--TEST-- +Generic syntax: bound and default both carry type arguments, closing >> +--FILE-- + {} + +// class A has a generic parameter T with bound B and default B. +// Same args on both so the default satisfies the bound under invariant T — +// the test exercises the parser's `>>` splitting in bound + default position. +class A=B> {} + +$p = (new ReflectionClass('A'))->getGenericParameters()[0]; + +$b = $p->getBound(); +echo "bound: ", $b->getName(), "<", $b->getGenericArguments()[0]->getName(), ">\n"; + +$d = $p->getDefault(); +echo "default: ", $d->getName(), "<", $d->getGenericArguments()[0]->getName(), ">\n"; +?> +--EXPECT-- +bound: B +default: B diff --git a/Zend/tests/generics/syntax/bound_dnf.phpt b/Zend/tests/generics/syntax/bound_dnf.phpt new file mode 100644 index 000000000000..7d65ae2dcbd7 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_dnf.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: bound is a DNF type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo get_class($p->getBound()), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/bound_intersection.phpt b/Zend/tests/generics/syntax/bound_intersection.phpt new file mode 100644 index 000000000000..f73eeb2e4357 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_intersection.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: bound is an intersection type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo get_class($p->getBound()), "\n"; +?> +--EXPECT-- +ReflectionIntersectionType diff --git a/Zend/tests/generics/syntax/bound_single_class.phpt b/Zend/tests/generics/syntax/bound_single_class.phpt new file mode 100644 index 000000000000..d6000909b072 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_single_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: bound on a single class type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->hasBound()); +echo $p->getBound()->getName(), "\n"; +?> +--EXPECT-- +bool(true) +Animal diff --git a/Zend/tests/generics/syntax/bound_union.phpt b/Zend/tests/generics/syntax/bound_union.phpt new file mode 100644 index 000000000000..2c9c71f9d4b1 --- /dev/null +++ b/Zend/tests/generics/syntax/bound_union.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: bound is a union type +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +$b = $p->getBound(); +echo get_class($b), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/Zend/tests/generics/syntax/builtin_self_parent_static.phpt b/Zend/tests/generics/syntax/builtin_self_parent_static.phpt new file mode 100644 index 000000000000..35934d41fa6e --- /dev/null +++ b/Zend/tests/generics/syntax/builtin_self_parent_static.phpt @@ -0,0 +1,20 @@ +--TEST-- +Generic syntax: self, parent, static +--FILE-- + {} + public function p(): parent {} + public function st(): static {} +} +$rc = new ReflectionClass('Foo'); +foreach (['s' => 'self', 'p' => 'parent', 'st' => 'static'] as $m => $expected) { + $rt = $rc->getMethod($m)->getReturnType(); + echo "$m: ", $rt->getName(), " hasArgs=", var_export($rt->hasGenericArguments(), true), "\n"; +} +?> +--EXPECT-- +s: Foo hasArgs=true +p: Base hasArgs=true +st: static hasArgs=true diff --git a/Zend/tests/generics/syntax/catch_with_args.phpt b/Zend/tests/generics/syntax/catch_with_args.phpt new file mode 100644 index 000000000000..bef722884955 --- /dev/null +++ b/Zend/tests/generics/syntax/catch_with_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: catch with type arguments compares against the monomorph canonical name +--FILE-- + extends Exception {} +// Throwing MyErr matches a `catch (MyErr)` block; a `catch (MyErr)` +// would not, because the two are distinct monomorphs. +try { + throw new MyErr::('boom'); +} catch (MyErr $e) { + echo "wrong-mono caught: ", $e->getMessage(), "\n"; +} catch (MyErr $e) { + echo "right-mono caught: ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +right-mono caught: boom diff --git a/Zend/tests/generics/syntax/class_multiple_params.phpt b/Zend/tests/generics/syntax/class_multiple_params.phpt new file mode 100644 index 000000000000..4f66e3173fe2 --- /dev/null +++ b/Zend/tests/generics/syntax/class_multiple_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: class with multiple type parameters +--FILE-- + {} +$r = new ReflectionClass('Map'); +$ps = $r->getGenericParameters(); +echo count($ps), "\n"; +foreach ($ps as $p) echo $p->getName(), "\n"; +?> +--EXPECT-- +2 +K +V diff --git a/Zend/tests/generics/syntax/class_single_param.phpt b/Zend/tests/generics/syntax/class_single_param.phpt new file mode 100644 index 000000000000..6a5a8bed0cef --- /dev/null +++ b/Zend/tests/generics/syntax/class_single_param.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: class with single type parameter +--FILE-- + {} +$r = new ReflectionClass('Box'); +var_dump($r->isGeneric()); +echo count($r->getGenericParameters()), "\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +1 +T diff --git a/Zend/tests/generics/syntax/closure_decl.phpt b/Zend/tests/generics/syntax/closure_decl.phpt new file mode 100644 index 000000000000..35620b3ae631 --- /dev/null +++ b/Zend/tests/generics/syntax/closure_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: closure with type parameters +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($f); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/default_value.phpt b/Zend/tests/generics/syntax/default_value.phpt new file mode 100644 index 000000000000..03f6a867fe3b --- /dev/null +++ b/Zend/tests/generics/syntax/default_value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type parameter with default +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->hasDefault()); +echo $p->getDefault(), "\n"; +?> +--EXPECT-- +bool(true) +mixed diff --git a/Zend/tests/generics/syntax/default_with_nested_args.phpt b/Zend/tests/generics/syntax/default_with_nested_args.phpt new file mode 100644 index 000000000000..ef09d58a7b47 --- /dev/null +++ b/Zend/tests/generics/syntax/default_with_nested_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: >>= splitting in default position +--FILE-- + {} +class C> {} +$p = (new ReflectionClass('C'))->getGenericParameters()[0]; +echo $p, "\n"; +?> +--EXPECT-- +T = MapT diff --git a/Zend/tests/generics/syntax/extends_with_args.phpt b/Zend/tests/generics/syntax/extends_with_args.phpt new file mode 100644 index 000000000000..9a475577183d --- /dev/null +++ b/Zend/tests/generics/syntax/extends_with_args.phpt @@ -0,0 +1,10 @@ +--TEST-- +Generic syntax: extends with type arguments +--FILE-- + {} +class Derived extends Base {} +echo (new ReflectionClass('Derived'))->getParentClass()->getName(), "\n"; +?> +--EXPECT-- +Base diff --git a/Zend/tests/generics/syntax/function_decl.phpt b/Zend/tests/generics/syntax/function_decl.phpt new file mode 100644 index 000000000000..8ba5cde4314a --- /dev/null +++ b/Zend/tests/generics/syntax/function_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: function declaration with type parameters +--FILE-- +(T $x): T { return $x; } +$r = new ReflectionFunction('id'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/implements_with_args.phpt b/Zend/tests/generics/syntax/implements_with_args.phpt new file mode 100644 index 000000000000..5221dffc4f13 --- /dev/null +++ b/Zend/tests/generics/syntax/implements_with_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: implements with type arguments +--FILE-- + {} +class C implements Iface {} +$r = new ReflectionClass('C'); +foreach ($r->getInterfaceNames() as $n) echo $n, "\n"; +?> +--EXPECT-- +Iface diff --git a/Zend/tests/generics/syntax/instanceof_with_args.phpt b/Zend/tests/generics/syntax/instanceof_with_args.phpt new file mode 100644 index 000000000000..1722eff49681 --- /dev/null +++ b/Zend/tests/generics/syntax/instanceof_with_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: instanceof with type arguments resolves to the monomorph canonical name +--FILE-- + {} +$c = new C::(); +var_dump($c instanceof C); +var_dump($c instanceof C); +var_dump($c instanceof C); +$x = new stdClass; +var_dump($x instanceof C); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) diff --git a/Zend/tests/generics/syntax/interface_decl.phpt b/Zend/tests/generics/syntax/interface_decl.phpt new file mode 100644 index 000000000000..e0dbb24a6457 --- /dev/null +++ b/Zend/tests/generics/syntax/interface_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: interface with type parameters +--FILE-- + {} +$r = new ReflectionClass('Iter'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/method_decl.phpt b/Zend/tests/generics/syntax/method_decl.phpt new file mode 100644 index 000000000000..8a76b03588db --- /dev/null +++ b/Zend/tests/generics/syntax/method_decl.phpt @@ -0,0 +1,14 @@ +--TEST-- +Generic syntax: method declaration with type parameters +--FILE-- +(callable $f): U { return $f(); } +} +$r = (new ReflectionClass('C'))->getMethod('map'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +U diff --git a/Zend/tests/generics/syntax/named_type_args.phpt b/Zend/tests/generics/syntax/named_type_args.phpt new file mode 100644 index 000000000000..7e4151ccd8e2 --- /dev/null +++ b/Zend/tests/generics/syntax/named_type_args.phpt @@ -0,0 +1,18 @@ +--TEST-- +Generic syntax: type arguments on a named type at use site +--FILE-- + {} +function f(Container $x): Container { return $x; } +$r = new ReflectionFunction('f'); +$pt = $r->getParameters()[0]->getType(); +// With reified params, the type's name is the canonical monomorph, not the +// bare base. Generic args are still queryable via getGenericArguments(). +echo $pt->getName(), "\n"; +var_dump($pt->hasGenericArguments()); +foreach ($pt->getGenericArguments() as $a) echo $a->getName(), "\n"; +?> +--EXPECT-- +Container +bool(true) +int diff --git a/Zend/tests/generics/syntax/nested_three_levels.phpt b/Zend/tests/generics/syntax/nested_three_levels.phpt new file mode 100644 index 000000000000..f68dae761c32 --- /dev/null +++ b/Zend/tests/generics/syntax/nested_three_levels.phpt @@ -0,0 +1,21 @@ +--TEST-- +Generic syntax: three levels of nesting (>>> splitting) +--FILE-- + {} +class B {} +class A {} +function f(): A>> { return new A::>>(); } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo $rt->getName(), "\n"; +$arg1 = $rt->getGenericArguments()[0]; +echo $arg1->getName(), "\n"; +$arg2 = $arg1->getGenericArguments()[0]; +echo $arg2->getName(), "\n"; +echo $arg2->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +A>> +B +C +int diff --git a/Zend/tests/generics/syntax/nested_two_levels.phpt b/Zend/tests/generics/syntax/nested_two_levels.phpt new file mode 100644 index 000000000000..d5c43c30a455 --- /dev/null +++ b/Zend/tests/generics/syntax/nested_two_levels.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: two levels of nesting (>> splitting) +--FILE-- + {} +class Outer {} +function f(): Outer> { return new Outer::>(); } +$rt = (new ReflectionFunction('f'))->getReturnType(); +echo $rt->getName(), "\n"; +$arg = $rt->getGenericArguments()[0]; +echo $arg->getName(), "\n"; +echo $arg->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +Outer> +Inner +int diff --git a/Zend/tests/generics/syntax/trailing_comma_args.phpt b/Zend/tests/generics/syntax/trailing_comma_args.phpt new file mode 100644 index 000000000000..1b8c2c93563f --- /dev/null +++ b/Zend/tests/generics/syntax/trailing_comma_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: trailing comma in type argument list +--FILE-- + {} +function f(C $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +echo count($pt->getGenericArguments()), "\n"; +?> +--EXPECT-- +2 diff --git a/Zend/tests/generics/syntax/trailing_comma_params.phpt b/Zend/tests/generics/syntax/trailing_comma_params.phpt new file mode 100644 index 000000000000..3a386ffdeeae --- /dev/null +++ b/Zend/tests/generics/syntax/trailing_comma_params.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: trailing comma in type parameter list +--FILE-- + {} +class Pair {} +echo (new ReflectionClass('Box'))->isGeneric() ? 'ok' : 'fail', "\n"; +echo (new ReflectionClass('Pair'))->isGeneric() ? 'ok' : 'fail', "\n"; +?> +--EXPECT-- +ok +ok diff --git a/Zend/tests/generics/syntax/trait_decl.phpt b/Zend/tests/generics/syntax/trait_decl.phpt new file mode 100644 index 000000000000..32d46440ea81 --- /dev/null +++ b/Zend/tests/generics/syntax/trait_decl.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: trait with type parameters +--FILE-- + {} +$r = new ReflectionClass('Container'); +var_dump($r->isGeneric()); +echo $r->getGenericParameters()[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +T diff --git a/Zend/tests/generics/syntax/trait_use_args.phpt b/Zend/tests/generics/syntax/trait_use_args.phpt new file mode 100644 index 000000000000..acf319fb65ba --- /dev/null +++ b/Zend/tests/generics/syntax/trait_use_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic syntax: trait use with type arguments +--FILE-- + { + public T $tval; +} +class Box { + use Container; +} +$r = new ReflectionClass('Box'); +echo $r->isGeneric() ? 'ok' : 'fail', "\n"; +?> +--EXPECT-- +ok diff --git a/Zend/tests/generics/syntax/turbofish_complex_args.phpt b/Zend/tests/generics/syntax/turbofish_complex_args.phpt new file mode 100644 index 000000000000..460f6d65fa1b --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_complex_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: turbofish with composite type arguments +--FILE-- +($x) { return $x; } +echo f::(42), "\n"; +echo f::('hi'), "\n"; +?> +--EXPECT-- +42 +hi diff --git a/Zend/tests/generics/syntax/turbofish_fcc.phpt b/Zend/tests/generics/syntax/turbofish_fcc.phpt new file mode 100644 index 000000000000..31080672c5b4 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_fcc.phpt @@ -0,0 +1,24 @@ +--TEST-- +Generic syntax: turbofish on first-class callable creation +--FILE-- +(int $x): int { return $x * 2; } +class C { + public static function s(int $x): int { return $x + 100; } + public function m(int $x): int { return $x - 1; } +} + +$f = f::(...); +echo $f(5), "\n"; + +$g = C::s::(...); +echo $g(5), "\n"; + +$c = new C; +$h = $c->m::(...); +echo $h(5), "\n"; +?> +--EXPECT-- +10 +105 +4 diff --git a/Zend/tests/generics/syntax/turbofish_function_call.phpt b/Zend/tests/generics/syntax/turbofish_function_call.phpt new file mode 100644 index 000000000000..e397c8344643 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_function_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: turbofish on function call +--FILE-- +($a, $b) { return $a + $b; } +echo add::(1, 2), "\n"; +echo add::(3, 4), "\n"; +echo add::(5, 6), "\n"; +?> +--EXPECT-- +3 +7 +11 diff --git a/Zend/tests/generics/syntax/turbofish_method_call.phpt b/Zend/tests/generics/syntax/turbofish_method_call.phpt new file mode 100644 index 000000000000..0bfe9f7a2230 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_method_call.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic syntax: turbofish on instance method call (including nullsafe) +--FILE-- +($x) { return $x * 3; } +} +$c = new C; +echo $c->m::(5), "\n"; +echo $c?->m::(7), "\n"; +$null = null; +var_dump($null?->m::(9)); +?> +--EXPECT-- +15 +21 +NULL diff --git a/Zend/tests/generics/syntax/turbofish_new.phpt b/Zend/tests/generics/syntax/turbofish_new.phpt new file mode 100644 index 000000000000..c79c15f90a4b --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_new.phpt @@ -0,0 +1,32 @@ +--TEST-- +Generic syntax: turbofish on new with various class-ref forms +--FILE-- + { + public function __construct(public int $v) {} +} + +$b = new Box::(1); +echo $b->v, "\n"; + +$cls = 'Box'; +$b = new $cls::(2); +echo $b->v, "\n"; + +$expr = fn() => 'Box'; +$b = new ($expr())::(3); +echo $b->v, "\n"; + +class Container { + public static function make(): self { + return new self::; + } +} +$c = Container::make(); +echo get_class($c), "\n"; +?> +--EXPECT-- +1 +2 +3 +Container diff --git a/Zend/tests/generics/syntax/turbofish_static_call.phpt b/Zend/tests/generics/syntax/turbofish_static_call.phpt new file mode 100644 index 000000000000..b2a1d74d4b61 --- /dev/null +++ b/Zend/tests/generics/syntax/turbofish_static_call.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic syntax: turbofish on static method call +--FILE-- +($x) { return $x * 2; } +} +echo C::s::(5), "\n"; +echo C::s::(7), "\n"; +?> +--EXPECT-- +10 +14 diff --git a/Zend/tests/generics/syntax/variance_bound_default.phpt b/Zend/tests/generics/syntax/variance_bound_default.phpt new file mode 100644 index 000000000000..55c6460fef55 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_bound_default.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic syntax: variance + bound + default combined +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +echo $p, "\n"; +?> +--EXPECT-- ++T : Bar = Bar diff --git a/Zend/tests/generics/syntax/variance_contravariant.phpt b/Zend/tests/generics/syntax/variance_contravariant.phpt new file mode 100644 index 000000000000..97d1b4d07e91 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_contravariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: contravariant type parameter (-T) +--FILE-- + {} +$p = (new ReflectionClass('Consumer'))->getGenericParameters()[0]; +echo $p->getName(), "\n"; +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +T +Contravariant diff --git a/Zend/tests/generics/syntax/variance_covariant.phpt b/Zend/tests/generics/syntax/variance_covariant.phpt new file mode 100644 index 000000000000..ea3d76f8d2d4 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_covariant.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: covariant type parameter (+T) +--FILE-- + {} +$p = (new ReflectionClass('Producer'))->getGenericParameters()[0]; +echo $p->getName(), "\n"; +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +T +Covariant diff --git a/Zend/tests/generics/syntax/variance_invariant_default.phpt b/Zend/tests/generics/syntax/variance_invariant_default.phpt new file mode 100644 index 000000000000..3fd243c44a06 --- /dev/null +++ b/Zend/tests/generics/syntax/variance_invariant_default.phpt @@ -0,0 +1,12 @@ +--TEST-- +Generic syntax: type parameter without variance is invariant +--FILE-- + {} +$p = (new ReflectionClass('Box'))->getGenericParameters()[0]; +var_dump($p->getVariance() === ReflectionGenericVariance::Invariant); +echo $p->getVariance()->name, "\n"; +?> +--EXPECT-- +bool(true) +Invariant diff --git a/Zend/tests/generics/traits/abstract_method_implemented.phpt b/Zend/tests/generics/traits/abstract_method_implemented.phpt new file mode 100644 index 000000000000..269e70b752a1 --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_implemented.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: an abstract trait method using T accepts an implementer matching the substituted type +--FILE-- + { + public abstract function foo(T $v): T; +} + +class A { + use Thing; + public function foo(string $v): string { return $v; } +} + +echo (new A)->foo("hi"), "\n"; +?> +--EXPECT-- +hi diff --git a/Zend/tests/generics/traits/abstract_method_two_classes.phpt b/Zend/tests/generics/traits/abstract_method_two_classes.phpt new file mode 100644 index 000000000000..1e5b4e67af64 --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_two_classes.phpt @@ -0,0 +1,24 @@ +--TEST-- +Traits: two classes using the same generic trait can implement the abstract method at distinct substituted types +--FILE-- + { + public abstract function foo(T $v): T; +} + +class StrThing { + use Thing; + public function foo(string $v): string { return $v; } +} + +class IntThing { + use Thing; + public function foo(int $v): int { return $v; } +} + +echo (new StrThing)->foo("a"), "\n"; +echo (new IntThing)->foo(7), "\n"; +?> +--EXPECT-- +a +7 diff --git a/Zend/tests/generics/traits/abstract_method_violates.phpt b/Zend/tests/generics/traits/abstract_method_violates.phpt new file mode 100644 index 000000000000..d1abe4e84cb0 --- /dev/null +++ b/Zend/tests/generics/traits/abstract_method_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Traits: an abstract trait method using T rejects an implementer with the wrong substituted type +--FILE-- + { + public abstract function foo(T $v): T; +} + +class A { + use Thing; + public function foo(int $v): int { return $v; } +} +?> +--EXPECTF-- +Fatal error: Declaration of A::foo(int $v): int must be compatible with Thing::foo(string $v): string in %s on line %d diff --git a/Zend/tests/generics/traits/basic_trait.phpt b/Zend/tests/generics/traits/basic_trait.phpt new file mode 100644 index 000000000000..7f459bb5f002 --- /dev/null +++ b/Zend/tests/generics/traits/basic_trait.phpt @@ -0,0 +1,18 @@ +--TEST-- +Traits: trait declares its own type parameter +--FILE-- + { + public T $value; + public function get(): T { return $this->value; } +} +class Box { + use Holder; +} +$rc = new ReflectionClass('Box'); +echo $rc->getProperty('value')->getType()->getName(), "\n"; +echo $rc->getMethod('get')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +object diff --git a/Zend/tests/generics/traits/method_concrete_unchanged.phpt b/Zend/tests/generics/traits/method_concrete_unchanged.phpt new file mode 100644 index 000000000000..ced7fe773e39 --- /dev/null +++ b/Zend/tests/generics/traits/method_concrete_unchanged.phpt @@ -0,0 +1,25 @@ +--TEST-- +Traits: concretely typed parameters and returns are unaffected by substitution +--FILE-- + { + public T $val; + public function takeT(T $x): T { return $x; } + public function bare(int $x): string { return (string)$x; } +} + +class StrHolder { use Holder; } + +$rm = (new ReflectionClass('StrHolder'))->getMethod('bare'); +echo "bare: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; + +$rm = (new ReflectionClass('StrHolder'))->getMethod('takeT'); +echo "takeT: ", + $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +bare: int -> string +takeT: string -> string diff --git a/Zend/tests/generics/traits/method_default_arg.phpt b/Zend/tests/generics/traits/method_default_arg.phpt new file mode 100644 index 000000000000..91eca8edefd1 --- /dev/null +++ b/Zend/tests/generics/traits/method_default_arg.phpt @@ -0,0 +1,20 @@ +--TEST-- +Traits: omitting use args fills the trait parameter defaults into method signatures +--FILE-- + { + public function f(T $x): T { return $x; } +} + +class Impl { use Defaulted; } + +$rm = (new ReflectionClass('Impl'))->getMethod('f'); +echo $rm->getParameters()[0]->getType()->getName(), " -> ", + $rm->getReturnType()->getName(), "\n"; + +$i = new Impl; +echo $i->f(42), "\n"; +?> +--EXPECT-- +int -> int +42 diff --git a/Zend/tests/generics/traits/method_signature_substituted.phpt b/Zend/tests/generics/traits/method_signature_substituted.phpt new file mode 100644 index 000000000000..578cf1d11976 --- /dev/null +++ b/Zend/tests/generics/traits/method_signature_substituted.phpt @@ -0,0 +1,25 @@ +--TEST-- +Traits: trait method param and return types are substituted from the use clause's bound arguments +--FILE-- + { + public function take(T $x): T { return $x; } +} + +class IntHolder { use Holder; } +class StrHolder { use Holder; } + +foreach (['IntHolder', 'StrHolder'] as $cn) { + $rm = (new ReflectionClass($cn))->getMethod('take'); + $p = $rm->getParameters()[0]->getType()->getName(); + $r = $rm->getReturnType()->getName(); + echo "$cn::take: $p -> $r\n"; +} + +$ih = new IntHolder; +echo $ih->take(7), "\n"; +?> +--EXPECT-- +IntHolder::take: int -> int +StrHolder::take: string -> string +7 diff --git a/Zend/tests/generics/traits/method_variadic_substituted.phpt b/Zend/tests/generics/traits/method_variadic_substituted.phpt new file mode 100644 index 000000000000..cf0cb4c50ede --- /dev/null +++ b/Zend/tests/generics/traits/method_variadic_substituted.phpt @@ -0,0 +1,19 @@ +--TEST-- +Traits: variadic parameter typed T is substituted in the using class +--FILE-- + { + public function all(T ...$items): int { return count($items); } +} + +class IntAcceptor { use Acceptor; } + +$rm = (new ReflectionClass('IntAcceptor'))->getMethod('all'); +echo "param type: ", $rm->getParameters()[0]->getType()->getName(), "\n"; + +$a = new IntAcceptor; +echo $a->all(1, 2, 3), "\n"; +?> +--EXPECT-- +param type: int +3 diff --git a/Zend/tests/generics/traits/multiple_traits.phpt b/Zend/tests/generics/traits/multiple_traits.phpt new file mode 100644 index 000000000000..b21a673dcab5 --- /dev/null +++ b/Zend/tests/generics/traits/multiple_traits.phpt @@ -0,0 +1,22 @@ +--TEST-- +Traits: class uses multiple generic traits +--FILE-- + { + public function a(): X { return $this->ax; } + public X $ax; +} +trait B { + public function b(): Y { return $this->by; } + public Y $by; +} +class C { + use A, B; +} +$rc = new ReflectionClass('C'); +echo $rc->getMethod('a')->getReturnType()->getName(), "\n"; +echo $rc->getMethod('b')->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +object +int diff --git a/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt b/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt new file mode 100644 index 000000000000..8d93cc907242 --- /dev/null +++ b/Zend/tests/generics/traits/property_no_substitution_when_concrete.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: property already concretely typed in the trait is unaffected by `use` clause arguments +--FILE-- + { + public int $count = 0; + public T $val; +} + +class StrBox { use Holder; } + +$rcCount = (new ReflectionClass('StrBox'))->getProperty('count'); +$rcVal = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcCount->getType()->getName(), "/", $rcVal->getType()->getName(), "\n"; +?> +--EXPECT-- +int/string diff --git a/Zend/tests/generics/traits/property_substituted_two_classes.phpt b/Zend/tests/generics/traits/property_substituted_two_classes.phpt new file mode 100644 index 000000000000..b0b3680f2d97 --- /dev/null +++ b/Zend/tests/generics/traits/property_substituted_two_classes.phpt @@ -0,0 +1,27 @@ +--TEST-- +Traits: two classes using the same generic trait with different bindings get distinct property types +--FILE-- + { + public T $val; +} + +class IntBox { use Holder; } +class StrBox { use Holder; } + +$ib = new IntBox; +$ib->val = 1; +$sb = new StrBox; +$sb->val = "x"; + +$rcInt = (new ReflectionClass('IntBox'))->getProperty('val'); +$rcStr = (new ReflectionClass('StrBox'))->getProperty('val'); +echo $rcInt->getType()->getName(), "/", $rcStr->getType()->getName(), "\n"; + +try { $ib->val = "no"; } catch (TypeError) { echo "TypeError IntBox\n"; } +try { $sb->val = []; } catch (TypeError) { echo "TypeError StrBox\n"; } +?> +--EXPECT-- +int/string +TypeError IntBox +TypeError StrBox diff --git a/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt b/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt new file mode 100644 index 000000000000..d2d8d39e370c --- /dev/null +++ b/Zend/tests/generics/traits/property_substituted_with_class_arg.phpt @@ -0,0 +1,29 @@ +--TEST-- +Traits: trait property substituted with a class type +--FILE-- + { + public T $val; +} + +class AnimalBox { use Holder; } + +$rp = (new ReflectionClass('AnimalBox'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; + +$b = new AnimalBox; +$b->val = new Animal; +echo get_class($b->val), "\n"; + +try { + $b->val = 5; +} catch (TypeError) { + echo "TypeError\n"; +} +?> +--EXPECT-- +Animal +Animal +TypeError diff --git a/Zend/tests/generics/traits/trait_constant_type.phpt b/Zend/tests/generics/traits/trait_constant_type.phpt new file mode 100644 index 000000000000..860a0cd34c75 --- /dev/null +++ b/Zend/tests/generics/traits/trait_constant_type.phpt @@ -0,0 +1,18 @@ +--TEST-- +Traits: trait class constant with type parameter +--FILE-- + { + const T MIN = 0; +} +class Box { + use Holder; +} +$rc = new ReflectionClass('Box'); +$consts = $rc->getReflectionConstants(); +foreach ($consts as $c) { + echo $c->getName(), ": ", $c->getValue(), "\n"; +} +?> +--EXPECT-- +MIN: 0 diff --git a/Zend/tests/generics/traits/trait_get_generic_params.phpt b/Zend/tests/generics/traits/trait_get_generic_params.phpt new file mode 100644 index 000000000000..0a437a414821 --- /dev/null +++ b/Zend/tests/generics/traits/trait_get_generic_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Traits: ReflectionClass::getGenericParameters on trait +--FILE-- + {} +$rc = new ReflectionClass('T'); +foreach ($rc->getGenericParameters() as $p) { + echo $p->getName(), "\n"; +} +?> +--EXPECT-- +A +B +C diff --git a/Zend/tests/generics/traits/trait_isgeneric.phpt b/Zend/tests/generics/traits/trait_isgeneric.phpt new file mode 100644 index 000000000000..b142dfe56deb --- /dev/null +++ b/Zend/tests/generics/traits/trait_isgeneric.phpt @@ -0,0 +1,12 @@ +--TEST-- +Traits: ReflectionClass::isGeneric on trait +--FILE-- + {} +trait U {} +echo (new ReflectionClass('T'))->isGeneric() ? "T:gen\n" : "T:not\n"; +echo (new ReflectionClass('U'))->isGeneric() ? "U:gen\n" : "U:not\n"; +?> +--EXPECT-- +T:gen +U:not diff --git a/Zend/tests/generics/traits/trait_method_isgeneric.phpt b/Zend/tests/generics/traits/trait_method_isgeneric.phpt new file mode 100644 index 000000000000..959155d68cfc --- /dev/null +++ b/Zend/tests/generics/traits/trait_method_isgeneric.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait method's isGeneric and getGenericParameters +--FILE-- +(U $x): U { return $x; } +} +class C { use T; } +$rm = (new ReflectionClass('C'))->getMethod('f'); +echo $rm->isGeneric() ? "gen\n" : "not\n"; +foreach ($rm->getGenericParameters() as $p) { + echo $p->getName(), "\n"; +} +?> +--EXPECT-- +gen +U diff --git a/Zend/tests/generics/traits/trait_method_runtime.phpt b/Zend/tests/generics/traits/trait_method_runtime.phpt new file mode 100644 index 000000000000..324d473e45c9 --- /dev/null +++ b/Zend/tests/generics/traits/trait_method_runtime.phpt @@ -0,0 +1,26 @@ +--TEST-- +Traits: methods from trait work at runtime +--FILE-- + { + public function get(T $x): T { return $x; } +} +class Box { + use Holder; +} + +$a = new A; +$b = new Box; +echo get_class($b->get($a)), "\n"; + +try { + $b->get("string"); + echo "no error\n"; +} catch (TypeError $e) { + echo "type error\n"; +} +?> +--EXPECT-- +A +type error diff --git a/Zend/tests/generics/traits/trait_property_substituted.phpt b/Zend/tests/generics/traits/trait_property_substituted.phpt new file mode 100644 index 000000000000..a10364cf9450 --- /dev/null +++ b/Zend/tests/generics/traits/trait_property_substituted.phpt @@ -0,0 +1,32 @@ +--TEST-- +Traits: trait property type is substituted from the `use` clause's bound arguments +--FILE-- + { + public T $val; +} + +class IntBox { use Holder; } +class StringBox { use Holder; } + +$rp = (new ReflectionClass('IntBox'))->getProperty('val'); +echo "IntBox::\$val: ", $rp->getType()->getName(), "\n"; + +$rp = (new ReflectionClass('StringBox'))->getProperty('val'); +echo "StringBox::\$val: ", $rp->getType()->getName(), "\n"; + +$ib = new IntBox; +$ib->val = 7; +echo $ib->val, "\n"; + +try { + $ib->val = "x"; +} catch (TypeError $e) { + echo "TypeError\n"; +} +?> +--EXPECT-- +IntBox::$val: int +StringBox::$val: string +7 +TypeError diff --git a/Zend/tests/generics/traits/trait_property_type.phpt b/Zend/tests/generics/traits/trait_property_type.phpt new file mode 100644 index 000000000000..f6510a351ed3 --- /dev/null +++ b/Zend/tests/generics/traits/trait_property_type.phpt @@ -0,0 +1,16 @@ +--TEST-- +Traits: trait property type +--FILE-- + { + public T $val; +} +class Box { + use Holder; +} +$rp = (new ReflectionClass('Box'))->getProperty('val'); +echo $rp->getType()->getName(), "\n"; +?> +--EXPECT-- +Foo diff --git a/Zend/tests/generics/traits/trait_use_args_compile.phpt b/Zend/tests/generics/traits/trait_use_args_compile.phpt new file mode 100644 index 000000000000..84af6901238c --- /dev/null +++ b/Zend/tests/generics/traits/trait_use_args_compile.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait use clause with type arguments compiles +--FILE-- + { + public function tag(X $x): X { return $x; } +} +class Box { + use Holder; +} +$b = new Box::(); +echo get_class($b), "\n"; +echo "ok\n"; +?> +--EXPECT-- +Box +ok diff --git a/Zend/tests/generics/traits/trait_with_method_param.phpt b/Zend/tests/generics/traits/trait_with_method_param.phpt new file mode 100644 index 000000000000..2203f9b20431 --- /dev/null +++ b/Zend/tests/generics/traits/trait_with_method_param.phpt @@ -0,0 +1,17 @@ +--TEST-- +Traits: trait method declares its own type parameter +--FILE-- +(U $x): U { return $x; } +} +class C { + use T; +} +$rm = (new ReflectionClass('C'))->getMethod('f'); +echo $rm->getParameters()[0]->getType()->getName(), "\n"; +echo $rm->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +int +int diff --git a/Zend/tests/generics/turbofish/arity/attribute_arity.phpt b/Zend/tests/generics/turbofish/arity/attribute_arity.phpt new file mode 100644 index 000000000000..9f6fa044538e --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/attribute_arity.phpt @@ -0,0 +1,38 @@ +--TEST-- +Turbofish arity: ReflectionAttribute::newInstance() enforces arity against the attribute class +--FILE-- + { + public function __construct(public string $tag) {} +} + +#[TaggedAttr::(tag: "ok")] +class A {} + +#[TaggedAttr::(tag: "bad")] +class B {} + +#[TaggedAttr(tag: "no-turbo")] +class D {} + +$ra = (new ReflectionClass("A"))->getAttributes()[0]; +echo "A: ", $ra->newInstance()->tag, "\n"; + +$rb = (new ReflectionClass("B"))->getAttributes()[0]; +try { + $rb->newInstance(); +} catch (ArgumentCountError $e) { + echo "B: ", $e->getMessage(), "\n"; +} + +// no turbofish at all -> no arity check, defaults apply (here T has no default) +// but newInstance() doesn't supply type args, which is arity 0; required is 1 -> still fails? No. +// arity 0 is treated as "no turbofish" and skips the check entirely. +$rd = (new ReflectionClass("D"))->getAttributes()[0]; +echo "D: ", $rd->newInstance()->tag, "\n"; +?> +--EXPECT-- +A: ok +B: Too many generic type arguments to new TaggedAttr, 2 passed and exactly 1 expected +D: no-turbo diff --git a/Zend/tests/generics/turbofish/arity/below_required.phpt b/Zend/tests/generics/turbofish/arity/below_required.phpt new file mode 100644 index 000000000000..09514d55a338 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/below_required.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: providing fewer than required even with some defaults still throws +--FILE-- +($x) { return $x; } +try { + f::(1); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too few generic type arguments to f(), 1 passed and at least 2 expected diff --git a/Zend/tests/generics/turbofish/arity/function_too_few.phpt b/Zend/tests/generics/turbofish/arity/function_too_few.phpt new file mode 100644 index 000000000000..d5469444288d --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/function_too_few.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: too few type arguments to a function throws ArgumentCountError +--FILE-- +($x, $y) { return [$x, $y]; } +try { + f::(1, 2); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too few generic type arguments to f(), 1 passed and exactly 2 expected diff --git a/Zend/tests/generics/turbofish/arity/function_too_many.phpt b/Zend/tests/generics/turbofish/arity/function_too_many.phpt new file mode 100644 index 000000000000..15747c2384fd --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/function_too_many.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: too many type arguments to a function throws ArgumentCountError +--FILE-- +($x) { return $x; } +try { + f::(1); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 2 passed and exactly 1 expected diff --git a/Zend/tests/generics/turbofish/arity/method_arity.phpt b/Zend/tests/generics/turbofish/arity/method_arity.phpt new file mode 100644 index 000000000000..81eb1cb37219 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/method_arity.phpt @@ -0,0 +1,24 @@ +--TEST-- +Turbofish arity: instance method, static method, nullsafe call all enforce arity +--FILE-- +($x) { return $x; } + public static function s($x) { return $x; } +} + +$c = new C; + +try { $c->m::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +try { $c?->m::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +try { C::s::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Too many generic type arguments to C::m(), 2 passed and exactly 1 expected +Too many generic type arguments to C::m(), 2 passed and exactly 1 expected +Too few generic type arguments to C::s(), 1 passed and exactly 2 expected diff --git a/Zend/tests/generics/turbofish/arity/new_arity.phpt b/Zend/tests/generics/turbofish/arity/new_arity.phpt new file mode 100644 index 000000000000..f7ff94b67a3e --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/new_arity.phpt @@ -0,0 +1,25 @@ +--TEST-- +Turbofish arity: new ::<...> enforces arity against class generic parameters +--FILE-- + { + public function __construct(public int $v) {} +} +class Pair {} + +try { new Box::(1); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } + +// Pair: K required, V has default - so 1 or 2 args is OK, 0 or 3 is not. +// With monomorphization, the missing trailing arg is filled from the default, +// so the canonical name reflects the substituted Pair. +$p = new Pair::(); +echo get_class($p), "\n"; + +try { new Pair::(); } +catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Too many generic type arguments to new Box, 2 passed and exactly 1 expected +Pair +Too many generic type arguments to new Pair, 3 passed and at most 2 expected diff --git a/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt b/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt new file mode 100644 index 000000000000..5e7a6a622205 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/non_generic_callee.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish arity: turbofish on a non-generic callee throws ArgumentCountError +--FILE-- +(42); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 1 passed and exactly 0 expected diff --git a/Zend/tests/generics/turbofish/arity/with_defaults.phpt b/Zend/tests/generics/turbofish/arity/with_defaults.phpt new file mode 100644 index 000000000000..24509e622cb2 --- /dev/null +++ b/Zend/tests/generics/turbofish/arity/with_defaults.phpt @@ -0,0 +1,27 @@ +--TEST-- +Turbofish arity: defaults extend the accepted arity range +--FILE-- +($x) { return $x; } + +// arity 1, 2, 3 all valid +var_dump(f::(1)); +var_dump(f::(2)); +var_dump(f::(3)); + +// arity 0 (i.e., no turbofish) is also valid +var_dump(f(4)); + +// arity 4 exceeds total +try { + f::(5); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) +Too many generic type arguments to f(), 4 passed and at most 3 expected diff --git a/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt b/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt new file mode 100644 index 000000000000..7d346132f2f6 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_satisfies.phpt @@ -0,0 +1,22 @@ +--TEST-- +Turbofish bound check: attribute newInstance accepts an arg that satisfies the bound +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + $attr->newInstance(); +} +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt b/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt new file mode 100644 index 000000000000..5cbc57dbd645 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_under_opcache.phpt @@ -0,0 +1,28 @@ +--TEST-- +Turbofish bound check: attribute generic args are persisted across opcache file cache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + try { + $attr->newInstance(); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +Type argument 1 to new Bounded does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt b/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt new file mode 100644 index 000000000000..c13cb4e0dd3a --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/attribute_violates.phpt @@ -0,0 +1,24 @@ +--TEST-- +Turbofish bound check: attribute newInstance rejects a type argument that violates the bound +--FILE-- + { + public function __construct() {} +} + +#[Bounded::] +function f(): void {} + +foreach ((new ReflectionFunction('f'))->getAttributes() as $attr) { + try { + $attr->newInstance(); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +Type argument 1 to new Bounded does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt b/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt new file mode 100644 index 000000000000..76993541f1ea --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/error_renders_args.phpt @@ -0,0 +1,16 @@ +--TEST-- +Bound error message: NAMED_WITH_ARGS bound is rendered with its type arguments +--FILE-- + {} + +class Sorter> {} + +try { + new Sorter::; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to new Sorter does not satisfy the bound Comparable on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt b/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt new file mode 100644 index 000000000000..354e183508e4 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/function_call_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: function call with a concrete arg violating the bound +--FILE-- +(): void {} + +try { + id::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call id() does not satisfy the bound Animal on parameter T, string given diff --git a/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt b/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt new file mode 100644 index 000000000000..7abf3a3d42cf --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/method_call_violates.phpt @@ -0,0 +1,25 @@ +--TEST-- +Turbofish bound check: method call with a concrete arg violating the bound +--FILE-- +(): void {} + public static function spick(): void {} +} + +try { + (new Service)->pick::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +try { + Service::spick::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call Service::pick() does not satisfy the bound Animal on parameter T, int given +Type argument 1 to call Service::spick() does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt b/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt new file mode 100644 index 000000000000..e0729fe4c0d8 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/multi_param_first_violates.phpt @@ -0,0 +1,17 @@ +--TEST-- +Turbofish bound check: multi-parameter callee, first arg violates +--FILE-- +(): void {} + +try { + pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to call pair() does not satisfy the bound Animal on parameter A, int given diff --git a/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt b/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt new file mode 100644 index 000000000000..83bfbede7afa --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/multi_param_second_violates.phpt @@ -0,0 +1,17 @@ +--TEST-- +Turbofish bound check: multi-parameter callee, second arg violates +--FILE-- +(): void {} + +try { + pair::(); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 2 to call pair() does not satisfy the bound Dog on parameter B, Animal given diff --git a/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt b/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt new file mode 100644 index 000000000000..a1bb8d8339d1 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/new_satisfies.phpt @@ -0,0 +1,14 @@ +--TEST-- +Turbofish bound check: new with a concrete arg satisfying the bound +--FILE-- + {} + +new Box::; +new Box::; +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/bounds/new_violates.phpt b/Zend/tests/generics/turbofish/bounds/new_violates.phpt new file mode 100644 index 000000000000..96734fb2c288 --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/new_violates.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: new with a concrete arg violating the bound throws TypeError +--FILE-- + {} + +try { + new Box::; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Type argument 1 to new Box does not satisfy the bound Animal on parameter T, int given diff --git a/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt b/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt new file mode 100644 index 000000000000..f2f586086d2a --- /dev/null +++ b/Zend/tests/generics/turbofish/bounds/no_bound_no_check.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish bound check: parameters without bounds accept any type +--FILE-- + {} +function id(): void {} + +new Box::; +new Box::; +id::(); +id::(); +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/tests/generics/turbofish/composite_args.phpt b/Zend/tests/generics/turbofish/composite_args.phpt new file mode 100644 index 000000000000..8c69b6555b8b --- /dev/null +++ b/Zend/tests/generics/turbofish/composite_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: union and intersection type arguments +--FILE-- +($x) { return $x; } +var_dump(f::(42)); +var_dump(f::(42)); +var_dump(f::<(A&B)|C>(42)); +?> +--EXPECT-- +int(42) +int(42) +int(42) diff --git a/Zend/tests/generics/turbofish/fcc_function.phpt b/Zend/tests/generics/turbofish/fcc_function.phpt new file mode 100644 index 000000000000..c304fb7ceed2 --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_function.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on function +--FILE-- +($x) { return $x; } +$cl = id::(...); +var_dump($cl(7)); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/turbofish/fcc_method.phpt b/Zend/tests/generics/turbofish/fcc_method.phpt new file mode 100644 index 000000000000..48436c4faee5 --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on instance method +--FILE-- +($x) { return $x * 2; } } +$cl = (new C)->m::(...); +var_dump($cl(5)); +?> +--EXPECT-- +int(10) diff --git a/Zend/tests/generics/turbofish/fcc_static_method.phpt b/Zend/tests/generics/turbofish/fcc_static_method.phpt new file mode 100644 index 000000000000..56eaf1d671cd --- /dev/null +++ b/Zend/tests/generics/turbofish/fcc_static_method.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: first-class callable on static method +--FILE-- +($x) { return $x; } } +$cl = C::m::(...); +var_dump($cl(11)); +?> +--EXPECT-- +int(11) diff --git a/Zend/tests/generics/turbofish/function_call.phpt b/Zend/tests/generics/turbofish/function_call.phpt new file mode 100644 index 000000000000..8de497d7136e --- /dev/null +++ b/Zend/tests/generics/turbofish/function_call.phpt @@ -0,0 +1,9 @@ +--TEST-- +Turbofish: function call with single type argument +--FILE-- +($x) { return $x; } +var_dump(f::(42)); +?> +--EXPECT-- +int(42) diff --git a/Zend/tests/generics/turbofish/function_call_multi.phpt b/Zend/tests/generics/turbofish/function_call_multi.phpt new file mode 100644 index 000000000000..f9f921f6751f --- /dev/null +++ b/Zend/tests/generics/turbofish/function_call_multi.phpt @@ -0,0 +1,14 @@ +--TEST-- +Turbofish: function call with multiple type arguments +--FILE-- +($a, $b) { return [$a, $b]; } +var_dump(combine::(1, "x")); +?> +--EXPECT-- +array(2) { + [0]=> + int(1) + [1]=> + string(1) "x" +} diff --git a/Zend/tests/generics/turbofish/in_chain.phpt b/Zend/tests/generics/turbofish/in_chain.phpt new file mode 100644 index 000000000000..f11b82a5c847 --- /dev/null +++ b/Zend/tests/generics/turbofish/in_chain.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: chained calls +--FILE-- +() { return $this; } + public function n($x) { return $x; } +} +$c = new C; +var_dump($c->m::()->n::("hello")); +?> +--EXPECT-- +string(5) "hello" diff --git a/Zend/tests/generics/turbofish/instance_method_call.phpt b/Zend/tests/generics/turbofish/instance_method_call.phpt new file mode 100644 index 000000000000..a93855b4fb14 --- /dev/null +++ b/Zend/tests/generics/turbofish/instance_method_call.phpt @@ -0,0 +1,12 @@ +--TEST-- +Turbofish: instance method call +--FILE-- +($x) { return $x; } +} +$c = new C; +var_dump($c->m::(99)); +?> +--EXPECT-- +int(99) diff --git a/Zend/tests/generics/turbofish/instanceof_with_args.phpt b/Zend/tests/generics/turbofish/instanceof_with_args.phpt new file mode 100644 index 000000000000..459885fcdfdf --- /dev/null +++ b/Zend/tests/generics/turbofish/instanceof_with_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Turbofish: instanceof with type arguments compares against the monomorph canonical name +--FILE-- + {} +class Animal {} +class Dog {} + +$b = new B::(); +var_dump($b instanceof B); // true: every mono extends the bare class +var_dump($b instanceof B); // true: same canonical mono +var_dump($b instanceof B); // false: distinct mono +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/Zend/tests/generics/turbofish/many_type_args.phpt b/Zend/tests/generics/turbofish/many_type_args.phpt new file mode 100644 index 000000000000..59d5193f5e1a --- /dev/null +++ b/Zend/tests/generics/turbofish/many_type_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: arity must match callee's declared generic parameters +--FILE-- +($x) { return $x; } +try { + f::(7); +} catch (ArgumentCountError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Too many generic type arguments to f(), 3 passed and exactly 1 expected diff --git a/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt b/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt new file mode 100644 index 000000000000..55afc9bc1f54 --- /dev/null +++ b/Zend/tests/generics/turbofish/naked_new_with_defaults.phpt @@ -0,0 +1,29 @@ +--TEST-- +Naked `new GenericClass()` synthesizes the monomorph when every parameter has a default +--FILE-- + { + public function __construct(public mixed $value) {} +} + +class Pair { + public function __construct(public mixed $left, public mixed $right) {} +} + +$b = new Box(42); +var_dump($b::class); +var_dump($b instanceof Box); + +$p = new Pair(1, "hi"); +var_dump($p::class); +var_dump($p instanceof Pair); + +// Identity: two naked-new's of the same class produce instances of the same monomorph. +var_dump((new Box(1))::class === (new Box(2))::class); +?> +--EXPECT-- +string(8) "Box" +bool(true) +string(16) "Pair" +bool(true) +bool(true) diff --git a/Zend/tests/generics/turbofish/nested_args.phpt b/Zend/tests/generics/turbofish/nested_args.phpt new file mode 100644 index 000000000000..ec9f4c25fcbf --- /dev/null +++ b/Zend/tests/generics/turbofish/nested_args.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: nested type arguments +--FILE-- + {} +function f($x) { return $x; } +var_dump(f::>(["a", "b"])); +?> +--EXPECT-- +array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" +} diff --git a/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt b/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt new file mode 100644 index 000000000000..e338c3a408ad --- /dev/null +++ b/Zend/tests/generics/turbofish/nested_type_arg_erasure.phpt @@ -0,0 +1,32 @@ +--TEST-- +Turbofish with a nested generic type argument erases the substituted leaf to the monomorph class +--FILE-- + {} +class L3 {} +class DBox {} + +function id_gen(T $x): T { return $x; } + +$d1 = new DBox::(); +$d2 = new DBox::>(); +$d3 = new DBox::>>(); + +/* T is a bare leaf substituted with a concrete generic instantiation. The + * binding arrives as a pre-erasure named-with-args type; it must be folded to + * the monomorph's canonical class name so the reified RECV / return checks see + * the same erased shape the value carries. Before the fix, depth >= 2 read the + * named-with-args payload as a class-name string (bogus huge allocation) and + * depth 1 raised a spurious TypeError. */ +var_dump(id_gen::>($d1) === $d1); +var_dump(id_gen::>>($d2) === $d2); +var_dump(id_gen::>>>($d3) === $d3); + +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +done diff --git a/Zend/tests/generics/turbofish/new_with_args.phpt b/Zend/tests/generics/turbofish/new_with_args.phpt new file mode 100644 index 000000000000..494aeb67dbe1 --- /dev/null +++ b/Zend/tests/generics/turbofish/new_with_args.phpt @@ -0,0 +1,13 @@ +--TEST-- +Turbofish: new with type arguments +--FILE-- + { + public int $x; + public function __construct(int $x) { $this->x = $x; } +} +$b = new Box::(42); +echo $b->x, "\n"; +?> +--EXPECT-- +42 diff --git a/Zend/tests/generics/turbofish/no_runtime_effect.phpt b/Zend/tests/generics/turbofish/no_runtime_effect.phpt new file mode 100644 index 000000000000..632fee88d045 --- /dev/null +++ b/Zend/tests/generics/turbofish/no_runtime_effect.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: with matching arity has zero runtime effect on values +--FILE-- +(int $x): int { return $x * 2; } +var_dump(f(5)); +var_dump(f::(5)); +var_dump(f::(5)); +var_dump(f::(5)); +?> +--EXPECT-- +int(10) +int(10) +int(10) +int(10) diff --git a/Zend/tests/generics/turbofish/nullsafe_method_call.phpt b/Zend/tests/generics/turbofish/nullsafe_method_call.phpt new file mode 100644 index 000000000000..fe4d04782ecc --- /dev/null +++ b/Zend/tests/generics/turbofish/nullsafe_method_call.phpt @@ -0,0 +1,15 @@ +--TEST-- +Turbofish: nullsafe method call +--FILE-- +($x) { return $x; } +} +$c = new C; +var_dump($c?->m::(5)); +$null = null; +var_dump($null?->m::(5)); +?> +--EXPECT-- +int(5) +NULL diff --git a/Zend/tests/generics/turbofish/static_method_call.phpt b/Zend/tests/generics/turbofish/static_method_call.phpt new file mode 100644 index 000000000000..681b35adac81 --- /dev/null +++ b/Zend/tests/generics/turbofish/static_method_call.phpt @@ -0,0 +1,11 @@ +--TEST-- +Turbofish: static method call +--FILE-- +($x) { return $x; } +} +var_dump(C::m::(7)); +?> +--EXPECT-- +int(7) diff --git a/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt b/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt new file mode 100644 index 000000000000..c8822c08ccf7 --- /dev/null +++ b/Zend/tests/generics/turbofish/turbofish_in_attribute.phpt @@ -0,0 +1,39 @@ +--TEST-- +Turbofish: type arguments are accepted on attribute declarations +--FILE-- + { + public function __construct(public int $priority = 0) {} +} + +class UserCreated {} +class UserDeleted {} +class OrderPlaced {} + +class EventListener { + #[Listens::] + public function onUserCreated(object $event): void {} + + #[Listens::()] + public function onUserDeleted(object $event): void {} + + #[Listens::(priority: -1)] + public function onOrderPlaced(object $event): void {} +} + +$rc = new ReflectionClass(EventListener::class); +foreach ($rc->getMethods() as $method) { + foreach ($method->getAttributes() as $attr) { + $instance = $attr->newInstance(); + echo $method->getName(), ": #[", $attr->getName(), + "] priority=", $instance->priority, "\n"; + } +} +?> +--EXPECT-- +onUserCreated: #[Listens] priority=0 +onUserDeleted: #[Listens] priority=0 +onOrderPlaced: #[Listens] priority=-1 diff --git a/Zend/tests/generics/turbofish/with_named_args.phpt b/Zend/tests/generics/turbofish/with_named_args.phpt new file mode 100644 index 000000000000..3514c99fb2ed --- /dev/null +++ b/Zend/tests/generics/turbofish/with_named_args.phpt @@ -0,0 +1,11 @@ +--TEST-- +Turbofish: combined with named arguments +--FILE-- +($a, $b) { return "$a:$b"; } +echo f::(a: 1, b: 2), "\n"; +echo f::(b: "x", a: 5), "\n"; +?> +--EXPECT-- +1:2 +5:x diff --git a/Zend/tests/generics/turbofish/with_spread.phpt b/Zend/tests/generics/turbofish/with_spread.phpt new file mode 100644 index 000000000000..c35332c212a6 --- /dev/null +++ b/Zend/tests/generics/turbofish/with_spread.phpt @@ -0,0 +1,10 @@ +--TEST-- +Turbofish: combined with spread/unpacking +--FILE-- +($a, $b, $c) { return $a + $b + $c; } +$args = [1, 2, 3]; +echo f::(...$args), "\n"; +?> +--EXPECT-- +6 diff --git a/Zend/zend.c b/Zend/zend.c index f16b1a30dbbc..5ba3b49e49fb 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -820,6 +820,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ #endif executor_globals->saved_fpu_cw_ptr = NULL; executor_globals->active = false; + executor_globals->monomorph_synthesis_active = false; executor_globals->bailout = NULL; executor_globals->error_handling = EH_NORMAL; executor_globals->exception_class = NULL; diff --git a/Zend/zend.h b/Zend/zend.h index 0d5303192b57..509dcc593577 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -228,6 +228,10 @@ struct _zend_class_entry { zend_string *doc_comment; + struct _zend_generic_parameter_list *generic_parameters; + struct _zend_generic_type_table *generic_types; + struct _zend_type_arg_table *generic_type_args; /* bindings on a monomorph; NULL on bases */ + union { struct { zend_string *filename; diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index f495c4c8e3bb..e599fe1d7b8e 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -136,7 +136,8 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_class_const_or_name(zend_ast * ZEND_API zend_ast *zend_ast_create_decl( zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, - zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4 + zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, + zend_ast *child4, zend_ast *child5 ) { zend_ast_decl *ast; @@ -153,6 +154,7 @@ ZEND_API zend_ast *zend_ast_create_decl( ast->child[2] = child2; ast->child[3] = child3; ast->child[4] = child4; + ast->child[5] = child5; return (zend_ast *) ast; } @@ -1504,7 +1506,8 @@ ZEND_API void ZEND_FASTCALL zend_ast_destroy(zend_ast *ast) zend_ast_destroy(decl->child[1]); zend_ast_destroy(decl->child[2]); zend_ast_destroy(decl->child[3]); - ast = decl->child[4]; + zend_ast_destroy(decl->child[4]); + ast = decl->child[5]; goto tail_call; } else if (EXPECTED(ast->kind == ZEND_AST_CALLABLE_CONVERT)) { zend_ast_fcc *fcc_ast = (zend_ast_fcc*) ast; @@ -2028,6 +2031,8 @@ static ZEND_COLD void zend_ast_export_visibility(smart_str *str, uint32_t flags, } } +static ZEND_COLD void zend_ast_export_generic_type_argument_list(smart_str *str, zend_ast *list_ast, int indent); + static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int indent) { if (ast->kind == ZEND_AST_TYPE_UNION) { const zend_ast_list *list = zend_ast_get_list(ast); @@ -2049,12 +2054,88 @@ static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int in } return; } + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + if (ast->attr & ZEND_TYPE_NULLABLE) { + smart_str_appendc(str, '?'); + } + + zend_ast *name_ast = ast->child[0]; + if (name_ast->kind == ZEND_AST_TYPE) { + zend_ast_export_type(str, name_ast, indent); + } else { + zend_ast_export_ns_name(str, name_ast, 0, indent); + } + zend_ast_export_generic_type_argument_list(str, ast->child[1], indent); + return; + } if (ast->attr & ZEND_TYPE_NULLABLE) { smart_str_appendc(str, '?'); } zend_ast_export_ns_name(str, ast, 0, indent); } +static ZEND_COLD void zend_ast_export_generic_type_argument_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appendc(str, '<'); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_type(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + +static ZEND_COLD void zend_ast_export_generic_call_type_argument_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appends(str, "::<"); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_type(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + +static ZEND_COLD void zend_ast_export_generic_type_parameter(smart_str *str, zend_ast *ast, int indent) +{ + switch ((zend_generic_variance) ast->attr) { + case ZEND_GENERIC_VARIANCE_COVARIANT: + smart_str_appendc(str, '+'); + break; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: + smart_str_appendc(str, '-'); + break; + case ZEND_GENERIC_VARIANCE_INVARIANT: + break; + } + smart_str_append(str, zend_ast_get_str(ast->child[0])); + if (ast->child[1]) { + smart_str_appends(str, " : "); + zend_ast_export_type(str, ast->child[1], indent); + } + if (ast->child[2]) { + smart_str_appends(str, " = "); + zend_ast_export_type(str, ast->child[2], indent); + } +} + +static ZEND_COLD void zend_ast_export_generic_type_parameter_list(smart_str *str, zend_ast *list_ast, int indent) +{ + const zend_ast_list *list = zend_ast_get_list(list_ast); + smart_str_appendc(str, '<'); + for (uint32_t i = 0; i < list->children; i++) { + if (i != 0) { + smart_str_appends(str, ", "); + } + zend_ast_export_generic_type_parameter(str, list->child[i], indent); + } + smart_str_appendc(str, '>'); +} + static ZEND_COLD void zend_ast_export_hook_list(smart_str *str, const zend_ast_list *hook_list, int indent) { smart_str_appends(str, " {"); @@ -2204,6 +2285,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio if (ast->kind != ZEND_AST_CLOSURE && ast->kind != ZEND_AST_ARROW_FUNC) { smart_str_append(str, decl->name); } + if (decl->child[5]) { + zend_ast_export_generic_type_parameter_list(str, decl->child[5], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, decl->child[0], 0, indent); smart_str_appendc(str, ')'); @@ -2261,6 +2345,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, "class "); } smart_str_append(str, decl->name); + if (decl->child[5]) { + zend_ast_export_generic_type_parameter_list(str, decl->child[5], indent); + } if (decl->flags & ZEND_ACC_ENUM && decl->child[4]) { smart_str_appends(str, ": "); zend_ast_export_type(str, decl->child[4], indent); @@ -2367,6 +2454,15 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio case ZEND_AST_NAME_LIST: zend_ast_export_name_list(str, zend_ast_get_list(ast), indent); break; + case ZEND_AST_GENERIC_TYPE_PARAMETER_LIST: + zend_ast_export_generic_type_parameter_list(str, ast, indent); + break; + case ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST: + zend_ast_export_generic_type_argument_list(str, ast, indent); + break; + case ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST: + zend_ast_export_generic_call_type_argument_list(str, ast, indent); + break; case ZEND_AST_USE: smart_str_appends(str, "use "); if (ast->attr == T_FUNCTION) { @@ -2542,6 +2638,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio } else { zend_ast_export_ns_name(str, left, 0, indent); } + if (ast->child[2]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[2], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[1], 0, indent); smart_str_appendc(str, ')'); @@ -2650,6 +2749,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_class_no_header(str, decl, indent); } else { zend_ast_export_ns_name(str, ast->child[0], 0, indent); + if (ast->child[2]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[2], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[1], 0, indent); smart_str_appendc(str, ')'); @@ -2660,6 +2762,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, " instanceof "); zend_ast_export_ns_name(str, ast->child[1], 0, indent); break; + case ZEND_AST_GENERIC_NAMED_TYPE: + zend_ast_export_type(str, ast, indent); + break; case ZEND_AST_YIELD: if (priority > 70) smart_str_appendc(str, '('); smart_str_appends(str, "yield "); @@ -2840,12 +2945,15 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio ast = ast->child[1]; goto tail_call; - /* 3 child nodes */ + /* 4 child nodes */ case ZEND_AST_METHOD_CALL: case ZEND_AST_NULLSAFE_METHOD_CALL: zend_ast_export_ex(str, ast->child[0], 0, indent); smart_str_appends(str, ast->kind == ZEND_AST_NULLSAFE_METHOD_CALL ? "?->" : "->"); zend_ast_export_var(str, ast->child[1], indent); + if (ast->child[3]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[3], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[2], 0, indent); smart_str_appendc(str, ')'); @@ -2854,6 +2962,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_ns_name(str, ast->child[0], 0, indent); smart_str_appends(str, "::"); zend_ast_export_var(str, ast->child[1], indent); + if (ast->child[3]) { + zend_ast_export_generic_call_type_argument_list(str, ast->child[3], indent); + } smart_str_appendc(str, '('); zend_ast_export_ex(str, ast->child[2], 0, indent); smart_str_appendc(str, ')'); @@ -2934,6 +3045,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio zend_ast_export_ex(str, ast->child[1], 0, indent); } break; + case ZEND_AST_GENERIC_TYPE_PARAMETER: + zend_ast_export_generic_type_parameter(str, ast, indent); + break; /* 4 child nodes */ case ZEND_AST_FOR: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 24b77d7d3493..77598f809f1c 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -69,6 +69,9 @@ enum _zend_ast_kind { ZEND_AST_ATTRIBUTE_GROUP, ZEND_AST_MATCH_ARM_LIST, ZEND_AST_MODIFIER_LIST, + ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, + ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, + ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST, /* 0 child nodes */ ZEND_AST_MAGIC_CONST = 0 << ZEND_AST_NUM_CHILDREN_SHIFT, @@ -117,7 +120,6 @@ enum _zend_ast_kind { ZEND_AST_PROP, ZEND_AST_NULLSAFE_PROP, ZEND_AST_STATIC_PROP, - ZEND_AST_CALL, ZEND_AST_CLASS_CONST, ZEND_AST_ASSIGN, ZEND_AST_ASSIGN_REF, @@ -128,7 +130,6 @@ enum _zend_ast_kind { ZEND_AST_AND, ZEND_AST_OR, ZEND_AST_ARRAY_ELEM, - ZEND_AST_NEW, ZEND_AST_INSTANCEOF, ZEND_AST_YIELD, ZEND_AST_COALESCE, @@ -148,23 +149,23 @@ enum _zend_ast_kind { ZEND_AST_USE_ELEM, ZEND_AST_TRAIT_ALIAS, ZEND_AST_GROUP_USE, - ZEND_AST_ATTRIBUTE, ZEND_AST_MATCH, ZEND_AST_MATCH_ARM, ZEND_AST_NAMED_ARG, ZEND_AST_PIPE, + ZEND_AST_GENERIC_NAMED_TYPE, /* 3 child nodes */ - ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, - ZEND_AST_NULLSAFE_METHOD_CALL, - ZEND_AST_STATIC_CALL, - ZEND_AST_CONDITIONAL, - + ZEND_AST_CONDITIONAL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, ZEND_AST_TRY, ZEND_AST_CATCH, ZEND_AST_PROP_GROUP, ZEND_AST_CONST_ELEM, ZEND_AST_CLASS_CONST_GROUP, + ZEND_AST_CALL, + ZEND_AST_NEW, + ZEND_AST_GENERIC_TYPE_PARAMETER, + ZEND_AST_ATTRIBUTE, /* 4 child nodes */ ZEND_AST_FOR = 4 << ZEND_AST_NUM_CHILDREN_SHIFT, @@ -175,6 +176,11 @@ enum _zend_ast_kind { // Pseudo node for initializing enums ZEND_AST_CONST_ENUM_INIT, + /* Method/static call forms with optional call_type_arguments slot. */ + ZEND_AST_METHOD_CALL, + ZEND_AST_NULLSAFE_METHOD_CALL, + ZEND_AST_STATIC_CALL, + /* 5 child nodes */ /* 6 child nodes */ @@ -225,7 +231,7 @@ typedef struct _zend_ast_decl { uint32_t flags; zend_string *doc_comment; zend_string *name; - zend_ast *child[5]; + zend_ast *child[6]; } zend_ast_decl; // TODO: rename @@ -337,7 +343,8 @@ ZEND_API zend_ast * ZEND_FASTCALL zend_ast_arg_list_add(zend_ast *list, zend_ast ZEND_API zend_ast *zend_ast_create_decl( zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, - zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, zend_ast *child4 + zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3, + zend_ast *child4, zend_ast *child5 ); ZEND_API zend_ast * ZEND_FASTCALL zend_ast_create_fcc(zend_ast *args); diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 71f0d788e921..416be8093ef2 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -472,6 +472,11 @@ static void attr_free(zval *v) zend_string_release(attr->validation_error); } + if (attr->generic_args) { + zend_type_release(*attr->generic_args, persistent); + pefree(attr->generic_args, persistent); + } + for (uint32_t i = 0; i < attr->argc; i++) { if (attr->args[i].name) { zend_string_release(attr->args[i].name); @@ -508,6 +513,8 @@ ZEND_API zend_attribute *zend_add_attribute(HashTable **attributes, zend_string attr->lineno = lineno; attr->offset = offset; attr->argc = argc; + attr->generic_arity = 0; + attr->generic_args = NULL; /* Initialize arguments to avoid partial initialization in case of fatal errors. */ for (uint32_t i = 0; i < argc; i++) { diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index c044ef073cc8..628b557a6584 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -67,6 +67,9 @@ typedef struct _zend_attribute { /* Parameter offsets start at 1, everything else uses 0. */ uint32_t offset; uint32_t argc; + /* Number of generic type arguments at the attribute use-site. */ + uint8_t generic_arity; + zend_type *generic_args; zend_attribute_arg args[1]; } zend_attribute; diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 2dceac2512db..6d63aab29170 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -371,6 +371,47 @@ ZEND_FUNCTION(func_get_args) } /* }}} */ +/* {{{ Get an array mapping generic type-parameter names to their resolved + * class names for the currently-executed generic function call. Unbound slots + * (parameter has no turbofish, no default, and no inferable position) yield + * NULL — the caller can detect them and fall back to the parameter's bound. */ +ZEND_FUNCTION(func_get_type_args) +{ + zend_execute_data *ex = EX(prev_execute_data); + + ZEND_PARSE_PARAMETERS_NONE(); + + if (ex && (ZEND_CALL_INFO(ex) & ZEND_CALL_CODE)) { + zend_throw_error(NULL, "func_get_type_args() cannot be called from the global scope"); + RETURN_THROWS(); + } + + if (zend_forbid_dynamic_call() == FAILURE) { + RETURN_THROWS(); + } + + const zend_generic_parameter_list *params = NULL; + if (ex && ZEND_USER_CODE(ex->func->type)) { + params = ex->func->op_array.generic_parameters; + } + if (!params || params->count == 0) { + RETURN_EMPTY_ARRAY(); + } + + zend_type_arg_table *table = ex->type_args; + array_init_size(return_value, params->count); + for (uint32_t i = 0; i < params->count; i++) { + zval entry; + if (table && i < table->count && table->entries[i].name) { + ZVAL_STR_COPY(&entry, table->entries[i].name); + } else { + ZVAL_NULL(&entry); + } + zend_hash_add(Z_ARRVAL_P(return_value), params->parameters[i].name, &entry); + } +} +/* }}} */ + /* {{{ Get string length Warning: This function is special-cased by zend_compile.c and so is usually bypassed */ ZEND_FUNCTION(strlen) diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 1d405587145d..1c587f9193fe 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -30,6 +30,12 @@ function func_get_arg(int $position): mixed {} */ function func_get_args(): array {} +/** + * @return array + * @forbid-dynamic-calls + */ +function func_get_type_args(): array {} + function strlen(string $string): int {} /** @compile-time-eval */ diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index b3af43fef340..d4aab19bbb82 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_builtin_functions.stub.php instead. - * Stub hash: 64c61862de86d9968930893bf21b516119724064 */ + * Stub hash: b435953eed1678fa83dd72073c0e95e997ce1d07 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) @@ -25,6 +25,8 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_func_get_args, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() +#define arginfo_func_get_type_args arginfo_func_get_args + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_strlen, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -254,6 +256,7 @@ ZEND_FUNCTION(zend_version); ZEND_FUNCTION(func_num_args); ZEND_FUNCTION(func_get_arg); ZEND_FUNCTION(func_get_args); +ZEND_FUNCTION(func_get_type_args); ZEND_FUNCTION(strlen); ZEND_FUNCTION(strcmp); ZEND_FUNCTION(strncmp); @@ -330,6 +333,11 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("func_get_args", zif_func_get_args, arginfo_func_get_args, ZEND_FENTRY_FLAGS(0, ZEND_ACC2_FORBID_DYN_CALLS), NULL, NULL) #elif (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY("func_get_args", zif_func_get_args, arginfo_func_get_args, 0, NULL, NULL) +#endif +#if (PHP_VERSION_ID >= 80600) + ZEND_RAW_FENTRY("func_get_type_args", zif_func_get_type_args, arginfo_func_get_type_args, ZEND_FENTRY_FLAGS(0, ZEND_ACC2_FORBID_DYN_CALLS), NULL, NULL) +#elif (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("func_get_type_args", zif_func_get_type_args, arginfo_func_get_type_args, 0, NULL, NULL) #endif ZEND_FE(strlen, arginfo_strlen) ZEND_RAW_FENTRY("strcmp", zif_strcmp, arginfo_strcmp, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index 840d2dbe32e1..9666fd000205 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -27,13 +27,8 @@ #include "zend_globals.h" #include "zend_closures_arginfo.h" -typedef struct _zend_closure { - zend_object std; - zend_function func; - zval this_ptr; - zend_class_entry *called_scope; - zif_handler orig_internal_handler; -} zend_closure; +/* zend_closure struct is defined in zend_closures.h so VM hot paths can + * inline the captured_type_args propagation in zend_vm_init_call_frame. */ /* non-static since it needs to be referenced */ ZEND_API zend_class_entry *zend_ce_closure; @@ -174,11 +169,17 @@ ZEND_METHOD(Closure, call) zend_function *my_function; fake_closure = emalloc(sizeof(zend_closure)); - memset(&fake_closure->std, 0, sizeof(fake_closure->std)); + memset(fake_closure, 0, sizeof(zend_closure)); fake_closure->std.gc.refcount = 1; fake_closure->std.gc.u.type_info = GC_NULL; ZVAL_UNDEF(&fake_closure->this_ptr); fake_closure->called_scope = NULL; + /* The fake closure runs the source closure's body; that body still + * resolves outer T-refs against the source's captured T-table. Borrow + * the pointer (no clone) — the source closure outlives the fake, + * which is efree'd at the end of this method without running + * free_storage, so no double-destroy. */ + fake_closure->captured_type_args = closure->captured_type_args; my_function = &fake_closure->func; if (ZEND_USER_CODE(closure->func.type)) { memcpy(my_function, &closure->func, sizeof(zend_op_array)); @@ -252,6 +253,14 @@ static void do_closure_bind(zval *return_value, zval *zclosure, zval *newthis, z } zend_create_closure(return_value, &closure->func, ce, called_scope, newthis); + + /* Re-binding produces a new closure that runs the same body; the body + * still references the outer T-bindings captured at lambda declaration + * time, so carry the T-table over. Without this Closure::bind and + * Closure::bindTo silently erase the reified bindings. */ + if (closure->captured_type_args) { + zend_closure_capture_type_args(return_value, closure->captured_type_args); + } } /* {{{ Create a closure from another one and bind to another object and scope */ @@ -567,6 +576,12 @@ static void zend_closure_free_storage(zend_object *object) /* {{{ */ if (Z_TYPE(closure->this_ptr) != IS_UNDEF) { zval_ptr_dtor(&closure->this_ptr); } + + if (closure->captured_type_args) { + closure->captured_type_args->persisted = false; + zend_type_arg_table_destroy(closure->captured_type_args); + closure->captured_type_args = NULL; + } } /* }}} */ @@ -590,6 +605,13 @@ static zend_object *zend_closure_clone(zend_object *zobject) /* {{{ */ zend_create_closure(&result, &closure->func, closure->func.common.scope, closure->called_scope, &closure->this_ptr); + + /* Carry over the source closure's captured T-table; the clone has the + * same body and resolves the same outer T-refs. */ + if (closure->captured_type_args) { + zend_closure_capture_type_args(&result, closure->captured_type_args); + } + return Z_OBJ(result); } /* }}} */ @@ -781,6 +803,11 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en if (closure->func.op_array.refcount) { (*closure->func.op_array.refcount)++; } + if ((closure->func.common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && closure->func.op_array.generic_types + && closure->func.op_array.generic_types->monomorph_type_args) { + closure->func.op_array.generic_types->monomorph_type_args->refcount++; + } /* For fake closures, we want to reuse the static variables of the original function. */ HashTable *ht = ZEND_MAP_PTR_GET(func->op_array.static_variables_ptr); @@ -857,6 +884,19 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en } } } + +ZEND_API void zend_closure_capture_type_args(zval *closure_zv, zend_type_arg_table *src) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(closure_zv); + if (closure->captured_type_args) { + closure->captured_type_args->persisted = false; + zend_type_arg_table_destroy(closure->captured_type_args); + closure->captured_type_args = NULL; + } + if (src) { + closure->captured_type_args = zend_type_arg_table_capture_clone(src); + } +} /* }}} */ ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h index 2ff4934f2a3a..b7ca1edd65f5 100644 --- a/Zend/zend_closures.h +++ b/Zend/zend_closures.h @@ -23,6 +23,19 @@ BEGIN_EXTERN_C() +typedef struct _zend_closure { + zend_object std; + zend_function func; + zval this_ptr; + zend_class_entry *called_scope; + zif_handler orig_internal_handler; + /* Snapshot of the generic type-arg table from the frame that created + * the closure. The closure owns the allocation; it is marked persisted + * so call-frame teardown skips destroying it. Cleared on closure + * free_obj. NULL when the closure was created outside a generic frame. */ + struct _zend_type_arg_table *captured_type_args; +} zend_closure; + /* This macro depends on zend_closure structure layout */ #define ZEND_CLOSURE_OBJECT(op_array) \ ((zend_object*)((char*)(op_array) - sizeof(zend_object))) @@ -36,6 +49,7 @@ extern ZEND_API zend_class_entry *zend_ce_closure; ZEND_API void zend_create_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr); ZEND_API void zend_create_fake_closure(zval *res, zend_function *op_array, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr); +ZEND_API void zend_closure_capture_type_args(zval *closure_zv, struct _zend_type_arg_table *src); ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj); ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); ZEND_API zval* zend_get_closure_this_ptr(zval *obj); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7e9f7ceac8db..df7831ac1481 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -22,6 +22,7 @@ #include "zend_ast.h" #include "zend_attributes.h" #include "zend_compile.h" +#include "zend_smart_str.h" #include "zend_constants.h" #include "zend_llist.h" #include "zend_API.h" @@ -101,6 +102,7 @@ static zend_op *zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t static void zend_compile_expr(znode *result, zend_ast *ast); static void zend_compile_stmt(zend_ast *ast); static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_t type); +static zend_type zend_compile_typename(zend_ast *ast); #ifdef ZEND_CHECK_STACK_LIMIT zend_never_inline static void zend_stack_limit_error(void) @@ -293,144 +295,2139 @@ static zend_always_inline uint8_t zend_lookup_builtin_type_by_name(const zend_st { const builtin_type_info *info = &builtin_types[0]; - for (; info->name; ++info) { - if (ZSTR_LEN(name) == info->name_len - && zend_binary_strcasecmp(ZSTR_VAL(name), ZSTR_LEN(name), info->name, info->name_len) == 0 - ) { - return info->type; + for (; info->name; ++info) { + if (ZSTR_LEN(name) == info->name_len + && zend_binary_strcasecmp(ZSTR_VAL(name), ZSTR_LEN(name), info->name, info->name_len) == 0 + ) { + return info->type; + } + } + + return 0; +} +/* }}} */ + +static zend_always_inline bool zend_is_confusable_type(const zend_string *name, const char **correct_name) /* {{{ */ +{ + const confusable_type_info *info = confusable_types; + + /* Intentionally using case-sensitive comparison here, because "integer" is likely intended + * as a scalar type, while "Integer" is likely a class type. */ + for (; info->name; ++info) { + if (zend_string_equals_cstr(name, info->name, info->name_len)) { + *correct_name = info->correct_name; + return 1; + } + } + + return 0; +} +/* }}} */ + +static bool zend_is_not_imported(zend_string *name) { + /* Assuming "name" is unqualified here. */ + return !FC(imports) || zend_hash_find_ptr_lc(FC(imports), name) == NULL; +} + +void zend_oparray_context_begin(zend_oparray_context *prev_context, zend_op_array *op_array) /* {{{ */ +{ + *prev_context = CG(context); + CG(context).prev = CG(context).op_array ? prev_context : NULL; + CG(context).op_array = op_array; + CG(context).opcodes_size = INITIAL_OP_ARRAY_SIZE; + CG(context).vars_size = 0; + CG(context).literals_size = 0; + CG(context).fast_call_var = -1; + CG(context).try_catch_offset = -1; + CG(context).current_brk_cont = -1; + CG(context).last_brk_cont = 0; + CG(context).has_assigned_to_http_response_header = false; + CG(context).brk_cont_array = NULL; + CG(context).labels = NULL; + CG(context).in_jmp_frameless_branch = false; + CG(context).active_property_info_name = NULL; + CG(context).active_property_hook_kind = (zend_property_hook_kind)-1; +} +/* }}} */ + +void zend_oparray_context_end(const zend_oparray_context *prev_context) /* {{{ */ +{ + if (CG(context).brk_cont_array) { + efree(CG(context).brk_cont_array); + CG(context).brk_cont_array = NULL; + } + if (CG(context).labels) { + zend_hash_destroy(CG(context).labels); + FREE_HASHTABLE(CG(context).labels); + CG(context).labels = NULL; + } + CG(context) = *prev_context; +} +/* }}} */ + +static void zend_reset_import_tables(void) /* {{{ */ +{ + if (FC(imports)) { + zend_hash_destroy(FC(imports)); + efree(FC(imports)); + FC(imports) = NULL; + } + + if (FC(imports_function)) { + zend_hash_destroy(FC(imports_function)); + efree(FC(imports_function)); + FC(imports_function) = NULL; + } + + if (FC(imports_const)) { + zend_hash_destroy(FC(imports_const)); + efree(FC(imports_const)); + FC(imports_const) = NULL; + } + + zend_hash_clean(&FC(seen_symbols)); +} +/* }}} */ + +static void zend_end_namespace(void) /* {{{ */ { + FC(in_namespace) = 0; + zend_reset_import_tables(); + if (FC(current_namespace)) { + zend_string_release_ex(FC(current_namespace), 0); + FC(current_namespace) = NULL; + } +} +/* }}} */ + +void zend_file_context_begin(zend_file_context *prev_context) /* {{{ */ +{ + *prev_context = CG(file_context); + FC(imports) = NULL; + FC(imports_function) = NULL; + FC(imports_const) = NULL; + FC(current_namespace) = NULL; + FC(in_namespace) = 0; + FC(has_bracketed_namespaces) = 0; + FC(declarables).ticks = 0; + zend_hash_init(&FC(seen_symbols), 8, NULL, NULL, 0); +} +/* }}} */ + +void zend_file_context_end(const zend_file_context *prev_context) /* {{{ */ +{ + zend_end_namespace(); + zend_hash_destroy(&FC(seen_symbols)); + CG(file_context) = *prev_context; +} +/* }}} */ + +void zend_init_compiler_data_structures(void) /* {{{ */ +{ + zend_stack_init(&CG(loop_var_stack), sizeof(zend_loop_var)); + zend_stack_init(&CG(delayed_oplines_stack), sizeof(zend_op)); + zend_stack_init(&CG(short_circuiting_opnums), sizeof(uint32_t)); + CG(active_class_entry) = NULL; + CG(in_compilation) = 0; + CG(skip_shebang) = 0; + + CG(encoding_declared) = 0; + CG(memoized_exprs) = NULL; + CG(memoize_mode) = ZEND_MEMOIZE_NONE; + CG(type_arg_depth) = 0; + CG(type_arg_residual_token) = 0; + CG(generic_scope) = NULL; + CG(in_static_member_type) = false; + CG(inheritance_binding_cache) = NULL; + CG(inheritance_binding_hint).target = NULL; + CG(inheritance_binding_hint).args = NULL; + CG(inheritance_binding_hint).arity = 0; +} +/* }}} */ + +static void zend_generic_scope_push(zend_generic_parameter_list *params, zend_generic_origin origin) /* {{{ */ +{ + zend_generic_scope_entry *entry = emalloc(sizeof(zend_generic_scope_entry)); + entry->params = params; + entry->visible_count = params ? params->count : 0; + entry->self_compiling = NULL; + entry->shadowing_classes = NULL; + entry->origin = origin; + entry->outer = CG(generic_scope); + CG(generic_scope) = entry; +} +/* }}} */ + +static void zend_generic_scope_pop(void) /* {{{ */ +{ + zend_generic_scope_entry *top = CG(generic_scope); + ZEND_ASSERT(top != NULL); + if (top->shadowing_classes) { + zend_hash_destroy(top->shadowing_classes); + FREE_HASHTABLE(top->shadowing_classes); + } + + CG(generic_scope) = top->outer; + efree(top); +} +/* }}} */ + +static zend_generic_parameter *zend_generic_lookup_full( + zend_string *name, zend_generic_origin *origin_out, uint32_t *index_out) /* {{{ */ +{ + zend_string *lc_name = NULL; + zend_generic_parameter *result = NULL; + for (zend_generic_scope_entry *e = CG(generic_scope); e; e = e->outer) { + if (e->shadowing_classes) { + if (!lc_name) { + lc_name = zend_string_tolower(name); + } + + if (zend_hash_exists(e->shadowing_classes, lc_name)) { + goto done; + } + } + + zend_generic_parameter_list *params = e->params; + for (uint32_t i = 0; i < e->visible_count; i++) { + if (zend_string_equals(params->parameters[i].name, name)) { + if (origin_out) *origin_out = e->origin; + if (index_out) *index_out = i; + result = ¶ms->parameters[i]; + goto done; + } + } + } + + done: + if (lc_name) { + zend_string_release(lc_name); + } + + return result; +} +/* }}} */ + +static void zend_check_class_origin_in_static_context(const zend_string *param_name) /* {{{ */ +{ + if (CG(in_static_member_type) && CG(active_class_entry)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s is bound to %s and cannot be used in static context", + ZSTR_VAL(param_name), ZSTR_VAL(CG(active_class_entry)->name)); + } +} +/* }}} */ + +/* Detects forward reference: a name that matches a parameter declared later in + * the innermost scope's parameter list. Returns the index of the not-yet-visible + * parameter, or -1 if no match. */ +static int zend_generic_lookup_forward(const zend_string *name) /* {{{ */ +{ + zend_generic_scope_entry *e = CG(generic_scope); + if (!e) { + return -1; + } + zend_generic_parameter_list *params = e->params; + for (uint32_t i = e->visible_count; i < params->count; i++) { + if (zend_string_equals(params->parameters[i].name, name)) { + return (int) i; + } + } + return -1; +} +/* }}} */ + +static bool zend_type_ast_has_generic_content(zend_ast *ast); +static zend_type zend_compile_pre_erasure_typename(zend_ast *ast); +static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type); + +static zend_generic_parameter *zend_generic_lookup(zend_string *name) /* {{{ */ +{ + return zend_generic_lookup_full(name, NULL, NULL); +} +/* }}} */ + +ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast) /* {{{ */ +{ + if (list_ast == NULL) { + return; + } + + uint32_t children = zend_ast_get_list(list_ast)->children; + if (children > ZEND_GENERIC_MAX_PARAMS) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot declare more than %u generic type parameters (got %u)", + ZEND_GENERIC_MAX_PARAMS, children); + } +} +/* }}} */ + +ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast) /* {{{ */ +{ + if (list_ast == NULL) { + return; + } + + uint32_t children = zend_ast_get_list(list_ast)->children; + if (children > ZEND_GENERIC_MAX_PARAMS) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot specify more than %u generic type arguments (got %u)", + ZEND_GENERIC_MAX_PARAMS, children); + } +} +/* }}} */ + +static zend_always_inline void zend_compute_generic_required_total( + const zend_generic_parameter_list *params, uint32_t *required, uint32_t *total) +{ + uint32_t t = params ? params->count : 0; + uint32_t r = 0; + if (params) { + while (r < t && !ZEND_TYPE_IS_SET(params->parameters[r].default_type)) { + r++; + } + } + + *required = r; + *total = t; +} + +static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_op_array *op_array); + +ZEND_API const zend_type *zend_generic_get_turbofish_args(const zend_op_array *caller_op_array, uint32_t args_id) +{ + zend_turbofish_args_entry *entry = zend_generic_get_turbofish_call_entry(caller_op_array, args_id); + return entry ? &entry->args_box : NULL; +} + +ZEND_API zend_turbofish_args_entry *zend_generic_get_turbofish_call_entry(const zend_op_array *caller_op_array, uint32_t args_id) +{ + if (!caller_op_array || !caller_op_array->generic_types || !caller_op_array->generic_types->turbofish_args || args_id == 0) { + return NULL; + } + + return zend_hash_index_find_ptr(caller_op_array->generic_types->turbofish_args, args_id); +} + +static void zend_check_generic_argument_bounds( + const zend_generic_parameter_list *params, + const zend_type *args_box, + uint32_t arity, + const char *callee_kind, + const zend_function *callee_fbc, + const zend_class_entry *callee_ce) +{ + if (!params || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return; + } + + const zend_type_named_with_args *args = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uint32_t check_count = arity < args->count ? arity : args->count; + if (check_count > params->count) check_count = params->count; + for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = params->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) { + continue; + } + if (zend_check_generic_arg_satisfies_bound(NULL, args->args[i], NULL, bound) != INHERITANCE_SUCCESS) { + zend_string *arg_str = zend_type_to_string(args->args[i]); + zend_type display = ZEND_TYPE_IS_SET(params->parameters[i].bound_pre_erasure) + ? params->parameters[i].bound_pre_erasure + : bound; + zend_string *bound_str = zend_type_to_string(display); + /* Build the callee's qualified display name only on the error + * path — the success path runs on every generic call. */ + zend_string *qname; + if (callee_fbc) { + qname = zend_strpprintf(0, "%s%s%s()", + callee_fbc->common.scope ? ZSTR_VAL(callee_fbc->common.scope->name) : "", + callee_fbc->common.scope ? "::" : "", + callee_fbc->common.function_name ? ZSTR_VAL(callee_fbc->common.function_name) : "{closure}"); + } else { + qname = zend_string_copy(callee_ce->name); + } + zend_throw_error(zend_ce_type_error, + "Type argument %u to %s %s does not satisfy the bound %s on parameter %s, %s given", + i + 1, callee_kind, ZSTR_VAL(qname), + ZSTR_VAL(bound_str), + params->parameters[i].name ? ZSTR_VAL(params->parameters[i].name) : "?", + ZSTR_VAL(arg_str)); + zend_string_release(qname); + zend_string_release(arg_str); + zend_string_release(bound_str); + return; + } + } +} + +/* Fingerprint the caller bindings that this args_box's T-refs resolve + * against, mixing in the interned-name pointers of each referenced caller + * entry. Used as the cache key on zend_turbofish_args_entry. Returns 0 when + * caching is unsafe (caller binding has only an owned_type — i.e., the + * type_ref would point into a frame-local zend_type that dies when the + * caller unwinds) or when there's no caller frame to read from. Concrete + * args_boxes (no T-refs) return the constant sentinel CONCRETE — the + * resulting table is invariant across calls and the cache slot acts as a + * once-built memo. */ +static uintptr_t zend_compute_call_cache_key( + const zend_type *args_box, const zend_execute_data *caller) +{ + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uintptr_t fp = 0; + 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) return 0; + if (!caller || !caller->type_args) return 0; + if (ref->index >= caller->type_args->count) return 0; + const zend_type_arg_entry *caller_entry = &caller->type_args->entries[ref->index]; + if (!caller_entry->name || !caller_entry->type_ref) return 0; + fp = fp * 0x9E3779B97F4A7C15ULL + (uintptr_t) caller_entry->name + i + 1; + } + if (!has_tref) { + return ZEND_TURBOFISH_CACHE_KEY_CONCRETE; + } + /* Steer clear of the sentinel values used to mean "empty" / "concrete". */ + if (fp == 0 || fp == ZEND_TURBOFISH_CACHE_KEY_CONCRETE) { + fp = (uintptr_t) -1; + } + return fp; +} + +/* Returns true when the cache key fully determines the call's type_arg_table, + * i.e. no inference will fire that could plug an unset slot from a runtime + * value. Inference depends on argument classes — not part of the key — so a + * call that triggers it must be excluded from caching even if the + * caller-binding fingerprint matches. + * + * Any inferable slot not pinned by a turbofish arg makes the call uncacheable, + * even when the slot has a declared default: inference outranks the default + * (see the phase order in zend_build_generic_call_type_args), so an object + * argument still resolves the slot from a runtime value. */ +static bool zend_call_is_cacheable_against_args( + const zend_function *fbc, const zend_type *args_box) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) return false; + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) return false; + uint64_t inferable = params->inferable_mask; + if (!inferable) return true; + uint32_t passed = 0; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + passed = ZEND_TYPE_NAMED_WITH_ARGS(*args_box)->count; + } + for (uint32_t i = 0; i < params->count && i < 64; i++) { + if (!(inferable & ((uint64_t) 1 << i))) continue; + if (i < passed) continue; + return false; + } + return true; +} + +/* Cache-aware wrapper: VERIFY/INSTALL handlers route through this instead of + * zend_build_generic_call_type_args directly. On hit, returns the cached + * shared table (already marked persisted, so frame teardown leaves it + * alone). On miss, builds normally; if the call site is single-binding + * stable, the freshly built table is also stashed in the cache for future + * iterations. The "store on first miss, never replace" policy avoids + * dangling: a replaced cached_table could still be referenced by a live + * frame's call->type_args. + * + * `cache_slot` points to a 2-pointer runtime cache slot owned by the + * 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) { + /* 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] + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { + cache_slot[0] = nt; + cache_slot[1] = (void *)nkey; + nt->persisted = true; + } + return nt; + } + zend_execute_data *caller = EG(current_execute_data); + uintptr_t key = zend_compute_call_cache_key(args_box, caller); + if (cache_slot && key && cache_slot[0] && (uintptr_t)cache_slot[1] == key) { + return (zend_type_arg_table *)cache_slot[0]; + } + zend_type_arg_table *t = zend_build_generic_call_type_args(call, args_box); + if (t && key && cache_slot && !cache_slot[0] + && zend_call_is_cacheable_against_args(call->func, args_box) + && (caller->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE)) { + cache_slot[0] = t; + cache_slot[1] = (void *)key; + t->persisted = true; + } + 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) { + zend_type_arg_table *cached = (zend_type_arg_table *) cache_slot[4]; + *out_type_args = cached ? cached : zend_build_concrete_call_type_args(base, args_box); + return (zend_function *) cache_slot[0]; + } + + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + return NULL; + } + + /* A partial turbofish that leaves an inferable slot unset must NOT be + * monomorphized on the turbofish args alone: the unset slot is filled by + * value-directed inference (a runtime value), so the resolved signature + * varies per call. Fall back to the erased VERIFY path, which infers. */ + if (!zend_call_is_cacheable_against_args(base, 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); + bool can_cache_table = table && cache_slot && EG(current_execute_data) + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_HEAP_RT_CACHE); + if (can_cache_table) { + table->persisted = true; + } + + if (cache_slot) { + cache_slot[0] = mono; + cache_slot[1] = (void *) ZEND_TURBOFISH_CACHE_KEY_MONOMORPH; + cache_slot[3] = (void *) base; + cache_slot[4] = can_cache_table ? table : NULL; + } + *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 (highest precedence first): explicit turbofish arg → + * value-directed inference from any argument whose pre-erasure type is a direct + * TYPE_PARAMETER reference to this slot → parameter's declared default. The + * default is the *lowest* precedence: it only fills a slot that neither an + * explicit turbofish arg nor inference could pin, so `function f(A $a)` + * called as `f($x)` reifies A from $x rather than collapsing to the `mixed` + * default. */ +ZEND_API zend_type_arg_table *zend_build_generic_call_type_args( + zend_execute_data *call, const zend_type *args_box) +{ + const zend_function *fbc = call->func; + const zend_generic_parameter_list *params = NULL; + const zend_op_array *op_array = NULL; + if (ZEND_USER_CODE(fbc->common.type)) { + params = fbc->op_array.generic_parameters; + op_array = &fbc->op_array; + } + if (!params || params->count == 0) { + return NULL; + } + + zend_type_arg_table *table = zend_type_arg_table_alloc(params->count); + 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_execute_data *caller = EG(current_execute_data); + /* Phase 1: explicit turbofish args (highest precedence). Defaults are + * deferred to phase 3 so inference can pin an inferable slot first. */ + for (uint32_t i = 0; i < passed && i < params->count; i++) { + const zend_type *src = &passed_args[i]; + if (!ZEND_TYPE_IS_SET(*src)) { + continue; + } + /* A turbofish arg can itself name an outer generic parameter (e.g. + * `inner::(...)` inside `function outer`). Resolve those + * against the caller frame's T-table so the binding chains — + * borrow whatever the caller's entry resolved to (which is itself + * either another borrowed ref or an owned synthesised type), and + * copy the canonical name. Both source lifetimes outlive this + * frame because frames nest. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*src)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*src); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && caller && caller->type_args + && ref->index < caller->type_args->count + && caller->type_args->entries[ref->index].name) { + table->entries[i].name = zend_string_copy(caller->type_args->entries[ref->index].name); + table->entries[i].type_ref = zend_type_arg_entry_type(&caller->type_args->entries[ref->index]); + } + continue; + } + table->entries[i].name = zend_type_arg_canonical_name(*src); + /* Borrow the args_box's slot — args_box lives on a caller-chain + * op_array and outlives this frame's type_args. */ + table->entries[i].type_ref = src; + } + + /* Phase 2: inference for unset slots. Composite shapes (array, Box, ?T) are + * skipped — only bare top-level T at a value-parameter position. The + * inferable_mask short-circuits when no inferable slot remains unset. */ + uint64_t unbound_inferable = params->inferable_mask; + for (uint32_t i = 0; i < params->count && i < 64; i++) { + if (table->entries[i].name) { + unbound_inferable &= ~((uint64_t) 1 << i); + } + } + if (unbound_inferable + && op_array + && op_array->generic_types + && op_array->generic_types->parameters) { + HashTable *pre = op_array->generic_types->parameters; + uint32_t num_args = ZEND_CALL_NUM_ARGS(call); + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (arg_idx >= num_args) continue; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_FULL_MASK(*pe_type_ptr) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= table->count) continue; + if (table->entries[ref->index].name) continue; + zval *arg = ZEND_CALL_ARG(call, (uint32_t) arg_idx + 1); + if (Z_TYPE_P(arg) == IS_OBJECT) { + zend_class_entry *arg_ce = Z_OBJCE_P(arg); + table->entries[ref->index].name = zend_string_copy(arg_ce->name); + /* No pre-existing zend_type to borrow — synthesise a + * class-name owned_type from the value's class so value-arg + * checks on forwarded T-refs compare instanceof correctly. */ + zend_string *inferred_name = zend_string_copy(arg_ce->name); + table->entries[ref->index].owned_type = + (zend_type) ZEND_TYPE_INIT_CLASS(inferred_name, 0, 0); + } else { + /* Scalar/array values infer to their builtin type. Map the + * zval type to a type code, synthesise a mask owned_type, and + * take its canonical name ("int", "string", ...) so reflection + * and a reified `: T` return enforce the inferred type instead + * of collapsing to the default. IS_TRUE/IS_FALSE both map to + * bool; IS_NULL and other non-reifiable kinds (resources) are + * left for the declared default. */ + uint32_t type_mask = 0; + switch (Z_TYPE_P(arg)) { + case IS_LONG: type_mask = MAY_BE_LONG; break; + case IS_DOUBLE: type_mask = MAY_BE_DOUBLE; break; + case IS_STRING: type_mask = MAY_BE_STRING; break; + case IS_ARRAY: type_mask = MAY_BE_ARRAY; break; + case IS_TRUE: + case IS_FALSE: type_mask = MAY_BE_BOOL; break; + default: break; + } + if (type_mask) { + zend_type inferred = (zend_type) ZEND_TYPE_INIT_MASK(type_mask); + table->entries[ref->index].name = + zend_type_arg_canonical_name(inferred); + table->entries[ref->index].owned_type = inferred; + } + } + } ZEND_HASH_FOREACH_END(); + } + + /* Phase 3: declared defaults (lowest precedence) — fill only slots that + * neither a turbofish arg nor inference pinned. A default may itself name + * an outer generic parameter, resolved against the caller frame exactly as + * a turbofish arg is in phase 1. */ + for (uint32_t i = 0; i < params->count; i++) { + if (table->entries[i].name) { + continue; + } + const zend_type *src = ¶ms->parameters[i].default_type; + if (!ZEND_TYPE_IS_SET(*src)) { + continue; + } + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*src)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*src); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && caller && caller->type_args + && ref->index < caller->type_args->count + && caller->type_args->entries[ref->index].name) { + table->entries[i].name = zend_string_copy(caller->type_args->entries[ref->index].name); + table->entries[i].type_ref = zend_type_arg_entry_type(&caller->type_args->entries[ref->index]); + } + continue; + } + table->entries[i].name = zend_type_arg_canonical_name(*src); + table->entries[i].type_ref = src; + } + + return table; +} + +/* Recursive value check for a pre-erasure zend_type. Pre-erasure stores + * unions / intersections as a LIST with CODE-only / NAME / NAMED_WITH_ARGS + * members (one per branch), which is not the canonical shape that + * zend_check_type_slow understands — it expects scalar bits on the outer + * mask. Walk the shape directly here: + * + * - LIST with INTERSECTION_BIT: all members must match (object only). + * - LIST (union): any member matches. + * - NAMED_WITH_ARGS: instanceof against the canonical monomorph class; + * null also accepted if MAY_BE_NULL is set on the type's mask. + * - NAME: instanceof against the class; same null short-circuit. + * - Otherwise (CODE-only mask): scalar-hint check + null short-circuit. + * + * Returns true if the value satisfies the type. The `strict` argument + * controls scalar coercion at the leaf. */ +static bool zend_check_pre_erasure_type_value( + const zend_type *type, zval *arg, const zend_reference *ref, bool strict) +{ + uint32_t mask = ZEND_TYPE_FULL_MASK(*type); + + if (ZEND_TYPE_IS_INTERSECTION(*type)) { + if (Z_TYPE_P(arg) != IS_OBJECT) { + return (mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL; + } + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), member) { + if (!zend_check_pre_erasure_type_value(member, arg, NULL, strict)) { + return false; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return true; + } + + if (ZEND_TYPE_HAS_LIST(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) { + return true; + } + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), member) { + if (zend_check_pre_erasure_type_value(member, arg, ref, strict)) { + return true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return false; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) return true; + if (Z_TYPE_P(arg) != IS_OBJECT) return false; + zend_string *canonical = zend_type_to_canonical_string(*type); + zend_class_entry *ce = zend_lookup_class_ex(canonical, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + zend_string_release(canonical); + return ce && instanceof_function(Z_OBJCE_P(arg), ce); + } + + if (ZEND_TYPE_HAS_NAME(*type)) { + if ((mask & MAY_BE_NULL) && Z_TYPE_P(arg) == IS_NULL) return true; + if (Z_TYPE_P(arg) != IS_OBJECT) return false; + zend_class_entry *ce = zend_lookup_class_ex(ZEND_TYPE_NAME(*type), NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + return ce && instanceof_function(Z_OBJCE_P(arg), ce); + } + + /* Mask-only — single scalar (e.g. INT) or a combined scalar mask. */ + if (ZEND_TYPE_CONTAINS_CODE(*type, Z_TYPE_P(arg))) return true; + if (ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) return false; + return zend_verify_scalar_type_hint(mask, arg, strict, /* is_internal */ false); +} + +/* Run PHP's normal arg-type machinery (coerce in weak mode, throw TypeError + * in strict mode) against the per-call substituted type for any value-parameter + * whose pre-erasure type is a direct TYPE_PARAMETER reference. Returns false + * 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; + if (args_box && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + nwa = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + } + /* When neither args_box nor a captured frame T-table is available there + * is nothing to verify against. */ + if (!nwa && !call->type_args) { + return true; + } + const zend_function *fbc = call->func; + if (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_types + || !fbc->op_array.generic_types->parameters) { + return true; + } + + 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) { + if (arg_idx >= num_args) continue; + 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; + 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; +} + +/* Reified counterpart to zend_verify_return_error's erased check. Consults + * op_array->generic_types->return_type (pre-erasure) and, if it's a direct + * T-ref, substitutes via call->type_args and verifies the value. Only bare + * T at the top level is handled; composite shapes (array, Box) still + * fall through to the existing erased-bound check. Returns true when no + * reified check applies *or* the check passes; false (with EG(exception) + * set) when the value violates the reified type. */ +ZEND_API bool zend_verify_generic_return_type(zend_execute_data *call, zval *retval_ptr) +{ + if (!call->type_args) { + return true; + } + const zend_function *fbc = call->func; + if (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_types + || !fbc->op_array.generic_types->return_type) { + return true; + } + const zend_type *pe_type = fbc->op_array.generic_types->return_type; + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type)) { + return true; + } + const zend_type_parameter_ref *param_ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type); + if (param_ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + return true; + } + if (param_ref->index >= call->type_args->count) { + return true; + } + const zend_type *resolved = zend_type_arg_entry_type( + &call->type_args->entries[param_ref->index]); + if (!resolved || !ZEND_TYPE_IS_SET(*resolved)) { + return true; + } + zend_type substituted = *resolved; + + zval *target = retval_ptr; + const zend_reference *zref = NULL; + if (Z_ISREF_P(target)) { + zref = Z_REF_P(target); + target = Z_REFVAL_P(target); + } + + /* `?T` carries the nullable bit on the outer T-ref; preserve it on the + * substituted type so a returned NULL still passes when declared `?T`. */ + if (ZEND_TYPE_FULL_MASK(*pe_type) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(substituted) |= _ZEND_TYPE_NULLABLE_BIT; + } + + bool strict = EG(current_execute_data) + && EG(current_execute_data)->func + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_STRICT_TYPES); + bool ok = zend_check_pre_erasure_type_value(&substituted, target, zref, strict); + if (!ok) { + zend_string *expected = zend_type_to_string(substituted); + zend_throw_error(zend_ce_type_error, + "%s%s%s(): Return value must be of type %s, %s returned", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + ZSTR_VAL(expected), zend_zval_value_name(target)); + zend_string_release(expected); + return false; + } + return true; +} + +ZEND_API void zend_check_generic_call_arguments(const zend_function *fbc, uint32_t arity, const zend_type *args_box) +{ + const zend_generic_parameter_list *params = NULL; + if (ZEND_USER_CODE(fbc->common.type)) { + params = fbc->op_array.generic_parameters; + } + + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + + if (arity > total) { + zend_throw_error(zend_ce_argument_count_error, + "Too many generic type arguments to %s%s%s(), %u passed and %s %u expected", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + arity, + required == total ? "exactly" : "at most", + total); + return; + } + if (arity > 0 && arity < required) { + /* A missing slot is treated as supplied for arity purposes if a + * value-parameter can pin it via inference. The compile-time + * inferable_mask precomputes that set. */ + uint64_t inferable = params ? params->inferable_mask : 0; + uint32_t still_required = required; + for (uint32_t i = arity; i < required; i++) { + if (inferable & ((uint64_t) 1 << i)) { + still_required--; + } + } + if (arity < still_required) { + zend_throw_error(zend_ce_argument_count_error, + "Too few generic type arguments to %s%s%s(), %u passed and %s %u expected", + fbc->common.scope ? ZSTR_VAL(fbc->common.scope->name) : "", + fbc->common.scope ? "::" : "", + fbc->common.function_name ? ZSTR_VAL(fbc->common.function_name) : "{closure}", + arity, + required == total ? "exactly" : "at least", + required); + return; + } + } + + if (args_box) { + zend_check_generic_argument_bounds(params, args_box, arity, "call", fbc, NULL); + } +} + +ZEND_API void zend_check_generic_new_arguments(const zend_class_entry *ce, uint32_t arity, const zend_type *args_box) +{ + uint32_t required, total; + zend_compute_generic_required_total(ce->generic_parameters, &required, &total); + + if (arity > total) { + zend_throw_error(zend_ce_argument_count_error, + "Too many generic type arguments to new %s, %u passed and %s %u expected", + ZSTR_VAL(ce->name), arity, + required == total ? "exactly" : "at most", total); + return; + } + if (arity < required) { + zend_throw_error(zend_ce_argument_count_error, + "Too few generic type arguments to new %s, %u passed and %s %u expected", + ZSTR_VAL(ce->name), arity, + required == total ? "exactly" : "at least", required); + return; + } + + if (args_box) { + zend_check_generic_argument_bounds(ce->generic_parameters, args_box, arity, "new", NULL, ce); + } +} + +#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) +{ + zend_ast_list *list = zend_ast_get_list(turbofish_ast); + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(list->children)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = list->children; + for (uint32_t i = 0; i < list->children; i++) { + payload->args[i] = zend_compile_pre_erasure_typename(list->child[i]); + } + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; +} + +/* Returns true when every supplied turbofish arg is concrete (no T-refs) + * and `zend_check_generic_arg_satisfies_bound` resolves to + * INHERITANCE_SUCCESS — i.e. the runtime arity+bound check is redundant + * and we can emit ZEND_INSTALL_GENERIC_ARGS in place of + * ZEND_VERIFY_GENERIC_ARGUMENTS. Returns false on T-refs, on a definite + * INHERITANCE_ERROR (so the runtime VERIFY can throw the catchable + * TypeError as before), or on INHERITANCE_UNRESOLVED (a class isn't + * loaded yet — the runtime retry may succeed). */ +static bool zend_static_check_generic_call_bounds( + const zend_generic_parameter_list *params, + const zend_type_named_with_args *args, uint32_t arity) +{ + uint32_t check_count = arity < args->count ? arity : args->count; + if (check_count > params->count) check_count = params->count; + for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = params->parameters[i].bound; + bool has_bound = ZEND_TYPE_IS_SET(bound); + if (zend_type_contains_type_parameter(args->args[i])) { + /* Unbounded parameter accepts any T-ref trivially; we can elide + * the runtime arity+bound check (INSTALL instead of VERIFY). + * A T-ref against a bounded parameter still needs runtime + * resolution to compare against the bound. */ + if (!has_bound) continue; + return false; + } + if (!has_bound) continue; + zend_inheritance_status status = + zend_check_generic_arg_satisfies_bound(NULL, args->args[i], NULL, bound); + if (status != INHERITANCE_SUCCESS) { + return false; + } + } + return true; +} + +/* Can the runtime VERIFY be replaced with the cheaper INSTALL at this CALL + * site? Requires the callee to be statically known and generic, the arity + * to be within [required, total], and the turbofish args to be concrete + * and bound-satisfying. */ +static bool zend_can_install_call_args_statically( + const zend_function *fbc, const zend_type *args_box, uint32_t arity) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) return false; + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params) return false; + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + if (arity < required || arity > total) return false; + if (!args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) return false; + return zend_static_check_generic_call_bounds( + 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_for(zend_op_array *caller, + const zend_function *fbc, const zend_type *args_box, uint32_t args_id) +{ + if (!fbc || !args_box || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*args_box)) { + 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 = caller->generic_types; + zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(gtt->turbofish_args, args_id); + ZEND_ASSERT(entry != NULL); + if (entry->concrete_table && !entry->concrete_table->persisted) { + zend_type_arg_table_destroy(entry->concrete_table); + } + entry->concrete_table = ct; + entry->concrete_skip_value_check = + fbc->op_array.generic_types + && fbc->op_array.generic_types->parameters + && zend_count_generic_value_checks( + fbc->op_array.generic_types->parameters) == 0; + return true; +} + +/* Literals only: inferring T from a variable's declared type is unsound, as the + * variable may be reassigned before the call. */ +static bool zend_infer_literal_arg_pre_erasure(zend_ast *arg, zend_type *out) +{ + if (arg->kind != ZEND_AST_ZVAL) { + return false; + } + const zval *zv = zend_ast_get_zval(arg); + switch (Z_TYPE_P(zv)) { + case IS_LONG: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_LONG, 0, 0); return true; + case IS_DOUBLE: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_DOUBLE, 0, 0); return true; + case IS_STRING: *out = (zend_type) ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); return true; + default: return false; + } +} + +static const zend_type *zend_infer_call_type_args( + const zend_function *fbc, zend_ast *args_ast, + uint32_t *out_arity, uint32_t *out_args_id) +{ + if (!fbc || !ZEND_USER_CODE(fbc->common.type)) { + return NULL; + } + const zend_generic_parameter_list *params = fbc->op_array.generic_parameters; + if (!params || params->count == 0 || params->inferable_mask == 0) { + return NULL; + } + const zend_op_array *callee = &fbc->op_array; + if (!callee->generic_types || !callee->generic_types->parameters) { + return NULL; + } + if (!args_ast || args_ast->kind != ZEND_AST_ARG_LIST) { + return NULL; + } + zend_ast_list *args = zend_ast_get_list(args_ast); + for (uint32_t i = 0; i < args->children; i++) { + zend_ast_kind k = args->child[i]->kind; + if (k == ZEND_AST_UNPACK || k == ZEND_AST_NAMED_ARG) { + return NULL; + } + } + + uint32_t required, total; + zend_compute_generic_required_total(params, &required, &total); + if (total == 0 || total > ZEND_GENERIC_MAX_PARAMS) { + return NULL; + } + + int arg_pos_for_gp[ZEND_GENERIC_MAX_PARAMS]; + for (uint32_t i = 0; i < total; i++) { + arg_pos_for_gp[i] = -1; + } + { + HashTable *pre = callee->generic_types->parameters; + zend_ulong arg_idx; + zend_type *pe_type_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR(pre, arg_idx, pe_type_ptr) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pe_type_ptr)) continue; + if (ZEND_TYPE_FULL_MASK(*pe_type_ptr) & MAY_BE_NULL) continue; + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pe_type_ptr); + if (ref->origin != ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) continue; + if (ref->index >= total) continue; + if (arg_pos_for_gp[ref->index] == -1 && arg_idx < (zend_ulong) args->children) { + arg_pos_for_gp[ref->index] = (int) arg_idx; + } + } ZEND_HASH_FOREACH_END(); + } + + zend_type inferred[ZEND_GENERIC_MAX_PARAMS]; + uint32_t arity = 0; + for (uint32_t g = 0; g < total; g++) { + int pos = arg_pos_for_gp[g]; + if (pos < 0) { + break; + } + zend_type entry; + if (!zend_infer_literal_arg_pre_erasure(args->child[pos], &entry)) { + break; + } + inferred[arity++] = entry; + } + + if (arity == 0 || arity < required) { + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(inferred[i], /* persistent */ false); + } + return NULL; + } + + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = inferred[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + + *out_arity = arity; + *out_args_id = args_id; + return zend_hash_index_find_ptr(table->turbofish_args, args_id); +} + +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id) +{ + /* arity == 0 would underflow ZEND_TYPE_NAMED_WITH_ARGS_SIZE (count-1). */ + if (arity == 0) { + return ZEND_NOP; + } + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = NULL; + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = arg_types[i]; + } + zend_type box = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(box, payload); + ZEND_TYPE_FULL_MASK(box) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + + if (!zend_can_install_call_args_statically(fbc, &box, arity)) { + zend_type_release(box, /* persistent */ false); + return ZEND_NOP; + } + + zend_generic_type_table *table = zend_generic_get_or_create_op_array_table(caller); + uint32_t args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, box); + const zend_type *args_box = zend_hash_index_find_ptr(table->turbofish_args, args_id); + zend_try_attach_concrete_call_table_for(caller, fbc, args_box, args_id); + + *out_args_id = args_id; + return ZEND_INSTALL_GENERIC_ARGS; +} + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity) +{ + if (!caller->generic_types || !caller->generic_types->turbofish_args) { + return ZEND_NOP; + } + const zend_turbofish_args_entry *entry = + zend_hash_index_find_ptr(caller->generic_types->turbofish_args, args_id); + if (!entry) { + return ZEND_NOP; + } + if (!zend_can_install_call_args_statically(fbc, &entry->args_box, arity)) { + return ZEND_NOP; + } + zend_try_attach_concrete_call_table_for(caller, fbc, &entry->args_box, args_id); + return ZEND_INSTALL_GENERIC_ARGS; +} + +static void zend_emit_verify_generic_arguments(zend_ast *turbofish_ast, uint8_t kind, const znode *new_result, const zend_function *fbc, zend_ast *call_args_ast) +{ + uint32_t arity = 0; + uint32_t args_id = 0; + const zend_type *args_box = NULL; + + if (turbofish_ast) { + arity = zend_ast_get_list(turbofish_ast)->children; + ZEND_ASSERT(arity > 0 && arity <= ZEND_GENERIC_MAX_PARAMS); + + zend_type args_type = zend_compile_turbofish_args_type(turbofish_ast); + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + args_id = (table->turbofish_args ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, args_type); + args_box = zend_hash_index_find_ptr(table->turbofish_args, args_id); + } else { + /* No turbofish: only call sites get a defaults/inference-driven VERIFY + * (NEW already emitted it above the args). Skip when fbc is known and + * statically non-generic; emit speculatively otherwise — the runtime + * handler short-circuits when the resolved callee is non-generic. */ + if (kind != ZEND_VERIFY_ARITY_KIND_CALL) { + return; + } + if (fbc && (!ZEND_USER_CODE(fbc->common.type) + || !fbc->op_array.generic_parameters)) { + return; + } + if (fbc) { + const zend_type *inferred = + zend_infer_call_type_args(fbc, call_args_ast, &arity, &args_id); + if (inferred) { + args_box = inferred; + } + } + } + + /* Replace the runtime VERIFY with INSTALL when arity+bound check is + * decidable (and passes) at compile time. Only CALL is handled for + * now — NEW's compile-time ce resolution path is more constrained. */ + uint8_t opcode = ZEND_VERIFY_GENERIC_ARGUMENTS; + 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_for(CG(active_op_array), fbc, args_box, args_id); + } else if (kind == ZEND_VERIFY_ARITY_KIND_CALL && args_id) { + /* Concrete args but VERIFY stays (bounds not statically decidable); + * still attach the invariant table for the handler to install. */ + zend_try_attach_concrete_call_table_for(CG(active_op_array), fbc, args_box, args_id); + } + + zend_op *opline = get_next_op(); + opline->opcode = opcode; + opline->op2_type = IS_UNUSED; + opline->op2.num = arity; + opline->extended_value = args_id; + if (kind == ZEND_VERIFY_ARITY_KIND_NEW) { + ZEND_ASSERT(new_result != NULL && new_result->op_type == IS_TMP_VAR); + opline->op1_type = new_result->op_type; + opline->op1.var = new_result->u.op.var; + } else { + opline->op1_type = IS_UNUSED; + opline->op1.num = 0; + } + opline->result_type = IS_UNUSED; + /* 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( + zend_ast *list_ast, zend_generic_origin origin) /* {{{ */ +{ + if (!list_ast) { + return NULL; + } + zend_ast_list *list = zend_ast_get_list(list_ast); + ZEND_ASSERT(list->children > 0); + + zend_generic_parameter_list *params = + zend_generic_parameter_list_alloc(list->children, /* persistent */ false); + + zend_string *prev_optional_name = NULL; + + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + ZEND_ASSERT(param_ast->kind == ZEND_AST_GENERIC_TYPE_PARAMETER); + zend_string *name = zval_make_interned_string(zend_ast_get_zval(param_ast->child[0])); + bool has_default = param_ast->child[2] != NULL; + + for (uint32_t j = 0; j < i; j++) { + if (zend_string_equals(params->parameters[j].name, name)) { + zend_string *dup = zend_string_copy(name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot redeclare type parameter %s", ZSTR_VAL(dup)); + } + } + + if (zend_generic_lookup(name)) { + zend_string *dup = zend_string_copy(name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s shadows enclosing type parameter", ZSTR_VAL(dup)); + } + + if (!has_default && prev_optional_name != NULL) { + zend_string *cur = zend_string_copy(name); + zend_string *prev = zend_string_copy(prev_optional_name); + zend_generic_parameter_list_destroy(params); + zend_error_noreturn(E_COMPILE_ERROR, + "Optional type parameter %s cannot be declared before required type parameter %s", + ZSTR_VAL(prev), ZSTR_VAL(cur)); + } + + if (has_default && prev_optional_name == NULL) { + prev_optional_name = name; + } + + params->parameters[i].name = zend_string_copy(name); + params->parameters[i].variance = (zend_generic_variance) param_ast->attr; + } + + params->count = list->children; + + zend_generic_scope_push(params, origin); + + CG(generic_scope)->visible_count = list->children; + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + if (!param_ast->child[1]) { + continue; + } + + CG(generic_scope)->self_compiling = ¶ms->parameters[i]; + params->parameters[i].bound = zend_compile_typename(param_ast->child[1]); + if (zend_type_ast_has_generic_content(param_ast->child[1])) { + params->parameters[i].bound_pre_erasure = zend_compile_pre_erasure_typename(param_ast->child[1]); + } + + CG(generic_scope)->self_compiling = NULL; + } + + CG(generic_scope)->visible_count = 0; + for (uint32_t i = 0; i < list->children; i++) { + zend_ast *param_ast = list->child[i]; + CG(generic_scope)->visible_count = i + 1; + if (!param_ast->child[2]) { + continue; + } + + CG(generic_scope)->self_compiling = ¶ms->parameters[i]; + params->parameters[i].default_type = zend_compile_typename(param_ast->child[2]); + if (zend_type_ast_has_generic_content(param_ast->child[2])) { + params->parameters[i].default_pre_erasure = zend_compile_pre_erasure_typename(param_ast->child[2]); + } + + if (ZEND_TYPE_IS_SET(params->parameters[i].bound) + && zend_check_generic_arg_satisfies_bound( + NULL, params->parameters[i].default_type, + NULL, params->parameters[i].bound) + == INHERITANCE_ERROR) { + zend_type bound_display = ZEND_TYPE_IS_SET(params->parameters[i].bound_pre_erasure) + ? params->parameters[i].bound_pre_erasure + : params->parameters[i].bound; + zend_type default_display = ZEND_TYPE_IS_SET(params->parameters[i].default_pre_erasure) + ? params->parameters[i].default_pre_erasure + : params->parameters[i].default_type; + zend_string *bound_str = zend_type_to_string(bound_display); + zend_string *default_str = zend_type_to_string(default_display); + zend_error_noreturn(E_COMPILE_ERROR, + "Default %s for type parameter %s does not satisfy its bound %s", + ZSTR_VAL(default_str), ZSTR_VAL(params->parameters[i].name), ZSTR_VAL(bound_str)); } + + CG(generic_scope)->self_compiling = NULL; } - return 0; + zend_generic_scope_pop(); + + return params; } /* }}} */ -static zend_always_inline bool zend_is_confusable_type(const zend_string *name, const char **correct_name) /* {{{ */ +/* Returns true if the AST contains any generic-aware constructs (a type-parameter + * reference or a named type with type arguments) that need pre-erasure capture. */ +static bool zend_type_ast_has_generic_content(zend_ast *ast) { - const confusable_type_info *info = confusable_types; + if (!ast) { + return false; + } + zend_ast_attr orig = ast->attr; + ast->attr &= ~ZEND_TYPE_NULLABLE; + bool result = false; - /* Intentionally using case-sensitive comparison here, because "integer" is likely intended - * as a scalar type, while "Integer" is likely a class type. */ - for (; info->name; ++info) { - if (zend_string_equals_cstr(name, info->name, info->name_len)) { - *correct_name = info->correct_name; - return 1; + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + result = true; + } else if (ast->kind == ZEND_AST_TYPE_UNION || ast->kind == ZEND_AST_TYPE_INTERSECTION) { + zend_ast_list *list = zend_ast_get_list(ast); + for (uint32_t i = 0; i < list->children; i++) { + if (zend_type_ast_has_generic_content(list->child[i])) { + result = true; + break; + } + } + } else if (ast->kind == ZEND_AST_ZVAL) { + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING && zend_generic_lookup(Z_STR_P(zv))) { + result = true; + } } } - return 0; -} -/* }}} */ - -static bool zend_is_not_imported(zend_string *name) { - /* Assuming "name" is unqualified here. */ - return !FC(imports) || zend_hash_find_ptr_lc(FC(imports), name) == NULL; + ast->attr = orig; + return result; } -void zend_oparray_context_begin(zend_oparray_context *prev_context, zend_op_array *op_array) /* {{{ */ +static void zend_reject_typearg_on_iterable(zend_ast *ast) { - *prev_context = CG(context); - CG(context).prev = CG(context).op_array ? prev_context : NULL; - CG(context).op_array = op_array; - CG(context).opcodes_size = INITIAL_OP_ARRAY_SIZE; - CG(context).vars_size = 0; - CG(context).literals_size = 0; - CG(context).fast_call_var = -1; - CG(context).try_catch_offset = -1; - CG(context).current_brk_cont = -1; - CG(context).last_brk_cont = 0; - CG(context).has_assigned_to_http_response_header = false; - CG(context).brk_cont_array = NULL; - CG(context).labels = NULL; - CG(context).in_jmp_frameless_branch = false; - CG(context).active_property_info_name = NULL; - CG(context).active_property_hook_kind = (zend_property_hook_kind)-1; -} -/* }}} */ + if (ast->kind != ZEND_AST_GENERIC_NAMED_TYPE) { + return; + } -void zend_oparray_context_end(const zend_oparray_context *prev_context) /* {{{ */ -{ - if (CG(context).brk_cont_array) { - efree(CG(context).brk_cont_array); - CG(context).brk_cont_array = NULL; + zend_ast *name_ast = ast->child[0]; + if (name_ast->kind != ZEND_AST_ZVAL) { + return; } - if (CG(context).labels) { - zend_hash_destroy(CG(context).labels); - FREE_HASHTABLE(CG(context).labels); - CG(context).labels = NULL; + + zval *zv = zend_ast_get_zval(name_ast); + if (Z_TYPE_P(zv) != IS_STRING) { + return; + } + + if ((name_ast->attr & ZEND_NAME_NOT_FQ) != ZEND_NAME_NOT_FQ) { + return; + } + + if (zend_string_equals_literal_ci(Z_STR_P(zv), "iterable")) { + zend_error_noreturn(E_COMPILE_ERROR, "Type arguments are not allowed on iterable"); } - CG(context) = *prev_context; } -/* }}} */ -static void zend_reset_import_tables(void) /* {{{ */ +/* When a named class type carries type arguments (Foo), the named class + * must actually be generic. If the class is already in CG(class_table) and + * has no generic_parameters, raise a hard compile-time error. Classes not yet + * seen by the compiler fall through; the runtime monomorph lookup raises the + * same error in that case. */ +static void zend_validate_class_accepts_type_args(zend_string *resolved_name) { - if (FC(imports)) { - zend_hash_destroy(FC(imports)); - efree(FC(imports)); - FC(imports) = NULL; + zend_string *lc = zend_string_tolower(resolved_name); + zend_class_entry *ce = zend_hash_find_ptr(CG(class_table), lc); + zend_string_release(lc); + if (ce && !ce->generic_parameters) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments are not allowed on non-generic class %s", + ZSTR_VAL(ce->name)); } +} - if (FC(imports_function)) { - zend_hash_destroy(FC(imports_function)); - efree(FC(imports_function)); - FC(imports_function) = NULL; +/* Build a pre-erasure zend_type from a type-expression AST. The returned type + * may carry type-parameter references (TYPE_PARAMETER bit) or named-with-args + * payloads (NAMED_WITH_ARGS bit). Used only for the side table; the runtime + * never sees this form. */ +static zend_type zend_compile_pre_erasure_typename(zend_ast *ast) +{ + zend_reject_typearg_on_iterable(ast); + bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; + zend_ast_attr orig_attr = ast->attr; + if (is_marked_nullable) { + ast->attr &= ~ZEND_TYPE_NULLABLE; } - if (FC(imports_const)) { - zend_hash_destroy(FC(imports_const)); - efree(FC(imports_const)); - FC(imports_const) = NULL; - } + zend_type result = ZEND_TYPE_INIT_NONE(0); - zend_hash_clean(&FC(seen_symbols)); -} -/* }}} */ + if (ast->kind == ZEND_AST_TYPE_UNION || ast->kind == ZEND_AST_TYPE_INTERSECTION) { + zend_ast_list *list = zend_ast_get_list(ast); + bool is_union = ast->kind == ZEND_AST_TYPE_UNION; + zend_type_list *type_list = emalloc(ZEND_TYPE_LIST_SIZE(list->children)); + uint32_t out_count = 0; + uint32_t merged_scalar_mask = 0; + for (uint32_t i = 0; i < list->children; i++) { + zend_type member = zend_compile_pre_erasure_typename(list->child[i]); + if (is_union) { + /* Mirror the erased form's normalization: aggregate scalar bits + * to the outer mask so covariance checks see them, and drop + * scalar-only members from the list. Complex members (T-refs, + * named-with-args, nested lists, plain class names) stay in + * the list with their scalar bits cleared. */ + bool is_complex = ZEND_TYPE_HAS_LIST(member) + || ZEND_TYPE_HAS_NAME(member) + || ZEND_TYPE_HAS_LITERAL_NAME(member) + || ZEND_TYPE_HAS_TYPE_PARAMETER(member) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(member); + merged_scalar_mask |= ZEND_TYPE_PURE_MASK(member); + if (is_complex) { + ZEND_TYPE_FULL_MASK(member) &= ~_ZEND_TYPE_MAY_BE_MASK; + type_list->types[out_count++] = member; + } + } else { + type_list->types[out_count++] = member; + } + } + if (out_count == 0) { + /* All members folded into the scalar mask. */ + efree(type_list); + ZEND_TYPE_FULL_MASK(result) |= merged_scalar_mask; + } else if (is_union && out_count == 1 + && (merged_scalar_mask & _ZEND_TYPE_MAY_BE_MASK & ~_ZEND_TYPE_NULLABLE_BIT) == 0) { + /* Degenerate union `T|null` (or `Box|null`): collapse to the + * single complex member carrying the nullable bit, the same shape + * `?T` compiles to and that zend_substitute_leaf_type_param produces + * for the substituted form. Keeping a one-element LIST here makes + * `T|null` diverge from `?T` in the inheritance variance check: the + * LIST escapes the bare-type-parameter fallback `?T` takes, so a + * child's `T|null` ends up compared against the parent's erased + * `mixed` and is wrongly rejected. */ + result = type_list->types[0]; + if (merged_scalar_mask & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + efree(type_list); + } else { + type_list->num_types = out_count; + ZEND_TYPE_SET_PTR(result, type_list); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | + (is_union ? _ZEND_TYPE_UNION_BIT : _ZEND_TYPE_INTERSECTION_BIT) | + merged_scalar_mask; + } + } else if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_ast *name_ast = ast->child[0]; + zend_ast_list *args_list = zend_ast_get_list(ast->child[1]); + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(args_list->children)); + if (name_ast->kind == ZEND_AST_TYPE) { + ZEND_ASSERT(name_ast->attr == IS_STATIC); + payload->name = zend_string_init_interned(ZEND_STRL("static"), 0); + payload->name_attr = 0; + } else { + zend_string *raw = zval_make_interned_string(zend_ast_get_zval(name_ast)); + if (zend_get_class_fetch_type(raw) != ZEND_FETCH_CLASS_DEFAULT) { + payload->name = zend_string_copy(raw); + } else { + bool is_type_param = ((name_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) + && zend_generic_lookup(raw) != NULL; + payload->name = zend_resolve_class_name(raw, name_ast->attr); + if (!is_type_param) { + zend_validate_class_accepts_type_args(payload->name); + } + } -static void zend_end_namespace(void) /* {{{ */ { - FC(in_namespace) = 0; - zend_reset_import_tables(); - if (FC(current_namespace)) { - zend_string_release_ex(FC(current_namespace), 0); - FC(current_namespace) = NULL; + payload->name_attr = name_ast->attr; + } + + payload->count = args_list->children; + for (uint32_t i = 0; i < args_list->children; i++) { + payload->args[i] = zend_compile_pre_erasure_typename(args_list->child[i]); + } + + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + } else if (ast->kind == ZEND_AST_TYPE) { + /* Builtin pseudo-type: same as erased. */ + result = (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0); + } else if (ast->kind == ZEND_AST_ZVAL) { + zend_generic_origin origin; + uint32_t index; + zend_generic_parameter *param = NULL; + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING) { + param = zend_generic_lookup_full(Z_STR_P(zv), &origin, &index); + } + } + if (param) { + if (origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_check_class_origin_in_static_context(param->name); + } + + zend_type_parameter_ref *ref = emalloc(sizeof(*ref)); + ref->name = zend_string_copy(param->name); + ref->index = index; + ref->origin = origin; + ZEND_TYPE_SET_PTR(result, ref); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_TYPE_PARAMETER_BIT; + } else { + if ((ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_generic_lookup_forward(Z_STR_P(zv)) >= 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s referenced before declaration", + Z_STRVAL_P(zv)); + } + } + zend_string *name = zval_make_interned_string(zend_ast_get_zval(ast)); + uint8_t code = zend_lookup_builtin_type_by_name(name); + if (code != 0) { + if (code == IS_ITERABLE) { + zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK( + ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + (MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT)); + result = iterable; + } else { + result = (zend_type) ZEND_TYPE_INIT_CODE(code, 0, 0); + } + } else { + /* Resolve against namespace/use, else `f::` in `namespace N` stays "C" not "N\C". */ + name = zend_resolve_class_name(name, ast->attr); + result = (zend_type) ZEND_TYPE_INIT_CLASS(name, 0, 0); + } + } + } else { + ZEND_UNREACHABLE(); + } + + if (is_marked_nullable) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; } + ast->attr = orig_attr; + return result; } -/* }}} */ -void zend_file_context_begin(zend_file_context *prev_context) /* {{{ */ +/* Ensure op_array->generic_types is allocated, then return it. */ +static zend_generic_type_table *zend_generic_get_or_create_op_array_table(zend_op_array *op_array) { - *prev_context = CG(file_context); - FC(imports) = NULL; - FC(imports_function) = NULL; - FC(imports_const) = NULL; - FC(current_namespace) = NULL; - FC(in_namespace) = 0; - FC(has_bracketed_namespaces) = 0; - FC(declarables).ticks = 0; - zend_hash_init(&FC(seen_symbols), 8, NULL, NULL, 0); + if (!op_array->generic_types) { + op_array->generic_types = zend_generic_type_table_alloc(); + } + return op_array->generic_types; } -/* }}} */ -void zend_file_context_end(const zend_file_context *prev_context) /* {{{ */ +/* Ensure ce->generic_types is allocated, then return it. */ +ZEND_API zend_generic_type_table *zend_generic_get_or_create_class_table(zend_class_entry *ce) { - zend_end_namespace(); - zend_hash_destroy(&FC(seen_symbols)); - CG(file_context) = *prev_context; + if (!ce->generic_types) { + ce->generic_types = zend_generic_type_table_alloc(); + } + return ce->generic_types; } -/* }}} */ -void zend_init_compiler_data_structures(void) /* {{{ */ +static zend_generic_parameter *zend_generic_lookup_name(zend_ast *ast) /* {{{ */ { - zend_stack_init(&CG(loop_var_stack), sizeof(zend_loop_var)); - zend_stack_init(&CG(delayed_oplines_stack), sizeof(zend_op)); - zend_stack_init(&CG(short_circuiting_opnums), sizeof(uint32_t)); - CG(active_class_entry) = NULL; - CG(in_compilation) = 0; - CG(skip_shebang) = 0; - - CG(encoding_declared) = 0; - CG(memoized_exprs) = NULL; - CG(memoize_mode) = ZEND_MEMOIZE_NONE; + if (!CG(generic_scope) || ast->kind != ZEND_AST_ZVAL) { + return NULL; + } + if ((ast->attr & ZEND_NAME_NOT_FQ) != ZEND_NAME_NOT_FQ) { + return NULL; + } + zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) != IS_STRING) { + return NULL; + } + return zend_generic_lookup(Z_STR_P(zv)); } /* }}} */ @@ -1223,10 +3220,15 @@ static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type) /* static zend_string *zend_resolve_class_name_ast(zend_ast *ast) /* {{{ */ { + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + ast = ast->child[0]; + } + const zval *class_name = zend_ast_get_zval(ast); if (Z_TYPE_P(class_name) != IS_STRING) { zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } + return zend_resolve_class_name(Z_STR_P(class_name), ast->attr); } /* }}} */ @@ -1415,6 +3417,19 @@ static zend_string *resolve_class_name(zend_string *name, const zend_class_entry return zend_string_copy(name); } +/* Render a single union / intersection list member. The list iteration used + * to assert NAME on every member, but reified types put scalar codes, + * TYPE_PARAMETER refs, and NAMED_WITH_ARGS payloads in list slots too — + * defer to the main renderer which already handles each case. The outer + * union absorbs nullability with a single trailing `|null`, so strip + * MAY_BE_NULL from the member's own mask to avoid emitting `?Member`. */ +static zend_string *render_list_member(const zend_type *member, const zend_class_entry *scope) +{ + zend_type clean = *member; + ZEND_TYPE_FULL_MASK(clean) &= ~MAY_BE_NULL; + return zend_type_to_string_resolved(clean, scope); +} + static zend_string *add_intersection_type(zend_string *str, const zend_type_list *intersection_type_list, bool is_bracketed) @@ -1424,9 +3439,11 @@ static zend_string *add_intersection_type(zend_string *str, ZEND_TYPE_LIST_FOREACH(intersection_type_list, single_type) { ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type)); - ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*single_type)); - - intersection_str = add_type_string(intersection_str, ZEND_TYPE_NAME(*single_type), /* is_intersection */ true); + zend_string *piece = render_list_member(single_type, NULL); + if (piece) { + intersection_str = add_type_string(intersection_str, piece, /* is_intersection */ true); + zend_string_release(piece); + } } ZEND_TYPE_LIST_FOREACH_END(); ZEND_ASSERT(intersection_str); @@ -1457,13 +3474,39 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class continue; } ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); - ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*list_type)); - - zend_string *name = ZEND_TYPE_NAME(*list_type); - zend_string *resolved = resolve_class_name(name, scope); - str = add_type_string(str, resolved, /* is_intersection */ false); - zend_string_release(resolved); + zend_string *piece = render_list_member(list_type, scope); + if (piece) { + str = add_type_string(str, piece, /* is_intersection */ false); + zend_string_release(piece); + } } ZEND_TYPE_LIST_FOREACH_END(); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + smart_str buf = {0}; + if (nwa->name) { + zend_string *resolved = resolve_class_name(nwa->name, scope); + smart_str_append(&buf, resolved); + zend_string_release(resolved); + } + + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appends(&buf, ", "); + zend_string *piece = zend_type_to_string_resolved(nwa->args[i], scope); + if (piece) { + smart_str_append(&buf, piece); + zend_string_release(piece); + } + } + + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + str = buf.s; + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->name) { + str = zend_string_copy(ref->name); + } } else if (ZEND_TYPE_HAS_NAME(type)) { str = resolve_class_name(ZEND_TYPE_NAME(type), scope); } @@ -1504,36 +3547,279 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class if (type_mask & MAY_BE_DOUBLE) { str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_FLOAT), /* is_intersection */ false); } - if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_BOOL), /* is_intersection */ false); - } else if (type_mask & MAY_BE_FALSE) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_FALSE), /* is_intersection */ false); - } else if (type_mask & MAY_BE_TRUE) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_TRUE), /* is_intersection */ false); + if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) { + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_BOOL), /* is_intersection */ false); + } else if (type_mask & MAY_BE_FALSE) { + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_FALSE), /* is_intersection */ false); + } else if (type_mask & MAY_BE_TRUE) { + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_TRUE), /* is_intersection */ false); + } + if (type_mask & MAY_BE_VOID) { + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_VOID), /* is_intersection */ false); + } + if (type_mask & MAY_BE_NEVER) { + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_NEVER), /* is_intersection */ false); + } + + if (type_mask & MAY_BE_NULL) { + bool is_union = !str || memchr(ZSTR_VAL(str), '|', ZSTR_LEN(str)) != NULL; + bool has_intersection = !str || memchr(ZSTR_VAL(str), '&', ZSTR_LEN(str)) != NULL; + if (!is_union && !has_intersection) { + zend_string *nullable_str = zend_string_concat2("?", 1, ZSTR_VAL(str), ZSTR_LEN(str)); + zend_string_release(str); + return nullable_str; + } + + str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE), /* is_intersection */ false); + } + return str; +} + +ZEND_API zend_string *zend_type_to_string(zend_type type) { + return zend_type_to_string_resolved(type, NULL); +} + +/* === Monomorph canonicalization === + * + * Produces a deterministic class-name string for a generic application. + * Properties: + * - No whitespace anywhere. + * - Class names appear in fully-resolved form (the caller is expected to + * pass already-resolved zend_types; type-param refs are not allowed). + * - Members of union and intersection lists are sorted by canonical + * sub-string, so int|string and string|int collapse to "int|string". + * - `?T` is normalized to the union form `T|null`. + * - Intersection branches inside a union are wrapped in parens: "(A&B)|C". + * - Nested generic applications recurse: Box>. + * + * Two type-arg lists that PHP would treat as equivalent types must produce + * the same canonical string. */ + +static int zend_canonical_strptr_cmp(const void *a, const void *b) +{ + const zend_string *const *sa = a; + const zend_string *const *sb = b; + return zend_binary_strcmp( + ZSTR_VAL(*sa), ZSTR_LEN(*sa), + ZSTR_VAL(*sb), ZSTR_LEN(*sb)); +} + +static zend_string *zend_canonical_join_sorted( + zend_string **pieces, uint32_t count, char sep) +{ + qsort(pieces, count, sizeof(zend_string *), zend_canonical_strptr_cmp); + smart_str buf = {0}; + for (uint32_t i = 0; i < count; i++) { + if (i > 0) smart_str_appendc(&buf, sep); + smart_str_append(&buf, pieces[i]); + } + smart_str_0(&buf); + return buf.s; +} + +static void zend_canonical_append_scalar_pieces( + uint32_t type_mask, zend_string ***pieces, uint32_t *count, uint32_t *cap) +{ + /* Fixed-order scalar emission so each scalar bit yields exactly one + * piece; the outer sort then merges these with class-name pieces. */ + struct { uint32_t bit; zend_string *str; } table[] = { + { MAY_BE_CALLABLE, ZSTR_KNOWN(ZEND_STR_CALLABLE) }, + { MAY_BE_OBJECT, ZSTR_KNOWN(ZEND_STR_OBJECT) }, + { MAY_BE_ARRAY, ZSTR_KNOWN(ZEND_STR_ARRAY) }, + { MAY_BE_STRING, ZSTR_KNOWN(ZEND_STR_STRING) }, + { MAY_BE_LONG, ZSTR_KNOWN(ZEND_STR_INT) }, + { MAY_BE_DOUBLE, ZSTR_KNOWN(ZEND_STR_FLOAT) }, + { MAY_BE_VOID, ZSTR_KNOWN(ZEND_STR_VOID) }, + { MAY_BE_NEVER, ZSTR_KNOWN(ZEND_STR_NEVER) }, + }; + for (size_t i = 0; i < sizeof(table)/sizeof(table[0]); i++) { + if (type_mask & table[i].bit) { + if (*count == *cap) { + *cap = *cap ? *cap * 2 : 4; + *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); + } + (*pieces)[(*count)++] = zend_string_copy(table[i].str); + } + } + if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_BOOL)); + } else { + if (type_mask & MAY_BE_FALSE) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_FALSE)); + } + if (type_mask & MAY_BE_TRUE) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_TRUE)); + } + } + if (type_mask & MAY_BE_NULL) { + if (*count == *cap) { *cap = *cap ? *cap * 2 : 4; *pieces = erealloc(*pieces, sizeof(zend_string *) * (*cap)); } + (*pieces)[(*count)++] = zend_string_copy(ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE)); + } +} + +static zend_string *zend_canonical_one(zend_type type); + +static zend_string *zend_canonical_intersection(const zend_type_list *list) +{ + zend_string **pieces = safe_emalloc(list->num_types, sizeof(zend_string *), 0); + uint32_t n = 0; + const zend_type *single; + ZEND_TYPE_LIST_FOREACH(list, single) { + /* Intersection branches are class-name leaves or NAMED_WITH_ARGS. */ + pieces[n++] = zend_canonical_one(*single); + } ZEND_TYPE_LIST_FOREACH_END(); + zend_string *result = zend_canonical_join_sorted(pieces, n, '&'); + for (uint32_t i = 0; i < n; i++) zend_string_release(pieces[i]); + efree(pieces); + return result; +} + +static zend_string *zend_canonical_one(zend_type type) +{ + uint32_t scalar_mask = ZEND_TYPE_PURE_MASK(type); + /* MAY_BE_ANY appearing alone is "mixed"; otherwise it's an unbounded + * bag of scalar bits we expand individually. */ + bool is_mixed_pure = scalar_mask == MAY_BE_ANY + && !ZEND_TYPE_HAS_LIST(type) + && !ZEND_TYPE_HAS_NAME(type) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(type); + if (is_mixed_pure) { + return zend_string_copy(ZSTR_KNOWN(ZEND_STR_MIXED)); + } + + zend_string **pieces = NULL; + uint32_t n = 0, cap = 0; + + if (ZEND_TYPE_IS_INTERSECTION(type)) { + zend_string *s = zend_canonical_intersection(ZEND_TYPE_LIST(type)); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = s; + } else if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *list_type; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) { + zend_string *s; + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { + zend_string *inner = zend_canonical_intersection(ZEND_TYPE_LIST(*list_type)); + s = zend_string_concat3("(", 1, ZSTR_VAL(inner), ZSTR_LEN(inner), ")", 1); + zend_string_release(inner); + } else { + s = zend_canonical_one(*list_type); + } + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = s; + } ZEND_TYPE_LIST_FOREACH_END(); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + smart_str buf = {0}; + if (nwa->name) smart_str_append(&buf, nwa->name); + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appendc(&buf, ','); + zend_string *inner = zend_canonical_one(nwa->args[i]); + smart_str_append(&buf, inner); + zend_string_release(inner); + } + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = buf.s; + } else if (ZEND_TYPE_HAS_NAME(type)) { + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = zend_string_copy(ZEND_TYPE_NAME(type)); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + /* Canonicalization runs against already-substituted types; a stray + * T-ref means somebody handed us pre-erasure data by mistake. */ + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (n == cap) { cap = cap ? cap * 2 : 4; pieces = erealloc(pieces, sizeof(zend_string *) * cap); } + pieces[n++] = ref->name ? zend_string_copy(ref->name) : ZSTR_EMPTY_ALLOC(); + } + + zend_canonical_append_scalar_pieces(scalar_mask, &pieces, &n, &cap); + + zend_string *result; + if (n == 0) { + result = ZSTR_EMPTY_ALLOC(); + } else if (n == 1) { + result = zend_string_copy(pieces[0]); + } else { + result = zend_canonical_join_sorted(pieces, n, '|'); } - if (type_mask & MAY_BE_VOID) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_VOID), /* is_intersection */ false); + for (uint32_t i = 0; i < n; i++) zend_string_release(pieces[i]); + if (pieces) efree(pieces); + return result; +} + +ZEND_API zend_string *zend_type_to_canonical_string(zend_type type) +{ + /* Intern the result so equivalent type-arg lists share storage and the + * downstream class-table lookup (zend_fetch_class_by_name) hashes the + * same pointer each time. Safe at runtime — zend_new_interned_string + * falls back to a heap string if interning isn't available. */ + return zend_new_interned_string(zend_canonical_one(type)); +} + +ZEND_API bool zend_type_contains_type_parameter(zend_type type) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) return true; + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_type_parameter(nwa->args[i])) return true; + } } - if (type_mask & MAY_BE_NEVER) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_NEVER), /* is_intersection */ false); + if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), member) { + if (zend_type_contains_type_parameter(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); } + return false; +} - if (type_mask & MAY_BE_NULL) { - bool is_union = !str || memchr(ZSTR_VAL(str), '|', ZSTR_LEN(str)) != NULL; - bool has_intersection = !str || memchr(ZSTR_VAL(str), '&', ZSTR_LEN(str)) != NULL; - if (!is_union && !has_intersection) { - zend_string *nullable_str = zend_string_concat2("?", 1, ZSTR_VAL(str), ZSTR_LEN(str)); - zend_string_release(str); - return nullable_str; +/* Like zend_type_contains_type_parameter, but only counts class-scope refs. + * Function-scope T-refs erase to their bound and aren't bound by class + * inheritance, so callers walking inheritance can ignore them. */ +ZEND_API bool zend_type_contains_class_scope_type_parameter(zend_type type) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + return ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE; + } + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(type); + for (uint32_t i = 0; i < nwa->count; i++) { + if (zend_type_contains_class_scope_type_parameter(nwa->args[i])) return true; } - - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE), /* is_intersection */ false); } - return str; + if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), member) { + if (zend_type_contains_class_scope_type_parameter(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); + } + return false; } -ZEND_API zend_string *zend_type_to_string(zend_type type) { - return zend_type_to_string_resolved(type, NULL); +ZEND_API zend_string *zend_generic_canonical_class_name( + zend_string *base_name, const zend_type *args, uint32_t arity) +{ + smart_str buf = {0}; + smart_str_append(&buf, base_name); + smart_str_appendc(&buf, '<'); + for (uint32_t i = 0; i < arity; i++) { + if (i > 0) smart_str_appendc(&buf, ','); + zend_string *piece = zend_canonical_one(args[i]); + smart_str_append(&buf, piece); + zend_string_release(piece); + } + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + /* Intern so repeat lookups of the same monomorph name share storage and + * EG(class_table) can dispatch on pointer identity. */ + return zend_new_interned_string(buf.s); } static bool is_generator_compatible_class_type(const zend_string *name) { @@ -1771,6 +4057,9 @@ static uint32_t zend_get_class_fetch_type_ast(zend_ast *name_ast) /* {{{ */ static zend_string *zend_resolve_const_class_name_reference(zend_ast *ast, const char *type) { + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + ast = ast->child[0]; + } zend_string *class_name = zend_ast_get_str(ast); if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type_ast(ast)) { zend_error_noreturn(E_COMPILE_ERROR, @@ -1811,6 +4100,13 @@ static bool zend_try_compile_const_expr_resolve_class_name(zval *zv, zend_ast *c zend_error_noreturn(E_COMPILE_ERROR, "Illegal class name"); } + /* T::class resolves at runtime through the runtime T-table (frame for + * function-level T, called scope for class-level T). */ + if ((class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ + && zend_generic_lookup(Z_STR_P(class_name))) { + return false; + } + fetch_type = zend_get_class_fetch_type(Z_STR_P(class_name)); zend_ensure_valid_class_fetch_type(fetch_type); @@ -2048,6 +4344,12 @@ int ZEND_FASTCALL zendlex(zend_parser_stack_elem *elem) /* {{{ */ zval zv; int ret; + if (CG(type_arg_residual_token)) { + ret = CG(type_arg_residual_token); + CG(type_arg_residual_token) = 0; + return ret; + } + if (CG(increment_lineno)) { CG(zend_lineno)++; CG(increment_lineno) = 0; @@ -2055,8 +4357,22 @@ int ZEND_FASTCALL zendlex(zend_parser_stack_elem *elem) /* {{{ */ ret = lex_scan(&zv, elem); ZEND_ASSERT(!EG(exception) || ret == T_ERROR); - return ret; + if (CG(type_arg_depth) > 0) { + switch (ret) { + case T_SR: + CG(type_arg_residual_token) = '>'; + return '>'; + case T_IS_GREATER_OR_EQUAL: + CG(type_arg_residual_token) = '='; + return '>'; + case T_SR_EQUAL: + CG(type_arg_residual_token) = T_IS_GREATER_OR_EQUAL; + return '>'; + } + } + + return ret; } /* }}} */ @@ -2090,6 +4406,9 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->attributes = NULL; ce->enum_backing_type = IS_UNDEF; ce->backed_enum_table = NULL; + ce->generic_parameters = NULL; + ce->generic_types = NULL; + ce->generic_type_args = NULL; if (nullify_handlers) { ce->constructor = NULL; @@ -2687,12 +5006,32 @@ static void zend_emit_return_type_check( } } + /* Generic functions: if the return references a type parameter, the + * runtime opcode is the only place the reified binding gets checked. + * Detect that here so the two elision paths below don't drop the opcode. + * Besides a bare `T`, a leaf union/intersection (`T|Other`, `A|B`) also + * reifies (see zend_monomorph_build_arg_info); a `Box`-style + * NAMED_WITH_ARGS composite stays erased, so it needs no opcode. */ + zend_op_array *active = CG(active_op_array); + const zend_type *pre_return = + active->generic_types ? active->generic_types->return_type : NULL; + bool return_is_generic = pre_return + && (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_return) + || zend_type_is_reifiable_leaf_composite(*pre_return)); + if (expr && ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY) { - /* we don't need run-time check for mixed return type */ - return; + /* Mixed normally needs no run-time check, but if the return is a + * generic parameter that erased to mixed, a child substituting T to + * a concrete type relies on this opcode being present in the shared + * body so its substituted arg_info gets checked. */ + if (!return_is_generic) { + return; + } } - if (expr && expr->op_type == IS_CONST && ZEND_TYPE_CONTAINS_CODE(type, Z_TYPE(expr->u.constant))) { + if (!return_is_generic + && expr && expr->op_type == IS_CONST + && ZEND_TYPE_CONTAINS_CODE(type, Z_TYPE(expr->u.constant))) { /* we don't need run-time check */ return; } @@ -2800,6 +5139,9 @@ static inline bool zend_can_write_to_variable(const zend_ast *ast) /* {{{ */ static inline bool zend_is_const_default_class_ref(zend_ast *name_ast) /* {{{ */ { + if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + name_ast = name_ast->child[0]; + } if (name_ast->kind != ZEND_AST_ZVAL) { return false; } @@ -2852,10 +5194,62 @@ static inline void zend_set_class_name_op1(zend_op *opline, znode *class_node) / } /* }}} */ +/* At a compile-time canonical-name production site, try to register the + * monomorph in the class table now so opcache picks it up via the regular + * CG(class_table) → script.class_table persistence path. If the base isn't + * compiled yet (forward reference, autoloaded class, etc.) or synthesis + * fails for any other reason, the runtime lookup hook synthesises lazily + * on first reference. + * + * Exceptions raised by synthesis (e.g. a bound violation in the monomorph + * being created) are suppressed here — the runtime path will rethrow them + * as a catchable TypeError exactly where it would have without this + * optimisation. */ +static void zend_try_compile_time_synthesize_monomorph(zend_string *canonical) +{ + zend_object *saved_exception = EG(exception); + EG(exception) = NULL; + + (void) zend_try_synthesize_monomorph_by_name(canonical, ZEND_FETCH_CLASS_NO_AUTOLOAD); + + if (EG(exception)) { + OBJ_RELEASE(EG(exception)); + EG(exception) = NULL; + } + EG(exception) = saved_exception; +} + static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t fetch_flags) /* {{{ */ { uint32_t fetch_type; + /* Generic named type Box: when the args are concrete (no T-refs) + * we resolve to the monomorph's canonical name "Box" so callers + * like `instanceof Box` and `catch (Box $e)` distinguish + * monos. When the args contain T-refs (e.g. `instanceof Box`) we stash + * the pre-erasure type in the op_array's side table and emit a deferred + * fetch; the runtime substitutes T against the current frame's bindings + * and looks up (or synthesises) the resulting monomorph. */ + if (name_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type ty = zend_compile_pre_erasure_typename(name_ast); + if (!zend_type_contains_type_parameter(ty)) { + zend_string *canonical = zend_type_to_canonical_string(ty); + zend_try_compile_time_synthesize_monomorph(canonical); + result->op_type = IS_CONST; + ZVAL_STR(&result->u.constant, canonical); + zend_type_release(ty, /* persistent */ false); + return; + } + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args + ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, ty); + result->op_type = IS_UNUSED; + result->u.op.num = zend_pack_generic_deferred_fetch(args_id, fetch_flags); + return; + } + if (name_ast->kind != ZEND_AST_ZVAL) { znode name_node; @@ -2895,6 +5289,22 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f return; } + /* T in expression position (new T(), T::FOO, instanceof T, T::method()) + * resolves through the runtime T-table — function/method-level T from the + * call frame, class-level T from the called scope's monomorph args. */ + if ((name_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(name_ast), &origin, ¶m_index); + if (param) { + result->op_type = IS_UNUSED; + result->u.op.num = zend_pack_type_param_fetch(param_index, fetch_flags, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); + return; + } + } + fetch_type = zend_get_class_fetch_type(zend_ast_get_str(name_ast)); if (ZEND_FETCH_CLASS_DEFAULT == fetch_type) { result->op_type = IS_CONST; @@ -4004,11 +6414,18 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, const zend_function *f } /* }}} */ -static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const zend_function *fbc, uint32_t lineno, uint32_t type) /* {{{ */ +static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const zend_function *fbc, uint32_t lineno, uint32_t type, zend_ast *turbofish_ast, uint8_t verify_kind, const znode *new_result) /* {{{ */ { zend_op *opline; uint32_t opnum_init = get_next_op_number() - 1; + /* NEW emits VERIFY before args so the constructor RECVs see the + * monomorph's substituted arg_info; CALL emits it after args so inference + * from arg values can fill type-parameter slots. */ + if (verify_kind == ZEND_VERIFY_ARITY_KIND_NEW) { + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, NULL); + } + if (args_ast->kind == ZEND_AST_CALLABLE_CONVERT) { opline = &CG(active_op_array)->opcodes[opnum_init]; opline->extended_value = 0; @@ -4042,6 +6459,10 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, const ze bool may_have_extra_named_args; uint32_t arg_count = zend_compile_args(args_ast, fbc, &may_have_extra_named_args); + if (verify_kind == ZEND_VERIFY_ARITY_KIND_CALL) { + zend_emit_verify_generic_arguments(turbofish_ast, verify_kind, new_result, fbc, args_ast); + } + zend_do_extended_fcall_begin(); opline = &CG(active_op_array)->opcodes[opnum_init]; @@ -4090,7 +6511,7 @@ static bool zend_compile_function_name(znode *name_node, zend_ast *name_ast) /* } /* }}} */ -static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast *args_ast, uint32_t lineno, uint32_t type) /* {{{ */ +static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast *args_ast, zend_ast *turbofish_ast, uint32_t lineno, uint32_t type) /* {{{ */ { if (name_node->op_type == IS_CONST && Z_TYPE(name_node->u.constant) == IS_STRING) { const char *colon; @@ -4120,7 +6541,7 @@ static void zend_compile_dynamic_call(znode *result, znode *name_node, zend_ast zend_emit_op(NULL, ZEND_INIT_DYNAMIC_CALL, NULL, name_node); } - zend_compile_call_common(result, args_ast, NULL, lineno, type); + zend_compile_call_common(result, args_ast, NULL, lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } /* }}} */ @@ -4488,7 +6909,7 @@ static void zend_compile_assert(znode *result, zend_ast_list *args, zend_string args = (zend_ast_list *)zend_ast_list_add((zend_ast *) args, arg); } - zend_compile_call_common(result, (zend_ast*)args, fbc, lineno, type); + zend_compile_call_common(result, (zend_ast*)args, fbc, lineno, type, NULL, ZEND_VERIFY_ARITY_KIND_CALL, NULL); opline = &CG(active_op_array)->opcodes[check_op_number]; opline->op2.opline_num = get_next_op_number(); @@ -4814,8 +7235,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); } -static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast *args_ast, uint32_t lineno, uint32_t 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. */ @@ -4849,7 +7373,7 @@ static void zend_compile_ns_call(znode *result, const znode *name_node, zend_ast opline->op2_type = IS_CONST; opline->op2.constant = name_constants; opline->result.num = zend_alloc_cache_slot(); - zend_compile_call_common(result, args_ast, NULL, lineno, type); + zend_compile_call_common(result, args_ast, NULL, lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); /* Compile frameless call. */ if (frameless_function_info) { @@ -5141,7 +7665,7 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg * breaking for the generated call. */ if (callback->kind == ZEND_AST_CALL - && callback->child[0]->kind == ZEND_AST_ZVAL + && callback->child[0]->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(callback->child[0])) == IS_STRING && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) { return FAILURE; @@ -5195,10 +7719,10 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg znode call_result; switch (callback->kind) { case ZEND_AST_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args)); + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args, NULL)); break; case ZEND_AST_STATIC_CALL: - zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args)); + zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args, NULL)); break; } opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key); @@ -5400,7 +7924,7 @@ static bool zend_compile_parent_property_hook_call(znode *result, const zend_ast opline->op1.constant = zend_add_literal_string(&property_name); opline->op2.num = hook_kind; - zend_compile_call_common(result, args_ast, NULL, zend_ast_get_lineno(method_ast), BP_VAR_R); + zend_compile_call_common(result, args_ast, NULL, zend_ast_get_lineno(method_ast), BP_VAR_R, NULL, ZEND_VERIFY_ARITY_KIND_CALL, NULL); return true; } @@ -5409,24 +7933,26 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) { zend_ast *name_ast = ast->child[0]; zend_ast *args_ast = ast->child[1]; + zend_ast *turbofish_ast = ast->child[2]; bool is_callable_convert = args_ast->kind == ZEND_AST_CALLABLE_CONVERT; znode name_node; if (name_ast->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(name_ast)) != IS_STRING) { zend_compile_expr(&name_node, name_ast); - zend_compile_dynamic_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_dynamic_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); return; } { bool runtime_resolution = zend_compile_function_name(&name_node, name_ast); if (runtime_resolution) { - if (zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert") + if (turbofish_ast == NULL + && zend_string_equals_literal_ci(zend_ast_get_str(name_ast), "assert") && !is_callable_convert) { zend_compile_assert(result, zend_ast_get_list(args_ast), Z_STR(name_node.u.constant), NULL, ast->lineno, type); } else { - zend_compile_ns_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_ns_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); } return; } @@ -5440,7 +7966,8 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) zend_op *opline; /* Special assert() handling should apply independently of compiler flags. */ - if (fbc && zend_string_equals_literal(lcname, "assert") && !is_callable_convert) { + if (turbofish_ast == NULL + && fbc && zend_string_equals_literal(lcname, "assert") && !is_callable_convert) { zend_compile_assert(result, zend_ast_get_list(args_ast), lcname, fbc, ast->lineno, type); zend_string_release(lcname); zval_ptr_dtor(&name_node.u.constant); @@ -5451,13 +7978,36 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) || !fbc_is_finalized(fbc) || zend_compile_ignore_function(fbc, CG(active_op_array)->filename)) { zend_string_release_ex(lcname, 0); - zend_compile_dynamic_call(result, &name_node, args_ast, ast->lineno, type); + zend_compile_dynamic_call(result, &name_node, args_ast, turbofish_ast, ast->lineno, type); return; } - if (!is_callable_convert && - zend_try_compile_special_func(result, lcname, - zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS + /* 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, + zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS ) { zend_string_release_ex(lcname, 0); zval_ptr_dtor(&name_node.u.constant); @@ -5476,7 +8026,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type) Z_EXTRA_P(CT_CONSTANT(opline->op2)) = fbc_bucket - CG(function_table)->arData; } - zend_compile_call_common(result, args_ast, fbc, ast->lineno, type); + zend_compile_call_common(result, args_ast, fbc, ast->lineno, type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } } /* }}} */ @@ -5486,6 +8036,7 @@ static void zend_compile_method_call(znode *result, zend_ast *ast, uint32_t type zend_ast *obj_ast = ast->child[0]; zend_ast *method_ast = ast->child[1]; zend_ast *args_ast = ast->child[2]; + zend_ast *turbofish_ast = ast->child[3]; znode obj_node, method_node; zend_op *opline; @@ -5540,7 +8091,7 @@ static void zend_compile_method_call(znode *result, zend_ast *ast, uint32_t type } } - if (zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type)) { + if (zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL)) { if (short_circuiting_checkpoint != zend_short_circuiting_checkpoint()) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot combine nullsafe operator with Closure creation"); @@ -5588,6 +8139,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type zend_ast *class_ast = ast->child[0]; zend_ast *method_ast = ast->child[1]; zend_ast *args_ast = ast->child[2]; + zend_ast *turbofish_ast = ast->child[3]; znode class_node, method_node; zend_op *opline; @@ -5655,7 +8207,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type } } - zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type); + zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast), type, turbofish_ast, ZEND_VERIFY_ARITY_KIND_CALL, NULL); } /* }}} */ @@ -5665,6 +8217,7 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ { zend_ast *class_ast = ast->child[0]; zend_ast *args_ast = ast->child[1]; + zend_ast *turbofish_ast = ast->child[2]; znode class_node, ctor_result; zend_op *opline; @@ -5685,6 +8238,10 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ } zend_class_entry *ce = NULL; + /* If parent_extends_args is non-NULL, we resolved `new parent()` where the + * active class's extends clause specified type arguments — in that case we + * use the user-supplied args directly rather than the parent's defaults. */ + const zend_type *parent_extends_args = NULL; if (opline->op1_type == IS_CONST) { zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op1) + 1); ce = zend_hash_find_ptr(CG(class_table), lcname); @@ -5697,21 +8254,186 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ ce = CG(active_class_entry); } } else if (opline->op1_type == IS_UNUSED - && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF && zend_is_scope_known()) { - ce = CG(active_class_entry); + uint32_t fetch = opline->op1.num & ZEND_FETCH_CLASS_MASK; + if (fetch == ZEND_FETCH_CLASS_SELF) { + ce = CG(active_class_entry); + } else if (fetch == ZEND_FETCH_CLASS_PARENT + && CG(active_class_entry) + && CG(active_class_entry)->parent_name) { + zend_string *lc_parent = zend_string_tolower(CG(active_class_entry)->parent_name); + ce = zend_hash_find_ptr(CG(class_table), lc_parent); + zend_string_release(lc_parent); + /* `new parent()` honours the active class's `extends Box` args + * if present, so it instantiates the same monomorph the active + * class extends rather than the parent's defaults. */ + if (ce && CG(active_class_entry)->generic_types + && CG(active_class_entry)->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS( + *CG(active_class_entry)->generic_types->extends) + && !zend_type_contains_type_parameter( + *CG(active_class_entry)->generic_types->extends)) { + parent_extends_args = CG(active_class_entry)->generic_types->extends; + } + } + } + + /* `new static::<...>` is always rejected at compile time. The type + * arguments are written in the lexical scope — they name the enclosing + * class's (`self`'s) type parameters — but `static` is the *runtime* type, + * whose parameter list is unknown here and need not match: it may be a + * generic subclass with a different parameter list, or a non-generic + * subclass of a monomorph (e.g. `class IntBox extends Box`) with no + * type parameters at all. `static` already carries its own bindings, so + * `new static()` reproduces the exact runtime type; `new self::<...>` + * re-applies the lexical template. */ + if (opline->op1_type == IS_UNUSED && turbofish_ast + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments cannot be applied to \"static\": " + "\"static\" is the runtime type and already carries its type arguments. " + "Use \"new static()\" to construct the exact runtime type, " + "or \"new self::<...>\" to apply type arguments to the enclosing class"); + } } + /* Reject turbofish on a known non-generic class at compile time so the + * error matches the type-position behavior (rather than waiting for + * VERIFY_GENERIC_ARGUMENTS to throw ArgumentCountError at runtime). */ + if (ce && !ce->generic_parameters && turbofish_ast) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type arguments are not allowed on non-generic class %s", + ZSTR_VAL(ce->name)); + } + } + + /* Compile-time monomorph synthesis for `new GenericClass(...)`. Builds the + * canonical name, synthesizes the monomorph, and rewrites op1 to the + * canonical so ZEND_NEW creates the monomorph directly. When this fires + * we also drop the runtime ZEND_VERIFY_GENERIC_ARGUMENTS opcode (the + * arity + bound check is now redundant) and swap fbc to the monomorph's + * constructor so RECV checks against the substituted arg_info. + * + * Three sources for the type args, in priority order: + * - concrete turbofish `new C::(...)` — synthesize when every arg + * is concrete (no T-refs) and bound-checks pass at compile time. + * - `new parent()` inside `class X extends Foo` — borrow the args + * from the extends clause. + * - declared parameter defaults (naked `new GenericClass()`). + * + * `static` and dynamic names are handled at runtime in ZEND_NEW. */ + bool ct_synthesized = false; + zend_class_entry *mono_ce = NULL; + if (ce && ce->generic_parameters) { + uint32_t count = ce->generic_parameters->count; + const zend_type *src_args = NULL; + uint32_t src_arity = 0; + zend_type tf_args_buf[ZEND_GENERIC_MAX_PARAMS]; + uint32_t tf_args_built_arity = 0; + bool tf_concrete = false; + + if (turbofish_ast) { + zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast); + if (tf_list->children > 0 && tf_list->children <= count) { + tf_concrete = true; + for (uint32_t i = 0; i < tf_list->children; i++) { + tf_args_buf[i] = zend_compile_pre_erasure_typename(tf_list->child[i]); + tf_args_built_arity = i + 1; + if (zend_type_contains_type_parameter(tf_args_buf[i])) { + tf_concrete = false; + } + } + if (tf_concrete) { + src_args = tf_args_buf; + src_arity = tf_list->children; + } + } + } else if (parent_extends_args) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*parent_extends_args); + src_args = nwa->args; + src_arity = nwa->count; + } + + bool attempt_synth = (src_args != NULL) || !turbofish_ast; + if (attempt_synth) { + zend_type chosen[ZEND_GENERIC_MAX_PARAMS]; + bool ok = true; + /* Strict (compile-error on missing defaults) when the naked call + * site refers to a generic class from outside that class. Lenient + * (fall through to the bare class / runtime VERIFY) when it's a + * lexical self-reference or when turbofish is present — the + * runtime can still throw a catchable ArgumentCountError. */ + bool is_lexical_self = ce == CG(active_class_entry); + for (uint32_t i = 0; i < count; i++) { + if (i < src_arity) { + chosen[i] = src_args[i]; + continue; + } + const zend_generic_parameter *p = &ce->generic_parameters->parameters[i]; + if (!ZEND_TYPE_IS_SET(p->default_type)) { + if (!is_lexical_self && !turbofish_ast) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot instantiate generic class %s without type arguments; " + "type parameter %s has no default", + ZSTR_VAL(ce->name), ZSTR_VAL(p->name)); + } + ok = false; + break; + } + chosen[i] = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? p->default_pre_erasure : p->default_type; + } + /* Concrete turbofish args must satisfy each parameter's bound at + * compile time before we elide the runtime check. UNRESOLVED (a + * referenced class not yet loaded) and ERROR both fall through to + * runtime VERIFY so the user gets a catchable TypeError. */ + if (ok && tf_concrete) { + for (uint32_t i = 0; i < src_arity; i++) { + zend_type bound = ce->generic_parameters->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) continue; + if (zend_check_generic_arg_satisfies_bound(NULL, src_args[i], NULL, bound) + != INHERITANCE_SUCCESS) { + ok = false; + break; + } + } + } + if (ok) { + zend_string *canonical = + zend_generic_canonical_class_name(ce->name, chosen, count); + zend_try_compile_time_synthesize_monomorph(canonical); + zend_string *lc = zend_string_tolower(canonical); + mono_ce = zend_hash_find_ptr(CG(class_table), lc); + zend_string_release(lc); + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(canonical); + opline->op2.num = zend_alloc_cache_slot(); + ct_synthesized = true; + } + } + + for (uint32_t i = 0; i < tf_args_built_arity; i++) { + zend_type_release(tf_args_buf[i], /* persistent */ false); + } + } const zend_function *fbc = NULL; - if (ce - && ce->default_object_handlers->get_constructor == zend_std_get_constructor - && ce->constructor - && is_func_accessible(ce->constructor)) { - fbc = ce->constructor; + zend_class_entry *ctor_ce = mono_ce ? mono_ce : ce; + if (ctor_ce + && ctor_ce->default_object_handlers->get_constructor == zend_std_get_constructor + && ctor_ce->constructor + && is_func_accessible(ctor_ce->constructor)) { + fbc = ctor_ce->constructor; } - zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R); + zend_compile_call_common(&ctor_result, args_ast, fbc, ast->lineno, BP_VAR_R, + ct_synthesized ? NULL : turbofish_ast, + ZEND_VERIFY_ARITY_KIND_NEW, result); zend_do_free(&ctor_result); } /* }}} */ @@ -6804,23 +9526,23 @@ static void zend_compile_pipe(znode *result, zend_ast *ast, uint32_t type) && callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT && zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) { fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast->child[0], arg_list_ast); + callable_ast->child[0], arg_list_ast, NULL); /* Turn $foo |> bar::baz(...) into bar::baz($foo). */ } else if (callable_ast->kind == ZEND_AST_STATIC_CALL && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { fcall_ast = zend_ast_create(ZEND_AST_STATIC_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); + callable_ast->child[0], callable_ast->child[1], arg_list_ast, NULL); /* Turn $foo |> $bar->baz(...) into $bar->baz($foo). */ } else if (callable_ast->kind == ZEND_AST_METHOD_CALL && callable_ast->child[2]->kind == ZEND_AST_CALLABLE_CONVERT) { fcall_ast = zend_ast_create(ZEND_AST_METHOD_CALL, - callable_ast->child[0], callable_ast->child[1], arg_list_ast); + callable_ast->child[0], callable_ast->child[1], arg_list_ast, NULL); /* Turn $foo |> $expr into ($expr)($foo) */ } else { zend_compile_expr(&callable_result, callable_ast); callable_ast = zend_ast_create_znode(&callable_result); fcall_ast = zend_ast_create(ZEND_AST_CALL, - callable_ast, arg_list_ast); + callable_ast, arg_list_ast, NULL); } zend_do_extended_stmt(&operand_result); @@ -7095,10 +9817,60 @@ static void zend_compile_try(const zend_ast *ast) /* {{{ */ opline = get_next_op(); opline->opcode = ZEND_CATCH; - opline->op1_type = IS_CONST; - opline->op1.constant = zend_add_class_name_literal( - zend_resolve_class_name_ast(class_ast)); - opline->extended_value = zend_alloc_cache_slot(); + + /* catch (T $e): resolve T at exception-throw time via the runtime + * T-table. Encoded the same way as ZEND_FETCH_CLASS — see + * zend_resolve_generic_type_param. Allocate 3 cache slots for the + * polymorphic inline cache (see zend_vm_def.h ZEND_CATCH handler). */ + bool emitted_type_param = false; + if (class_ast->kind == ZEND_AST_ZVAL + && (class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(class_ast), &origin, ¶m_index); + if (param) { + opline->op1_type = IS_UNUSED; + opline->op1.num = zend_pack_type_param_fetch(param_index, 0, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); + opline->extended_value = zend_alloc_cache_slots(3); + emitted_type_param = true; + } + } + if (!emitted_type_param) { + zend_string *resolved_name = NULL; + /* catch (Box): canonical monomorph name when args are + * concrete, matching the semantics of `instanceof Box`. + * catch (Box $e) with T-refs: stash the pre-erasure type and + * emit a deferred fetch; the runtime substitutes T against the + * current frame's bindings and resolves the monomorph. */ + if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type ty = zend_compile_pre_erasure_typename(class_ast); + if (!zend_type_contains_type_parameter(ty)) { + resolved_name = zend_type_to_canonical_string(ty); + zend_try_compile_time_synthesize_monomorph(resolved_name); + zend_type_release(ty, /* persistent */ false); + } else { + zend_generic_type_table *table = + zend_generic_get_or_create_op_array_table(CG(active_op_array)); + uint32_t args_id = (table->turbofish_args + ? zend_hash_num_elements(table->turbofish_args) : 0) + 1; + zend_generic_type_table_set_turbofish_args(table, args_id, ty); + opline->op1_type = IS_UNUSED; + opline->op1.num = zend_pack_generic_deferred_fetch(args_id, 0); + opline->extended_value = zend_alloc_cache_slots(3); + emitted_type_param = true; + } + } + if (!emitted_type_param) { + if (!resolved_name) { + resolved_name = zend_resolve_class_name_ast(class_ast); + } + opline->op1_type = IS_CONST; + opline->op1.constant = zend_add_class_name_literal(resolved_name); + opline->extended_value = zend_alloc_cache_slot(); + } + } if (var_name && zend_string_equals(var_name, ZSTR_KNOWN(ZEND_STR_THIS))) { zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this"); @@ -7361,9 +10133,112 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */ } /* }}} */ +static zend_type_list *zend_arena_deep_copy_type_list(zend_type_list *src) +{ + size_t list_size = ZEND_TYPE_LIST_SIZE(src->num_types); + zend_type_list *copy = zend_arena_alloc(&CG(arena), list_size); + memcpy(copy, src, list_size); + for (uint32_t i = 0; i < copy->num_types; i++) { + if (ZEND_TYPE_HAS_LIST(copy->types[i])) { + ZEND_TYPE_SET_PTR(copy->types[i], zend_arena_deep_copy_type_list(ZEND_TYPE_LIST(copy->types[i]))); + } else if (ZEND_TYPE_HAS_NAME(copy->types[i])) { + zend_string_addref(ZEND_TYPE_NAME(copy->types[i])); + } + } + + return copy; +} + static zend_type zend_compile_single_typename(zend_ast *ast) { ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); + zend_reject_typearg_on_iterable(ast); + if (ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + /* For concrete args (no T-refs), rewrite the named type as the + * canonical monomorph class — so the runtime arg_info-driven RECV + * check enforces `Box` identity, not just bare `Box`. T-ref + * args (`Box`) can't be resolved here and fall back to the + * erased base; the pre-erasure side table still carries the shape + * for reflection and inference. `self`/`parent`/`static` keep + * their lexical-scope meaning and also fall through. + * + * Builtin pseudo-types (`int`, `string`, ...) cannot carry type + * arguments — those are caught by the validation in + * zend_compile_pre_erasure_typename / the non-generic-class check. */ + zend_ast *name_ast = ast->child[0]; + bool is_relative_scope = false; + if (name_ast->kind == ZEND_AST_TYPE) { + is_relative_scope = true; + } else if (name_ast->kind == ZEND_AST_ZVAL) { + const zval *zv = zend_ast_get_zval(name_ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_get_class_fetch_type(Z_STR_P(zv)) != ZEND_FETCH_CLASS_DEFAULT) { + is_relative_scope = true; + } + } + if (!is_relative_scope) { + zend_type pre_erasure = zend_compile_pre_erasure_typename(ast); + if (!zend_type_contains_type_parameter(pre_erasure)) { + zend_string *canonical = zend_type_to_canonical_string(pre_erasure); + zend_try_compile_time_synthesize_monomorph(canonical); + zend_type_release(pre_erasure, /* persistent */ false); + return (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + } + zend_type_release(pre_erasure, /* persistent */ false); + } + ast = ast->child[0]; + } + /* Generic type parameter reference: erase to the parameter's bound (or `mixed` when unbounded). */ + { + zend_generic_origin origin = ZEND_GENERIC_ORIGIN_CLASS_LIKE; + zend_generic_parameter *param = NULL; + if (CG(generic_scope) && ast->kind == ZEND_AST_ZVAL + && (ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING) { + param = zend_generic_lookup_full(Z_STR_P(zv), &origin, NULL); + } + } + + if (param) { + if (origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_check_class_origin_in_static_context(param->name); + } + + for (zend_generic_scope_entry *e = CG(generic_scope); e; e = e->outer) { + if (e->self_compiling == param) { + zend_string *self = zend_string_copy(param->name); + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s cannot reference itself in its own bound or default outside of a generic type argument", + ZSTR_VAL(self)); + } + } + + if (ZEND_TYPE_IS_SET(param->bound)) { + zend_type result = param->bound; + if (ZEND_TYPE_HAS_NAME(result)) { + zend_string_addref(ZEND_TYPE_NAME(result)); + } else if (ZEND_TYPE_HAS_LIST(result)) { + ZEND_TYPE_SET_PTR(result, zend_arena_deep_copy_type_list(ZEND_TYPE_LIST(result))); + } + + return result; + } + + return (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + } + + if (ast->kind == ZEND_AST_ZVAL + && (ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + const zval *zv = zend_ast_get_zval(ast); + if (Z_TYPE_P(zv) == IS_STRING + && zend_generic_lookup_forward(Z_STR_P(zv)) >= 0) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s referenced before declaration", + Z_STRVAL_P(zv)); + } + } + } if (ast->kind == ZEND_AST_TYPE) { if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) { zend_error_noreturn(E_COMPILE_ERROR, @@ -7538,6 +10413,7 @@ static zend_type zend_compile_typename_ex( zend_ast *ast, bool force_allow_null, bool *forced_allow_null) /* {{{ */ { bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; + bool is_t_ref_erasure = false; zend_ast_attr orig_ast_attr = ast->attr; zend_type type = ZEND_TYPE_INIT_NONE(0); @@ -7550,6 +10426,7 @@ static zend_type zend_compile_typename_ex( zend_type_list *type_list; bool is_composite = false; bool has_only_iterable_class = true; + bool collapse_union_to_mixed = false; ALLOCA_FLAG(use_heap) type_list = do_alloca(ZEND_TYPE_LIST_SIZE(list->children), use_heap); @@ -7595,6 +10472,16 @@ static zend_type zend_compile_typename_ex( uint32_t single_type_mask = ZEND_TYPE_PURE_MASK(single_type); if (single_type_mask == MAY_BE_ANY) { + zend_ast *check_ast = type_ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + + if (zend_generic_lookup_name(check_ast) != NULL) { + collapse_union_to_mixed = true; + break; + } + zend_error_noreturn(E_COMPILE_ERROR, "Type mixed can only be used as a standalone type"); } if (ZEND_TYPE_IS_COMPLEX(single_type) && !ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { @@ -7641,25 +10528,30 @@ static zend_type zend_compile_typename_ex( } } - if (type_list->num_types) { - zend_type_list *list = zend_arena_alloc( - &CG(arena), ZEND_TYPE_LIST_SIZE(type_list->num_types)); - memcpy(list, type_list, ZEND_TYPE_LIST_SIZE(type_list->num_types)); - ZEND_TYPE_SET_LIST(type, list); - ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; - /* Inform that the type list is a union type */ - ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_UNION_BIT; - } + if (collapse_union_to_mixed) { + free_alloca(type_list, use_heap); + type = (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + } else { + if (type_list->num_types) { + zend_type_list *list = zend_arena_alloc( + &CG(arena), ZEND_TYPE_LIST_SIZE(type_list->num_types)); + memcpy(list, type_list, ZEND_TYPE_LIST_SIZE(type_list->num_types)); + ZEND_TYPE_SET_LIST(type, list); + ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; + /* Inform that the type list is a union type */ + ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_UNION_BIT; + } - free_alloca(type_list, use_heap); + free_alloca(type_list, use_heap); - uint32_t type_mask = ZEND_TYPE_FULL_MASK(type); - if ((type_mask & MAY_BE_OBJECT) && - ((!has_only_iterable_class && ZEND_TYPE_IS_COMPLEX(type)) || (type_mask & MAY_BE_STATIC))) { - zend_string *type_str = zend_type_to_string(type); - zend_error_noreturn(E_COMPILE_ERROR, - "Type %s contains both object and a class type, which is redundant", - ZSTR_VAL(type_str)); + uint32_t type_mask = ZEND_TYPE_FULL_MASK(type); + if ((type_mask & MAY_BE_OBJECT) && + ((!has_only_iterable_class && ZEND_TYPE_IS_COMPLEX(type)) || (type_mask & MAY_BE_STATIC))) { + zend_string *type_str = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, + "Type %s contains both object and a class type, which is redundant", + ZSTR_VAL(type_str)); + } } } else if (ast->kind == ZEND_AST_TYPE_INTERSECTION) { const zend_ast_list *list = zend_ast_get_list(ast); @@ -7674,18 +10566,37 @@ static zend_type zend_compile_typename_ex( for (uint32_t i = 0; i < list->children; i++) { zend_ast *type_ast = list->child[i]; + zend_ast *check_ast = type_ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + zend_generic_parameter *t_param = zend_generic_lookup_name(check_ast); zend_type single_type = zend_compile_single_typename(type_ast); /* An intersection of union types cannot exist so invalidate it * Currently only can happen with iterable getting canonicalized to Traversable|array */ if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { zend_string *standard_type_str = zend_type_to_string(single_type); + if (t_param) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s with bound %s cannot be part of an intersection type; " + "bound it to a class or interface (e.g. %s: SomeInterface)", + ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), + ZSTR_VAL(t_param->name)); + } zend_error_noreturn(E_COMPILE_ERROR, "Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str)); } /* An intersection of standard types cannot exist so invalidate it */ if (ZEND_TYPE_IS_ONLY_MASK(single_type)) { zend_string *standard_type_str = zend_type_to_string(single_type); + if (t_param) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s with bound %s cannot be part of an intersection type; " + "bound it to a class or interface (e.g. %s: SomeInterface)", + ZSTR_VAL(t_param->name), ZSTR_VAL(standard_type_str), + ZSTR_VAL(t_param->name)); + } zend_error_noreturn(E_COMPILE_ERROR, "Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str)); } @@ -7728,17 +10639,31 @@ static zend_type zend_compile_typename_ex( ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT; } } else { + zend_ast *check_ast = ast; + if (check_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + check_ast = check_ast->child[0]; + } + + is_t_ref_erasure = (zend_generic_lookup_name(check_ast) != NULL); type = zend_compile_single_typename(ast); } uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); if (type_mask == MAY_BE_ANY && is_marked_nullable) { - zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null"); + if (!is_t_ref_erasure) { + zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null"); + } + + is_marked_nullable = false; } if ((type_mask & MAY_BE_NULL) && is_marked_nullable) { - zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); + if (!is_t_ref_erasure) { + zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); + } + + is_marked_nullable = false; } if (force_allow_null && !is_marked_nullable && !(type_mask & MAY_BE_NULL)) { @@ -7830,6 +10755,12 @@ static void zend_compile_attributes( ? ZEND_ATTRIBUTE_STRICT_TYPES : 0; attr = zend_add_attribute( attributes, name, args ? args->children : 0, flags, offset, el->lineno); + attr->generic_arity = (uint8_t) el->attr; + if (el->child[2]) { + zend_type compiled = zend_compile_turbofish_args_type(el->child[2]); + attr->generic_args = emalloc(sizeof(zend_type)); + *attr->generic_args = compiled; + } zend_string_release(name); /* Populate arguments */ @@ -8022,6 +10953,11 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 arg_infos->type = zend_compile_typename(return_type_ast); ZEND_TYPE_FULL_MASK(arg_infos->type) |= _ZEND_ARG_INFO_FLAGS( (op_array->fn_flags & ZEND_ACC_RETURN_REFERENCE) != 0, /* is_variadic */ 0, /* is_tentative */ 0); + if (zend_type_ast_has_generic_content(return_type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(return_type_ast); + zend_generic_type_table_set_return( + zend_generic_get_or_create_op_array_table(op_array), pre); + } } else { arg_infos->type = (zend_type) ZEND_TYPE_INIT_CODE(fallback_return_type, 0, 0); } @@ -8140,6 +11076,26 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; arg_info->type = zend_compile_typename_ex(type_ast, force_nullable, &forced_allow_nullable); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + if (force_nullable || forced_allow_nullable) { + ZEND_TYPE_FULL_MASK(pre) |= _ZEND_TYPE_NULLABLE_BIT; + } + zend_generic_type_table_set_parameter( + zend_generic_get_or_create_op_array_table(op_array), i, pre); + /* Track which generic params can be inferred from a value + * argument: only bare, non-nullable T at the top level. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(pre) + && !(ZEND_TYPE_FULL_MASK(pre) & MAY_BE_NULL)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(pre); + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE + && ref->index < 64 + && op_array->generic_parameters) { + op_array->generic_parameters->inferable_mask |= + (uint64_t) 1 << ref->index; + } + } + } if (forced_allow_nullable) { zend_string *func_name = get_function_or_method_name((zend_function *) op_array); zend_error(E_DEPRECATED, @@ -8238,6 +11194,11 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 zend_type type = ZEND_TYPE_INIT_NONE(0); if (type_ast) { type = zend_compile_typename(type_ast); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_property( + zend_generic_get_or_create_class_table(scope), name, pre); + } } /* Don't give the property an explicit default value. For typed properties this means @@ -8738,6 +11699,7 @@ static zend_op_array *zend_compile_func_decl_ex( zend_ast *uses_ast = decl->child[1]; zend_ast *stmt_ast = decl->child[2]; zend_ast *return_type_ast = decl->child[3]; + zend_ast *generic_params_ast = decl->child[5]; bool is_method = decl->kind == ZEND_AST_METHOD; zend_string *lcname = NULL; bool is_hook = decl->kind == ZEND_AST_PROPERTY_HOOK; @@ -8840,8 +11802,23 @@ static zend_op_array *zend_compile_func_decl_ex( zend_stack_push(&CG(loop_var_stack), (void *) &dummy_var); } + bool save_in_static_member_type = CG(in_static_member_type); + CG(in_static_member_type) = is_method && (op_array->fn_flags & ZEND_ACC_STATIC); + + /* Harvest function/method generic type parameters and push them into scope + * so that parameter, return, and body type annotations erase correctly. + * See GENERICS.md §6.9. */ + if (generic_params_ast) { + op_array->generic_parameters = zend_compile_generic_type_parameter_list(generic_params_ast, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); + zend_generic_scope_push(op_array->generic_parameters, ZEND_GENERIC_ORIGIN_FUNCTION_LIKE); + } + zend_compile_params(params_ast, return_type_ast, is_method && zend_string_equals_literal(lcname, ZEND_TOSTRING_FUNC_LCNAME) ? IS_STRING : 0); + + zend_check_function_variance_markers(op_array); + + CG(in_static_member_type) = save_in_static_member_type; if (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) { zend_mark_function_as_generator(); zend_emit_op(NULL, ZEND_GENERATOR_CREATE, NULL, NULL); @@ -8923,6 +11900,10 @@ static zend_op_array *zend_compile_func_decl_ex( zend_string_release_ex(lcname, 0); } + if (op_array->generic_parameters) { + zend_generic_scope_pop(); + } + CG(active_op_array) = orig_op_array; CG(active_class_entry) = orig_class_entry; @@ -9184,8 +12165,19 @@ static void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t f } if (type_ast) { + bool save_in_static_member_type = CG(in_static_member_type); + if (flags & ZEND_ACC_STATIC) { + CG(in_static_member_type) = true; + } + type = zend_compile_typename(type_ast); + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_property( + zend_generic_get_or_create_class_table(ce), name, pre); + } + CG(in_static_member_type) = save_in_static_member_type; if (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_VOID|MAY_BE_NEVER|MAY_BE_CALLABLE)) { zend_string *str = zend_type_to_string(type); zend_error_noreturn(E_COMPILE_ERROR, @@ -9320,6 +12312,15 @@ static void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_as zend_error_noreturn(E_COMPILE_ERROR, "Class constant %s::%s cannot have type %s", ZSTR_VAL(ce->name), ZSTR_VAL(name), ZSTR_VAL(type_str)); } + + /* Capture the pre-erasure type when the declared type references a + * generic type parameter, so monomorph synthesis can substitute + * T → arg in the mono's class-constant type. */ + if (zend_type_ast_has_generic_content(type_ast)) { + zend_type pre = zend_compile_pre_erasure_typename(type_ast); + zend_generic_type_table_set_class_constant( + zend_generic_get_or_create_class_table(ce), name, pre); + } } if (UNEXPECTED((flags & ZEND_ACC_PRIVATE) && (flags & ZEND_ACC_FINAL))) { @@ -9439,15 +12440,24 @@ static void zend_compile_use_trait(const zend_ast *ast) /* {{{ */ zend_ast *trait_ast = traits->child[i]; if (ce->ce_flags & ZEND_ACC_INTERFACE) { - zend_string *name = zend_ast_get_str(trait_ast); + zend_string *name = trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE + ? zend_ast_get_str(trait_ast->child[0]) + : zend_ast_get_str(trait_ast); zend_error_noreturn(E_COMPILE_ERROR, "Cannot use traits inside of interfaces. " "%s is used in %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); } - ce->trait_names[ce->num_traits].name = + uint32_t trait_index = ce->num_traits; + ce->trait_names[trait_index].name = zend_resolve_const_class_name_reference(trait_ast, "trait name"); - ce->trait_names[ce->num_traits].lc_name = zend_string_tolower(ce->trait_names[ce->num_traits].name); + ce->trait_names[trait_index].lc_name = zend_string_tolower(ce->trait_names[trait_index].name); ce->num_traits++; + + if (trait_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(trait_ast); + zend_generic_type_table_set_trait_use( + zend_generic_get_or_create_class_table(ce), trait_index, pre); + } } if (!adaptations) { @@ -9483,6 +12493,12 @@ static void zend_compile_implements(zend_ast *ast) /* {{{ */ interface_names[i].name = zend_resolve_const_class_name_reference(class_ast, "interface name"); interface_names[i].lc_name = zend_string_tolower(interface_names[i].name); + + if (class_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(class_ast); + zend_generic_type_table_set_implements( + zend_generic_get_or_create_class_table(ce), i, pre); + } } ce->num_interfaces = list->children; @@ -9573,6 +12589,20 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top } zend_register_seen_symbol(lcname, ZEND_SYMBOL_CLASS); + + /* If a generic scope is active, this declaration shadows any generic + * type parameter with the same (case-insensitive) name. Register the + * unqualified lc_name in the innermost scope so subsequent lookups + * resolve to the class instead of the type parameter. */ + if (CG(generic_scope)) { + zend_string *unqualified_lc = zend_string_tolower(unqualified_name); + if (!CG(generic_scope)->shadowing_classes) { + ALLOC_HASHTABLE(CG(generic_scope)->shadowing_classes); + zend_hash_init(CG(generic_scope)->shadowing_classes, 4, NULL, NULL, 0); + } + zend_hash_add_empty_element(CG(generic_scope)->shadowing_classes, unqualified_lc); + zend_string_release(unqualified_lc); + } } else { /* Find an anon class name that is not in use yet. */ name = NULL; @@ -9613,9 +12643,36 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; } + if (decl->child[5]) { + ZEND_ASSERT(!(decl->flags & ZEND_ACC_ANON_CLASS)); + ce->generic_parameters = zend_compile_generic_type_parameter_list(decl->child[5], ZEND_GENERIC_ORIGIN_CLASS_LIKE); + zend_generic_scope_push(ce->generic_parameters, ZEND_GENERIC_ORIGIN_CLASS_LIKE); + bool all_defaults = true; + for (uint32_t i = 0; i < ce->generic_parameters->count; i++) { + if (!ZEND_TYPE_IS_SET(ce->generic_parameters->parameters[i].default_type)) { + all_defaults = false; + break; + } + } + if (all_defaults) { + ce->ce_flags |= ZEND_ACC_GENERIC_ALL_DEFAULTS; + } + } + if (extends_ast) { ce->parent_name = zend_resolve_const_class_name_reference(extends_ast, "class name"); + /* Capture the pre-erasure type when the extends clause specifies type + * arguments (e.g. `extends Box`). At link time + * (`zend_do_link_class`) this side-table entry triggers + * pre-synthesis of the `Box` monomorph and a rewrite of + * `parent_name` to the canonical name, so this class's direct + * parent is the monomorph and `instanceof Box` holds. */ + if (extends_ast->kind == ZEND_AST_GENERIC_NAMED_TYPE) { + zend_type pre = zend_compile_pre_erasure_typename(extends_ast); + zend_generic_type_table_set_extends( + zend_generic_get_or_create_class_table(ce), pre); + } } CG(active_class_entry) = ce; @@ -9645,6 +12702,12 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top zend_verify_abstract_class(ce); } + zend_check_generic_variance_markers(ce); + + if (ce->generic_parameters) { + zend_generic_scope_pop(); + } + CG(active_class_entry) = original_ce; if (toplevel) { @@ -11089,6 +14152,14 @@ static void zend_compile_instanceof(znode *result, zend_ast *ast) /* {{{ */ opline->extended_value = zend_alloc_cache_slot(); } else { SET_NODE(opline->op2, &class_node); + /* Generic / T-ref class refs resolve through zend_fetch_class against + * the current frame's bindings, which are stable within a frame but + * vary across frames. Allocate a 3-slot polymorphic inline cache — + * keys (EX(type_args), called_scope), value resolved CE — so a hot + * loop inside one frame skips the resolver after the first hit. */ + if (class_node.op_type == IS_UNUSED) { + opline->extended_value = zend_alloc_cache_slots(3); + } } } /* }}} */ @@ -11220,7 +14291,7 @@ static void zend_compile_shell_exec(znode *result, const zend_ast *ast) /* {{{ * ZVAL_STRING(&fn_name, "shell_exec"); name_ast = zend_ast_create_zval(&fn_name); args_ast = zend_ast_create_list(1, ZEND_AST_ARG_LIST, expr_ast); - call_ast = zend_ast_create(ZEND_AST_CALL, name_ast, args_ast); + call_ast = zend_ast_create(ZEND_AST_CALL, name_ast, args_ast, NULL); zend_compile_expr(result, call_ast); @@ -11409,7 +14480,19 @@ static void zend_compile_class_name(znode *result, const zend_ast *ast) /* {{{ * if (class_ast->kind == ZEND_AST_ZVAL) { zend_op *opline = zend_emit_op_tmp(result, ZEND_FETCH_CLASS_NAME, NULL, NULL); - opline->op1.num = zend_get_class_fetch_type(zend_ast_get_str(class_ast)); + uint32_t op1_num = zend_get_class_fetch_type(zend_ast_get_str(class_ast)); + if (op1_num == ZEND_FETCH_CLASS_DEFAULT + && (class_ast->attr & ZEND_NAME_NOT_FQ) == ZEND_NAME_NOT_FQ) { + zend_generic_origin origin; + uint32_t param_index = 0; + zend_generic_parameter *param = zend_generic_lookup_full( + zend_ast_get_str(class_ast), &origin, ¶m_index); + if (param) { + op1_num = zend_pack_type_param_fetch(param_index, 0, + origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE); + } + } + opline->op1.num = op1_num; } else { znode expr_node; zend_compile_expr(&expr_node, class_ast); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 2351882a560d..d05ff5770173 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -121,6 +121,233 @@ typedef struct _zend_file_context { HashTable seen_symbols; } zend_file_context; +/* Maximum number of generic type parameters or arguments at any single + * position. Capped at 7 bits so a uint8_t count leaves its top bit + * available for future runtime-model flags. */ +#define ZEND_GENERIC_MAX_PARAMS 127 + +C23_ENUM(zend_generic_variance, uint8_t) { + ZEND_GENERIC_VARIANCE_INVARIANT = 0, + ZEND_GENERIC_VARIANCE_COVARIANT = 1, + ZEND_GENERIC_VARIANCE_CONTRAVARIANT = 2, +}; + +C23_ENUM(zend_generic_origin, uint8_t) { + ZEND_GENERIC_ORIGIN_CLASS_LIKE = 0, /* class, interface, trait, enum */ + ZEND_GENERIC_ORIGIN_FUNCTION_LIKE = 1, /* function, method, closure, arrow function */ +}; + +typedef struct _zend_generic_parameter { + zend_string *name; /* type-parameter name */ + zend_generic_variance variance; + zend_type bound; /* runtime erased; ZEND_TYPE_NONE if unbounded */ + zend_type bound_pre_erasure; /* pre-erasure; ZEND_TYPE_NONE if same as `bound` */ + zend_type default_type; /* runtime erased; ZEND_TYPE_NONE if no default */ + zend_type default_pre_erasure; /* pre-erasure; ZEND_TYPE_NONE if same as `default_type` */ +} zend_generic_parameter; + +typedef struct _zend_generic_parameter_list { + uint32_t count; + uint64_t inferable_mask; /* bit i set when some value-parameter's pre-erasure type is exactly param i */ + zend_generic_parameter parameters[1]; +} 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 */ + HashTable *parameters; /* parameter index -> zend_type * */ + HashTable *properties; /* zend_string * -> zend_type * */ + HashTable *class_constants; /* zend_string * -> zend_type * */ + HashTable *implements; /* implements index -> zend_type * */ + 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. */ +typedef struct _zend_generic_scope_entry { + zend_generic_parameter_list *params; + uint32_t visible_count; /* number of parameters in `params` already declared */ + struct _zend_generic_parameter *self_compiling; /* param whose bound/default is being compiled, or NULL */ + HashTable *shadowing_classes; /* lc_names of class-likes declared inside this scope; lazy-allocated */ + zend_generic_origin origin; + struct _zend_generic_scope_entry *outer; +} zend_generic_scope_entry; + +ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t count, bool persistent); +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); +ZEND_API void zend_generic_type_table_set_parameter(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_property(zend_generic_type_table *t, zend_string *name, zend_type type); +ZEND_API void zend_generic_type_table_set_class_constant(zend_generic_type_table *t, zend_string *name, zend_type type); +ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type); +ZEND_API void zend_generic_type_table_set_turbofish_args(zend_generic_type_table *t, uint32_t op_num, zend_type type); + +/* Takes ownership of arg_types. */ +ZEND_API uint8_t zend_generic_install_inferred_call(zend_op_array *caller, + const zend_function *fbc, zend_type *arg_types, uint32_t arity, + uint32_t *out_args_id); + +ZEND_API uint8_t zend_generic_try_install_resolved_turbofish(zend_op_array *caller, + const zend_function *fbc, uint32_t args_id, uint32_t arity); + +ZEND_API void zend_check_generic_param_list_size(zend_ast *list_ast); +ZEND_API void zend_check_generic_arg_list_size(zend_ast *list_ast); + +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); + +/* Per-call-frame mapping of generic type parameters to their bindings. + * `name` is the canonical class name (or scalar/composite canonical string) + * for the bound type, used for class lookups and equality. The bound type + * itself is reachable via: + * - `type_ref`: a borrowed pointer to a zend_type in longer-lived + * storage — typically a turbofish args_box (held by some op_array's + * side table) or a parent frame's entry. Frames nest, so the source + * is guaranteed to outlive the entry. + * - `owned_type`: a zend_type owned by this entry. Used when no + * pre-existing zend_type can be borrowed — currently only for + * value-directed inference, where the inferred class-name zend_type + * is synthesised fresh. Released by zend_type_arg_table_destroy. + * Reads prefer `type_ref` when non-NULL, otherwise `owned_type`. When both + * are unset the parameter is unbound — consumers fall back to the + * parameter's declared bound. + * + * Lifetime: allocated by VERIFY_GENERIC_ARGUMENTS for a function call (or + * inferred at RECV time), freed when the frame unwinds. `generation` is a + * monotonically-increasing nonce assigned at allocation time so cache + * slots that key off the table pointer can detect ABA reuse. */ +typedef struct _zend_type_arg_entry { + zend_string *name; + const zend_type *type_ref; + zend_type owned_type; +} zend_type_arg_entry; + +typedef struct _zend_type_arg_table { + uint32_t count; + uint32_t generation; + /* True when the table has been relocated into opcache SHM / file cache + * as part of a persisted class entry's `generic_type_args`. Tells the + * destroy path to leave the table and its name/type contents alone. */ + bool persisted; + uint32_t refcount; + zend_type_arg_entry entries[1]; +} zend_type_arg_table; + +#define ZEND_TYPE_ARG_TABLE_SIZE(count) \ + (sizeof(zend_type_arg_table) + ((count) - 1) * sizeof(zend_type_arg_entry)) + +/* Read the bound type for an entry — returns NULL if unbound. */ +static zend_always_inline const zend_type *zend_type_arg_entry_type(const zend_type_arg_entry *entry) { + if (entry->type_ref) return entry->type_ref; + if (ZEND_TYPE_IS_SET(entry->owned_type)) return &entry->owned_type; + return NULL; +} + +/* Per-call-site side entry: stores the compile-time args_box. The one-slot + * runtime cache that pairs with this (cached zend_type_arg_table* keyed on + * the caller's binding identities) lives in the caller op_array's runtime + * cache, addressed by opline->result.num — that storage is per-process and + * 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 void zend_type_arg_table_release(zend_type_arg_table *table); +ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src); +ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type); +ZEND_API zend_type_arg_table *zend_build_generic_call_type_args(zend_execute_data *call, const zend_type *args_box); +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); +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 { zend_ast *ast; zend_string *str; @@ -260,7 +487,7 @@ typedef struct _zend_oparray_context { /* has #[\Override] attribute | | | */ #define ZEND_ACC_OVERRIDE (1 << 28) /* | X | X | */ /* | | | */ -/* Property Flags (unused: 13-27,29...) | | | */ +/* Property Flags (unused: 14-27,29...) | | | */ /* =========== | | | */ /* | | | */ /* Promoted property / parameter | | | */ @@ -274,6 +501,9 @@ typedef struct _zend_oparray_context { #define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */ #define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */ /* | | | */ +/* Parametric LSP substitution clone owned by a child | | | */ +#define ZEND_ACC_GENERIC_CLONE (1 << 13) /* | | X | */ +/* | | | */ /* Class Flags (unused: 31) | | | */ /* =========== | | | */ /* | | | */ @@ -340,6 +570,11 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ +/* Generic class whose every parameter has a default — | | | */ +/* hot-path bit for ZEND_NEW to skip the per-parameter | | | */ +/* default scan when synthesizing a defaults monomorph. | | | */ +#define ZEND_ACC_GENERIC_ALL_DEFAULTS (1u << 31) /* X | | | */ +/* | | | */ /* Class Flags 2 (ce_flags2) (unused: 0-31) | | | */ /* ========================= | | | */ /* | | | */ @@ -417,6 +652,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) @@ -576,6 +817,10 @@ struct _zend_op_array { * referenced by index from opcodes. */ zend_op_array **dynamic_func_defs; + /* Generic-syntax metadata. NULL on non-generic functions/methods. */ + zend_generic_parameter_list *generic_parameters; + zend_generic_type_table *generic_types; + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; @@ -652,6 +897,7 @@ struct _zend_execute_data { zend_array *symbol_table; void **run_time_cache; /* cache op_array->run_time_cache */ zend_array *extra_named_params; + zend_type_arg_table *type_args; /* generic type-argument table; NULL when none */ }; #define ZEND_CALL_HAS_THIS IS_OBJECT_EX @@ -1038,6 +1284,16 @@ void zend_assert_valid_class_name(const zend_string *const_name, const char *typ zend_string *zend_type_to_string_resolved(zend_type type, const zend_class_entry *scope); ZEND_API zend_string *zend_type_to_string(zend_type type); +/* Monomorphization: a canonical form of a generic class application like + * "Box>". Stable across permutations of union/intersection + * order so that semantically equivalent type-arg lists hash to the same name + * and resolve to the same synthesized class entry. */ +ZEND_API zend_string *zend_type_to_canonical_string(zend_type type); +ZEND_API zend_string *zend_generic_canonical_class_name( + zend_string *base_name, const zend_type *args, uint32_t arity); +ZEND_API bool zend_type_contains_type_parameter(zend_type type); +ZEND_API bool zend_type_contains_class_scope_type_parameter(zend_type type); + /* class fetches */ #define ZEND_FETCH_CLASS_DEFAULT 0 #define ZEND_FETCH_CLASS_SELF 1 @@ -1046,7 +1302,31 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); #define ZEND_FETCH_CLASS_AUTO 4 #define ZEND_FETCH_CLASS_INTERFACE 5 #define ZEND_FETCH_CLASS_TRAIT 6 +#define ZEND_FETCH_CLASS_TYPE_PARAM 7 /* function/method-level T */ +#define ZEND_FETCH_CLASS_TYPE_PARAM_CLASS 8 /* class-level T (read from called scope's mono args) */ +#define ZEND_FETCH_CLASS_GENERIC_DEFERRED 9 /* generic application with T-refs (e.g. Box); resolves to a monomorph at runtime */ #define ZEND_FETCH_CLASS_MASK 0x0f +#define ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT 16 + +static zend_always_inline bool zend_fetch_is_type_param(uint32_t fetch_type) { + uint32_t sub = fetch_type & ZEND_FETCH_CLASS_MASK; + return sub == ZEND_FETCH_CLASS_TYPE_PARAM || sub == ZEND_FETCH_CLASS_TYPE_PARAM_CLASS; +} + +static zend_always_inline uint32_t zend_pack_type_param_fetch(uint32_t param_index, uint32_t flags, bool class_level) { + ZEND_ASSERT(param_index < (1u << (32 - ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT))); + uint32_t code = class_level ? ZEND_FETCH_CLASS_TYPE_PARAM_CLASS : ZEND_FETCH_CLASS_TYPE_PARAM; + return code | (param_index << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; +} + +static zend_always_inline uint32_t zend_pack_generic_deferred_fetch(uint32_t args_id, uint32_t flags) { + ZEND_ASSERT(args_id < (1u << (32 - ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT))); + return ZEND_FETCH_CLASS_GENERIC_DEFERRED | (args_id << ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT) | flags; +} + +static zend_always_inline uint32_t zend_unpack_type_param_index(uint32_t fetch_type) { + return fetch_type >> ZEND_FETCH_CLASS_TYPE_PARAM_SHIFT; +} #define ZEND_FETCH_CLASS_NO_AUTOLOAD 0x80 #define ZEND_FETCH_CLASS_SILENT 0x0100 #define ZEND_FETCH_CLASS_EXCEPTION 0x0200 diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 1b28ce25fe37..2ea5c6d349f6 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4302,6 +4302,13 @@ static zend_always_inline void i_free_compiled_variables(zend_execute_data *exec cv++; count--; } + if (UNEXPECTED(execute_data->type_args)) { + /* Persisted monomorph tables are not frame-owned; only free request-built tables. */ + if (!execute_data->type_args->persisted) { + zend_type_arg_table_destroy(execute_data->type_args); + } + execute_data->type_args = NULL; + } } /* }}} */ @@ -4489,6 +4496,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 ba48b19bcfe1..815b4917c721 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -21,6 +21,7 @@ #define ZEND_EXECUTE_H #include "zend_compile.h" +#include "zend_closures.h" #include "zend_hash.h" #include "zend_operators.h" #include "zend_variables.h" @@ -338,6 +339,29 @@ static zend_always_inline void zend_vm_init_call_frame(zend_execute_data *call, Z_PTR(call->This) = object_or_called_scope; ZEND_CALL_INFO(call) = call_info; ZEND_CALL_NUM_ARGS(call) = num_args; + /* Closures created inside a generic frame capture that frame's T-table. + * Propagate it onto the call frame so the body's RECV-time checks + * resolve outer T-refs. The closure owns the table (persisted=true); + * call-frame teardown skips destroying it. VERIFY_GENERIC_ARGUMENTS may + * still overwrite type_args later for explicit-turbofish calls. */ + if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CLOSURE)) { +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Warray-bounds" +#endif + const zend_closure *closure = + (const zend_closure *) ZEND_CLOSURE_OBJECT(func); + call->type_args = closure->captured_type_args; +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +#endif + } else if (UNEXPECTED(ZEND_USER_CODE(func->type) + && (func->op_array.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS))) { + /* Monomorph reached via by-name dispatch: bind its concrete type-arg table. */ + call->type_args = func->op_array.generic_types->monomorph_type_args; + } else { + call->type_args = NULL; + } } static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, void *object_or_called_scope) @@ -411,6 +435,11 @@ static zend_always_inline void zend_vm_stack_free_call_frame_ex(uint32_t call_in { ZEND_ASSERT_VM_STACK_GLOBAL; + if (UNEXPECTED(call->type_args != NULL)) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + if (UNEXPECTED(call_info & ZEND_CALL_ALLOCATED)) { zend_vm_stack p = EG(vm_stack); zend_vm_stack prev = p->prev; @@ -481,6 +510,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_execute_API.c b/Zend/zend_execute_API.c index 71e0c56a51c8..e09f42b58a14 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -38,6 +38,7 @@ #include "zend_observer.h" #include "zend_call_stack.h" #include "zend_frameless_function.h" +#include "zend_smart_str.h" #ifdef HAVE_SYS_TIME_H #include #endif @@ -1011,6 +1012,26 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ if (func->type == ZEND_USER_FUNCTION) { uint32_t orig_jit_trace_num = EG(jit_trace_num); + /* C-dispatched calls (usort, array_map, Closure::call, etc.) don't go + * through DO_FCALL, so the closure-side reified arg check that lives + * there is bypassed. Run it here for closures whose captured T-table + * was propagated by zend_vm_init_call_frame. Must fire before + * zend_init_func_execute_data, because that relocates variadic args + * past the CV slots and the helper would then read UNDEF positions. */ + if (UNEXPECTED(call->type_args + && (func->common.fn_flags & ZEND_ACC_CLOSURE))) { + if (!zend_verify_generic_arg_types(call, NULL)) { + zend_vm_stack_free_args(call); + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + zend_vm_stack_free_call_frame(call); + EG(fake_scope) = orig_fake_scope; + zend_release_fcall_info_cache(fci_cache); + return SUCCESS; + } + } + zend_init_func_execute_data(call, &func->op_array, fci->retval); ZEND_OBSERVER_FCALL_BEGIN(call); zend_execute_ex(call); @@ -1235,6 +1256,26 @@ ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string * return ce; } + /* Monomorphization hook: if the requested name has generic shape + * ("Box"), parse it and synthesize the monomorph. This makes + * unserialize, dynamic `new $name`, `class_exists`, and `instanceof + * "Foo"` all transparently materialize the monomorph on demand. + * Runs before autoload so a Box name doesn't trigger a doomed + * autoload for "Box" itself. The synthesizer only succeeds when + * the base class exists and is generic. */ + if (!zend_is_compiling() && zend_class_name_is_monomorph(name)) { + zend_class_entry *mono = zend_try_synthesize_monomorph_by_name(name, flags); + if (mono) { + if (!key) zend_string_release_ex(lc_name, 0); + if (ce_cache) SET_CE_CACHE(ce_cache, mono); + return mono; + } + if (EG(exception)) { + if (!key) zend_string_release_ex(lc_name, 0); + return NULL; + } + } + /* The compiler is not-reentrant. Make sure we autoload only during run-time. */ if ((flags & ZEND_FETCH_CLASS_NO_AUTOLOAD) || zend_is_compiling()) { if (!key) { @@ -1709,6 +1750,218 @@ static ZEND_COLD void report_class_fetch_error(const zend_string *class_name, ui } } +/* Resolve a function/method-level generic type parameter at runtime by reading + * the current frame's T-table. Falls back to the parameter's declared bound when + * no concrete binding was supplied (e.g. the function was called without + * turbofish and inference produced nothing). Throws when neither a binding nor + * a usable class bound is available. */ +ZEND_API zend_class_entry *zend_resolve_generic_type_param(uint32_t param_index, uint32_t fetch_type) +{ + zend_execute_data *ex = EG(current_execute_data); + bool class_level = (fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TYPE_PARAM_CLASS; + zend_type_arg_table *table = NULL; + zend_generic_parameter_list *params = NULL; + + if (class_level) { + /* Class-level T's binding lives on the *monomorph* of the lexical + * scope — that may BE the called scope (`new Box::()`) or live + * deeper in the chain (`class IntBox extends Box`). Walk up + * from the called scope until we hit a direct child of the lexical + * scope, which is the mono that holds the binding. */ + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur) { + table = cur->generic_type_args; + } + if (lexical) { + params = lexical->generic_parameters; + } + } else if (ex && ZEND_USER_CODE(ex->func->type)) { + table = ex->type_args; + params = ex->func->op_array.generic_parameters; + } + + if (table && param_index < table->count && table->entries[param_index].name) { + /* Try the bound binding as a class name. If it isn't a class (e.g. + * scalar T-arg like `int`) the lookup returns NULL silently and we + * fall through to the parameter's declared bound — which is the only + * remaining thing a bare T-ref can resolve to. */ + uint32_t silent_fetch = (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) + | ZEND_FETCH_CLASS_SILENT | ZEND_FETCH_CLASS_NO_AUTOLOAD; + zend_class_entry *ce = zend_fetch_class_by_name(table->entries[param_index].name, NULL, silent_fetch); + if (ce) return ce; + /* Not in the class table yet — retry with autoload before giving up, + * but only if the original fetch allowed it. */ + if (!(fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD)) { + ce = zend_fetch_class_by_name(table->entries[param_index].name, NULL, + (fetch_type & ~ZEND_FETCH_CLASS_EXCEPTION) | ZEND_FETCH_CLASS_SILENT); + if (ce) return ce; + } + } + + /* Resolution failed. Suppress the diagnostic when SILENT and no thrown + * exception is expected (catch path passes SILENT without EXCEPTION; the + * caller treats NULL as "no match" and proceeds). */ + bool suppress = (fetch_type & ZEND_FETCH_CLASS_SILENT) + && !(fetch_type & ZEND_FETCH_CLASS_EXCEPTION); + + if (params && param_index < params->count) { + zend_type bound = params->parameters[param_index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) { + return zend_fetch_class_by_name(ZEND_TYPE_NAME(bound), NULL, fetch_type); + } + if (suppress) { + return NULL; + } + ZEND_ASSERT(params->parameters[param_index].name); + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve generic type parameter %s at runtime: no binding was supplied and its bound is not a class", + ZSTR_VAL(params->parameters[param_index].name)); + return NULL; + } + + if (suppress) { + return NULL; + } + zend_throw_or_error(fetch_type, NULL, "Cannot resolve generic type parameter at runtime: not in a generic function or class scope"); + return NULL; +} + +/* Look up the function-level T-ref binding (current frame's T-table); returns + * NULL when the table is missing the slot, in which case the caller should + * fall back to the parameter's declared bound. */ +static zend_string *zend_resolve_fn_type_param_name(uint32_t index) +{ + zend_execute_data *ex = EG(current_execute_data); + if (!ex || !ZEND_USER_CODE(ex->func->type)) return NULL; + zend_type_arg_table *table = ex->type_args; + if (table && index < table->count && table->entries[index].name) { + return table->entries[index].name; + } + return NULL; +} + +/* Look up the class-level T-ref binding. The binding lives on the monomorph + * of the lexical scope — see the matching walk in zend_resolve_generic_type_param. */ +static zend_string *zend_resolve_class_type_param_name(uint32_t index) +{ + zend_execute_data *ex = EG(current_execute_data); + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur && cur->generic_type_args && index < cur->generic_type_args->count) { + return cur->generic_type_args->entries[index].name; + } + return NULL; +} + +/* Resolve a function/method-level generic type parameter's name to its bound's + * class name; for class-level params the bound is consulted on the lexical + * scope's parameter list. Used by deferred-generic substitution to fall back + * when no concrete binding was supplied at the call site. */ +static zend_string *zend_resolve_type_param_bound_name(const zend_type_parameter_ref *ref) +{ + zend_generic_parameter_list *params = NULL; + zend_execute_data *ex = EG(current_execute_data); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + zend_class_entry *lexical = ex ? ex->func->common.scope : NULL; + if (lexical) params = lexical->generic_parameters; + } else if (ex && ZEND_USER_CODE(ex->func->type)) { + params = ex->func->op_array.generic_parameters; + } + if (!params || ref->index >= params->count) return NULL; + zend_type bound = params->parameters[ref->index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) return ZEND_TYPE_NAME(bound); + return NULL; +} + +/* Recursively append the canonical text for a substituted type fragment. The + * walk mirrors zend_generic_canonical_class_name, but substitutes T-refs + * against the current frame's bindings as it goes. Sets *failed when a + * binding is missing and no class bound is available. */ +static void zend_append_substituted_canonical(smart_str *buf, const zend_type *t, bool *failed) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*t)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*t); + zend_string *name = (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) + ? zend_resolve_class_type_param_name(ref->index) + : zend_resolve_fn_type_param_name(ref->index); + if (!name) name = zend_resolve_type_param_bound_name(ref); + if (!name) { + *failed = true; + return; + } + smart_str_append(buf, name); + return; + } + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*t)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*t); + if (nwa->name) smart_str_append(buf, nwa->name); + smart_str_appendc(buf, '<'); + for (uint32_t i = 0; i < nwa->count; i++) { + if (i > 0) smart_str_appendc(buf, ','); + zend_append_substituted_canonical(buf, &nwa->args[i], failed); + if (*failed) return; + } + smart_str_appendc(buf, '>'); + return; + } + /* Plain leaf (class name or pure scalar): defer to the canonical formatter. */ + zend_string *piece = zend_type_to_canonical_string(*t); + smart_str_append(buf, piece); + zend_string_release(piece); +} + +/* Resolve `instanceof Box` / `catch (Box $e)` and friends at runtime. + * Reads the compile-time pre-erasure type from the executing op_array's + * side table, substitutes T-refs against the current frame's bindings, and + * looks up the resulting monomorph (synthesizing it if needed via the + * standard class-lookup path). */ +ZEND_API zend_class_entry *zend_resolve_deferred_generic_class(uint32_t args_id, uint32_t fetch_type) +{ + zend_execute_data *ex = EG(current_execute_data); + bool suppress = (fetch_type & ZEND_FETCH_CLASS_SILENT) + && !(fetch_type & ZEND_FETCH_CLASS_EXCEPTION); + if (!ex || !ZEND_USER_CODE(ex->func->type)) { + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: not in a user function"); + return NULL; + } + + const zend_type *boxed = zend_generic_get_turbofish_args(&ex->func->op_array, args_id); + if (!boxed || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: missing side-table entry"); + return NULL; + } + + smart_str buf = {0}; + bool failed = false; + zend_append_substituted_canonical(&buf, boxed, &failed); + if (failed) { + smart_str_free(&buf); + if (suppress) return NULL; + zend_throw_or_error(fetch_type, NULL, + "Cannot resolve deferred generic type at runtime: no binding supplied for one of its type parameters and the parameter has no class bound"); + return NULL; + } + smart_str_0(&buf); + /* Intern the canonical name so EG(class_table) lookups can dispatch on + * pointer identity and repeat executions of the same opline with the + * same bindings hit a stable, shared string. */ + zend_string *canonical = zend_new_interned_string(buf.s); + zend_class_entry *ce = zend_fetch_class_by_name(canonical, NULL, fetch_type); + zend_string_release(canonical); + return ce; +} + zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) /* {{{ */ { zend_class_entry *ce, *scope; @@ -1716,6 +1969,13 @@ zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type) check_fetch_type: switch (fetch_sub_type) { + case ZEND_FETCH_CLASS_TYPE_PARAM: + case ZEND_FETCH_CLASS_TYPE_PARAM_CLASS: + return zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + case ZEND_FETCH_CLASS_GENERIC_DEFERRED: + return zend_resolve_deferred_generic_class( + zend_unpack_type_param_index(fetch_type), fetch_type); case ZEND_FETCH_CLASS_SELF: scope = zend_get_executed_scope(); if (UNEXPECTED(!scope)) { diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c index 52d409f2e69b..50504be4ea36 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -46,6 +46,7 @@ ZEND_API void zend_generator_restore_call_stack(zend_generator *generator) /* {{ Z_PTR(call->This)); memcpy(((zval*)new_call) + ZEND_CALL_FRAME_SLOT, ((zval*)call) + ZEND_CALL_FRAME_SLOT, ZEND_CALL_NUM_ARGS(call) * sizeof(zval)); new_call->extra_named_params = call->extra_named_params; + new_call->type_args = call->type_args; new_call->prev_execute_data = prev_call; prev_call = new_call; @@ -84,6 +85,10 @@ ZEND_API zend_execute_data* zend_generator_freeze_call_stack(zend_execute_data * new_call->prev_execute_data = prev_call; prev_call = new_call; + /* Frozen copy owns type_args; null the original so frame teardown + * below does not free what the generator still needs on resume. */ + call->type_args = NULL; + new_call = call->prev_execute_data; zend_vm_stack_free_call_frame(call); call = new_call; @@ -146,6 +151,10 @@ ZEND_API void zend_generator_close(zend_generator *generator, bool finished_exec if (EX_CALL_INFO() & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) { zend_free_extra_named_params(execute_data->extra_named_params); } + if (execute_data->type_args) { + zend_type_arg_table_destroy(execute_data->type_args); + execute_data->type_args = NULL; + } if (EX_CALL_INFO() & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(execute_data->This)); diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 8257df32e831..4ae7660c42f8 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -164,6 +164,25 @@ struct _zend_compiler_globals { uint32_t internal_run_time_cache_size; zend_stack short_circuiting_opnums; + + uint32_t type_arg_depth; + int type_arg_residual_token; + + struct _zend_generic_scope_entry *generic_scope; + + /* Set while compiling a type expression that has no instance binding for + * the enclosing class's type parameters (i.e. a static method's + * parameter/return types or a static property's type). Class-origin type + * parameter references resolved in this context are a compile error. */ + bool in_static_member_type; + + struct _zend_inheritance_binding_cache *inheritance_binding_cache; + + struct { + zend_class_entry *target; + const zend_type *args; + uint32_t arity; + } inheritance_binding_hint; #ifdef ZTS uint32_t copied_functions_count; #endif @@ -206,6 +225,14 @@ struct _zend_executor_globals { struct _zend_execute_data *current_execute_data; const zend_class_entry *fake_scope; /* used to avoid checks accessing properties */ + /* Set while a monomorph (e.g. Box) is being linked against its base. + * Bypasses the FINAL/READONLY checks the linker would otherwise raise + * on a user `extends FinalClass`, since monomorph synthesis is engine- + * internal and the resulting monomorph re-acquires the suppressed bits. + * Replaces a previous design that mutated base->ce_flags in place, which + * would SIGSEGV when base lived in read-only opcache SHM. */ + bool monomorph_synthesis_active; + uint32_t jit_trace_num; /* Used by tracing JIT to reference the currently running trace */ zend_execute_data *current_observed_frame; diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 8df6a5599d30..8e1a9e292b49 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -34,6 +34,21 @@ ZEND_API zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces) = NULL; ZEND_API zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies) = NULL; +static void zend_check_generic_link_arity( + const zend_class_entry *target_ce, uint32_t arity, + const char *clause, zend_string *child_name); +static void zend_check_generic_link_bounds( + zend_class_entry *target_ce, const zend_type *args_box, + const char *clause, zend_class_entry *ce); +static void zend_substitute_trait_method_arg_info( + zend_function *new_fn, const zend_function *orig_fn, + const zend_class_entry *using_ce, + const zend_type *bind_args, uint32_t bind_arity); +static zend_arg_info *zend_clone_arg_info_block( + const zend_arg_info *orig_block, uint32_t total); +static bool zend_diamond_types_equal(zend_type a, zend_type b); +static const zend_type_named_with_args *zend_get_implements_binding(const zend_class_entry *ce, uint32_t idx); + /* Unresolved means that class declarations that are currently not available are needed to * determine whether the inheritance is valid or not. At runtime UNRESOLVED should be treated * as an ERROR. */ @@ -63,7 +78,7 @@ static void ZEND_COLD emit_incompatible_method_error( const zend_function *parent, const zend_class_entry *parent_scope, inheritance_status status); -static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); static void zend_type_list_copy_ctor( zend_type *const parent_type, @@ -87,9 +102,48 @@ static void zend_type_list_copy_ctor( } ZEND_TYPE_LIST_FOREACH_END(); } -static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent) { +static void zend_type_named_with_args_copy_ctor( + zend_type *const parent_type, bool use_arena, bool persistent) +{ + const zend_type_named_with_args *src = ZEND_TYPE_NAMED_WITH_ARGS(*parent_type); + size_t size = ZEND_TYPE_NAMED_WITH_ARGS_SIZE(src->count); + zend_type_named_with_args *dst = use_arena + ? zend_arena_alloc(&CG(arena), size) : pemalloc(size, persistent); + memcpy(dst, src, size); + if (dst->name) { + zend_string_addref(dst->name); + } + ZEND_TYPE_SET_PTR(*parent_type, dst); + for (uint32_t i = 0; i < dst->count; i++) { + zend_type_copy_ctor(&dst->args[i], use_arena, persistent); + } +} + +static void zend_type_parameter_ref_copy_ctor( + zend_type *const parent_type, bool persistent) +{ + const zend_type_parameter_ref *src = ZEND_TYPE_TYPE_PARAMETER(*parent_type); + zend_type_parameter_ref *dst = pemalloc(sizeof(*dst), persistent); + *dst = *src; + if (dst->name) { + zend_string_addref(dst->name); + } + ZEND_TYPE_SET_PTR(*parent_type, dst); +} + +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent) { if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list_copy_ctor(type, use_arena, persistent); + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + /* Deep-clone the NWA payload + recurse into args. Without this, + * a copied type shares the source's heap-allocated NWA and either + * dangles (when the source frees it) or double-frees (when both + * release paths run). */ + zend_type_named_with_args_copy_ctor(type, use_arena, persistent); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + /* Same story for type-parameter refs: each clone needs its own + * heap-allocated ref so release paths don't collide. */ + zend_type_parameter_ref_copy_ctor(type, persistent); } else if (ZEND_TYPE_HAS_NAME(*type)) { zend_string_addref(ZEND_TYPE_NAME(*type)); } @@ -296,7 +350,7 @@ static zend_class_entry *lookup_class_ex( } /* The current class may not be registered yet, so check for it explicitly. */ - if (zend_string_equals_ci(scope->name, name)) { + if (scope && zend_string_equals_ci(scope->name, name)) { return scope; } } @@ -372,7 +426,9 @@ static bool zend_type_permits_self( const zend_type *single_type; ZEND_TYPE_FOREACH(type, single_type) { if (ZEND_TYPE_HAS_NAME(*single_type)) { - zend_string *name = resolve_class_name(scope, ZEND_TYPE_NAME(*single_type)); + zend_string *name = scope + ? resolve_class_name(scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); const zend_class_entry *ce = lookup_class(self, name); if (ce && unlinked_instanceof(self, ce)) { return true; @@ -440,8 +496,9 @@ static inheritance_status zend_is_intersection_subtype_of_class( zend_class_entry *fe_ce; zend_string *fe_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { - fe_class_name = - resolve_class_name(fe_scope, ZEND_TYPE_NAME(*single_type)); + fe_class_name = fe_scope + ? resolve_class_name(fe_scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); if (zend_string_equals_ci(fe_class_name, proto_class_name)) { return INHERITANCE_SUCCESS; } @@ -543,8 +600,9 @@ static inheritance_status zend_is_class_subtype_of_type( zend_class_entry *proto_ce; zend_string *proto_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { - proto_class_name = - resolve_class_name(proto_scope, ZEND_TYPE_NAME(*single_type)); + proto_class_name = proto_scope + ? resolve_class_name(proto_scope, ZEND_TYPE_NAME(*single_type)) + : ZEND_TYPE_NAME(*single_type); if (zend_string_equals_ci(fe_class_name, proto_class_name)) { if (!is_intersection) { return INHERITANCE_SUCCESS; @@ -585,7 +643,9 @@ static inheritance_status zend_is_class_subtype_of_type( static zend_string *get_class_from_type(const zend_class_entry *scope, const zend_type single_type) { if (ZEND_TYPE_HAS_NAME(single_type)) { - return resolve_class_name(scope, ZEND_TYPE_NAME(single_type)); + return scope + ? resolve_class_name(scope, ZEND_TYPE_NAME(single_type)) + : ZEND_TYPE_NAME(single_type); } return NULL; } @@ -675,6 +735,7 @@ static inheritance_status zend_perform_covariant_type_check( zend_class_entry *proto_scope, const zend_type proto_type) { ZEND_ASSERT(ZEND_TYPE_IS_SET(fe_type) && ZEND_TYPE_IS_SET(proto_type)); + ZEND_ASSERT((fe_scope == NULL) == (proto_scope == NULL)); /* Apart from void, everything is trivially covariant to the mixed type. * Handle this case separately to ensure it never requires class loading. */ @@ -756,21 +817,755 @@ static inheritance_status zend_perform_covariant_type_check( return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR; } - register_unresolved_classes(fe_scope, fe_type); - register_unresolved_classes(proto_scope, proto_type); + if (fe_scope) { + register_unresolved_classes(fe_scope, fe_type); + register_unresolved_classes(proto_scope, proto_type); + } + return INHERITANCE_UNRESOLVED; } +/* Direct binding only: the args ce supplies at its own extends/implements + * site. For transitive lookups, use zend_get_inheritance_binding_full. */ +static bool zend_get_inheritance_binding( + const zend_class_entry *ce, + const zend_class_entry *target_ce, + const zend_type **out_args, + uint32_t *out_arity) +{ + if (!ce->generic_types) { + return false; + } + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + const zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + bool matches = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent == target_ce + : (named->name && target_ce->name && zend_string_equals_ci(named->name, target_ce->name)); + + if (matches) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (zend_string_equals_ci(named->name, target_ce->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + if (ce->generic_types->trait_uses) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (named->name && zend_string_equals_ci(named->name, target_ce->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +/* True when `t` carries a NAMED_WITH_ARGS payload (`Box`) anywhere inside it. + * Building block for zend_type_is_reifiable_leaf_composite below. */ +static bool zend_type_contains_named_with_args(zend_type t) +{ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) return true; + if (ZEND_TYPE_HAS_LIST(t)) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(t), member) { + if (zend_type_contains_named_with_args(*member)) return true; + } ZEND_TYPE_LIST_FOREACH_END(); + } + return false; +} + +/* A union/intersection whose leaves include a type parameter but which carries + * no `Box`-style NAMED_WITH_ARGS reifies to a concrete erased-model shape + * (`T|Other` -> `Foo|Other`). An NWA composite stays erased — folding its + * monomorph name into a check would reject the plain instances a body emits, so + * callers leave it alone. A bare top-level T-ref is handled separately by + * callers (they substitute the leaf directly). Shared by the return-opcode + * elision (zend_emit_return_type_check) and the monomorph arg_info builder so + * the two decisions can't drift. */ +ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t) +{ + return ZEND_TYPE_HAS_LIST(t) + && zend_type_contains_type_parameter(t) + && !zend_type_contains_named_with_args(t); +} + +/* Two union members are "the same" for dedup purposes when they name the same + * class (case-insensitively) or refer to the same type parameter. Substituting a + * binding that already appears as a sibling member (`T|Other` with `T = Other`) + * would otherwise leave a redundant `Other|Other` in the list. */ +static bool zend_union_member_equals(zend_type a, zend_type b) +{ + if (ZEND_TYPE_HAS_NAME(a) && ZEND_TYPE_HAS_NAME(b)) { + return zend_string_equals_ci(ZEND_TYPE_NAME(a), ZEND_TYPE_NAME(b)); + } + if (ZEND_TYPE_HAS_TYPE_PARAMETER(a) && ZEND_TYPE_HAS_TYPE_PARAMETER(b)) { + const zend_type_parameter_ref *ra = ZEND_TYPE_TYPE_PARAMETER(a); + const zend_type_parameter_ref *rb = ZEND_TYPE_TYPE_PARAMETER(b); + return ra->origin == rb->origin && ra->index == rb->index; + } + return false; +} + +/* Append `member` to a union-build buffer unless an equal member (per + * zend_union_member_equals) is already present. */ +static zend_always_inline void zend_union_push_unique( + zend_type *out, uint32_t *out_count, zend_type member) +{ + for (uint32_t j = 0; j < *out_count; j++) { + if (zend_union_member_equals(out[j], member)) { + return; + } + } + out[(*out_count)++] = member; +} + +/* Substitutes class-scope T-refs with their bound arguments. + * + * Handles three shapes: + * - a bare T-ref ("T" or "?T") — return the substituted leaf; + * - a union/intersection list — walk the list and substitute any class-scope + * T-refs found inside, folding scalar bindings into the outer scalar mask + * and keeping named/intersection bindings in the list; + * - anything else — return unchanged. + * + * 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". + * + * `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 != origin || ref->index >= arity) { + return t; + } + + zend_type result = args[ref->index]; + + /* When the binding is itself a concrete generic instantiation + * (e.g. `T = DBox>`, supplied as a pre-erasure + * NAMED_WITH_ARGS type), fold it to a plain CLASS reference to the + * monomorph — the erased shape the runtime arg/property/return checks + * understand. This mirrors the named-with-args branch below; without + * it the substituted leaf keeps its NWA payload and zend_fetch_ce_from_type + * reads that payload as a zend_string, dereferencing garbage (a bogus + * multi-terabyte allocation, or a spurious TypeError at shallower depth). */ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(result) + && !zend_type_contains_type_parameter(result)) { + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(result); + zend_string *canonical = zend_generic_canonical_class_name( + nwa->name, nwa->args, nwa->count); + bool result_nullable = (ZEND_TYPE_FULL_MASK(result) & _ZEND_TYPE_NULLABLE_BIT) != 0; + result = (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + if (result_nullable) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + } + + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + + return result; + } + + /* Named-with-args (e.g. `I`): recurse into args. If every arg ends up + * ground, materialize the monomorph and return a plain CLASS type — that's + * the shape the runtime property/arg type checks already understand. If + * any T-ref remains, return a new NWA with the partially-substituted args. */ + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) { + 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_origin(src_nwa->args[i], args, arity, origin); + if (memcmp(&probe, &src_nwa->args[i], sizeof(zend_type)) != 0) { + needs_rebuild = true; + break; + } + } + if (!needs_rebuild) { + return t; + } + + ALLOCA_FLAG(use_heap) + 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_origin(src_nwa->args[i], args, arity, origin); + if (zend_type_contains_type_parameter(new_args[i])) { + all_concrete = false; + } + } + + zend_type result; + if (all_concrete) { + /* All bound — fold to a plain CLASS reference to the monomorph. + * The class-lookup hook synthesizes it on demand if needed. */ + zend_string *canonical = zend_generic_canonical_class_name( + src_nwa->name, new_args, src_nwa->count); + result = (zend_type) ZEND_TYPE_INIT_CLASS(canonical, 0, 0); + } else { + /* Partial substitution — keep as NWA with substituted args. */ + size_t size = ZEND_TYPE_NAMED_WITH_ARGS_SIZE(src_nwa->count); + zend_type_named_with_args *new_nwa = zend_arena_alloc(&CG(arena), size); + new_nwa->name = zend_string_copy(src_nwa->name); + new_nwa->name_attr = src_nwa->name_attr; + new_nwa->count = src_nwa->count; + for (uint32_t i = 0; i < src_nwa->count; i++) { + new_nwa->args[i] = new_args[i]; + zend_type_copy_ctor(&new_nwa->args[i], /* use_arena */ true, /* persistent */ false); + } + ZEND_TYPE_SET_PTR(result, new_nwa); + ZEND_TYPE_FULL_MASK(result) = ZEND_TYPE_FULL_MASK(t); + } + + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + free_alloca(new_args, use_heap); + return result; + } + + if (!ZEND_TYPE_HAS_LIST(t)) { + return t; + } + + const zend_type_list *src_list = ZEND_TYPE_LIST(t); + bool needs_rebuild = false; + for (uint32_t i = 0; i < src_list->num_types; i++) { + 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 == origin && ref->index < arity) { + needs_rebuild = true; + break; + } + } 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_origin(*elem, args, arity, origin); + if (memcmp(&probe, elem, sizeof(zend_type)) != 0) { + needs_rebuild = true; + break; + } + } + } + if (!needs_rebuild) { + return t; + } + + bool is_intersection = (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_INTERSECTION_BIT) != 0; + uint32_t carried_flags = ZEND_TYPE_FULL_MASK(t) & ~_ZEND_TYPE_KIND_MASK + & ~_ZEND_TYPE_LIST_BIT & ~_ZEND_TYPE_ARENA_BIT + & ~_ZEND_TYPE_UNION_BIT & ~_ZEND_TYPE_INTERSECTION_BIT; + uint32_t merged_mask = carried_flags; + + /* Substitute every member up front, then size the output: a union binding + * spliced into a union flattens (PHP unions can't nest), so reserve room for + * each of its members rather than one slot. */ + ALLOCA_FLAG(sub_heap) + zend_type *subbed = (zend_type *) do_alloca(sizeof(zend_type) * src_list->num_types, sub_heap); + uint32_t cap = 0; + for (uint32_t i = 0; i < src_list->num_types; i++) { + subbed[i] = zend_substitute_leaf_type_param_origin(src_list->types[i], args, arity, origin); + if (!is_intersection && ZEND_TYPE_HAS_LIST(subbed[i]) + && !ZEND_TYPE_IS_INTERSECTION(subbed[i])) { + cap += ZEND_TYPE_LIST(subbed[i])->num_types; + } else { + cap += 1; + } + } + + ALLOCA_FLAG(use_heap) + zend_type *out = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t out_count = 0; + + for (uint32_t i = 0; i < src_list->num_types; i++) { + zend_type substituted = subbed[i]; + /* The substituted scalar contribution is OR'd into the outer mask in case + * it carries a NULLABLE bit (or, for a union binding, member scalars). */ + merged_mask |= ZEND_TYPE_PURE_MASK(substituted); + + /* Flatten a union binding spliced into a union: (Foo|Other)|Other + * collapses to Foo|Other|Other (then dedupes). A nested union member + * would otherwise reach the runtime union check, which only expects + * plain or intersection members. */ + bool flatten = !is_intersection && ZEND_TYPE_HAS_LIST(substituted) + && !ZEND_TYPE_IS_INTERSECTION(substituted); + if (flatten) { + const zend_type *member; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(substituted), member) { + zend_union_push_unique(out, &out_count, *member); + } ZEND_TYPE_LIST_FOREACH_END(); + continue; + } + + /* Keep complex elements (named types, intersection sublists, unresolved + * T-refs) in the list. */ + bool keeps_complex = ZEND_TYPE_HAS_LIST(substituted) + || ZEND_TYPE_HAS_NAME(substituted) + || ZEND_TYPE_HAS_LITERAL_NAME(substituted) + || ZEND_TYPE_HAS_TYPE_PARAMETER(substituted) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(substituted); + + if (keeps_complex) { + zend_union_push_unique(out, &out_count, substituted); + } + } + free_alloca(subbed, sub_heap); + + zend_type result; + if (out_count == 0) { + result = (zend_type) ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(result) |= merged_mask; + } else if (out_count == 1 && !is_intersection + && (merged_mask & _ZEND_TYPE_MAY_BE_MASK & ~_ZEND_TYPE_NULLABLE_BIT) == 0) { + /* Degenerate union "Foo" or "?Foo" — represent as a single name with + * the nullable bit, matching how the parser builds the same shape. */ + result = out[0]; + zend_type_copy_ctor(&result, /* use_arena */ true, /* persistent */ false); + if (merged_mask & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + } else { + zend_type_list *new_list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(out_count)); + new_list->num_types = out_count; + for (uint32_t i = 0; i < out_count; i++) { + new_list->types[i] = out[i]; + zend_type_copy_ctor(&new_list->types[i], /* use_arena */ true, /* persistent */ false); + } + result = (zend_type) ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, new_list); + uint32_t kind_bit = is_intersection ? _ZEND_TYPE_INTERSECTION_BIT : _ZEND_TYPE_UNION_BIT; + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | kind_bit | merged_mask; + } + + free_alloca(out, use_heap); + 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) +{ + if (!ce->generic_types || !ce->generic_types->trait_uses) return false; + zval *zv = zend_hash_index_find(ce->generic_types->trait_uses, trait_index); + if (!zv) return false; + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) return false; + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + *out_args = nwa->args; + *out_arity = nwa->count; + return true; +} + +static const zend_type *zend_get_trait_property_pre_erasure( + const zend_class_entry *trait_ce, zend_string *prop_name) +{ + if (!trait_ce->generic_types || !trait_ce->generic_types->properties) return NULL; + zval *zv = zend_hash_find(trait_ce->generic_types->properties, prop_name); + if (!zv) return NULL; + return (const zend_type *) Z_PTR_P(zv); +} + +static zend_class_entry *zend_find_interface_by_name( + const zend_class_entry *ce, const zend_string *name) +{ + /* ce->interfaces[] is only meaningful once resolution has populated it. */ + if (!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { + return NULL; + } + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (ce->interfaces[i] && zend_string_equals_ci(ce->interfaces[i]->name, name)) { + return ce->interfaces[i]; + } + } + return NULL; +} + +/* Walks the inheritance chain and composes substitutions when ce's binding + * for `target` is transitive. */ +ZEND_API bool zend_get_inheritance_binding_full( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity) +{ + const zend_type *direct_args; + if (zend_get_inheritance_binding(ce, target, &direct_args, out_arity)) { + if (*out_arity > out_capacity) return false; + for (uint32_t i = 0; i < *out_arity; i++) { + out_args[i] = direct_args[i]; + } + return true; + } + + if (!ce->generic_types) return false; + + uint32_t intermediate_cap = (target && target->generic_parameters) ? target->generic_parameters->count : 0; + if (intermediate_cap == 0) return false; + ALLOCA_FLAG(use_heap) + zend_type *intermediate_args = (zend_type *) do_alloca(sizeof(zend_type) * intermediate_cap, use_heap); + bool found = false; + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + zend_class_entry *parent_ce = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent + : zend_lookup_class_ex( + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends)->name, + NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + if (parent_ce) { + uint32_t intermediate_arity; + if (zend_get_inheritance_binding_full(parent_ce, target, intermediate_args, intermediate_cap, &intermediate_arity)) { + const zend_type *ce_to_parent; + uint32_t ce_to_parent_arity; + if (zend_get_inheritance_binding(ce, parent_ce, &ce_to_parent, &ce_to_parent_arity)) { + if (intermediate_arity > out_capacity) { + goto done; + } + + for (uint32_t i = 0; i < intermediate_arity; i++) { + out_args[i] = zend_substitute_leaf_type_param(intermediate_args[i], ce_to_parent, ce_to_parent_arity); + } + + *out_arity = intermediate_arity; + found = true; + goto done; + } + } + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + zend_class_entry *intermediate = zend_find_interface_by_name(ce, named->name); + if (!intermediate || intermediate == target) continue; + + uint32_t intermediate_arity; + if (!zend_get_inheritance_binding_full(intermediate, target, + intermediate_args, intermediate_cap, &intermediate_arity)) { + continue; + } + + if (intermediate_arity > out_capacity) { + goto done; + } + + for (uint32_t i = 0; i < intermediate_arity; i++) { + out_args[i] = zend_substitute_leaf_type_param( + intermediate_args[i], named->args, named->count); + } + *out_arity = intermediate_arity; + found = true; + goto done; + } ZEND_HASH_FOREACH_END(); + } + + done: + free_alloca(intermediate_args, use_heap); + return found; +} + +static bool zend_get_inheritance_binding_full_cached( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity) +{ + if (CG(inheritance_binding_hint).target == target && CG(inheritance_binding_hint).args) { + uint32_t arity = CG(inheritance_binding_hint).arity; + if (arity > out_capacity) { + return false; + } + + const zend_type *hint_args = CG(inheritance_binding_hint).args; + for (uint32_t i = 0; i < arity; i++) { + out_args[i] = hint_args[i]; + } + + *out_arity = arity; + return true; + } + + zend_inheritance_binding_cache *cache = CG(inheritance_binding_cache); + if (cache && cache->present && cache->ce == ce && cache->target == target) { + if (!cache->valid) { + return false; + } + + if (cache->arity > out_capacity) { + return false; + } + + for (uint32_t i = 0; i < cache->arity; i++) { + out_args[i] = cache->args[i]; + } + + *out_arity = cache->arity; + return true; + } + + bool result = zend_get_inheritance_binding_full( + ce, target, out_args, out_capacity, out_arity); + + if (cache) { + cache->ce = ce; + cache->target = target; + cache->present = true; + cache->valid = result; + if (result && *out_arity <= ZEND_GENERIC_MAX_PARAMS) { + cache->arity = *out_arity; + for (uint32_t i = 0; i < *out_arity; i++) { + cache->args[i] = out_args[i]; + } + } + } + + return result; +} + +/* Fills out_args with target_ce's parameter defaults if every parameter + * has one. */ +static bool zend_get_target_default_args( + const zend_class_entry *target_ce, + zend_type *out_args, uint32_t out_capacity, uint32_t *out_arity) +{ + if (!target_ce->generic_parameters) return false; + uint32_t total = target_ce->generic_parameters->count; + if (total > out_capacity) return false; + for (uint32_t i = 0; i < total; i++) { + zend_type d = target_ce->generic_parameters->parameters[i].default_type; + if (!ZEND_TYPE_IS_SET(d)) return false; + out_args[i] = d; + } + *out_arity = total; + return true; +} + +/* Internal: substitute proto's pre-erasure against ce's binding without + * applying the top-level type-parameter fallback. May return a type that + * still has TYPE_PARAMETER refs at the top — callers use this to detect + * the "would fall back" case so the child side can mirror it. Returns + * `fallback` (unchanged) when no substitution applies at all. */ +static zend_type zend_substitute_proto_type_raw( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *proto, + zend_class_entry *ce) +{ + if (!ce || !pre_erasure) { + return fallback; + } + + /* Top-level type-param ref of function-scope origin: dereference to the + * param's bound's pre-erasure. This lets `set(U $x)` substitute + * T → ce's binding in U's bound, so the inheritance check compares the + * substituted bound rather than the erased mixed. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) + && ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre_erasure); + if (!ZEND_USER_CODE(proto->common.type)) return fallback; + const zend_op_array *op = &proto->op_array; + if (!op->generic_parameters || ref->index >= op->generic_parameters->count) { + return fallback; + } + const zend_generic_parameter *p = &op->generic_parameters->parameters[ref->index]; + const zend_type *bound_pre = ZEND_TYPE_IS_SET(p->bound_pre_erasure) + ? &p->bound_pre_erasure + : (ZEND_TYPE_IS_SET(p->bound) ? &p->bound : NULL); + if (!bound_pre || !zend_type_contains_type_parameter(*bound_pre)) { + return fallback; + } + /* Recurse with the bound as the pre-erasure. The recursive call may + * also need to deref (chained method-level bounds), but loop detection + * isn't needed for inheritance because the user-visible bound is a + * finite expression. */ + return zend_substitute_proto_type_raw(fallback, bound_pre, proto, ce); + } + if (!zend_type_contains_type_parameter(*pre_erasure)) { + return fallback; + } + + zend_class_entry *proto_scope = proto->common.scope; + if (!proto_scope || !proto_scope->generic_parameters) { + return fallback; + } + + uint32_t cap = proto_scope->generic_parameters->count; + if (cap == 0) { + return fallback; + } + + ALLOCA_FLAG(use_heap) + zend_type *args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t arity; + zend_type result; + if (!zend_get_inheritance_binding_full_cached(ce, proto_scope, args, cap, &arity) + && !zend_get_target_default_args(proto_scope, args, cap, &arity)) { + result = fallback; + } else { + result = zend_substitute_leaf_type_param(*pre_erasure, args, arity); + } + + free_alloca(args, use_heap); + return result; +} + +/* If ce supplies type arguments to proto's declaring scope (directly, + * transitively, or via parameter defaults), returns proto's pre-erasure type + * with class-scope T-refs substituted. Applies a top-level fallback when the + * substituted result is still a bare TYPE_PARAMETER (couldn't be ground). */ +static zend_type zend_substitute_proto_type( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *proto, + zend_class_entry *ce) +{ + zend_type result = zend_substitute_proto_type_raw(fallback, pre_erasure, proto, ce); + /* If the result is *still* a bare type-parameter (couldn't be ground), + * fall back to the erased form. Compound types containing leftover + * type-param leaves are kept — the structural comparison handles them + * by identity. */ + return ZEND_TYPE_HAS_TYPE_PARAMETER(result) ? fallback : result; +} + +/* Returns the type the inheritance check should use for the child (fe) side. + * When the child has a pre-erasure stash and that stash carries class-scope + * shape the arg_info erased away (e.g. `Tl|Tr` collapsed to mixed because + * both members are unbounded class-scope type parameters), prefer the + * pre-erasure so the check sees the same structure that's substituted in + * for the parent side. Function-scope refs aren't bound by inheritance — + * they erase to their bound and we keep using the erased form. + * + * `proto_substituted_had_type_param` tells us whether the parent's + * substitution would have fallen back to its erased form (its substituted + * result was a bare TYPE_PARAMETER). When that's true and the child's + * pre-erasure is also a bare TYPE_PARAMETER, the child falls back too — + * keeping both sides at the erased mixed so the comparison stays symmetric. + * Otherwise the child uses its pre-erasure so structural compounds (unions + * like `Tl|Tr`, named-with-args like `I`) line up with the parent's + * substituted form. */ +static zend_type zend_resolve_fe_type( + zend_type fallback, + const zend_type *pre_erasure, + const zend_function *fe, + zend_class_entry *ce, + bool proto_substituted_had_type_param) +{ + if (!pre_erasure || !ZEND_TYPE_IS_SET(*pre_erasure)) { + return fallback; + } + /* Function-scope TYPE_PARAMETER ref: deref to the param's bound's + * pre-erasure so its class-scope content can still drive the comparison. + * Mirrors the symmetric deref in zend_substitute_proto_type_raw. */ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure) + && ZEND_TYPE_TYPE_PARAMETER(*pre_erasure)->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre_erasure); + if (!ZEND_USER_CODE(fe->common.type)) return fallback; + const zend_op_array *op = &fe->op_array; + if (!op->generic_parameters || ref->index >= op->generic_parameters->count) { + return fallback; + } + const zend_generic_parameter *p = &op->generic_parameters->parameters[ref->index]; + const zend_type *bound_pre = ZEND_TYPE_IS_SET(p->bound_pre_erasure) + ? &p->bound_pre_erasure + : (ZEND_TYPE_IS_SET(p->bound) ? &p->bound : NULL); + if (!bound_pre) return fallback; + return zend_resolve_fe_type(fallback, bound_pre, fe, ce, proto_substituted_had_type_param); + } + if (!zend_type_contains_class_scope_type_parameter(*pre_erasure)) { + /* Pre-erasure has structure but no class-scope refs to substitute. + * If proto fell back (its substituted result was still a bare type + * param), fall back here too for symmetric mixed-vs-mixed compare; + * otherwise use the pre-erasure structure directly so unions like + * `string|int` compare properly. */ + if (proto_substituted_had_type_param + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + return fallback; + } + return *pre_erasure; + } + /* Mirror the parent's fall-back: if proto fell back AND fe's pre-erasure + * is itself a bare TYPE_PARAMETER, fall back to the erased form too. */ + if (proto_substituted_had_type_param + && ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + return fallback; + } + /* Immediate-child case: the pre-erasure type-param refs are already in + * fe_scope, which is the same scope the comparison runs in. */ + if (ce == fe->common.scope) { + return *pre_erasure; + } + /* Transitive: substitute the child's class-scope refs against ce's + * binding to fe's scope. */ + return zend_substitute_proto_type(fallback, pre_erasure, fe, ce); +} + +static const zend_type *zend_get_param_pre_erasure(const zend_function *proto, uint32_t param_idx) +{ + if (!ZEND_USER_CODE(proto->common.type)) return NULL; + const zend_op_array *op_array = &proto->op_array; + if (!op_array->generic_types || !op_array->generic_types->parameters) return NULL; + return zend_hash_index_find_ptr(op_array->generic_types->parameters, param_idx); +} + +static const zend_type *zend_get_return_pre_erasure(const zend_function *proto) +{ + if (!ZEND_USER_CODE(proto->common.type)) return NULL; + const zend_op_array *op_array = &proto->op_array; + if (!op_array->generic_types) return NULL; + return op_array->generic_types->return_type; +} + static inheritance_status zend_do_perform_arg_type_hint_check( - zend_class_entry *fe_scope, const zend_arg_info *fe_arg_info, - zend_class_entry *proto_scope, const zend_arg_info *proto_arg_info) /* {{{ */ + zend_class_entry *fe_scope, zend_type fe_type, + zend_class_entry *proto_scope, zend_type proto_type) /* {{{ */ { - if (!ZEND_TYPE_IS_SET(fe_arg_info->type) || ZEND_TYPE_PURE_MASK(fe_arg_info->type) == MAY_BE_ANY) { + if (!ZEND_TYPE_IS_SET(fe_type) || ZEND_TYPE_PURE_MASK(fe_type) == MAY_BE_ANY) { /* Child with no type or mixed type is always compatible */ return INHERITANCE_SUCCESS; } - if (!ZEND_TYPE_IS_SET(proto_arg_info->type)) { + if (!ZEND_TYPE_IS_SET(proto_type)) { /* Child defines a type, but parent doesn't, violates LSP */ return INHERITANCE_ERROR; } @@ -778,7 +1573,7 @@ static inheritance_status zend_do_perform_arg_type_hint_check( /* Contravariant type check is performed as a covariant type check with swapped * argument order. */ return zend_perform_covariant_type_check( - proto_scope, proto_arg_info->type, fe_scope, fe_arg_info->type); + proto_scope, proto_type, fe_scope, fe_type); } /* }}} */ @@ -787,7 +1582,8 @@ static inheritance_status zend_do_perform_arg_type_hint_check( * the method was declared in. */ static inheritance_status zend_do_perform_implementation_check( const zend_function *fe, zend_class_entry *fe_scope, - const zend_function *proto, zend_class_entry *proto_scope) /* {{{ */ + const zend_function *proto, zend_class_entry *proto_scope, + zend_class_entry *ce) /* {{{ */ { uint32_t num_args, proto_num_args, fe_num_args; inheritance_status status, local_status; @@ -848,8 +1644,20 @@ static inheritance_status zend_do_perform_implementation_check( return INHERITANCE_ERROR; } + uint32_t proto_param_idx = i < proto_num_args ? i : proto_num_args - 1; + zend_type proto_raw = zend_substitute_proto_type_raw( + proto_arg_info->type, + zend_get_param_pre_erasure(proto, proto_param_idx), + proto, ce); + bool proto_fell_back = ZEND_TYPE_HAS_TYPE_PARAMETER(proto_raw); + zend_type proto_type = proto_fell_back ? proto_arg_info->type : proto_raw; + uint32_t fe_param_idx = i < fe_num_args ? i : fe_num_args - 1; + zend_type fe_type = zend_resolve_fe_type( + fe_arg_info->type, + zend_get_param_pre_erasure(fe, fe_param_idx), + fe, ce, proto_fell_back); local_status = zend_do_perform_arg_type_hint_check( - fe_scope, fe_arg_info, proto_scope, proto_arg_info); + fe_scope, fe_type, proto_scope, proto_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (UNEXPECTED(local_status == INHERITANCE_ERROR)) { @@ -879,8 +1687,19 @@ static inheritance_status zend_do_perform_implementation_check( return status; } + zend_type proto_raw_ret = zend_substitute_proto_type_raw( + proto->common.arg_info[-1].type, + zend_get_return_pre_erasure(proto), + proto, ce); + bool proto_ret_fell_back = ZEND_TYPE_HAS_TYPE_PARAMETER(proto_raw_ret); + zend_type proto_return_type = proto_ret_fell_back + ? proto->common.arg_info[-1].type : proto_raw_ret; + zend_type fe_return_type = zend_resolve_fe_type( + fe->common.arg_info[-1].type, + zend_get_return_pre_erasure(fe), + fe, ce, proto_ret_fell_back); local_status = zend_perform_covariant_type_check( - fe_scope, fe->common.arg_info[-1].type, proto_scope, proto->common.arg_info[-1].type); + fe_scope, fe_return_type, proto_scope, proto_return_type); if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) { if (local_status == INHERITANCE_ERROR @@ -896,10 +1715,11 @@ static inheritance_status zend_do_perform_implementation_check( /* }}} */ static ZEND_COLD void zend_append_type_hint( - smart_str *str, const zend_class_entry *scope, const zend_arg_info *arg_info, bool return_hint) /* {{{ */ + smart_str *str, const zend_class_entry *scope, const zend_arg_info *arg_info, + zend_type display_type, bool return_hint) /* {{{ */ { - if (ZEND_TYPE_IS_SET(arg_info->type)) { - zend_string *type_str = zend_type_to_string_resolved(arg_info->type, scope); + if (ZEND_TYPE_IS_SET(display_type)) { + zend_string *type_str = zend_type_to_string_resolved(display_type, scope); smart_str_append(str, type_str); zend_string_release(type_str); if (!return_hint) { @@ -910,7 +1730,8 @@ static ZEND_COLD void zend_append_type_hint( /* }}} */ static ZEND_COLD zend_string *zend_get_function_declaration( - const zend_function *fptr, const zend_class_entry *scope) /* {{{ */ + const zend_function *fptr, const zend_class_entry *scope, + zend_class_entry *subst_ce) /* {{{ */ { smart_str str = {0}; @@ -941,8 +1762,22 @@ static ZEND_COLD zend_string *zend_get_function_declaration( num_args++; } for (uint32_t i = 0; i < num_args;) { - zend_append_type_hint(&str, scope, arg_info, false); - + uint32_t param_idx = i < fptr->common.num_args ? i : fptr->common.num_args; + const zend_type *param_pre = zend_get_param_pre_erasure(fptr, param_idx); + zend_type display_type; + if (subst_ce) { + display_type = zend_substitute_proto_type(arg_info->type, param_pre, fptr, subst_ce); + } else if (param_pre && ZEND_TYPE_IS_SET(*param_pre) + && zend_type_contains_class_scope_type_parameter(*param_pre)) { + /* Render the pre-erasure shape so messages mirror what the + * inheritance check actually compared. Function-scope refs + * erase to their bound and aren't bound by inheritance, so + * we keep the erased rendering for those. */ + display_type = *param_pre; + } else { + display_type = arg_info->type; + } + zend_append_type_hint(&str, scope, arg_info, display_type, false); if (ZEND_ARG_SEND_MODE(arg_info)) { smart_str_appendc(&str, '&'); } @@ -1036,7 +1871,20 @@ static ZEND_COLD zend_string *zend_get_function_declaration( if (fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { smart_str_appends(&str, ": "); - zend_append_type_hint(&str, scope, fptr->common.arg_info - 1, true); + const zend_arg_info *ret_info = fptr->common.arg_info - 1; + const zend_type *ret_pre = zend_get_return_pre_erasure(fptr); + zend_type ret_display; + if (subst_ce) { + ret_display = zend_substitute_proto_type(ret_info->type, ret_pre, fptr, subst_ce); + } else if (ret_pre && ZEND_TYPE_IS_SET(*ret_pre) + && zend_type_contains_class_scope_type_parameter(*ret_pre)) { + /* Mirror the pre-erasure shape so messages match what the + * inheritance check actually compared. */ + ret_display = *ret_pre; + } else { + ret_display = ret_info->type; + } + zend_append_type_hint(&str, scope, ret_info, ret_display, true); } smart_str_0(&str); @@ -1056,8 +1904,11 @@ static void ZEND_COLD emit_incompatible_method_error( const zend_function *child, const zend_class_entry *child_scope, const zend_function *parent, const zend_class_entry *parent_scope, inheritance_status status) { - zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope); - zend_string *child_prototype = zend_get_function_declaration(child, child_scope); + /* When the child class binds the parent's type parameters (extends/implements + * with type args), display the parent signature in its substituted form so + * the message matches the form being checked. */ + zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope, (zend_class_entry *) child_scope); + zend_string *child_prototype = zend_get_function_declaration(child, child_scope, NULL); if (status == INHERITANCE_UNRESOLVED) { // TODO Improve error message if first unresolved class is present in child and parent? /* Fetch the first unresolved class from registered autoloads */ @@ -1099,7 +1950,7 @@ static void perform_delayable_implementation_check( const zend_function *proto, zend_class_entry *proto_scope) { inheritance_status status = - zend_do_perform_implementation_check(fe, fe_scope, proto, proto_scope); + zend_do_perform_implementation_check(fe, fe_scope, proto, proto_scope, ce); if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { if (EXPECTED(status == INHERITANCE_UNRESOLVED)) { add_compatibility_obligation(ce, fe, fe_scope, proto, proto_scope); @@ -1228,8 +2079,9 @@ static inheritance_status do_inheritance_check_on_method( if (flags & ZEND_INHERITANCE_CHECK_PROTO) { if (flags & ZEND_INHERITANCE_CHECK_SILENT) { - return zend_do_perform_implementation_check(child, child_scope, parent, parent_scope); + return zend_do_perform_implementation_check(child, child_scope, parent, parent_scope, ce); } + perform_delayable_implementation_check(ce, child, child_scope, parent, parent_scope); } @@ -1245,40 +2097,485 @@ static inheritance_status do_inheritance_check_on_method( } /* }}} */ -static void do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce, bool is_interface, uint32_t flags) /* {{{ */ +static zend_function *zend_maybe_substitute_inherited_method( + zend_function *parent_fn, zend_class_entry *ce) { - zval *child = zend_hash_find_known_hash(&ce->function_table, key); + if (parent_fn->type != ZEND_USER_FUNCTION) { + return NULL; + } - if (child) { - zend_function *func = (zend_function*)Z_PTR_P(child); + if (!parent_fn->common.scope || !parent_fn->common.scope->generic_parameters) { + return NULL; + } - if (is_interface && UNEXPECTED(func == parent)) { - /* The same method in interface may be inherited few times */ - return; - } + const zend_op_array *op = &parent_fn->op_array; + if (!op->generic_types) { + return NULL; + } - do_inheritance_check_on_method( - func, func->common.scope, parent, parent->common.scope, ce, child, flags); - } else { + if (!op->generic_types->parameters && !op->generic_types->return_type) { + return NULL; + } - if (is_interface || (parent->common.fn_flags & (ZEND_ACC_ABSTRACT))) { - ce->ce_flags |= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; - } + zend_class_entry *defining_ce = parent_fn->common.scope; + uint32_t cap = defining_ce->generic_parameters->count; + if (cap == 0) { + return NULL; + } - parent = zend_duplicate_function(parent, ce); + ALLOCA_FLAG(use_heap) + zend_type *bound_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t bound_arity = 0; + bool have = zend_get_inheritance_binding_full_cached(ce, defining_ce, bound_args, cap, &bound_arity); - if (!is_interface) { - _zend_hash_append_ptr(&ce->function_table, key, parent); - } else { - zend_hash_add_new_ptr(&ce->function_table, key, parent); - } + if (!have) { + have = zend_get_target_default_args(defining_ce, bound_args, cap, &bound_arity); } -} -/* }}} */ -static inheritance_status full_property_types_compatible( - const zend_property_info *parent_info, const zend_property_info *child_info, - prop_variance variance) { + if (!have) { + free_alloca(bound_args, use_heap); + return NULL; + } + + zend_function *clone = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(clone, parent_fn, sizeof(zend_op_array)); + clone->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + + zend_substitute_trait_method_arg_info(clone, parent_fn, ce, bound_args, bound_arity); + free_alloca(bound_args, use_heap); + if (clone->op_array.arg_info == parent_fn->op_array.arg_info) { + return NULL; + } + + /* The clone shares the parent's opcode stream but holds tightened arg_info + * from generic substitution. Mark it like a trait-method clone so opcache + * persistence and the RECV handler can detect that the inline RECV type + * mask may be looser than the live arg_info. */ + clone->common.fn_flags |= ZEND_ACC_TRAIT_CLONE; + + function_add_ref(clone); + return clone; +} + +/* True iff `t` can be a component of an intersection: a class-name leaf, + * or an already-built intersection of class-name leaves. */ +static bool zend_type_intersectable(zend_type t) +{ + if (ZEND_TYPE_HAS_NAME(t)) { + return ZEND_TYPE_PURE_MASK(t) == 0 || (ZEND_TYPE_PURE_MASK(t) & ~MAY_BE_NULL) == 0; + } + + if (ZEND_TYPE_HAS_LIST(t) && (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_INTERSECTION_BIT)) { + return true; + } + + return false; +} + +static ZEND_COLD ZEND_NORETURN void zend_diamond_uninhabited_intersection_error( + const zend_class_entry *ce, + const zend_class_entry *defining_ce, + const zend_function *fn, + bool is_return_slot, + uint32_t param_slot_idx, + zend_type a, zend_type b) +{ + zend_string *a_str = zend_type_to_string_resolved(a, (zend_class_entry *) defining_ce); + zend_string *b_str = zend_type_to_string_resolved(b, (zend_class_entry *) defining_ce); + if (is_return_slot) { + zend_error_noreturn(E_COMPILE_ERROR, + "Diamond inheritance of %s::%s() in %s would require return type %s&%s, which is uninhabited; constrain the type parameter with an object bound", + ZSTR_VAL(defining_ce->name), ZSTR_VAL(fn->common.function_name), + ZSTR_VAL(ce->name), ZSTR_VAL(a_str), ZSTR_VAL(b_str)); + } else { + zend_error_noreturn(E_COMPILE_ERROR, + "Diamond inheritance of %s::%s() in %s would require parameter #%u type %s&%s, which is uninhabited; constrain the type parameter with an object bound", + ZSTR_VAL(defining_ce->name), ZSTR_VAL(fn->common.function_name), + ZSTR_VAL(ce->name), param_slot_idx + 1, ZSTR_VAL(a_str), ZSTR_VAL(b_str)); + } +} + +static zend_type zend_synth_variance_merged_type(zend_type a, zend_type b, bool intersect) +{ + if (intersect) { + uint32_t total = 0; + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_INTERSECTION_BIT)) { + total += ZEND_TYPE_LIST(a)->num_types; + } else { + total += 1; + } + + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_INTERSECTION_BIT)) { + total += ZEND_TYPE_LIST(b)->num_types; + } else { + total += 1; + } + + zend_type_list *list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(total)); + uint32_t idx = 0; + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_INTERSECTION_BIT)) { + const zend_type_list *al = ZEND_TYPE_LIST(a); + for (uint32_t k = 0; k < al->num_types; k++) { + list->types[idx] = al->types[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + } else { + list->types[idx] = a; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_INTERSECTION_BIT)) { + const zend_type_list *bl = ZEND_TYPE_LIST(b); + for (uint32_t k = 0; k < bl->num_types; k++) { + list->types[idx] = bl->types[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + } else { + list->types[idx] = b; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + list->num_types = idx; + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, list); + ZEND_TYPE_FULL_MASK(result) |= + _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | _ZEND_TYPE_INTERSECTION_BIT; + if (ZEND_TYPE_ALLOW_NULL(a) && ZEND_TYPE_ALLOW_NULL(b)) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + + return result; + } + + uint32_t mask = ZEND_TYPE_PURE_MASK(a) | ZEND_TYPE_PURE_MASK(b); + + const zend_type *a_complex_arr = NULL; + uint32_t a_complex_count = 0; + zend_type a_single = ZEND_TYPE_INIT_NONE(0); + if (ZEND_TYPE_HAS_LIST(a) && (ZEND_TYPE_FULL_MASK(a) & _ZEND_TYPE_UNION_BIT)) { + a_complex_arr = ZEND_TYPE_LIST(a)->types; + a_complex_count = ZEND_TYPE_LIST(a)->num_types; + } else if (ZEND_TYPE_HAS_NAME(a)) { + a_single = a; + a_single.type_mask &= ~_ZEND_TYPE_MAY_BE_MASK; + a_complex_arr = &a_single; + a_complex_count = 1; + } + + const zend_type *b_complex_arr = NULL; + uint32_t b_complex_count = 0; + zend_type b_single = ZEND_TYPE_INIT_NONE(0); + if (ZEND_TYPE_HAS_LIST(b) && (ZEND_TYPE_FULL_MASK(b) & _ZEND_TYPE_UNION_BIT)) { + b_complex_arr = ZEND_TYPE_LIST(b)->types; + b_complex_count = ZEND_TYPE_LIST(b)->num_types; + } else if (ZEND_TYPE_HAS_NAME(b)) { + b_single = b; + b_single.type_mask &= ~_ZEND_TYPE_MAY_BE_MASK; + b_complex_arr = &b_single; + b_complex_count = 1; + } + + uint32_t total = a_complex_count + b_complex_count; + zend_type result = ZEND_TYPE_INIT_NONE(0); + if (total == 0) { + ZEND_TYPE_FULL_MASK(result) |= mask; + return result; + } + + if (total == 1) { + const zend_type *only = a_complex_count ? a_complex_arr : b_complex_arr; + result = *only; + zend_type_copy_ctor(&result, /* use_arena */ true, /* persistent */ false); + ZEND_TYPE_FULL_MASK(result) |= mask; + return result; + } + + zend_type_list *list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(total)); + list->num_types = total; + uint32_t idx = 0; + for (uint32_t k = 0; k < a_complex_count; k++) { + list->types[idx] = a_complex_arr[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + for (uint32_t k = 0; k < b_complex_count; k++) { + list->types[idx] = b_complex_arr[k]; + zend_type_copy_ctor(&list->types[idx], /* use_arena */ true, /* persistent */ false); + idx++; + } + + ZEND_TYPE_SET_PTR(result, list); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_LIST_BIT | _ZEND_TYPE_ARENA_BIT | _ZEND_TYPE_UNION_BIT | mask; + return result; +} + +/* Declared covariant merges as intersection; contravariant as union; invariant + * falls back to use-site variance (return: intersection, param: union). + * Returns true and writes `*intersect_out` when `pre` is a class-origin T-ref. */ +static bool zend_generic_merge_polarity( + const zend_class_entry *defining_ce, + const zend_type *pre, + bool is_return_slot, + bool *intersect_out) +{ + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { + return false; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*pre); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE + || ref->index >= defining_ce->generic_parameters->count) { + return false; + } + switch (defining_ce->generic_parameters->parameters[ref->index].variance) { + case ZEND_GENERIC_VARIANCE_COVARIANT: + *intersect_out = true; + return true; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: + *intersect_out = false; + return true; + default: + *intersect_out = is_return_slot; + return true; + } +} + +static bool zend_iface_merge_slot_decision( + const zend_class_entry *defining_ce, + const zend_type *pre, + zend_type existing_type, + zend_type incoming_type, + const zend_type *hint_args, uint32_t hint_arity, + bool is_return_slot, + zend_type *b_out, bool *intersect_out) +{ + if (!zend_generic_merge_polarity(defining_ce, pre, is_return_slot, intersect_out)) { + return false; + } + + zend_type b = incoming_type; + if (hint_args) { + zend_type substituted = zend_substitute_leaf_type_param(*pre, hint_args, hint_arity); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(substituted)) { + b = substituted; + } + } + + if (zend_diamond_types_equal(existing_type, b)) { + return false; + } + + *b_out = b; + return true; +} + +static zend_function *zend_iface_build_merged_clone( + zend_function *existing, zend_function *incoming, + zend_class_entry *ce) +{ + if (!(ce->ce_flags & ZEND_ACC_INTERFACE) + || existing->type != ZEND_USER_FUNCTION + || incoming->type != ZEND_USER_FUNCTION + || existing->common.scope != incoming->common.scope) { + return NULL; + } + + zend_class_entry *defining_ce = existing->common.scope; + if (!defining_ce || !defining_ce->generic_parameters) { + return NULL; + } + + const zend_op_array *eop = &existing->op_array; + if (!eop->generic_types) { + return NULL; + } + + if (existing->common.num_args != incoming->common.num_args + || (existing->common.fn_flags & ZEND_ACC_VARIADIC) + != (incoming->common.fn_flags & ZEND_ACC_VARIADIC)) { + return NULL; + } + + uint32_t num_args = existing->common.num_args + ((existing->common.fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + bool has_return = (existing->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return NULL; + } + + const zend_arg_info *e_block = has_return ? existing->op_array.arg_info - 1 : existing->op_array.arg_info; + const zend_arg_info *i_block = has_return ? incoming->op_array.arg_info - 1 : incoming->op_array.arg_info; + uint32_t return_slot_offset = has_return ? 1 : 0; + + const zend_type *hint_args = NULL; + uint32_t hint_arity = 0; + if (CG(inheritance_binding_hint).target == defining_ce) { + hint_args = CG(inheritance_binding_hint).args; + hint_arity = CG(inheritance_binding_hint).arity; + } + + bool any_needs_merge = false; + if (has_return && eop->generic_types->return_type) { + zend_type b; bool intersect; + any_needs_merge = zend_iface_merge_slot_decision( + defining_ce, eop->generic_types->return_type, + e_block[0].type, i_block[0].type, + hint_args, hint_arity, /* is_return_slot */ true, + &b, &intersect); + } + + if (!any_needs_merge && eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) continue; + zend_type b; bool intersect; + if (zend_iface_merge_slot_decision( + defining_ce, (const zend_type *) Z_PTR_P(zv), + e_block[return_slot_offset + idx].type, + i_block[return_slot_offset + idx].type, + hint_args, hint_arity, /* is_return_slot */ false, + &b, &intersect)) { + any_needs_merge = true; + break; + } + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + if (!any_needs_merge) { + return NULL; + } + + zend_arg_info *new_block = zend_clone_arg_info_block(e_block, total); + + if (has_return && eop->generic_types->return_type) { + zend_type b; bool intersect; + if (zend_iface_merge_slot_decision( + defining_ce, eop->generic_types->return_type, + e_block[0].type, i_block[0].type, + hint_args, hint_arity, /* is_return_slot */ true, + &b, &intersect)) { + if (intersect && (!zend_type_intersectable(new_block[0].type) || !zend_type_intersectable(b))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ true, 0, + new_block[0].type, b); + } + + new_block[0].type = zend_synth_variance_merged_type(new_block[0].type, b, intersect); + } + } + + if (eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) continue; + zend_type b; bool intersect; + if (!zend_iface_merge_slot_decision( + defining_ce, (const zend_type *) Z_PTR_P(zv), + e_block[return_slot_offset + idx].type, + i_block[return_slot_offset + idx].type, + hint_args, hint_arity, /* is_return_slot */ false, + &b, &intersect)) { + continue; + } + + uint32_t slot = return_slot_offset + idx; + if (intersect && (!zend_type_intersectable(new_block[slot].type) || !zend_type_intersectable(b))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ false, (uint32_t) idx, + new_block[slot].type, b); + } + + new_block[slot].type = zend_synth_variance_merged_type(new_block[slot].type, b, intersect); + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + zend_function *merged_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(merged_fn, existing, sizeof(zend_op_array)); + merged_fn->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + merged_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + ZEND_MAP_PTR_INIT(merged_fn->op_array.run_time_cache, NULL); + ZEND_MAP_PTR_INIT(merged_fn->op_array.static_variables_ptr, NULL); + return merged_fn; +} + +static bool zend_iface_merge_generic_inherited_method( + zend_function *existing, zend_function *incoming, + zval *existing_zv, zend_class_entry *ce) +{ + zend_function *merged = zend_iface_build_merged_clone(existing, incoming, ce); + if (!merged) { + return false; + } + + Z_PTR_P(existing_zv) = merged; + return true; +} + +static bool zend_iface_merge_generic_inherited_hook( + zend_function *existing, zend_function *incoming, + zend_property_info *child_info, zend_property_hook_kind kind, + zend_class_entry *ce) +{ + zend_function *merged = zend_iface_build_merged_clone(existing, incoming, ce); + if (!merged) { + return false; + } + + child_info->hooks[kind] = merged; + return true; +} + +static void do_inherit_method(zend_string *key, zend_function *parent, zend_class_entry *ce, bool is_interface, uint32_t flags) /* {{{ */ +{ + zval *child = zend_hash_find_known_hash(&ce->function_table, key); + + if (child) { + zend_function *func = (zend_function*)Z_PTR_P(child); + + if (is_interface && UNEXPECTED(func == parent)) { + /* The same method in interface may be inherited few times */ + return; + } + + if (is_interface && zend_iface_merge_generic_inherited_method(func, parent, child, ce)) { + return; + } + + do_inheritance_check_on_method( + func, func->common.scope, parent, parent->common.scope, ce, child, flags); + } else { + + if (is_interface || (parent->common.fn_flags & (ZEND_ACC_ABSTRACT))) { + ce->ce_flags |= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS; + } + + zend_function *substituted = zend_maybe_substitute_inherited_method(parent, ce); + parent = substituted ? substituted : zend_duplicate_function(parent, ce); + + if (!is_interface) { + _zend_hash_append_ptr(&ce->function_table, key, parent); + } else { + zend_hash_add_new_ptr(&ce->function_table, key, parent); + } + } +} +/* }}} */ + +static inheritance_status full_property_types_compatible( + const zend_property_info *parent_info, const zend_property_info *child_info, + prop_variance variance) { if (ZEND_TYPE_PURE_MASK(parent_info->type) == ZEND_TYPE_PURE_MASK(child_info->type) && ZEND_TYPE_NAME(parent_info->type) == ZEND_TYPE_NAME(child_info->type)) { return INHERITANCE_SUCCESS; @@ -1418,6 +2715,10 @@ static void inherit_property_hook( ZSTR_VAL(parent->common.function_name)); } + if ((ce->ce_flags & ZEND_ACC_INTERFACE) && zend_iface_merge_generic_inherited_hook(child, parent, child_info, kind, ce)) { + return; + } + do_inheritance_check_on_method( child, child->common.scope, parent, parent->common.scope, ce, /* child */ NULL, ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_CHECK_VISIBILITY @@ -1573,7 +2874,81 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke } } - _zend_hash_append_ptr(&ce->properties_info, key, parent_info); + zend_property_info *info = parent_info; + if (parent_info->ce->generic_types && parent_info->ce->generic_types->properties) { + zval *zv = zend_hash_find(parent_info->ce->generic_types->properties, key); + if (zv && parent_info->ce->generic_parameters && parent_info->ce->generic_parameters->count > 0) { + const zend_type *pre_erasure = (const zend_type *) Z_PTR_P(zv); + uint32_t cap = parent_info->ce->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *bound_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t bound_arity = 0; + bool have_args = zend_get_inheritance_binding_full_cached(ce, parent_info->ce, bound_args, cap, &bound_arity); + if (!have_args) { + have_args = zend_get_target_default_args(parent_info->ce, bound_args, cap, &bound_arity); + } + + zend_type sub = have_args + ? zend_substitute_leaf_type_param(*pre_erasure, bound_args, bound_arity) + : (zend_type) ZEND_TYPE_INIT_NONE(0); + free_alloca(bound_args, use_heap); + if (have_args) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + zend_property_info *clone = zend_arena_alloc(&CG(arena), sizeof(*clone)); + *clone = *parent_info; + clone->flags |= ZEND_ACC_GENERIC_CLONE; + clone->type = sub; + zend_type_copy_ctor(&clone->type, /* use_arena */ true, /* persistent */ false); + /* The shallow copy borrows the name pointer from + * parent_info but doesn't addref it. zend_strings are + * refcounted and downstream paths (e.g. when this + * clone is destroyed) release the name, which would + * underflow the parent's count and corrupt the + * string. Take our own reference. */ + if (clone->name) { + zend_string_addref(clone->name); + } + + if (hooks && (hooks[ZEND_PROPERTY_HOOK_GET] || hooks[ZEND_PROPERTY_HOOK_SET])) { + zend_function **clone_hooks = zend_arena_alloc( + &CG(arena), ZEND_PROPERTY_HOOK_STRUCT_SIZE); + memcpy(clone_hooks, hooks, ZEND_PROPERTY_HOOK_STRUCT_SIZE); + + for (uint32_t hi = 0; hi < ZEND_PROPERTY_HOOK_COUNT; hi++) { + zend_function *orig = hooks[hi]; + if (!orig) continue; + + uint32_t num_args = orig->op_array.num_args; + if (orig->op_array.fn_flags & ZEND_ACC_VARIADIC) num_args++; + uint32_t total = num_args + 1; + + zend_arg_info *new_arg_info = zend_clone_arg_info_block(orig->op_array.arg_info - 1, total); + + uint32_t slot = (hi == ZEND_PROPERTY_HOOK_GET) ? 0 : 1; + new_arg_info[slot].type = sub; + zend_type_copy_ctor(&new_arg_info[slot].type, /* use_arena */ true, /* persistent */ false); + + zend_function *clone_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(clone_fn, orig, sizeof(zend_op_array)); + clone_fn->op_array.arg_info = new_arg_info + 1; + function_add_ref(clone_fn); + + clone_hooks[hi] = clone_fn; + } + + clone->hooks = clone_hooks; + } + + if (ZEND_TYPE_HAS_LIST(sub) || ZEND_TYPE_HAS_NAMED_WITH_ARGS(sub)) { + zend_type_release(sub, /* persistent */ false); + } + info = clone; + } + } + } + } + + _zend_hash_append_ptr(&ce->properties_info, key, info); } } /* }}} */ @@ -1838,6 +3213,41 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par zend_property_info *property_info; zend_string *key; + zend_inheritance_binding_cache binding_cache; + binding_cache.present = false; + zend_inheritance_binding_cache *prev_binding_cache = CG(inheritance_binding_cache); + CG(inheritance_binding_cache) = &binding_cache; + + if (parent_ce && !(ce->ce_flags & ZEND_ACC_INTERFACE)) { + const zend_type *extends_args = NULL; + uint32_t arity = 0; + if (ce->generic_types + && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + extends_args = ce->generic_types->extends; + arity = ZEND_TYPE_NAMED_WITH_ARGS(*extends_args)->count; + } + + /* If parent_ce is a synthesized monomorph (its name carries `<`), + * the type args were already validated against the base's bounds + * during synthesis. The side-table args we have here refer to the + * original base, not the mono — re-checking them against the + * mono's (empty) generic_parameters would spuriously fail. */ + if (!zend_class_is_monomorph(parent_ce)) { + if (arity > 0 || parent_ce->generic_parameters) { + zend_check_generic_link_arity(parent_ce, arity, "extends", ce->name); + } + + zend_check_generic_link_bounds(parent_ce, extends_args, "extends", ce); + } + } + + /* Monomorph synthesis (Box extending Box) is engine-internal and + * needs to bypass FINAL / READONLY-mismatch errors that would block a + * user `extends FinalClass`. The synthesized child re-acquires those + * bits after linking via the propagated/suppressed logic in + * zend_synthesize_monomorph. */ + bool mono_link = EG(monomorph_synthesis_active); if (UNEXPECTED(ce->ce_flags & ZEND_ACC_INTERFACE)) { /* Interface can only inherit other interfaces */ if (UNEXPECTED(!(parent_ce->ce_flags & ZEND_ACC_INTERFACE))) { @@ -1850,7 +3260,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot extend enum %s", ZSTR_VAL(ce->name), ZSTR_VAL(parent_ce->name)); } /* Class must not extend a final class */ - if (parent_ce->ce_flags & ZEND_ACC_FINAL) { + if (!mono_link && (parent_ce->ce_flags & ZEND_ACC_FINAL)) { zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot extend final class %s", ZSTR_VAL(ce->name), ZSTR_VAL(parent_ce->name)); } @@ -1862,7 +3272,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par } } - if (UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) { + if (!mono_link && UNEXPECTED((ce->ce_flags & ZEND_ACC_READONLY_CLASS) != (parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS))) { zend_error_noreturn(E_COMPILE_ERROR, "%s class %s cannot extend %s class %s", ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "Readonly" : "Non-readonly", ZSTR_VAL(ce->name), parent_ce->ce_flags & ZEND_ACC_READONLY_CLASS ? "readonly" : "non-readonly", ZSTR_VAL(parent_ce->name) @@ -2044,6 +3454,8 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par } } ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_READONLY_PROPS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES); + + CG(inheritance_binding_cache) = prev_binding_cache; } /* }}} */ @@ -2239,6 +3651,64 @@ ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry } /* }}} */ +static bool zend_iface_diamond_bindings_allowed( + const zend_class_entry *iface, + const zend_type *prior_args, uint32_t prior_arity, + const zend_type *cur_args, uint32_t cur_arity) +{ + if (!iface->generic_parameters || prior_arity != cur_arity) { + return false; + } + + for (uint32_t k = 0; k < prior_arity; k++) { + if (!zend_diamond_types_equal(prior_args[k], cur_args[k])) { + return true; + } + } + + return false; +} + +static bool zend_iface_diamond_direct_dup_allowed( + const zend_class_entry *ce, const zend_class_entry *iface, + uint32_t prior_clause_idx, uint32_t cur_clause_idx) +{ + const zend_type_named_with_args *prior = zend_get_implements_binding(ce, prior_clause_idx); + const zend_type_named_with_args *cur = zend_get_implements_binding(ce, cur_clause_idx); + if (!prior || !cur) { + return false; + } + + return zend_iface_diamond_bindings_allowed(iface, prior->args, prior->count, cur->args, cur->count); +} + +static bool zend_iface_diamond_parent_vs_own_allowed( + const zend_class_entry *ce, const zend_class_entry *iface, + uint32_t cur_clause_idx) +{ + if (!ce->parent || !iface->generic_parameters) { + return false; + } + + const zend_type_named_with_args *cur = zend_get_implements_binding(ce, cur_clause_idx); + if (!cur) { + return false; + } + + uint32_t cap = iface->generic_parameters->count; + if (cap == 0) { + return false; + } + + ALLOCA_FLAG(use_heap) + zend_type *prior_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t prior_arity; + bool allowed = zend_get_inheritance_binding_full(ce->parent, iface, prior_args, cap, &prior_arity) + && zend_iface_diamond_bindings_allowed(iface, prior_args, prior_arity, cur->args, cur->count); + free_alloca(prior_args, use_heap); + return allowed; +} + static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry **interfaces) /* {{{ */ { uint32_t num_parent_interfaces = ce->parent ? ce->parent->num_interfaces : 0; @@ -2259,12 +3729,20 @@ static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry for (uint32_t j = 0; j < num_interfaces; j++) { if (interfaces[j] == iface) { if (j >= num_parent_interfaces) { + if (zend_iface_diamond_direct_dup_allowed(ce, iface, j - num_parent_interfaces, i)) { + break; + } + efree(interfaces); zend_error_noreturn(E_COMPILE_ERROR, "%s %s cannot implement previously implemented interface %s", zend_get_object_type_uc(ce), ZSTR_VAL(ce->name), ZSTR_VAL(iface->name)); } + + if (zend_iface_diamond_parent_vs_own_allowed(ce, iface, i)) { + break; + } /* skip duplications */ ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&iface->constants_table, key, c) { do_inherit_constant_check(ce, c, key); @@ -2295,10 +3773,25 @@ static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry for (i = 0; i < num_parent_interfaces; i++) { do_implement_interface(ce, ce->interfaces[i]); } + /* Note that new interfaces can be added during this loop due to interface inheritance. * Use num_interfaces rather than ce->num_interfaces to not re-process the new ones. */ for (; i < num_interfaces; i++) { - do_interface_implementation(ce, ce->interfaces[i]); + zend_class_entry *iface_ce = ce->interfaces[i]; + const zend_type_named_with_args *binding = iface_ce->generic_parameters + ? zend_get_implements_binding(ce, i - num_parent_interfaces) + : NULL; + + if (binding) { + CG(inheritance_binding_hint).target = iface_ce; + CG(inheritance_binding_hint).args = binding->args; + CG(inheritance_binding_hint).arity = binding->count; + } + + do_interface_implementation(ce, iface_ce); + CG(inheritance_binding_hint).target = NULL; + CG(inheritance_binding_hint).args = NULL; + CG(inheritance_binding_hint).arity = 0; } } /* }}} */ @@ -2352,40 +3845,353 @@ static zend_class_entry *fixup_trait_scope(const zend_function *fn, zend_class_e return fn->common.scope->ce_flags & ZEND_ACC_TRAIT ? ce : fn->common.scope; } -static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_string *key, zend_function *fn) /* {{{ */ +static const zend_type_named_with_args *zend_find_trait_use_binding_by_name( + const zend_class_entry *ce, zend_string *trait_name) { - zend_function *existing_fn = NULL; - zend_function *new_fn; + if (!ce->generic_types || !ce->generic_types->trait_uses) return NULL; + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) continue; + const zend_type_named_with_args *nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (nwa->name && zend_string_equals_ci(nwa->name, trait_name)) { + return nwa; + } + } ZEND_HASH_FOREACH_END(); - if ((existing_fn = zend_hash_find_ptr(&ce->function_table, key)) != NULL) { - /* if it is the same function with the same visibility and has not been assigned a class scope yet, regardless - * of where it is coming from there is no conflict and we do not need to add it again */ - if (existing_fn->op_array.opcodes == fn->op_array.opcodes && - (existing_fn->common.fn_flags & ZEND_ACC_PPP_MASK) == (fn->common.fn_flags & ZEND_ACC_PPP_MASK) && - (existing_fn->common.scope->ce_flags & ZEND_ACC_TRAIT)) { - return; + return NULL; +} + +static zend_arg_info *zend_clone_arg_info_block(const zend_arg_info *orig_block, uint32_t total) +{ + 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); + for (uint32_t i = 0; i < total; i++) { + if (new_block[i].name) { + zend_string_addref(new_block[i].name); } - /* Abstract method signatures from the trait must be satisfied. */ - if (fn->common.fn_flags & ZEND_ACC_ABSTRACT) { - /* "abstract private" methods in traits were not available prior to PHP 8. - * As such, "abstract protected" was sometimes used to indicate trait requirements, - * even though the "implementing" method was private. Do not check visibility - * requirements to maintain backwards-compatibility with such usage. - */ - do_inheritance_check_on_method( - existing_fn, fixup_trait_scope(existing_fn, ce), fn, fixup_trait_scope(fn, ce), - ce, NULL, ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_RESET_CHILD_OVERRIDE); - return; + if (new_block[i].doc_comment) { + zend_string_addref(new_block[i].doc_comment); } - if (existing_fn->common.scope == ce) { - /* members from the current class override trait methods */ - return; - } else if (UNEXPECTED((existing_fn->common.scope->ce_flags & ZEND_ACC_TRAIT) - && !(existing_fn->common.fn_flags & ZEND_ACC_ABSTRACT))) { - /* two traits can't define the same non-abstract method */ - zend_error_noreturn(E_COMPILE_ERROR, "Trait method %s::%s has not been applied as %s::%s, because of collision with %s::%s", + zend_type_copy_ctor(&new_block[i].type, /* use_arena */ true, /* persistent */ false); + } + + return new_block; +} + +/* After substitution lands on a CLASS_LIKE T-ref into the using class itself + * (e.g., `use Holder` in `class Box` rewrites Holder's X to + * Box's T), the substituted T-ref must be erased to the using class's + * declared bound. Without this, the trait's original "unbound X → mixed" + * erasure leaks into the using class's signature and the runtime + * type-check accepts values that violate the bound. Returns the original + * type when the ref isn't a using-class T-ref we can erase. */ +static zend_type zend_erase_using_class_t_ref( + zend_type t, const zend_class_entry *using_ce) +{ + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + return t; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + if (ref->origin != ZEND_GENERIC_ORIGIN_CLASS_LIKE + || !using_ce->generic_parameters + || ref->index >= using_ce->generic_parameters->count) { + return t; + } + const zend_generic_parameter *gp = + &using_ce->generic_parameters->parameters[ref->index]; + zend_type erased = ZEND_TYPE_IS_SET(gp->bound) + ? gp->bound + : (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ANY); + if (ZEND_TYPE_FULL_MASK(t) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(erased) |= _ZEND_TYPE_NULLABLE_BIT; + } + return erased; +} + +static void zend_substitute_trait_method_arg_info( + zend_function *new_fn, const zend_function *orig_fn, + const zend_class_entry *using_ce, + const zend_type *bind_args, uint32_t bind_arity) +{ + if (orig_fn->type != ZEND_USER_FUNCTION) return; + const zend_op_array *orig_op = &orig_fn->op_array; + if (!orig_op->generic_types) { + return; + } + + if (!orig_op->generic_types->parameters && !orig_op->generic_types->return_type) { + return; + } + + uint32_t num_args = orig_op->num_args; + if (orig_op->fn_flags & ZEND_ACC_VARIADIC) num_args++; + bool has_return = (orig_op->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return; + } + + const zend_arg_info *orig_block = has_return ? orig_op->arg_info - 1 : orig_op->arg_info; + zend_arg_info *new_block = NULL; + + uint32_t return_slot_offset = has_return ? 1 : 0; + + /* The substituted `sub` zend_type comes from compiling the pre-erasure + * AST and carries only the type bits — none of the arg_info-level flags + * (send mode / by-ref, variadic, promoted, tentative) which live in the + * type_mask's extra-flag region. Preserve those from the original slot. */ + const uint32_t arg_extra_flags_mask = + (3u << _ZEND_SEND_MODE_SHIFT) + | _ZEND_IS_VARIADIC_BIT + | _ZEND_IS_PROMOTED_BIT + | _ZEND_IS_TENTATIVE_BIT; + + if (has_return && orig_op->generic_types->return_type) { + const zend_type *pre = orig_op->generic_types->return_type; + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*pre)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, bind_args, bind_arity); + sub = zend_erase_using_class_t_ref(sub, using_ce); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + if (!new_block) { + new_block = zend_clone_arg_info_block(orig_block, total); + } + uint32_t carry = ZEND_TYPE_FULL_MASK(new_block[0].type) & arg_extra_flags_mask; + new_block[0].type = sub; + ZEND_TYPE_FULL_MASK(new_block[0].type) |= carry; + zend_type_copy_ctor(&new_block[0].type, /* use_arena */ true, /* persistent */ false); + } + } + } + + if (orig_op->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(orig_op->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + zend_type *pre_erasure = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(*pre_erasure)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre_erasure, bind_args, bind_arity); + sub = zend_erase_using_class_t_ref(sub, using_ce); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + + if (!new_block) { + new_block = zend_clone_arg_info_block(orig_block, total); + } + uint32_t carry = ZEND_TYPE_FULL_MASK(new_block[return_slot_offset + idx].type) & arg_extra_flags_mask; + new_block[return_slot_offset + idx].type = sub; + ZEND_TYPE_FULL_MASK(new_block[return_slot_offset + idx].type) |= carry; + zend_type_copy_ctor(&new_block[return_slot_offset + idx].type, /* use_arena */ true, /* persistent */ false); + } ZEND_HASH_FOREACH_END(); + } + + if (new_block) { + new_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + } +} + +static const zend_type_named_with_args *zend_get_trait_use_binding_by_index( + const zend_class_entry *ce, uint32_t trait_idx) +{ + if (!ce->generic_types || !ce->generic_types->trait_uses) { + return NULL; + } + + zval *zv = zend_hash_index_find(ce->generic_types->trait_uses, trait_idx); + if (!zv) { + return NULL; + } + + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return NULL; + } + + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); +} + +static void zend_trait_diamond_merge_method( + zend_class_entry *ce, zend_string *key, + zend_function *existing, zend_function *fn, + const zend_type_named_with_args *binding) +{ + if (!binding || existing->type != ZEND_USER_FUNCTION) { + return; + } + + zend_class_entry *defining_ce = existing->common.scope; + if (!defining_ce || !defining_ce->generic_parameters) { + return; + } + + const zend_op_array *eop = &existing->op_array; + if (!eop->generic_types) { + return; + } + + uint32_t num_args = existing->common.num_args + ((existing->common.fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + bool has_return = (existing->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = num_args + (has_return ? 1 : 0); + if (total == 0) { + return; + } + + const zend_arg_info *e_block = has_return ? existing->op_array.arg_info - 1 : existing->op_array.arg_info; + uint32_t return_slot_offset = has_return ? 1 : 0; + + bool any_needs_merge = false; + if (has_return && eop->generic_types->return_type) { + const zend_type *pre = eop->generic_types->return_type; + bool intersect; + if (zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ true, &intersect)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(e_block[0].type, sub)) { + any_needs_merge = true; + } + } + } + + if (!any_needs_merge && eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + bool intersect; + if (!zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ false, &intersect)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(e_block[return_slot_offset + idx].type, sub)) { + any_needs_merge = true; + break; + } + } ZEND_HASH_FOREACH_END(); + + (void) idx; + } + + if (!any_needs_merge) { + return; + } + + zend_arg_info *new_block = zend_clone_arg_info_block(e_block, total); + + if (has_return && eop->generic_types->return_type) { + const zend_type *pre = eop->generic_types->return_type; + bool intersect; + if (zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ true, &intersect)) { + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(sub) && !zend_diamond_types_equal(new_block[0].type, sub)) { + if (intersect && (!zend_type_intersectable(new_block[0].type) || !zend_type_intersectable(sub))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ true, 0, + new_block[0].type, sub); + } + + new_block[0].type = zend_synth_variance_merged_type(new_block[0].type, sub, intersect); + } + } + } + + if (eop->generic_types->parameters) { + zval *zv; + zend_ulong idx; + ZEND_HASH_FOREACH_NUM_KEY_VAL(eop->generic_types->parameters, idx, zv) { + if (idx >= num_args) { + continue; + } + + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + bool intersect; + if (!zend_generic_merge_polarity(defining_ce, pre, /* is_return_slot */ false, &intersect)) { + continue; + } + + zend_type sub = zend_substitute_leaf_type_param(*pre, binding->args, binding->count); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + + uint32_t slot = return_slot_offset + idx; + if (zend_diamond_types_equal(new_block[slot].type, sub)) { + continue; + } + + if (intersect && (!zend_type_intersectable(new_block[slot].type) || !zend_type_intersectable(sub))) { + zend_diamond_uninhabited_intersection_error( + ce, defining_ce, existing, + /* is_return_slot */ false, (uint32_t) idx, + new_block[slot].type, sub); + } + + new_block[slot].type = zend_synth_variance_merged_type(new_block[slot].type, sub, intersect); + } ZEND_HASH_FOREACH_END(); + (void) idx; + } + + zend_function *merged_fn = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(merged_fn, existing, sizeof(zend_op_array)); + merged_fn->op_array.fn_flags &= ~ZEND_ACC_IMMUTABLE; + merged_fn->op_array.arg_info = has_return ? new_block + 1 : new_block; + ZEND_MAP_PTR_INIT(merged_fn->op_array.run_time_cache, NULL); + ZEND_MAP_PTR_INIT(merged_fn->op_array.static_variables_ptr, NULL); + zval *slot = zend_hash_find_known_hash(&ce->function_table, key); + ZEND_ASSERT(slot != NULL && Z_PTR_P(slot) == existing); + Z_PTR_P(slot) = merged_fn; +} + +static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_string *key, zend_function *fn, uint32_t trait_idx) /* {{{ */ +{ + zend_function *existing_fn = NULL; + zend_function *new_fn; + + if ((existing_fn = zend_hash_find_ptr(&ce->function_table, key)) != NULL) { + /* if it is the same function with the same visibility and has not been assigned a class scope yet, regardless + * of where it is coming from there is no conflict and we do not need to add it again */ + if (existing_fn->op_array.opcodes == fn->op_array.opcodes && + (existing_fn->common.fn_flags & ZEND_ACC_PPP_MASK) == (fn->common.fn_flags & ZEND_ACC_PPP_MASK) && + (existing_fn->common.scope->ce_flags & ZEND_ACC_TRAIT)) { + const zend_type_named_with_args *binding = zend_get_trait_use_binding_by_index(ce, trait_idx); + zend_trait_diamond_merge_method(ce, key, existing_fn, fn, binding); + return; + } + + /* Abstract method signatures from the trait must be satisfied. */ + if (fn->common.fn_flags & ZEND_ACC_ABSTRACT) { + /* "abstract private" methods in traits were not available prior to PHP 8. + * As such, "abstract protected" was sometimes used to indicate trait requirements, + * even though the "implementing" method was private. Do not check visibility + * requirements to maintain backwards-compatibility with such usage. + */ + do_inheritance_check_on_method( + existing_fn, fixup_trait_scope(existing_fn, ce), fn, fixup_trait_scope(fn, ce), + ce, NULL, ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_RESET_CHILD_OVERRIDE); + return; + } + + if (existing_fn->common.scope == ce) { + /* members from the current class override trait methods */ + return; + } else if (UNEXPECTED((existing_fn->common.scope->ce_flags & ZEND_ACC_TRAIT) + && !(existing_fn->common.fn_flags & ZEND_ACC_ABSTRACT))) { + /* two traits can't define the same non-abstract method */ + zend_error_noreturn(E_COMPILE_ERROR, "Trait method %s::%s has not been applied as %s::%s, because of collision with %s::%s", ZSTR_VAL(fn->common.scope->name), ZSTR_VAL(fn->common.function_name), ZSTR_VAL(ce->name), ZSTR_VAL(name), ZSTR_VAL(existing_fn->common.scope->name), ZSTR_VAL(existing_fn->common.function_name)); @@ -2406,6 +4212,28 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ /* Reassign method name, in case it is an alias. */ new_fn->common.function_name = name; function_add_ref(new_fn); + + if (fn->type == ZEND_USER_FUNCTION && fn->common.scope && fn->common.scope->generic_parameters) { + const zend_type_named_with_args *binding = zend_find_trait_use_binding_by_name(ce, fn->common.scope->name); + uint32_t cap = fn->common.scope->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *default_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + const zend_type *bind_args = NULL; + uint32_t bind_arity = 0; + if (binding) { + bind_args = binding->args; + bind_arity = binding->count; + } else if (zend_get_target_default_args(fn->common.scope, default_args, cap, &bind_arity)) { + bind_args = default_args; + } + + if (bind_args) { + zend_substitute_trait_method_arg_info(new_fn, fn, ce, bind_args, bind_arity); + } + + free_alloca(default_args, use_heap); + } + fn = zend_hash_update_ptr(&ce->function_table, key, new_fn); zend_add_magic_method(ce, fn, key); } @@ -2440,7 +4268,7 @@ static void zend_traits_check_private_final_inheritance(uint32_t original_fn_fla } } -static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, zend_class_entry *ce, HashTable *exclude_table, zend_class_entry **aliases) /* {{{ */ +static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, zend_class_entry *ce, HashTable *exclude_table, zend_class_entry **aliases, uint32_t trait_idx) /* {{{ */ { zend_trait_alias *alias, **alias_ptr; zend_function fn_copy; @@ -2467,7 +4295,7 @@ static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, z zend_traits_check_private_final_inheritance(fn->common.fn_flags, &fn_copy, alias->alias); zend_string *lcname = zend_string_tolower(alias->alias); - zend_add_trait_method(ce, alias->alias, lcname, &fn_copy); + zend_add_trait_method(ce, alias->alias, lcname, &fn_copy, trait_idx); zend_string_release_ex(lcname, 0); } alias_ptr++; @@ -2505,7 +4333,7 @@ static void zend_traits_copy_functions(zend_string *fnname, zend_function *fn, z zend_traits_check_private_final_inheritance(fn->common.fn_flags, &fn_copy, fnname); - zend_add_trait_method(ce, fn->common.function_name, fnname, &fn_copy); + zend_add_trait_method(ce, fn->common.function_name, fnname, &fn_copy, trait_idx); } } /* }}} */ @@ -2695,7 +4523,7 @@ static void zend_do_traits_method_binding(zend_class_entry *ce, zend_class_entry if (verify_abstract != is_abstract) { continue; } - zend_traits_copy_functions(key, fn, ce, exclude_tables[i], aliases); + zend_traits_copy_functions(key, fn, ce, exclude_tables[i], aliases, i); } ZEND_HASH_FOREACH_END(); if (exclude_tables[i]) { @@ -2714,7 +4542,7 @@ static void zend_do_traits_method_binding(zend_class_entry *ce, zend_class_entry if (verify_abstract != is_abstract) { continue; } - zend_traits_copy_functions(key, fn, ce, NULL, aliases); + zend_traits_copy_functions(key, fn, ce, NULL, aliases, i); } ZEND_HASH_FOREACH_END(); } } @@ -2966,6 +4794,25 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent zend_string *doc_comment = property_info->doc_comment ? zend_string_copy(property_info->doc_comment) : NULL; zend_type type = property_info->type; + const zend_type *pre_erasure = zend_get_trait_property_pre_erasure(traits[i], prop_name); + if (pre_erasure && traits[i]->generic_parameters && traits[i]->generic_parameters->count > 0) { + const zend_type *bind_args; + uint32_t bind_arity; + uint32_t cap = traits[i]->generic_parameters->count; + ALLOCA_FLAG(use_heap) + zend_type *default_args = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + if (zend_get_trait_use_binding(ce, i, &bind_args, &bind_arity)) { + type = zend_substitute_leaf_type_param(*pre_erasure, bind_args, bind_arity); + } else if (zend_get_target_default_args(traits[i], default_args, cap, &bind_arity)) { + type = zend_substitute_leaf_type_param(*pre_erasure, default_args, bind_arity); + } + + free_alloca(default_args, use_heap); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + type = property_info->type; + } + } + /* Assumption: only userland classes can use traits, as such the type must be arena allocated */ zend_type_copy_ctor(&type, /* use arena */ true, /* persistent */ false); zend_property_info *new_prop = zend_declare_typed_property(ce, prop_name, prop_value, flags, doc_comment, type); @@ -3224,7 +5071,7 @@ static void add_property_hook_obligation( static void resolve_delayed_variance_obligations(zend_class_entry *ce); -static void check_variance_obligation(const variance_obligation *obligation) { +static void check_variance_obligation(zend_class_entry *ce, const variance_obligation *obligation) { if (obligation->type == OBLIGATION_DEPENDENCY) { zend_class_entry *dependency_ce = obligation->dependency_ce; if (dependency_ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { @@ -3238,7 +5085,7 @@ static void check_variance_obligation(const variance_obligation *obligation) { } else if (obligation->type == OBLIGATION_COMPATIBILITY) { inheritance_status status = zend_do_perform_implementation_check( &obligation->child_fn, obligation->child_scope, - &obligation->parent_fn, obligation->parent_scope); + &obligation->parent_fn, obligation->parent_scope, ce); if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { emit_incompatible_method_error( &obligation->child_fn, obligation->child_scope, @@ -3301,7 +5148,7 @@ static void resolve_delayed_variance_obligations(zend_class_entry *ce) { variance_obligation *obligation; ZEND_HASH_FOREACH_PTR(obligations, obligation) { - check_variance_obligation(obligation); + check_variance_obligation(ce, obligation); } ZEND_HASH_FOREACH_END(); zend_inheritance_check_override(ce); @@ -3482,548 +5329,2971 @@ static zend_class_entry *zend_lazy_class_load(const zend_class_entry *pce) } while (0) #endif -ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key) /* {{{ */ +static void zend_check_generic_link_arity( + const zend_class_entry *target_ce, + uint32_t arity, + const char *clause, + zend_string *child_name) { - /* Load parent/interface dependencies first, so we can still gracefully abort linking - * with an exception and remove the class from the class table. This is only possible - * if no variance obligations on the current class have been added during autoloading. */ - zend_class_entry *parent = NULL; - zend_class_entry **traits_and_interfaces = NULL; - zend_class_entry *proto = NULL; - zend_class_entry *orig_linking_class; - uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; - uint32_t i, j; - zval *zv; - ALLOCA_FLAG(use_heap) - - SET_ALLOCA_FLAG(use_heap); - ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_LINKED)); - - if (ce->parent_name) { - parent = zend_fetch_class_by_name( - ce->parent_name, lc_parent_name, - ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); - if (!parent) { - check_unrecoverable_load_failure(ce); - return NULL; + uint32_t total = target_ce->generic_parameters ? target_ce->generic_parameters->count : 0; + uint32_t required = 0; + if (target_ce->generic_parameters) { + while (required < total + && !ZEND_TYPE_IS_SET(target_ce->generic_parameters->parameters[required].default_type)) { + required++; } - UPDATE_IS_CACHEABLE(parent); } - if (ce->num_traits || ce->num_interfaces) { - traits_and_interfaces = do_alloca(sizeof(zend_class_entry*) * (ce->num_traits + ce->num_interfaces), use_heap); + if (arity > total) { + zend_error_noreturn(E_COMPILE_ERROR, + "Too many generic type arguments to %s %s in %s, %u passed and %s %u expected", + clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(child_name), arity, + required == total ? "exactly" : "at most", total); + } - for (i = 0; i < ce->num_traits; i++) { - zend_class_entry *trait = zend_fetch_class_by_name(ce->trait_names[i].name, - ce->trait_names[i].lc_name, ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); - if (UNEXPECTED(trait == NULL)) { - free_alloca(traits_and_interfaces, use_heap); - return NULL; - } - if (UNEXPECTED(!(trait->ce_flags & ZEND_ACC_TRAIT))) { - zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(trait->name)); - free_alloca(traits_and_interfaces, use_heap); - return NULL; - } - if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { - zend_use_of_deprecated_trait(trait, ce->name); - if (UNEXPECTED(EG(exception))) { - free_alloca(traits_and_interfaces, use_heap); - return NULL; - } - } - for (j = 0; j < i; j++) { - if (traits_and_interfaces[j] == trait) { - /* skip duplications */ - trait = NULL; - break; - } - } - traits_and_interfaces[i] = trait; - if (trait) { - UPDATE_IS_CACHEABLE(trait); - } - } + if (arity < required) { + zend_error_noreturn(E_COMPILE_ERROR, + "Too few generic type arguments to %s %s in %s, %u passed and %s %u expected", + clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(child_name), arity, + required == total ? "exactly" : "at least", required); } +} - if (ce->num_interfaces) { - for (i = 0; i < ce->num_interfaces; i++) { - zend_class_entry *iface = zend_fetch_class_by_name( - ce->interface_names[i].name, ce->interface_names[i].lc_name, - ZEND_FETCH_CLASS_INTERFACE | - ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); - if (!iface) { - check_unrecoverable_load_failure(ce); - free_alloca(traits_and_interfaces, use_heap); - return NULL; - } - traits_and_interfaces[ce->num_traits + i] = iface; - if (iface) { - UPDATE_IS_CACHEABLE(iface); - } - } +static const zend_type *zend_lookup_inheritance_args(const HashTable *side_table, zend_ulong idx) +{ + if (!side_table) { + return NULL; } -#ifndef ZEND_WIN32 - if (ce->ce_flags & ZEND_ACC_ENUM) { - /* We will add internal methods. */ - is_cacheable = false; + zval *zv = zend_hash_index_find(side_table, idx); + if (!zv) { + return NULL; } -#endif - if (ce->ce_flags & ZEND_ACC_IMMUTABLE && is_cacheable) { - if (zend_inheritance_cache_get && zend_inheritance_cache_add) { - zend_class_entry *ret = zend_inheritance_cache_get(ce, parent, traits_and_interfaces); - if (ret) { - if (traits_and_interfaces) { - free_alloca(traits_and_interfaces, use_heap); - } - zv = zend_hash_find_known_hash(CG(class_table), key); - Z_CE_P(zv) = ret; - return ret; - } - } else { - is_cacheable = 0; - } - proto = ce; + const zend_type *boxed = (const zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return NULL; } - /* Delay and record warnings (such as deprecations) thrown during - * inheritance, so they will be recorded in the inheritance cache. - * Warnings must be delayed in all cases so that we get a consistent - * behavior regardless of cacheability. */ - bool orig_record_errors = EG(record_errors); - if (!orig_record_errors) { - zend_begin_record_errors(); + return boxed; +} + +typedef enum _zend_variance_polarity { + ZEND_VAR_POL_COVARIANT, + ZEND_VAR_POL_CONTRAVARIANT, + ZEND_VAR_POL_INVARIANT, +} zend_variance_polarity; + +static zend_variance_polarity zend_variance_compose( + zend_variance_polarity outer, zend_variance_polarity slot) +{ + if (outer == ZEND_VAR_POL_INVARIANT || slot == ZEND_VAR_POL_INVARIANT) { + return ZEND_VAR_POL_INVARIANT; } + return (outer == slot) ? ZEND_VAR_POL_COVARIANT : ZEND_VAR_POL_CONTRAVARIANT; +} - zend_try { - if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { - /* Lazy class loading */ - ce = zend_lazy_class_load(ce); - zv = zend_hash_find_known_hash(CG(class_table), key); - Z_CE_P(zv) = ce; - } else if (ce->ce_flags & ZEND_ACC_FILE_CACHED) { - /* Lazy class loading */ - ce = zend_lazy_class_load(ce); - ce->ce_flags &= ~ZEND_ACC_FILE_CACHED; - zv = zend_hash_find_known_hash(CG(class_table), key); - Z_CE_P(zv) = ce; - } +static zend_variance_polarity zend_variance_polarity_from(zend_generic_variance v) +{ + switch (v) { + case ZEND_GENERIC_VARIANCE_COVARIANT: return ZEND_VAR_POL_COVARIANT; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: return ZEND_VAR_POL_CONTRAVARIANT; + default: return ZEND_VAR_POL_INVARIANT; + } +} - if (CG(unlinked_uses)) { - zend_hash_index_del(CG(unlinked_uses), (zend_ulong)(uintptr_t) ce); - } +static const char *zend_variance_polarity_name(zend_variance_polarity p) +{ + switch (p) { + case ZEND_VAR_POL_COVARIANT: return "covariant"; + case ZEND_VAR_POL_CONTRAVARIANT: return "contravariant"; + default: return "invariant"; + } +} - orig_linking_class = CG(current_linking_class); - CG(current_linking_class) = is_cacheable ? ce : NULL; +static const char *zend_variance_marker(zend_generic_variance v) +{ + switch (v) { + case ZEND_GENERIC_VARIANCE_COVARIANT: return "+"; + case ZEND_GENERIC_VARIANCE_CONTRAVARIANT: return "-"; + default: return ""; + } +} - if (ce->ce_flags & ZEND_ACC_ENUM) { - /* Only register builtin enum methods during inheritance to avoid persisting them in - * opcache. */ - zend_enum_register_funcs(ce); - } +static bool zend_variance_compatible( + zend_generic_variance declared, zend_variance_polarity at) +{ + if (declared == ZEND_GENERIC_VARIANCE_INVARIANT) { + return true; + } -#ifdef ZEND_OPCACHE_SHM_REATTACHMENT - zend_link_hooked_object_iter(ce); -#endif + if (at == ZEND_VAR_POL_INVARIANT) { + return false; + } - HashTable **trait_exclude_tables; - zend_class_entry **trait_aliases; - bool trait_contains_abstract_methods = false; - if (ce->num_traits) { - zend_traits_init_trait_structures(ce, traits_and_interfaces, &trait_exclude_tables, &trait_aliases); - zend_do_traits_method_binding(ce, traits_and_interfaces, trait_exclude_tables, trait_aliases, false, &trait_contains_abstract_methods); - zend_do_traits_constant_binding(ce, traits_and_interfaces); - zend_do_traits_property_binding(ce, traits_and_interfaces); + return zend_variance_polarity_from(declared) == at; +} - zend_function *fn; - ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { - zend_fixup_trait_method(fn, ce); - } ZEND_HASH_FOREACH_END(); +static void zend_variance_walk( + const zend_generic_parameter_list *class_params, + const zend_generic_parameter_list *func_params, + zend_type t, + zend_variance_polarity pol) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(t)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(t); + const zend_generic_parameter_list *params = + (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) ? class_params : func_params; + if (!params) { + return; } - if (parent) { - if (!(parent->ce_flags & ZEND_ACC_LINKED)) { - add_dependency_obligation(ce, parent); - } - zend_do_inheritance(ce, parent); + + ZEND_ASSERT(ref->index < params->count); + zend_generic_variance declared = params->parameters[ref->index].variance; + if (!zend_variance_compatible(declared, pol)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Type parameter %s declared %s (%sT) cannot appear in %s position", + ZSTR_VAL(ref->name), + zend_variance_polarity_name(zend_variance_polarity_from(declared)), + zend_variance_marker(declared), + zend_variance_polarity_name(pol)); } - if (ce->num_traits) { - if (trait_contains_abstract_methods) { - zend_do_traits_method_binding(ce, traits_and_interfaces, trait_exclude_tables, trait_aliases, true, &trait_contains_abstract_methods); - /* New abstract methods may have been added, make sure to add - * ZEND_ACC_IMPLICIT_ABSTRACT_CLASS to ce. */ - zend_function *fn; - ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { - zend_fixup_trait_method(fn, ce); - } ZEND_HASH_FOREACH_END(); - } + return; + } - if (trait_exclude_tables) { - for (i = 0; i < ce->num_traits; i++) { - if (traits_and_interfaces[i]) { - if (trait_exclude_tables[i]) { - zend_hash_destroy(trait_exclude_tables[i]); - FREE_HASHTABLE(trait_exclude_tables[i]); - } + if (ZEND_TYPE_HAS_LIST(t)) { + const zend_type_list *list = ZEND_TYPE_LIST(t); + for (uint32_t i = 0; i < list->num_types; i++) { + zend_variance_walk(class_params, func_params, list->types[i], pol); + } + + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(t)) { + const zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(t); + zend_class_entry *target = NULL; + if (named->name) { + zend_class_entry *active = CG(active_class_entry); + if (active) { + if (zend_string_equals_literal_ci(named->name, "self") + || zend_string_equals_literal_ci(named->name, "static") + || (active->name && zend_string_equals_ci(named->name, active->name))) { + target = active; + } else if (zend_string_equals_literal_ci(named->name, "parent")) { + if (active->ce_flags & ZEND_ACC_RESOLVED_PARENT) { + target = active->parent; + } else if (active->parent_name) { + target = zend_lookup_class_ex(active->parent_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); } } - efree(trait_exclude_tables); - } - if (trait_aliases) { - efree(trait_aliases); } - } - if (ce->num_interfaces) { - /* Also copy the parent interfaces here, so we don't need to reallocate later. */ - uint32_t num_parent_interfaces = parent ? parent->num_interfaces : 0; - zend_class_entry **interfaces = emalloc( - sizeof(zend_class_entry *) * (ce->num_interfaces + num_parent_interfaces)); - if (num_parent_interfaces) { - memcpy(interfaces, parent->interfaces, - sizeof(zend_class_entry *) * num_parent_interfaces); + if (!target) { + target = zend_lookup_class_ex(named->name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); } - memcpy(interfaces + num_parent_interfaces, traits_and_interfaces + ce->num_traits, - sizeof(zend_class_entry *) * ce->num_interfaces); - - zend_do_implement_interfaces(ce, interfaces); - } else if (parent && parent->num_interfaces) { - zend_do_inherit_interfaces(ce, parent); } - if (!(ce->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT)) - && (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) - ) { - zend_verify_abstract_class(ce); + + if (!target || !target->generic_parameters) { + return; } - if (ce->ce_flags & ZEND_ACC_ENUM) { - zend_verify_enum(ce); + + for (uint32_t i = 0; i < named->count; i++) { + zend_variance_polarity slot = i < target->generic_parameters->count + ? zend_variance_polarity_from(target->generic_parameters->parameters[i].variance) + : ZEND_VAR_POL_INVARIANT; + + zend_variance_walk(class_params, func_params, named->args[i], zend_variance_compose(pol, slot)); } - if (ce->num_hooked_prop_variance_checks) { - const zend_property_info *prop_info; - ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop_info) { - if (prop_info->ce == ce && prop_info->hooks && prop_info->hooks[ZEND_PROPERTY_HOOK_SET]) { - switch (zend_verify_property_hook_variance(prop_info, prop_info->hooks[ZEND_PROPERTY_HOOK_SET])) { - case INHERITANCE_SUCCESS: - break; - case INHERITANCE_ERROR: - zend_hooked_property_variance_error(prop_info); - break; - case INHERITANCE_UNRESOLVED: - add_property_hook_obligation(ce, prop_info, prop_info->hooks[ZEND_PROPERTY_HOOK_SET]); - break; - case INHERITANCE_WARNING: - ZEND_UNREACHABLE(); - } - } + + return; + } +} + +static void zend_variance_walk_function( + const zend_generic_parameter_list *class_params, + const zend_generic_parameter_list *func_params, + const zend_op_array *op_array) +{ + if (!op_array->generic_types && !op_array->generic_parameters) { + return; + } + + if (op_array->generic_types) { + if (op_array->generic_types->parameters) { + zval *zv; + zend_ulong h; + ZEND_HASH_FOREACH_NUM_KEY_VAL(op_array->generic_types->parameters, h, zv) { + const zend_type *t = (const zend_type *) Z_PTR_P(zv); + zend_variance_walk(class_params, func_params, *t, ZEND_VAR_POL_CONTRAVARIANT); } ZEND_HASH_FOREACH_END(); + (void) h; } - /* Normally Stringable is added during compilation. However, if it is imported from a trait, - * we need to explicitly add the interface here. */ - if (ce->__tostring && !(ce->ce_flags & ZEND_ACC_TRAIT) - && !zend_class_implements_interface(ce, zend_ce_stringable)) { - ZEND_ASSERT(ce->__tostring->common.fn_flags & ZEND_ACC_TRAIT_CLONE); - ce->ce_flags |= ZEND_ACC_RESOLVED_INTERFACES; - ce->num_interfaces++; - ce->interfaces = perealloc(ce->interfaces, - sizeof(zend_class_entry *) * ce->num_interfaces, ce->type == ZEND_INTERNAL_CLASS); - ce->interfaces[ce->num_interfaces - 1] = zend_ce_stringable; - do_interface_implementation(ce, zend_ce_stringable); + if (op_array->generic_types->return_type) { + zend_variance_walk(class_params, func_params, *op_array->generic_types->return_type, ZEND_VAR_POL_COVARIANT); } + } - zend_build_properties_info_table(ce); - } zend_catch { - /* Do not leak recorded errors to the next linked class. */ - if (!orig_record_errors) { - EG(record_errors) = false; - zend_free_recorded_errors(); - } - zend_bailout(); - } zend_end_try(); + if (op_array->generic_parameters) { + for (uint32_t i = 0; i < op_array->generic_parameters->count; i++) { + const zend_generic_parameter *p = &op_array->generic_parameters->parameters[i]; + if (ZEND_TYPE_IS_SET(p->bound_pre_erasure)) { + zend_variance_walk(class_params, func_params, p->bound_pre_erasure, ZEND_VAR_POL_INVARIANT); + } - EG(record_errors) = orig_record_errors; + if (ZEND_TYPE_IS_SET(p->default_pre_erasure)) { + zend_variance_walk(class_params, func_params, p->default_pre_erasure, ZEND_VAR_POL_INVARIANT); + } + } + } +} - if (!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)) { - zend_inheritance_check_override(ce); - ce->ce_flags |= ZEND_ACC_LINKED; - } else { - ce->ce_flags |= ZEND_ACC_NEARLY_LINKED; - if (CG(current_linking_class)) { - ce->ce_flags |= ZEND_ACC_CACHEABLE; +static zend_variance_polarity zend_variance_polarity_for_property(const zend_property_info *prop) +{ + if (prop->hooks) { + const zend_function *get = prop->hooks[ZEND_PROPERTY_HOOK_GET]; + const zend_function *set = prop->hooks[ZEND_PROPERTY_HOOK_SET]; + bool by_ref_get = get && (get->common.fn_flags & ZEND_ACC_RETURN_REFERENCE); + if (by_ref_get || (get && set)) { + return ZEND_VAR_POL_INVARIANT; } - load_delayed_classes(ce); - if (ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { - resolve_delayed_variance_obligations(ce); + + if (get) { + return ZEND_VAR_POL_COVARIANT; } - /* Delayed variance resolution can re-enter linking before the full - * hierarchy is linked. See ext/opcache/tests/gh20469*.phpt. */ - if (CG(unlinked_uses) && zend_hash_index_exists(CG(unlinked_uses), (zend_long)(uintptr_t) ce)) { - ce->ce_flags &= ~ZEND_ACC_CACHEABLE; + + if (set) { + return ZEND_VAR_POL_CONTRAVARIANT; } - if (ce->ce_flags & ZEND_ACC_CACHEABLE) { - ce->ce_flags &= ~ZEND_ACC_CACHEABLE; - } else { - CG(current_linking_class) = NULL; + + return ZEND_VAR_POL_INVARIANT; + } + + if (prop->flags & ZEND_ACC_READONLY) { + return ZEND_VAR_POL_COVARIANT; + } + + return ZEND_VAR_POL_INVARIANT; +} + +void zend_check_generic_variance_markers(zend_class_entry *ce) +{ + if (!ce->generic_parameters) { + return; + } + + bool any_marked = false; + for (uint32_t i = 0; i < ce->generic_parameters->count; i++) { + if (ce->generic_parameters->parameters[i].variance != ZEND_GENERIC_VARIANCE_INVARIANT) { + any_marked = true; + break; } } - bool was_cacheable = is_cacheable; - if (!CG(current_linking_class)) { - is_cacheable = 0; + if (!any_marked) { + return; } - CG(current_linking_class) = orig_linking_class; - if (is_cacheable) { - HashTable *ht = (HashTable*)ce->inheritance_cache; - zend_class_entry *new_ce; + const zend_generic_parameter_list *class_params = ce->generic_parameters; + uint32_t orig_lineno = CG(zend_lineno); - ce->inheritance_cache = NULL; - new_ce = zend_inheritance_cache_add(ce, proto, parent, traits_and_interfaces, ht); - if (new_ce) { - zv = zend_hash_find_known_hash(CG(class_table), key); - ce = new_ce; - Z_CE_P(zv) = ce; + for (uint32_t i = 0; i < class_params->count; i++) { + const zend_generic_parameter *p = &class_params->parameters[i]; + if (ZEND_TYPE_IS_SET(p->bound_pre_erasure)) { + zend_variance_walk(class_params, NULL, p->bound_pre_erasure, ZEND_VAR_POL_INVARIANT); } - if (ht) { - zend_hash_destroy(ht); - FREE_HASHTABLE(ht); + + if (ZEND_TYPE_IS_SET(p->default_pre_erasure)) { + zend_variance_walk(class_params, NULL, p->default_pre_erasure, ZEND_VAR_POL_INVARIANT); } - } else if (was_cacheable && ce->inheritance_cache) { - /* Cacheability can be disabled after dependency tracking prepared - * an inheritance-cache dependency table. Discard it here. */ - HashTable *ht = (HashTable*)ce->inheritance_cache; - ce->inheritance_cache = NULL; - zend_hash_destroy(ht); - FREE_HASHTABLE(ht); } - if (!orig_record_errors) { - zend_emit_recorded_errors(); - zend_free_recorded_errors(); + zend_function *fn; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { + if (fn->common.fn_flags & (ZEND_ACC_STATIC | ZEND_ACC_CTOR)) { + continue; + } + + if (fn->common.scope != ce) { + continue; + } + + if (!ZEND_USER_CODE(fn->common.type)) { + continue; + } + + CG(zend_lineno) = fn->op_array.line_start; + zend_variance_walk_function(class_params, NULL, &fn->op_array); + } ZEND_HASH_FOREACH_END(); + CG(zend_lineno) = orig_lineno; + + if (ce->generic_types && ce->generic_types->properties) { + zend_property_info *prop_info; + ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) { + if (prop_info->ce != ce) { + continue; + } + + zend_variance_polarity prop_pol = zend_variance_polarity_for_property(prop_info); + zval *pre_zv = zend_hash_find(ce->generic_types->properties, prop_info->name); + if (pre_zv) { + const zend_type *pre = (const zend_type *) Z_PTR_P(pre_zv); + zend_variance_walk(class_params, NULL, *pre, prop_pol); + } + + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + zend_function *hook = prop_info->hooks[i]; + if (hook && ZEND_USER_CODE(hook->common.type)) { + CG(zend_lineno) = hook->op_array.line_start; + zend_variance_walk_function(class_params, NULL, &hook->op_array); + } + } + + CG(zend_lineno) = orig_lineno; + } + } ZEND_HASH_FOREACH_END(); } - if (traits_and_interfaces) { - free_alloca(traits_and_interfaces, use_heap); + + if (ce->generic_types) { + if (ce->generic_types->extends) { + zend_variance_walk(class_params, NULL, *ce->generic_types->extends, ZEND_VAR_POL_COVARIANT); + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_variance_walk(class_params, NULL, *(const zend_type *) Z_PTR_P(zv), ZEND_VAR_POL_COVARIANT); + } ZEND_HASH_FOREACH_END(); + } + + if (ce->generic_types->trait_uses) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->trait_uses, zv) { + zend_variance_walk(class_params, NULL, *(const zend_type *) Z_PTR_P(zv), ZEND_VAR_POL_COVARIANT); + } ZEND_HASH_FOREACH_END(); + } + } +} + +void zend_check_function_variance_markers(zend_op_array *op_array) +{ + if (!op_array->generic_parameters) { + return; + } + + bool any_marked = false; + for (uint32_t i = 0; i < op_array->generic_parameters->count; i++) { + if (op_array->generic_parameters->parameters[i].variance != ZEND_GENERIC_VARIANCE_INVARIANT) { + any_marked = true; + break; + } + } + + if (!any_marked) { + return; + } + + zend_variance_walk_function(NULL, op_array->generic_parameters, op_array); +} + +static void zend_check_generic_link_bounds( + zend_class_entry *target_ce, + const zend_type *args_box, + const char *clause, + zend_class_entry *ce) +{ + if (!args_box || !target_ce->generic_parameters) { + return; + } + + const zend_type_named_with_args *args = ZEND_TYPE_NAMED_WITH_ARGS(*args_box); + uint32_t check_count = args->count; + if (check_count > target_ce->generic_parameters->count) { + check_count = target_ce->generic_parameters->count; + } + + for (uint32_t i = 0; i < check_count; i++) { + zend_type bound = target_ce->generic_parameters->parameters[i].bound; + if (!ZEND_TYPE_IS_SET(bound)) { + continue; + } + + zend_type arg = args->args[i]; + zend_type effective = arg; + bool ce_bound_unset = false; + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(arg)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(arg); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE + && ce->generic_parameters + && ref->index < ce->generic_parameters->count) { + zend_type ce_bound = ce->generic_parameters->parameters[ref->index].bound; + if (ZEND_TYPE_IS_SET(ce_bound)) { + effective = ce_bound; + } else { + ce_bound_unset = true; + } + } + } + + if (ce_bound_unset || zend_check_generic_arg_satisfies_bound(ce, effective, target_ce, bound) != INHERITANCE_SUCCESS) { + zend_type bound_display = ZEND_TYPE_IS_SET(target_ce->generic_parameters->parameters[i].bound_pre_erasure) + ? target_ce->generic_parameters->parameters[i].bound_pre_erasure + : bound; + zend_string *bound_str = zend_type_to_string(bound_display); + zend_string *arg_str; + if (ZEND_TYPE_HAS_TYPE_PARAMETER(arg)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(arg); + arg_str = ref->name ? zend_string_copy(ref->name) : zend_string_init("T", 1, 0); + } else { + arg_str = zend_type_to_string(arg); + } + + const char *param_name = + target_ce->generic_parameters->parameters[i].name + ? ZSTR_VAL(target_ce->generic_parameters->parameters[i].name) + : "?"; + zend_error_noreturn(E_COMPILE_ERROR, + "Type argument %u to %s %s in %s does not satisfy the bound %s on parameter %s, %s given", + i + 1, clause, ZSTR_VAL(target_ce->name), ZSTR_VAL(ce->name), + ZSTR_VAL(bound_str), param_name, ZSTR_VAL(arg_str)); + zend_string_release(bound_str); + zend_string_release(arg_str); + } + } +} + +static uint32_t zend_lookup_inheritance_arity(const HashTable *side_table, zend_ulong idx) +{ + if (!side_table) { + return 0; + } + + zval *zv = zend_hash_index_find(side_table, idx); + if (!zv) { + return 0; + } + + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return 0; + } + + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed)->count; +} + +static bool zend_diamond_types_equal(zend_type a, zend_type b) +{ + if (ZEND_TYPE_PURE_MASK(a) != ZEND_TYPE_PURE_MASK(b)) { + return false; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(a)) { + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(b)) { + return false; + } + const zend_type_named_with_args *na = ZEND_TYPE_NAMED_WITH_ARGS(a); + const zend_type_named_with_args *nb = ZEND_TYPE_NAMED_WITH_ARGS(b); + if (na->count != nb->count) { + return false; + } + + if ((na->name == NULL) != (nb->name == NULL)) { + return false; + } + + if (na->name && !zend_string_equals_ci(na->name, nb->name)) { + return false; + } + + for (uint32_t i = 0; i < na->count; i++) { + if (!zend_diamond_types_equal(na->args[i], nb->args[i])) { + return false; + } + } + + return true; + } + + if (ZEND_TYPE_HAS_LIST(a)) { + if (!ZEND_TYPE_HAS_LIST(b)) { + return false; + } + + const zend_type_list *la = ZEND_TYPE_LIST(a); + const zend_type_list *lb = ZEND_TYPE_LIST(b); + if (la->num_types != lb->num_types) { + return false; + } + + for (uint32_t i = 0; i < la->num_types; i++) { + if (!zend_diamond_types_equal(la->types[i], lb->types[i])) { + return false; + } + } + + return true; + } + + if (ZEND_TYPE_HAS_NAME(a)) { + if (!ZEND_TYPE_HAS_NAME(b)) { + return false; + } + + return zend_string_equals_ci(ZEND_TYPE_NAME(a), ZEND_TYPE_NAME(b)); + } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(a)) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(b)) { + return false; + } + + const zend_type_parameter_ref *ra = ZEND_TYPE_TYPE_PARAMETER(a); + const zend_type_parameter_ref *rb = ZEND_TYPE_TYPE_PARAMETER(b); + return ra->origin == rb->origin && ra->index == rb->index; + } + + return true; +} + +typedef struct { + uint32_t arity; + zend_string *first_source_name; + zend_type args[1]; +} zend_diamond_record; + +#define ZEND_DIAMOND_RECORD_SIZE(arity) \ + (offsetof(zend_diamond_record, args) + sizeof(zend_type) * (arity)) + +static zend_string *zend_diamond_format_args(const zend_type *args, uint32_t arity) +{ + smart_str buf = {0}; + smart_str_appendc(&buf, '<'); + for (uint32_t j = 0; j < arity; j++) { + if (j > 0) smart_str_appends(&buf, ", "); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(args[j])) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(args[j]); + smart_str_append(&buf, ref->name ? ref->name : ZSTR_KNOWN(ZEND_STR_MIXED)); + } else { + zend_string *piece = zend_type_to_string(args[j]); + if (piece) { + smart_str_append(&buf, piece); + zend_string_release(piece); + } else { + smart_str_appendc(&buf, '?'); + } + } + } + + smart_str_appendc(&buf, '>'); + smart_str_0(&buf); + return buf.s; +} + +static void zend_diamond_record_dtor(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +static void zend_diamond_record_or_check( + zend_class_entry *ce, + zend_class_entry *target, + const zend_type *args, uint32_t arity, + zend_string *source_name, + HashTable *records) +{ + zend_ulong key = (zend_ulong)(uintptr_t) target; + zend_diamond_record *prior = zend_hash_index_find_ptr(records, key); + if (prior) { + if (prior->arity == arity) { + return; + } + + zend_string *first_args = zend_diamond_format_args(prior->args, prior->arity); + zend_string *next_args = zend_diamond_format_args(args, arity); + zend_error_noreturn(E_COMPILE_ERROR, + "%s inherits %s%s via %s and %s%s via %s", + ZSTR_VAL(ce->name), + ZSTR_VAL(target->name), ZSTR_VAL(first_args), ZSTR_VAL(prior->first_source_name), + ZSTR_VAL(target->name), ZSTR_VAL(next_args), ZSTR_VAL(source_name)); + } + + zend_diamond_record *record = emalloc(ZEND_DIAMOND_RECORD_SIZE(arity)); + record->arity = arity; + record->first_source_name = source_name; + for (uint32_t j = 0; j < arity; j++) { + record->args[j] = args[j]; + } + + zend_hash_index_add_new_ptr(records, key, record); +} + +static const zend_type_named_with_args *zend_get_extends_binding(const zend_class_entry *ce) +{ + if (!ce->generic_types || !ce->generic_types->extends) return NULL; + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) return NULL; + return ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); +} + +static const zend_type_named_with_args *zend_get_implements_binding(const zend_class_entry *ce, uint32_t idx) +{ + if (!ce->generic_types || !ce->generic_types->implements) return NULL; + zval *zv = zend_hash_index_find(ce->generic_types->implements, idx); + if (!zv) return NULL; + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) return NULL; + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); +} + +static void zend_diamond_collect_via_provider( + zend_class_entry *ce, + zend_class_entry *provider, + const zend_type *ce_to_provider, + uint32_t ce_to_provider_arity, + zend_string *source_name, + HashTable *records) +{ + if (!provider) return; + + if (provider->generic_parameters && ce_to_provider) { + zend_diamond_record_or_check(ce, provider, ce_to_provider, ce_to_provider_arity, + source_name, records); + } + + for (uint32_t i = 0; i < provider->num_interfaces; i++) { + zend_class_entry *target = provider->interfaces[i]; + if (!target || !target->generic_parameters) continue; + + uint32_t cap = target->generic_parameters->count; + if (cap == 0) continue; + ALLOCA_FLAG(use_heap) + zend_type *via = (zend_type *) do_alloca(sizeof(zend_type) * cap, use_heap); + uint32_t via_arity; + if (!zend_get_inheritance_binding_full_cached(provider, target, via, cap, &via_arity)) { + free_alloca(via, use_heap); + continue; + } + + if (ce_to_provider) { + for (uint32_t j = 0; j < via_arity; j++) { + via[j] = zend_substitute_leaf_type_param(via[j], ce_to_provider, ce_to_provider_arity); + } + } + + zend_diamond_record_or_check(ce, target, via, via_arity, + source_name, records); + free_alloca(via, use_heap); + } +} + +static void zend_validate_generic_diamond_bindings( + zend_class_entry *ce, + zend_class_entry *parent_ce, + zend_class_entry **traits_and_interfaces) +{ + if (!parent_ce && (!traits_and_interfaces || ce->num_interfaces == 0)) return; + + HashTable records; + zend_hash_init(&records, 8, NULL, zend_diamond_record_dtor, /* persistent */ 0); + + if (parent_ce) { + const zend_type_named_with_args *binding = zend_get_extends_binding(ce); + zend_diamond_collect_via_provider(ce, parent_ce, + binding ? binding->args : NULL, + binding ? binding->count : 0, + parent_ce->name, &records); + } + + if (traits_and_interfaces) { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + zend_class_entry *DI = traits_and_interfaces[ce->num_traits + i]; + if (!DI) continue; + const zend_type_named_with_args *binding = zend_get_implements_binding(ce, i); + zend_diamond_collect_via_provider(ce, DI, + binding ? binding->args : NULL, + binding ? binding->count : 0, + DI->name, &records); + } + } + + zend_hash_destroy(&records); +} + +static void zend_validate_generic_inheritance_arities( + zend_class_entry *ce, + zend_class_entry *parent_ce, + zend_class_entry **traits_and_interfaces) +{ + if (!traits_and_interfaces && !parent_ce) { + return; + } + + zend_validate_generic_diamond_bindings(ce, parent_ce, traits_and_interfaces); + if (!traits_and_interfaces) { + return; + } + + const HashTable *trait_uses_table = ce->generic_types ? ce->generic_types->trait_uses : NULL; + for (uint32_t i = 0; i < ce->num_traits; i++) { + zend_class_entry *trait_ce = traits_and_interfaces[i]; + if (!trait_ce) continue; + /* Skip when the resolved trait is a synthesized monomorph — its args + * were already validated against the base trait's bounds at synthesis + * time and the side-table args are pre-rewrite (refer to the base). */ + if (zend_class_is_monomorph(trait_ce)) { + continue; + } + uint32_t arity = zend_lookup_inheritance_arity(trait_uses_table, i); + if (arity > 0 || trait_ce->generic_parameters) { + zend_check_generic_link_arity(trait_ce, arity, "use", ce->name); + } + + zend_check_generic_link_bounds(trait_ce, zend_lookup_inheritance_args(trait_uses_table, i), "use", ce); + } + + const HashTable *implements_table = ce->generic_types ? ce->generic_types->implements : NULL; + const char *clause = (ce->ce_flags & ZEND_ACC_INTERFACE) ? "extends" : "implements"; + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + zend_class_entry *iface_ce = traits_and_interfaces[ce->num_traits + i]; + if (!iface_ce) continue; + /* Same monomorph-skip as the trait loop above. */ + if (zend_class_is_monomorph(iface_ce)) { + continue; + } + uint32_t arity = zend_lookup_inheritance_arity(implements_table, i); + if (arity > 0 || iface_ce->generic_parameters) { + zend_check_generic_link_arity(iface_ce, arity, clause, ce->name); + } + + zend_check_generic_link_bounds(iface_ce, zend_lookup_inheritance_args(implements_table, i), clause, ce); + } +} + +/* Mark entries in `names` that share an lc_name with another entry — the + * diamond-of-same-base case for `implements Sink, Sink` and + * `use Box, Box`. The marked indices fall back to the bare-base + * + side-table inheritance path so the existing diamond-merge machinery in + * `do_inherit_method` can compute the merged signature. Skipped entirely when + * count <= 1 (the common case). */ +static void zend_mark_duplicate_lc_names( + const zend_class_name *names, uint32_t count, bool *out) +{ + if (count <= 1) { + memset(out, 0, sizeof(bool) * count); + return; + } + memset(out, 0, sizeof(bool) * count); + for (uint32_t a = 0; a < count; a++) { + for (uint32_t b = a + 1; b < count; b++) { + if (zend_string_equals(names[a].lc_name, names[b].lc_name)) { + out[a] = true; + out[b] = true; + } + } + } +} + +ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key) /* {{{ */ +{ + /* Load parent/interface dependencies first, so we can still gracefully abort linking + * with an exception and remove the class from the class table. This is only possible + * if no variance obligations on the current class have been added during autoloading. */ + zend_class_entry *parent = NULL; + zend_class_entry **traits_and_interfaces = NULL; + zend_class_entry *proto = NULL; + zend_class_entry *orig_linking_class; + uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; + uint32_t i, j; + zval *zv; + zend_string *synthesized_lc_parent = NULL; + zend_class_entry *cache_key_proto = NULL; + zend_class_name *detached_interface_names = NULL; + uint32_t detached_interface_count = 0; + ALLOCA_FLAG(use_heap) + + SET_ALLOCA_FLAG(use_heap); + ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_LINKED)); + + /* When this link will rewrite ce->parent_name or ce->interface_names + * during monomorph synthesis (extends/implements/use with concrete + * generic args), the writes happen below — but ce may live in read-only + * opcache SHM under opcache.protect_memory=1. Lazy-load the mutable copy + * now and remember the immutable pointer so the inheritance cache lookup + * further down still keys against the same proto opcache stored under. + * Limited to classes that actually carry a generic side-table so we don't + * pay the copy cost on every link. */ + if ((ce->ce_flags & ZEND_ACC_IMMUTABLE) + && ce->generic_types + && (ce->generic_types->extends + || ce->generic_types->implements + || ce->generic_types->trait_uses)) { + cache_key_proto = ce; + ce = zend_lazy_class_load(ce); + zv = zend_hash_find_known_hash(CG(class_table), key); + Z_CE_P(zv) = ce; + /* zend_lazy_class_load deep-copies properties/methods/etc. but the + * interface_names/trait_names arrays themselves still point into the + * immutable SHM. Monomorph synthesis below rewrites entries in + * those arrays, so detach them into emalloc'd copies first. The + * name strings are interned, so addref/release stays a no-op. */ + if (ce->num_interfaces) { + zend_class_name *src = ce->interface_names; + ce->interface_names = emalloc(sizeof(zend_class_name) * ce->num_interfaces); + memcpy(ce->interface_names, src, sizeof(zend_class_name) * ce->num_interfaces); + for (uint32_t k = 0; k < ce->num_interfaces; k++) { + zend_string_addref(ce->interface_names[k].name); + zend_string_addref(ce->interface_names[k].lc_name); + } + detached_interface_names = ce->interface_names; + detached_interface_count = ce->num_interfaces; + } + if (ce->num_traits) { + zend_class_name *src = ce->trait_names; + ce->trait_names = zend_arena_alloc(&CG(arena), sizeof(zend_class_name) * ce->num_traits); + memcpy(ce->trait_names, src, sizeof(zend_class_name) * ce->num_traits); + for (uint32_t k = 0; k < ce->num_traits; k++) { + zend_string_addref(ce->trait_names[k].name); + zend_string_addref(ce->trait_names[k].lc_name); + } + } + } + + /* Bound-erased generics, extends-with-args: when this class declared + * `extends Box`, its compile-time side table holds the pre-erasure + * `Box` payload while `parent_name` is the bare base name. Before + * touching anything else, synthesize the `Box` monomorph as a + * fully separate top-level link and rewrite `parent_name` to the + * canonical name. That makes this class's direct parent be the + * monomorph entry, so `$this instanceof Box` holds and inherited + * substituted member types come through the normal inheritance + * pipeline. Doing it here (rather than inline during compile) keeps + * the synthesizer's recursive link sequential — it completes before + * we begin linking ce, which is what fixes the property-hook + * corruption seen in the earlier inline attempt. + * + * Skip when ce is itself a synthesized monomorph (its name carries `<`, + * which is invalid in user class names) — in that case the side-table + * extends entry exists for substitution, not for redirection. */ + if (ce->parent_name && ce->generic_types && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends) + && !zend_class_is_monomorph(ce) + && !zend_type_contains_type_parameter(*ce->generic_types->extends)) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + zend_class_entry *base = zend_fetch_class_by_name( + ce->parent_name, lc_parent_name, + ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); + if (!base) { + check_unrecoverable_load_failure(ce); + return NULL; + } + if (base->generic_parameters) { + /* Validate arity and bounds against the base's parameters first + * (so the user gets the contextual "extends X in Y" error wording + * rather than a generic synthesizer error). */ + zend_check_generic_link_arity(base, nwa->count, "extends", ce->name); + zend_check_generic_link_bounds(base, ce->generic_types->extends, + "extends", ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph(base, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + return NULL; + } + zend_string_release(ce->parent_name); + ce->parent_name = zend_string_copy(mono->name); + synthesized_lc_parent = zend_string_tolower(mono->name); + lc_parent_name = synthesized_lc_parent; + } + } + + if (ce->parent_name) { + parent = zend_fetch_class_by_name( + ce->parent_name, lc_parent_name, + ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); + if (!parent) { + check_unrecoverable_load_failure(ce); + if (synthesized_lc_parent) zend_string_release(synthesized_lc_parent); + return NULL; + } + UPDATE_IS_CACHEABLE(parent); + } + if (synthesized_lc_parent) { + zend_string_release(synthesized_lc_parent); + synthesized_lc_parent = NULL; + } + + if (ce->num_traits || ce->num_interfaces) { + traits_and_interfaces = do_alloca(sizeof(zend_class_entry*) * (ce->num_traits + ce->num_interfaces), use_heap); + + const HashTable *trait_uses_table_for_synth = + (ce->generic_types && ce->generic_types->trait_uses) + ? ce->generic_types->trait_uses : NULL; + bool ce_is_mono_for_trait = zend_class_is_monomorph(ce); + bool *trait_skip_mono = NULL; + if (trait_uses_table_for_synth && ce->num_traits > 1) { + trait_skip_mono = emalloc(sizeof(bool) * ce->num_traits); + zend_mark_duplicate_lc_names(ce->trait_names, ce->num_traits, trait_skip_mono); + } + + for (i = 0; i < ce->num_traits; i++) { + /* Bound-erased generics, trait-use-with-args: if the use clause is + * `use Foo;`, synthesize the Foo mono trait now and + * rewrite trait_names[i] to the canonical name. Skip when ce is + * itself a mono, when args carry a type-parameter ref (still + * symbolic), or when the same base trait is used multiple times + * with different args (diamond fallback). */ + const zend_type *trait_args = trait_uses_table_for_synth + ? (const zend_type *) zend_hash_index_find_ptr(trait_uses_table_for_synth, i) + : NULL; + if (!ce_is_mono_for_trait && trait_args + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*trait_args) + && !zend_type_contains_type_parameter(*trait_args) + && !(trait_skip_mono && trait_skip_mono[i])) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*trait_args); + zend_class_entry *base_trait = zend_fetch_class_by_name( + ce->trait_names[i].name, ce->trait_names[i].lc_name, + ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); + if (UNEXPECTED(base_trait == NULL)) { + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + if (UNEXPECTED(!(base_trait->ce_flags & ZEND_ACC_TRAIT))) { + zend_throw_error(NULL, "%s cannot use %s - it is not a trait", + ZSTR_VAL(ce->name), ZSTR_VAL(base_trait->name)); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + if (base_trait->generic_parameters) { + zend_check_generic_link_arity(base_trait, nwa->count, "use", ce->name); + zend_check_generic_link_bounds(base_trait, trait_args, "use", ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph( + base_trait, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + zend_string_release(ce->trait_names[i].name); + zend_string_release(ce->trait_names[i].lc_name); + ce->trait_names[i].name = zend_string_copy(mono->name); + ce->trait_names[i].lc_name = zend_string_tolower(mono->name); + } + } + + zend_class_entry *trait = zend_fetch_class_by_name(ce->trait_names[i].name, + ce->trait_names[i].lc_name, ZEND_FETCH_CLASS_TRAIT | ZEND_FETCH_CLASS_EXCEPTION); + if (UNEXPECTED(trait == NULL)) { + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + if (UNEXPECTED(!(trait->ce_flags & ZEND_ACC_TRAIT))) { + zend_throw_error(NULL, "%s cannot use %s - it is not a trait", ZSTR_VAL(ce->name), ZSTR_VAL(trait->name)); + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { + zend_use_of_deprecated_trait(trait, ce->name); + if (UNEXPECTED(EG(exception))) { + free_alloca(traits_and_interfaces, use_heap); + if (trait_skip_mono) efree(trait_skip_mono); + return NULL; + } + } + for (j = 0; j < i; j++) { + if (traits_and_interfaces[j] == trait) { + bool keep_for_diamond = false; + if (trait->generic_parameters) { + const zend_type_named_with_args *prior = zend_get_trait_use_binding_by_index(ce, j); + const zend_type_named_with_args *cur = zend_get_trait_use_binding_by_index(ce, i); + if (prior && cur + && zend_iface_diamond_bindings_allowed( + trait, prior->args, prior->count, + cur->args, cur->count)) { + keep_for_diamond = true; + } + } + + if (!keep_for_diamond) { + trait = NULL; + } + + break; + } + } + traits_and_interfaces[i] = trait; + if (trait) { + UPDATE_IS_CACHEABLE(trait); + } + } + if (trait_skip_mono) { + efree(trait_skip_mono); + } + } + + if (ce->num_interfaces) { + const HashTable *impl_table = (ce->generic_types && ce->generic_types->implements) + ? ce->generic_types->implements : NULL; + bool ce_is_mono = zend_class_is_monomorph(ce); + bool *skip_mono = NULL; + ALLOCA_FLAG(skip_mono_use_heap) + if (impl_table && ce->num_interfaces > 1) { + skip_mono = do_alloca(sizeof(bool) * ce->num_interfaces, skip_mono_use_heap); + zend_mark_duplicate_lc_names(ce->interface_names, ce->num_interfaces, skip_mono); + } + for (i = 0; i < ce->num_interfaces; i++) { + /* Bound-erased generics, implements-with-args: if this interface was + * declared as `implements Iter`, the side-table holds the + * pre-erasure args. Synthesize Iter as a sequential top-level + * link, then rewrite ce->interface_names[i] to the canonical name + * so the existing fetch+inheritance machinery sees the monomorph as + * the implemented interface (and `$obj instanceof Iter` + * resolves correctly via the inheritance chain). Skip when ce is + * itself a monomorph or when args contain a type-parameter ref. */ + const zend_type *impl_args = impl_table + ? (const zend_type *) zend_hash_index_find_ptr(impl_table, i) + : NULL; + if (!ce_is_mono && impl_args && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*impl_args) + && !zend_type_contains_type_parameter(*impl_args) + && !(skip_mono && skip_mono[i])) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*impl_args); + zend_class_entry *base_iface = zend_fetch_class_by_name( + ce->interface_names[i].name, ce->interface_names[i].lc_name, + ZEND_FETCH_CLASS_INTERFACE | ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED + | ZEND_FETCH_CLASS_EXCEPTION); + if (!base_iface) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + if (base_iface->generic_parameters) { + const char *clause = (ce->ce_flags & ZEND_ACC_INTERFACE) + ? "extends" : "implements"; + zend_check_generic_link_arity(base_iface, nwa->count, + clause, ce->name); + zend_check_generic_link_bounds(base_iface, impl_args, + clause, ce); + if (EG(exception)) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + zend_class_entry *mono = zend_synthesize_monomorph( + base_iface, nwa->args, nwa->count); + if (!mono) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + zend_string_release(ce->interface_names[i].name); + zend_string_release(ce->interface_names[i].lc_name); + ce->interface_names[i].name = zend_string_copy(mono->name); + ce->interface_names[i].lc_name = zend_string_tolower(mono->name); + } + } + + zend_class_entry *iface = zend_fetch_class_by_name( + ce->interface_names[i].name, ce->interface_names[i].lc_name, + ZEND_FETCH_CLASS_INTERFACE | + ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION); + if (!iface) { + check_unrecoverable_load_failure(ce); + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + traits_and_interfaces[ce->num_traits + i] = iface; + if (iface) { + UPDATE_IS_CACHEABLE(iface); + } + } + if (skip_mono) { + free_alloca(skip_mono, skip_mono_use_heap); + } + } + + zend_validate_generic_inheritance_arities(ce, parent, traits_and_interfaces); + + zend_class_entry *orig_active = CG(active_class_entry); + CG(active_class_entry) = ce; + zend_check_generic_variance_markers(ce); + CG(active_class_entry) = orig_active; + +#ifndef ZEND_WIN32 + if (ce->ce_flags & ZEND_ACC_ENUM) { + /* We will add internal methods. */ + is_cacheable = false; + } +#endif + + if ((ce->ce_flags & ZEND_ACC_IMMUTABLE || cache_key_proto) && is_cacheable) { + if (zend_inheritance_cache_get && zend_inheritance_cache_add) { + /* When we lazy-loaded early (cache_key_proto != NULL), the + * immutable original is the cache key opcache stored under. */ + zend_class_entry *key_ce = cache_key_proto ? cache_key_proto : ce; + zend_class_entry *ret = zend_inheritance_cache_get(key_ce, parent, traits_and_interfaces); + if (ret) { + if (traits_and_interfaces) { + free_alloca(traits_and_interfaces, use_heap); + } + zv = zend_hash_find_known_hash(CG(class_table), key); + Z_CE_P(zv) = ret; + return ret; + } + } else { + is_cacheable = 0; + } + proto = cache_key_proto ? cache_key_proto : ce; + } + + /* Delay and record warnings (such as deprecations) thrown during + * inheritance, so they will be recorded in the inheritance cache. + * Warnings must be delayed in all cases so that we get a consistent + * behavior regardless of cacheability. */ + bool orig_record_errors = EG(record_errors); + if (!orig_record_errors) { + zend_begin_record_errors(); + } + + zend_try { + if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { + /* Lazy class loading */ + ce = zend_lazy_class_load(ce); + zv = zend_hash_find_known_hash(CG(class_table), key); + Z_CE_P(zv) = ce; + } else if (ce->ce_flags & ZEND_ACC_FILE_CACHED) { + /* Lazy class loading */ + ce = zend_lazy_class_load(ce); + ce->ce_flags &= ~ZEND_ACC_FILE_CACHED; + zv = zend_hash_find_known_hash(CG(class_table), key); + Z_CE_P(zv) = ce; + } + + if (CG(unlinked_uses)) { + zend_hash_index_del(CG(unlinked_uses), (zend_ulong)(uintptr_t) ce); + } + + orig_linking_class = CG(current_linking_class); + CG(current_linking_class) = is_cacheable ? ce : NULL; + + if (ce->ce_flags & ZEND_ACC_ENUM) { + /* Only register builtin enum methods during inheritance to avoid persisting them in + * opcache. */ + zend_enum_register_funcs(ce); + } + +#ifdef ZEND_OPCACHE_SHM_REATTACHMENT + zend_link_hooked_object_iter(ce); +#endif + + HashTable **trait_exclude_tables; + zend_class_entry **trait_aliases; + bool trait_contains_abstract_methods = false; + if (ce->num_traits) { + zend_traits_init_trait_structures(ce, traits_and_interfaces, &trait_exclude_tables, &trait_aliases); + zend_do_traits_method_binding(ce, traits_and_interfaces, trait_exclude_tables, trait_aliases, false, &trait_contains_abstract_methods); + zend_do_traits_constant_binding(ce, traits_and_interfaces); + zend_do_traits_property_binding(ce, traits_and_interfaces); + + zend_function *fn; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { + zend_fixup_trait_method(fn, ce); + } ZEND_HASH_FOREACH_END(); + } + if (parent) { + if (!(parent->ce_flags & ZEND_ACC_LINKED)) { + add_dependency_obligation(ce, parent); + } + zend_do_inheritance(ce, parent); + } + if (ce->num_traits) { + if (trait_contains_abstract_methods) { + zend_do_traits_method_binding(ce, traits_and_interfaces, trait_exclude_tables, trait_aliases, true, &trait_contains_abstract_methods); + + /* New abstract methods may have been added, make sure to add + * ZEND_ACC_IMPLICIT_ABSTRACT_CLASS to ce. */ + zend_function *fn; + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { + zend_fixup_trait_method(fn, ce); + } ZEND_HASH_FOREACH_END(); + } + + if (trait_exclude_tables) { + for (i = 0; i < ce->num_traits; i++) { + if (traits_and_interfaces[i]) { + if (trait_exclude_tables[i]) { + zend_hash_destroy(trait_exclude_tables[i]); + FREE_HASHTABLE(trait_exclude_tables[i]); + } + } + } + efree(trait_exclude_tables); + } + if (trait_aliases) { + efree(trait_aliases); + } + } + if (ce->num_interfaces) { + /* Also copy the parent interfaces here, so we don't need to reallocate later. */ + uint32_t num_parent_interfaces = parent ? parent->num_interfaces : 0; + zend_class_entry **interfaces = emalloc( + sizeof(zend_class_entry *) * (ce->num_interfaces + num_parent_interfaces)); + + if (num_parent_interfaces) { + memcpy(interfaces, parent->interfaces, + sizeof(zend_class_entry *) * num_parent_interfaces); + } + memcpy(interfaces + num_parent_interfaces, traits_and_interfaces + ce->num_traits, + sizeof(zend_class_entry *) * ce->num_interfaces); + + zend_do_implement_interfaces(ce, interfaces); + if (detached_interface_names) { + for (uint32_t k = 0; k < detached_interface_count; k++) { + zend_string_release_ex(detached_interface_names[k].name, 0); + zend_string_release_ex(detached_interface_names[k].lc_name, 0); + } + efree(detached_interface_names); + } + } else if (parent && parent->num_interfaces) { + zend_do_inherit_interfaces(ce, parent); + } + if (!(ce->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT)) + && (ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) + ) { + zend_verify_abstract_class(ce); + } + if (ce->ce_flags & ZEND_ACC_ENUM) { + zend_verify_enum(ce); + } + if (ce->num_hooked_prop_variance_checks) { + const zend_property_info *prop_info; + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop_info) { + if (prop_info->ce == ce && prop_info->hooks && prop_info->hooks[ZEND_PROPERTY_HOOK_SET]) { + switch (zend_verify_property_hook_variance(prop_info, prop_info->hooks[ZEND_PROPERTY_HOOK_SET])) { + case INHERITANCE_SUCCESS: + break; + case INHERITANCE_ERROR: + zend_hooked_property_variance_error(prop_info); + break; + case INHERITANCE_UNRESOLVED: + add_property_hook_obligation(ce, prop_info, prop_info->hooks[ZEND_PROPERTY_HOOK_SET]); + break; + case INHERITANCE_WARNING: + ZEND_UNREACHABLE(); + } + } + } ZEND_HASH_FOREACH_END(); + } + + /* Normally Stringable is added during compilation. However, if it is imported from a trait, + * we need to explicitly add the interface here. */ + if (ce->__tostring && !(ce->ce_flags & ZEND_ACC_TRAIT) + && !zend_class_implements_interface(ce, zend_ce_stringable)) { + ZEND_ASSERT(ce->__tostring->common.fn_flags & ZEND_ACC_TRAIT_CLONE); + ce->ce_flags |= ZEND_ACC_RESOLVED_INTERFACES; + ce->num_interfaces++; + ce->interfaces = perealloc(ce->interfaces, + sizeof(zend_class_entry *) * ce->num_interfaces, ce->type == ZEND_INTERNAL_CLASS); + ce->interfaces[ce->num_interfaces - 1] = zend_ce_stringable; + do_interface_implementation(ce, zend_ce_stringable); + } + + zend_build_properties_info_table(ce); + } zend_catch { + /* Do not leak recorded errors to the next linked class. */ + if (!orig_record_errors) { + EG(record_errors) = false; + zend_free_recorded_errors(); + } + zend_bailout(); + } zend_end_try(); + + EG(record_errors) = orig_record_errors; + + if (!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)) { + zend_inheritance_check_override(ce); + ce->ce_flags |= ZEND_ACC_LINKED; + } else { + ce->ce_flags |= ZEND_ACC_NEARLY_LINKED; + if (CG(current_linking_class)) { + ce->ce_flags |= ZEND_ACC_CACHEABLE; + } + load_delayed_classes(ce); + if (ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { + resolve_delayed_variance_obligations(ce); + } + /* Delayed variance resolution can re-enter linking before the full + * hierarchy is linked. See ext/opcache/tests/gh20469*.phpt. */ + if (CG(unlinked_uses) && zend_hash_index_exists(CG(unlinked_uses), (zend_long)(uintptr_t) ce)) { + ce->ce_flags &= ~ZEND_ACC_CACHEABLE; + } + if (ce->ce_flags & ZEND_ACC_CACHEABLE) { + ce->ce_flags &= ~ZEND_ACC_CACHEABLE; + } else { + CG(current_linking_class) = NULL; + } + } + + bool was_cacheable = is_cacheable; + if (!CG(current_linking_class)) { + is_cacheable = 0; + } + CG(current_linking_class) = orig_linking_class; + + if (is_cacheable) { + HashTable *ht = (HashTable*)ce->inheritance_cache; + zend_class_entry *new_ce; + + ce->inheritance_cache = NULL; + new_ce = zend_inheritance_cache_add(ce, proto, parent, traits_and_interfaces, ht); + if (new_ce) { + zv = zend_hash_find_known_hash(CG(class_table), key); + ce = new_ce; + Z_CE_P(zv) = ce; + } + if (ht) { + zend_hash_destroy(ht); + FREE_HASHTABLE(ht); + } + } else if (was_cacheable && ce->inheritance_cache) { + /* Cacheability can be disabled after dependency tracking prepared + * an inheritance-cache dependency table. Discard it here. */ + HashTable *ht = (HashTable*)ce->inheritance_cache; + ce->inheritance_cache = NULL; + zend_hash_destroy(ht); + FREE_HASHTABLE(ht); + } + + if (!orig_record_errors) { + zend_emit_recorded_errors(); + zend_free_recorded_errors(); + } + if (traits_and_interfaces) { + free_alloca(traits_and_interfaces, use_heap); + } + + if (ZSTR_HAS_CE_CACHE(ce->name)) { + ZSTR_SET_CE_CACHE(ce->name, ce); + } + + return ce; +} +/* }}} */ + +/* Check whether early binding is prevented due to unresolved types in inheritance checks. */ +static inheritance_status zend_can_early_bind(zend_class_entry *ce, const zend_class_entry *parent_ce) /* {{{ */ +{ + zend_string *key; + zend_function *parent_func; + const zend_property_info *parent_info; + const zend_class_constant *parent_const; + inheritance_status overall_status = INHERITANCE_SUCCESS; + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, parent_func) { + zval *zv = zend_hash_find_known_hash(&ce->function_table, key); + if (zv) { + zend_function *child_func = Z_FUNC_P(zv); + inheritance_status status = + do_inheritance_check_on_method( + child_func, child_func->common.scope, + parent_func, parent_func->common.scope, + ce, NULL, + ZEND_INHERITANCE_CHECK_SILENT | ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_CHECK_VISIBILITY); + if (UNEXPECTED(status == INHERITANCE_WARNING)) { + overall_status = INHERITANCE_WARNING; + } else if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { + return status; + } + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->properties_info, key, parent_info) { + const zval *zv; + if ((parent_info->flags & ZEND_ACC_PRIVATE) || !ZEND_TYPE_IS_SET(parent_info->type)) { + continue; + } + + zv = zend_hash_find_known_hash(&ce->properties_info, key); + if (zv) { + const zend_property_info *child_info = Z_PTR_P(zv); + if (ZEND_TYPE_IS_SET(child_info->type)) { + inheritance_status status = verify_property_type_compatibility(parent_info, child_info, prop_get_variance(parent_info), false, false); + if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { + return status; + } + } + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, parent_const) { + const zval *zv; + if ((ZEND_CLASS_CONST_FLAGS(parent_const) & ZEND_ACC_PRIVATE) || !ZEND_TYPE_IS_SET(parent_const->type)) { + continue; + } + + zv = zend_hash_find_known_hash(&ce->constants_table, key); + if (zv) { + const zend_class_constant *child_const = Z_PTR_P(zv); + if (ZEND_TYPE_IS_SET(child_const->type)) { + inheritance_status status = class_constant_types_compatible(parent_const, child_const); + ZEND_ASSERT(status != INHERITANCE_WARNING); + if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { + return status; + } + } + } + } ZEND_HASH_FOREACH_END(); + + return overall_status; +} +/* }}} */ + +static zend_always_inline bool register_early_bound_ce(zval *delayed_early_binding, zend_string *lcname, zend_class_entry *ce) { + if (delayed_early_binding) { + if (EXPECTED(!(ce->ce_flags & ZEND_ACC_PRELOADED))) { + if (zend_hash_set_bucket_key(EG(class_table), (Bucket *)delayed_early_binding, lcname) != NULL) { + Z_CE_P(delayed_early_binding) = ce; + return true; + } + } else { + /* If preloading is used, don't replace the existing bucket, add a new one. */ + if (zend_hash_add_ptr(EG(class_table), lcname, ce) != NULL) { + return true; + } + } + zend_class_entry *old_ce = zend_hash_find_ptr(EG(class_table), lcname); + ZEND_ASSERT(old_ce); + zend_class_redeclaration_error(E_COMPILE_ERROR, old_ce); + return false; + } + if (zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL) { + return true; + } + return false; +} + +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) /* {{{ */ +{ + inheritance_status status; + zend_class_entry *proto = NULL; + zend_class_entry *orig_linking_class; + + if (ce->ce_flags & ZEND_ACC_LINKED) { + ZEND_ASSERT(ce->parent == NULL); + if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ce))) { + return NULL; + } + zend_observer_class_linked_notify(ce, lcname); + return ce; + } + + /* Bound-erased generics, extends-with-args: same redirect as in + * zend_do_link_class but for the early-binding path. Validate arity + * against the base's parameter list (so we keep the original "Too + * few/many type arguments to extends" error wording), then synthesize + * the canonical monomorph as a completed top-level link before ce's + * link begins, then rewrite parent_ce (local) and ce->parent_name to + * point at the monomorph. Skip when any arg contains a type-parameter + * ref (e.g. `class Derived extends Base`) — the args are still + * symbolic and would only become concrete when Derived itself is + * monomorphized. */ + if (parent_ce && parent_ce->generic_parameters + && ce->generic_types && ce->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends) + && !zend_class_is_monomorph(ce) + && !zend_type_contains_type_parameter(*ce->generic_types->extends)) { + const zend_type_named_with_args *nwa = + ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + zend_check_generic_link_arity(parent_ce, nwa->count, "extends", ce->name); + zend_check_generic_link_bounds(parent_ce, ce->generic_types->extends, + "extends", ce); + zend_class_entry *mono = zend_synthesize_monomorph(parent_ce, nwa->args, nwa->count); + if (!mono) { + return NULL; + } + if (ce->parent_name) { + zend_string_release(ce->parent_name); + } + ce->parent_name = zend_string_copy(mono->name); + parent_ce = mono; + } + + uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; + UPDATE_IS_CACHEABLE(parent_ce); + if (is_cacheable) { + if (zend_inheritance_cache_get && zend_inheritance_cache_add) { + zend_class_entry *ret = zend_inheritance_cache_get(ce, parent_ce, NULL); + if (ret) { + if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ret))) { + return NULL; + } + zend_observer_class_linked_notify(ret, lcname); + return ret; + } + } else { + is_cacheable = 0; + } + proto = ce; + } + + orig_linking_class = CG(current_linking_class); + CG(current_linking_class) = NULL; + status = zend_can_early_bind(ce, parent_ce); + CG(current_linking_class) = orig_linking_class; + if (EXPECTED(status != INHERITANCE_UNRESOLVED)) { + if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { + /* Lazy class loading */ + ce = zend_lazy_class_load(ce); + } else if (ce->ce_flags & ZEND_ACC_FILE_CACHED) { + /* Lazy class loading */ + ce = zend_lazy_class_load(ce); + ce->ce_flags &= ~ZEND_ACC_FILE_CACHED; + } + + if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ce))) { + return NULL; + } + + orig_linking_class = CG(current_linking_class); + CG(current_linking_class) = is_cacheable ? ce : NULL; + + bool orig_record_errors = EG(record_errors); + + zend_try{ + CG(zend_lineno) = ce->info.user.line_start; + + if (!orig_record_errors) { + zend_begin_record_errors(); + } + +#ifdef ZEND_OPCACHE_SHM_REATTACHMENT + zend_link_hooked_object_iter(ce); +#endif + + zend_do_inheritance_ex(ce, parent_ce, status == INHERITANCE_SUCCESS); + if (parent_ce && parent_ce->num_interfaces) { + zend_do_inherit_interfaces(ce, parent_ce); + } + zend_build_properties_info_table(ce); + if ((ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) == ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) { + zend_verify_abstract_class(ce); + } + zend_inheritance_check_override(ce); + ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)); + ce->ce_flags |= ZEND_ACC_LINKED; + + CG(current_linking_class) = orig_linking_class; + } zend_catch { + if (!orig_record_errors) { + EG(record_errors) = false; + zend_free_recorded_errors(); + } + zend_bailout(); + } zend_end_try(); + + if (is_cacheable) { + HashTable *ht = (HashTable*)ce->inheritance_cache; + zend_class_entry *new_ce; + + ce->inheritance_cache = NULL; + new_ce = zend_inheritance_cache_add(ce, proto, parent_ce, NULL, ht); + if (new_ce) { + zval *zv = zend_hash_find_known_hash(CG(class_table), lcname); + ce = new_ce; + Z_CE_P(zv) = ce; + } + if (ht) { + zend_hash_destroy(ht); + FREE_HASHTABLE(ht); + } + } + + if (!orig_record_errors) { + zend_emit_recorded_errors(); + zend_free_recorded_errors(); + } + + if (ZSTR_HAS_CE_CACHE(ce->name)) { + ZSTR_SET_CE_CACHE(ce->name, ce); + } + zend_observer_class_linked_notify(ce, lcname); + + return ce; + } + return NULL; +} +/* }}} */ + +ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( + zend_class_entry *arg_scope, zend_type arg, + zend_class_entry *bound_scope, zend_type bound) +{ + if (!ZEND_TYPE_IS_SET(bound)) { + return INHERITANCE_SUCCESS; + } + + return zend_perform_covariant_type_check(arg_scope, arg, bound_scope, bound); +} + +/* === Monomorph synthesis === + * + * Builds a real class_entry for a generic application like `Box` and + * registers it in EG(class_table) under the canonical name. The synthesized + * class extends the base with the supplied type args; inheritance does the + * rest (substituted arg_info via the TRAIT_CLONE machinery, substituted + * property types, etc.). */ + +static zend_type zend_monomorph_dup_type(zend_type t) +{ + zend_type_copy_ctor(&t, /* use_arena */ false, /* persistent */ false); + return t; +} + +static zend_type zend_monomorph_build_extends_payload( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(arity)); + payload->name = zend_string_copy(base->name); + payload->name_attr = 0; + payload->count = arity; + for (uint32_t i = 0; i < arity; i++) { + payload->args[i] = zend_monomorph_dup_type(args[i]); + } + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; +} + +ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base) +{ + if (!base->generic_parameters) { + return base; + } + if (EXPECTED(base->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS)) { + return zend_synthesize_monomorph(base, NULL, 0); + } + uint32_t total = base->generic_parameters->count; + for (uint32_t i = 0; i < total; i++) { + if (!ZEND_TYPE_IS_SET(base->generic_parameters->parameters[i].default_type)) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments; " + "type parameter %s has no default", + ZSTR_VAL(base->name), + ZSTR_VAL(base->generic_parameters->parameters[i].name)); + return NULL; + } + } + ZEND_UNREACHABLE(); +} + +/* For a naked `new self()` / `new ThisClass()` that lexically names the generic + * class `lexical`, return the monomorph carrying the current frame's class-level + * binding: walk from the called scope up to the monomorph whose base is + * `lexical` (the same walk zend_resolve_generic_type_param uses to find a + * class-level T-ref's binding). Returns NULL when no such binding is in scope + * (e.g. a static call on the bare generic itself), so the caller can fall back + * to defaults or raise an error. */ +ZEND_API zend_class_entry *zend_resolve_lexical_self_monomorph( + zend_class_entry *lexical, const zend_execute_data *ex) +{ + zend_class_entry *cur = ex ? zend_get_called_scope(ex) : NULL; + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + return (cur && cur->parent == lexical) ? cur : NULL; +} + +/* When `new C::<...>(...)` is compiled inside a generic function/class, the + * turbofish args may reference enclosing-scope T parameters by ref. The op_array + * side-table stores those refs verbatim — at synth time they must be resolved + * against the executing frame's bindings, or the monomorph would carry literal + * "Box"-style args and its method arg_info would never resolve to a concrete + * type. Walks args[i]; for top-level T-refs, substitutes via the frame's + * function-level type_args (FUNCTION_LIKE) or the lexical class's monomorph + * descendant's generic_type_args (CLASS_LIKE). When the frame has no binding, + * falls back to the referenced parameter's class bound (matching the runtime + * fallback used by `new T()` / `instanceof T` resolution). Throws and returns + * NULL when neither a binding nor a class-bound fallback is available — same + * shape and message as the existing zend_resolve_generic_type_param error so + * users see one consistent diagnostic across `new T()` and `new C::()`. */ +static bool zend_resolve_synth_args_against_frame( + const zend_type *args, uint32_t arity, zend_type *out) +{ + zend_execute_data *ex = EG(current_execute_data); + for (uint32_t i = 0; i < arity; i++) { + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(args[i])) { + out[i] = args[i]; + continue; + } + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(args[i]); + const zend_type *resolved = NULL; + const zend_generic_parameter_list *params = NULL; + if (ref->origin == ZEND_GENERIC_ORIGIN_FUNCTION_LIKE) { + if (ex && ZEND_USER_CODE(ex->func->type)) { + if (ex->type_args && ref->index < ex->type_args->count) { + resolved = zend_type_arg_entry_type(&ex->type_args->entries[ref->index]); + } + params = ex->func->op_array.generic_parameters; + } + } else { + /* Walk from called scope up to the direct child of the lexical + * class — that's the monomorph carrying the binding (see the + * matching walk in zend_resolve_generic_type_param). */ + if (ex) { + zend_class_entry *lexical = ex->func->common.scope; + zend_class_entry *cur = zend_get_called_scope(ex); + while (cur && cur->parent != lexical) { + cur = cur->parent; + } + if (cur && cur->generic_type_args + && ref->index < cur->generic_type_args->count) { + resolved = zend_type_arg_entry_type( + &cur->generic_type_args->entries[ref->index]); + } + if (lexical) { + params = lexical->generic_parameters; + } + } + } + if (resolved && ZEND_TYPE_IS_SET(*resolved)) { + out[i] = *resolved; + continue; + } + /* No binding from the frame. Fall back to the referenced parameter's + * class bound — same fallback as `new T()` / `instanceof T`. A + * non-class bound (default `mixed`, scalar, union, etc.) has no + * class name to substitute, so synth cannot proceed: throw. */ + if (params && ref->index < params->count) { + zend_type bound = params->parameters[ref->index].bound; + if (ZEND_TYPE_HAS_NAME(bound)) { + out[i] = bound; + continue; + } + zend_throw_error(NULL, + "Cannot resolve generic type parameter %s at runtime: " + "no binding was supplied and its bound is not a class", + ZSTR_VAL(params->parameters[ref->index].name)); + } else { + zend_throw_error(NULL, + "Cannot resolve generic type parameter at runtime: " + "no binding was supplied and no parameter scope is available"); + } + return false; + } + return true; +} + +/* Same as zend_synthesize_monomorph, but resolves TYPE_PARAMETER refs in args + * against the executing frame's T-tables first. Use this at runtime `new` + * sites where the args originate from a compile-time side-table that may + * reference enclosing-scope T's. Static callers (class-build extends/implements + * with already-resolved args) keep using zend_synthesize_monomorph directly. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + if (arity == 0) { + return zend_synthesize_monomorph(base, args, arity); + } + zend_type resolved[ZEND_GENERIC_MAX_PARAMS]; + if (!zend_resolve_synth_args_against_frame(args, arity, resolved)) { + return NULL; + } + return zend_synthesize_monomorph(base, resolved, arity); +} + +ZEND_API zend_class_entry *zend_synthesize_monomorph( + zend_class_entry *base, const zend_type *args, uint32_t arity) +{ + if (!base->generic_parameters) { + zend_throw_error(NULL, + "Cannot monomorphize non-generic class %s", + ZSTR_VAL(base->name)); + return NULL; + } + uint32_t total = base->generic_parameters->count; + if (arity > total) { + zend_throw_error(NULL, + "Cannot monomorphize %s: expected at most %u type argument(s), got %u", + ZSTR_VAL(base->name), total, arity); + return NULL; + } + /* Fill in defaults for any trailing parameters not supplied. */ + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + 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 = &base->generic_parameters->parameters[i]; + if (!ZEND_TYPE_IS_SET(p->default_type)) { + zend_throw_error(NULL, + "Cannot monomorphize %s: type parameter %s has no default and was not supplied", + ZSTR_VAL(base->name), ZSTR_VAL(p->name)); + return NULL; + } + filled[i] = p->default_type; + } + args = filled; + arity = total; + } + + zend_string *canonical = zend_generic_canonical_class_name(base->name, args, arity); + zend_string *lc_canonical = zend_string_tolower(canonical); + + zend_class_entry *existing = zend_hash_find_ptr(EG(class_table), lc_canonical); + if (existing) { + zend_string_release(canonical); + zend_string_release(lc_canonical); + return existing; + } + + /* Validate bounds before committing to a class entry. */ + zend_type extends_payload = zend_monomorph_build_extends_payload(base, args, arity); + { + zend_class_entry tmp_scope; + memset(&tmp_scope, 0, sizeof(tmp_scope)); + tmp_scope.name = canonical; + tmp_scope.type = ZEND_USER_CLASS; + zend_check_generic_link_bounds(base, &extends_payload, "new", &tmp_scope); + if (EG(exception)) { + zend_type_release(extends_payload, /* persistent */ false); + zend_string_release(canonical); + zend_string_release(lc_canonical); + return NULL; + } + } + + zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry)); + ce->type = ZEND_USER_CLASS; + zend_initialize_class_data(ce, /* nullify_handlers */ true); + ce->ce_flags &= ~ZEND_ACC_IMMUTABLE; + ce->ce_flags |= ZEND_ACC_TOP_LEVEL; + /* Carry the base's class-shape flags (interface, trait, enum) so the + * monomorph is the same kind of class-like as the base. Without this, + * an interface base would synthesize a class trying to extend the + * interface, which the linker rejects. */ + ce->ce_flags |= base->ce_flags & (ZEND_ACC_INTERFACE | ZEND_ACC_TRAIT | ZEND_ACC_ENUM); + ce->info.user.filename = base->info.user.filename + ? zend_string_copy(base->info.user.filename) + : ZSTR_EMPTY_ALLOC(); + ce->info.user.line_start = base->info.user.line_start; + ce->info.user.line_end = base->info.user.line_end; + ce->name = canonical; + + /* Stash the bindings the runtime needs when method bodies reference the + * class-level T directly (e.g. `new T()` inside `class Box`'s body). + * The bound type is borrowed from extends_payload's NWA — that payload + * gets installed into ce->generic_types below, so it has the same + * lifetime as ce itself, and ce->generic_type_args has the same + * lifetime as ce too. The canonical name is owned by the entry. */ + const zend_type_named_with_args *binding_nwa = + ZEND_TYPE_NAMED_WITH_ARGS(extends_payload); + ce->generic_type_args = zend_type_arg_table_alloc(arity); + for (uint32_t i = 0; i < arity; i++) { + ce->generic_type_args->entries[i].name = zend_type_arg_canonical_name(binding_nwa->args[i]); + ce->generic_type_args->entries[i].type_ref = &binding_nwa->args[i]; + } + + bool base_is_interface = (base->ce_flags & ZEND_ACC_INTERFACE) != 0; + bool base_is_trait = (base->ce_flags & ZEND_ACC_TRAIT) != 0; + if (base_is_interface) { + /* Interface inheritance in PHP goes through `interface_names[]`, not + * `parent_name`. Wire the mono to the base interface via one entry in + * `interfaces[]` and store the substitution args in the `implements` + * side-table so the inheritance pipeline can substitute T → arg in + * the inherited methods. */ + ce->num_interfaces = 1; + ce->interface_names = emalloc(sizeof(zend_class_name)); + ce->interface_names[0].name = zend_string_copy(base->name); + ce->interface_names[0].lc_name = zend_string_tolower(base->name); + zend_generic_type_table_set_implements( + zend_generic_get_or_create_class_table(ce), 0, extends_payload); + } else if (base_is_trait) { + /* Trait composition in PHP goes through `trait_names[]`/`num_traits`. + * The mono `uses` the base trait with the substitution args recorded + * in `trait_uses` so the trait-method import path picks them up. */ + ce->num_traits = 1; + ce->trait_names = emalloc(sizeof(zend_class_name)); + ce->trait_names[0].name = zend_string_copy(base->name); + ce->trait_names[0].lc_name = zend_string_tolower(base->name); + zend_generic_type_table_set_trait_use( + zend_generic_get_or_create_class_table(ce), 0, extends_payload); + } else { + ce->parent_name = zend_string_copy(base->name); + zend_generic_type_table_set_extends( + zend_generic_get_or_create_class_table(ce), extends_payload); + } + + zval ce_zv; + ZVAL_PTR(&ce_zv, ce); + if (zend_hash_add(EG(class_table), lc_canonical, &ce_zv) == NULL) { + /* Race: another path registered this canonical name first. */ + zend_class_entry *winner = zend_hash_find_ptr(EG(class_table), lc_canonical); + destroy_zend_class(&ce_zv); + zend_string_release(lc_canonical); + return winner; + } + + zend_string *parent_lc = (base_is_interface || base_is_trait) + ? NULL : zend_string_tolower(base->name); + + /* Monomorph synthesis is an engine-internal child of the base, not a + * user-declared subclass. final/readonly restrictions that gate + * user-level extension are bypassed via EG(monomorph_synthesis_active) + * for the duration of the link, then propagated to the monomorph so user + * code still can't `extends Box` if Box itself was final. Abstract + * is propagated so the monomorph of an abstract class stays abstract + * (and a concrete subclass can implement its methods). Note: we don't + * mutate base->ce_flags here because base may live in read-only opcache + * SHM (opcache.protect_memory=1). */ + uint32_t inherited = base->ce_flags & (ZEND_ACC_FINAL | ZEND_ACC_READONLY_CLASS); + uint32_t propagated = base->ce_flags & (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS); + ce->ce_flags |= propagated; + bool prev_mono_active = EG(monomorph_synthesis_active); + EG(monomorph_synthesis_active) = true; + zend_class_entry *linked = zend_do_link_class(ce, parent_lc, lc_canonical); + EG(monomorph_synthesis_active) = prev_mono_active; + if (linked) { + linked->ce_flags |= inherited; + } + if (parent_lc) zend_string_release(parent_lc); + + if (!linked) { + zend_hash_del(EG(class_table), lc_canonical); + zend_string_release(lc_canonical); + return NULL; + } + + /* Substitute T-typed implements on the inherited interface chain. When + * the base declares `class B implements I` and is monomorphized as + * `B`, the inherited interfaces[] still points at the erased + * base interface I — so `instanceof I` returns false. Walk the + * base's parent chain too, since `class B extends A` where + * `class A implements I` needs the same substitution for B. + * For each generic ancestor, resolve the binding from `base` to that + * ancestor, substitute each implements entry's args, synthesize the + * corresponding interface monomorph, and add it to linked->interfaces. + * The standard `instanceof` walks both the parent chain and interfaces[], + * so the substituted forms become discoverable and the erased base + * interfaces stay reachable transitively. */ + { + ALLOCA_FLAG(extras_use_heap) + SET_ALLOCA_FLAG(extras_use_heap); + uint32_t max_extras = 0; + for (zend_class_entry *a = base; a; a = a->parent) { + max_extras += a->num_interfaces; + } + zend_class_entry **extras = max_extras + ? do_alloca(sizeof(zend_class_entry *) * max_extras, extras_use_heap) + : NULL; + uint32_t extra_count = 0; + + for (zend_class_entry *ancestor = base; ancestor; ancestor = ancestor->parent) { + if (!ancestor->generic_types || !ancestor->generic_types->implements + || !ancestor->generic_parameters) { + continue; + } + uint32_t a_cap = ancestor->generic_parameters->count; + if (a_cap == 0) continue; + + /* Resolve the binding from the synthesized monomorph (whose direct + * binding is to `base`) to `ancestor`. For the immediate-base case, + * the binding is the args we were called with. For deeper ancestors, + * compose base→ancestor's binding and then substitute its T-refs + * with the args we hold. */ + zend_type bound_args[ZEND_GENERIC_MAX_PARAMS]; + uint32_t bound_arity = 0; + if (ancestor == base) { + if (arity > ZEND_GENERIC_MAX_PARAMS) continue; + for (uint32_t k = 0; k < arity; k++) bound_args[k] = args[k]; + bound_arity = arity; + } else { + bool have = zend_get_inheritance_binding_full_cached( + base, ancestor, bound_args, ZEND_GENERIC_MAX_PARAMS, &bound_arity); + if (!have) continue; + for (uint32_t k = 0; k < bound_arity; k++) { + bound_args[k] = zend_substitute_leaf_type_param( + bound_args[k], args, arity); + } + } + + const HashTable *impl_table = ancestor->generic_types->implements; + for (uint32_t i = 0; i < ancestor->num_interfaces; i++) { + const zend_type *impl_args_t = (const zend_type *) zend_hash_index_find_ptr(impl_table, i); + if (!impl_args_t || !ZEND_TYPE_HAS_NAMED_WITH_ARGS(*impl_args_t)) { + continue; + } + const zend_type_named_with_args *impl_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*impl_args_t); + + ALLOCA_FLAG(sub_use_heap) + zend_type *sub_args = do_alloca( + sizeof(zend_type) * impl_nwa->count, sub_use_heap); + bool all_ground = true; + for (uint32_t j = 0; j < impl_nwa->count; j++) { + sub_args[j] = zend_substitute_leaf_type_param( + impl_nwa->args[j], bound_args, bound_arity); + if (zend_type_contains_type_parameter(sub_args[j])) { + all_ground = false; + break; + } + } + if (!all_ground) { + free_alloca(sub_args, sub_use_heap); + continue; + } + zend_class_entry *iface_base = ancestor->interfaces + ? ancestor->interfaces[i] : NULL; + if (!iface_base) { + iface_base = zend_lookup_class(impl_nwa->name); + } + zend_class_entry *iface_mono = NULL; + if (iface_base && iface_base->generic_parameters) { + iface_mono = zend_synthesize_monomorph( + iface_base, sub_args, impl_nwa->count); + } + free_alloca(sub_args, sub_use_heap); + if (!iface_mono) continue; + + bool already = false; + for (uint32_t k = 0; k < linked->num_interfaces; k++) { + if (linked->interfaces[k] == iface_mono) { already = true; break; } + } + if (already) continue; + for (uint32_t k = 0; k < extra_count; k++) { + if (extras[k] == iface_mono) { already = true; break; } + } + if (already) continue; + extras[extra_count++] = iface_mono; + } + } + + if (extra_count > 0) { + uint32_t new_count = linked->num_interfaces + extra_count; + linked->interfaces = perealloc( + linked->interfaces, + sizeof(zend_class_entry *) * new_count, + linked->type == ZEND_INTERNAL_CLASS); + for (uint32_t k = 0; k < extra_count; k++) { + linked->interfaces[linked->num_interfaces + k] = extras[k]; + do_implement_interface(linked, extras[k]); + } + linked->num_interfaces = new_count; + } + if (extras) free_alloca(extras, extras_use_heap); + } + + /* Substitute T-typed class constants. When the base declares `const T FOO`, + * the inherited entry in the mono is a shared pointer to the base's + * `zend_class_constant` with type T (erased to mixed). For the mono, + * clone any entry whose pre-erasure side-table type references a generic + * parameter, and substitute T → arg so reflection and assignability + * checks see the concrete type for this monomorph. */ + if (base->generic_types && base->generic_types->class_constants) { + zend_string *cname; + zval *zv; + ZEND_HASH_FOREACH_STR_KEY_VAL(base->generic_types->class_constants, cname, zv) { + const zend_type *pre = (const zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_IS_SET(*pre) || !zend_type_contains_type_parameter(*pre)) { + continue; + } + zend_type sub = zend_substitute_leaf_type_param(*pre, args, arity); + if (ZEND_TYPE_HAS_TYPE_PARAMETER(sub)) { + continue; + } + zval *cv = zend_hash_find_known_hash(&linked->constants_table, cname); + if (!cv) continue; + zend_class_constant *orig = (zend_class_constant *) Z_PTR_P(cv); + zend_class_constant *clone = zend_arena_alloc(&CG(arena), sizeof(zend_class_constant)); + memcpy(clone, orig, sizeof(zend_class_constant)); + clone->type = sub; + zend_type_copy_ctor(&clone->type, /* use_arena */ true, /* persistent */ false); + Z_PTR_P(cv) = clone; + } ZEND_HASH_FOREACH_END(); + } + + zend_string_release(lc_canonical); + return linked; +} + +/* === Monomorph name parser === + * + * Parses class-name strings like "Box>" into a base + * class name plus a type-args list. Used by the class-lookup hook so that + * unserialize, dynamic `new $name`, and `class_exists` all synthesize the + * monomorph on demand if it hasn't been materialized in this request yet. + * + * Accepts the same shape the canonicalizer produces, plus generally any + * type-expression syntax: unions with `|`, intersections with `&`, DNF + * parens, nested generics, fully-qualified namespaced names, scalar + * keywords (int, string, bool, float, array, object, callable, mixed, + * void, never, null, true, false). Whitespace is permitted. + * + * The parser is intentionally permissive about input form; the synthesizer + * canonicalizes anyway, so semantically-equivalent inputs collapse to one + * class entry. */ + +typedef struct { + const char *p; + const char *end; + bool error; +} zend_monomorph_parser; + +static void zend_mp_skip_ws(zend_monomorph_parser *s) { + while (s->p < s->end && (*s->p == ' ' || *s->p == '\t')) s->p++; +} + +static bool zend_mp_eat(zend_monomorph_parser *s, char c) { + zend_mp_skip_ws(s); + if (s->p < s->end && *s->p == c) { s->p++; return true; } + return false; +} + +static bool zend_mp_peek(zend_monomorph_parser *s, char c) { + zend_mp_skip_ws(s); + return s->p < s->end && *s->p == c; +} + +static zend_string *zend_mp_read_ident(zend_monomorph_parser *s) { + zend_mp_skip_ws(s); + const char *start = s->p; + while (s->p < s->end) { + unsigned char ch = (unsigned char)*s->p; + bool ok = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') + || (ch >= '0' && ch <= '9') || ch == '_' || ch == '\\' || ch >= 0x80; + if (!ok) break; + s->p++; + } + if (s->p == start) return NULL; + return zend_string_init(start, s->p - start, 0); +} + +static struct { const char *kw; uint32_t bit; } zend_mp_scalars[] = { + { "int", MAY_BE_LONG }, + { "string", MAY_BE_STRING }, + { "float", MAY_BE_DOUBLE }, + { "bool", MAY_BE_BOOL }, + { "true", MAY_BE_TRUE }, + { "false", MAY_BE_FALSE }, + { "null", MAY_BE_NULL }, + { "array", MAY_BE_ARRAY }, + { "object", MAY_BE_OBJECT }, + { "callable", MAY_BE_CALLABLE }, + { "void", MAY_BE_VOID }, + { "never", MAY_BE_NEVER }, + { "mixed", MAY_BE_ANY }, + { NULL, 0 } +}; + +static uint32_t zend_mp_scalar_bit(const zend_string *ident) { + for (size_t i = 0; zend_mp_scalars[i].kw; i++) { + size_t kwlen = strlen(zend_mp_scalars[i].kw); + if (ZSTR_LEN(ident) == kwlen + && zend_binary_strcasecmp( + ZSTR_VAL(ident), ZSTR_LEN(ident), + zend_mp_scalars[i].kw, kwlen) == 0) { + return zend_mp_scalars[i].bit; + } + } + return 0; +} + +static zend_type zend_mp_parse_type(zend_monomorph_parser *s); + +/* Single atomic type: scalar keyword, class name (with optional <...>), + * or a parenthesized intersection used inside a union (DNF). */ +static zend_type zend_mp_parse_atom(zend_monomorph_parser *s) +{ + zend_mp_skip_ws(s); + if (zend_mp_eat(s, '(')) { + /* DNF intersection branch */ + zend_type inner = zend_mp_parse_type(s); + if (s->error || !zend_mp_eat(s, ')')) { + s->error = true; + zend_type_release(inner, /* persistent */ false); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + return inner; + } + + zend_string *ident = zend_mp_read_ident(s); + if (!ident) { s->error = true; return (zend_type) ZEND_TYPE_INIT_NONE(0); } + + /* Nested generic? */ + if (zend_mp_peek(s, '<')) { + s->p++; /* consume '<' */ + 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(s); + if (s->error) break; + } while (zend_mp_eat(s, ',')); + if (!s->error && !zend_mp_eat(s, '>')) s->error = true; + if (s->error) { + for (uint32_t i = 0; i < count; i++) { + zend_type_release(args[i], false); + } + efree(args); + zend_string_release(ident); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + zend_type_named_with_args *payload = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count)); + payload->name = ident; + payload->name_attr = 0; + payload->count = count; + for (uint32_t i = 0; i < count; i++) payload->args[i] = args[i]; + efree(args); + zend_type result = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_PTR(result, payload); + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NAMED_WITH_ARGS_BIT; + return result; } - if (ZSTR_HAS_CE_CACHE(ce->name)) { - ZSTR_SET_CE_CACHE(ce->name, ce); + uint32_t scalar = zend_mp_scalar_bit(ident); + if (scalar) { + zend_string_release(ident); + zend_type t = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(t) = scalar; + return t; } - - return ce; + return (zend_type) ZEND_TYPE_INIT_CLASS(ident, 0, 0); } -/* }}} */ -/* Check whether early binding is prevented due to unresolved types in inheritance checks. */ -static inheritance_status zend_can_early_bind(zend_class_entry *ce, const zend_class_entry *parent_ce) /* {{{ */ +/* `atom (& atom)*` -- a single intersection branch. + * Returns either a single atom or an intersection list. */ +static zend_type zend_mp_parse_intersection(zend_monomorph_parser *s) { - zend_string *key; - zend_function *parent_func; - const zend_property_info *parent_info; - const zend_class_constant *parent_const; - inheritance_status overall_status = INHERITANCE_SUCCESS; - - ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, parent_func) { - zval *zv = zend_hash_find_known_hash(&ce->function_table, key); - if (zv) { - zend_function *child_func = Z_FUNC_P(zv); - inheritance_status status = - do_inheritance_check_on_method( - child_func, child_func->common.scope, - parent_func, parent_func->common.scope, - ce, NULL, - ZEND_INHERITANCE_CHECK_SILENT | ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_CHECK_VISIBILITY); - if (UNEXPECTED(status == INHERITANCE_WARNING)) { - overall_status = INHERITANCE_WARNING; - } else if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { - return status; - } - } - } ZEND_HASH_FOREACH_END(); + zend_type first = zend_mp_parse_atom(s); + if (s->error || !zend_mp_peek(s, '&')) return first; + + uint32_t cap = 4, count = 1; + zend_type *parts = emalloc(sizeof(zend_type) * cap); + parts[0] = first; + while (zend_mp_eat(s, '&')) { + if (count == cap) { cap *= 2; parts = erealloc(parts, sizeof(zend_type) * cap); } + parts[count++] = zend_mp_parse_atom(s); + if (s->error) break; + } + if (s->error) { + for (uint32_t i = 0; i < count; i++) zend_type_release(parts[i], false); + efree(parts); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + zend_type_list *list = emalloc(ZEND_TYPE_LIST_SIZE(count)); + list->num_types = count; + for (uint32_t i = 0; i < count; i++) list->types[i] = parts[i]; + efree(parts); + zend_type result = ZEND_TYPE_INIT_INTERSECTION(list, 0); + return result; +} - ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->properties_info, key, parent_info) { - const zval *zv; - if ((parent_info->flags & ZEND_ACC_PRIVATE) || !ZEND_TYPE_IS_SET(parent_info->type)) { - continue; +/* `intersection ('|' intersection)*` -- top-level type expression. + * Returns either a single intersection, a single atom, or a union list. */ +static zend_type zend_mp_parse_type(zend_monomorph_parser *s) +{ + zend_type first = zend_mp_parse_intersection(s); + if (s->error || !zend_mp_peek(s, '|')) return first; + + uint32_t cap = 4, count = 1; + zend_type *parts = emalloc(sizeof(zend_type) * cap); + parts[0] = first; + while (zend_mp_eat(s, '|')) { + if (count == cap) { cap *= 2; parts = erealloc(parts, sizeof(zend_type) * cap); } + parts[count++] = zend_mp_parse_intersection(s); + if (s->error) break; + } + if (s->error) { + for (uint32_t i = 0; i < count; i++) zend_type_release(parts[i], false); + efree(parts); + return (zend_type) ZEND_TYPE_INIT_NONE(0); + } + + /* Collapse scalar-bit-only parts into the type_mask of the resulting list + * so e.g. "int|string" lands as a single zend_type with both bits set, no + * list needed. */ + uint32_t scalar_mask = 0; + uint32_t complex_count = 0; + for (uint32_t i = 0; i < count; i++) { + bool is_pure_scalar = !ZEND_TYPE_HAS_NAME(parts[i]) + && !ZEND_TYPE_HAS_LIST(parts[i]) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i]); + if (is_pure_scalar) { + scalar_mask |= ZEND_TYPE_FULL_MASK(parts[i]); + } else { + complex_count++; } - - zv = zend_hash_find_known_hash(&ce->properties_info, key); - if (zv) { - const zend_property_info *child_info = Z_PTR_P(zv); - if (ZEND_TYPE_IS_SET(child_info->type)) { - inheritance_status status = verify_property_type_compatibility(parent_info, child_info, prop_get_variance(parent_info), false, false); - if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { - return status; - } + } + if (complex_count == 0) { + efree(parts); + zend_type t = ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_FULL_MASK(t) = scalar_mask; + return t; + } + if (complex_count == 1 && scalar_mask == 0) { + zend_type only = (zend_type) ZEND_TYPE_INIT_NONE(0); + for (uint32_t i = 0; i < count; i++) { + if (ZEND_TYPE_HAS_NAME(parts[i]) || ZEND_TYPE_HAS_LIST(parts[i]) + || ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i])) { + only = parts[i]; + break; } } - } ZEND_HASH_FOREACH_END(); - - ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, parent_const) { - const zval *zv; - if ((ZEND_CLASS_CONST_FLAGS(parent_const) & ZEND_ACC_PRIVATE) || !ZEND_TYPE_IS_SET(parent_const->type)) { - continue; + efree(parts); + return only; + } + zend_type_list *list = emalloc(ZEND_TYPE_LIST_SIZE(complex_count)); + list->num_types = complex_count; + uint32_t li = 0; + for (uint32_t i = 0; i < count; i++) { + bool is_pure_scalar = !ZEND_TYPE_HAS_NAME(parts[i]) + && !ZEND_TYPE_HAS_LIST(parts[i]) + && !ZEND_TYPE_HAS_NAMED_WITH_ARGS(parts[i]); + if (!is_pure_scalar) { + list->types[li++] = parts[i]; } + } + efree(parts); + zend_type result = ZEND_TYPE_INIT_UNION(list, 0); + ZEND_TYPE_FULL_MASK(result) |= scalar_mask; + return result; +} - zv = zend_hash_find_known_hash(&ce->constants_table, key); - if (zv) { - const zend_class_constant *child_const = Z_PTR_P(zv); - if (ZEND_TYPE_IS_SET(child_const->type)) { - inheritance_status status = class_constant_types_compatible(parent_const, child_const); - ZEND_ASSERT(status != INHERITANCE_WARNING); - if (UNEXPECTED(status != INHERITANCE_SUCCESS)) { - return status; - } - } +/* Find the matching `>` for the outermost `<` in `name`. Returns the offset + * of `<` (in bytes from the start) and the length of the args span. Returns + * false if `name` doesn't have generic shape or has unbalanced angles. */ +static bool zend_mp_split_name( + const zend_string *name, size_t *out_lt_pos, size_t *out_args_len) +{ + const char *s = ZSTR_VAL(name); + size_t n = ZSTR_LEN(name); + if (n < 4) return false; /* shortest possible: X */ + if (s[n - 1] != '>') return false; + + int depth = 0; + size_t lt = SIZE_MAX; + for (size_t i = 0; i < n; i++) { + if (s[i] == '<') { + if (depth == 0) lt = i; + depth++; + } else if (s[i] == '>') { + depth--; + if (depth == 0 && i != n - 1) return false; /* trailing data after closing */ + } + } + if (depth != 0 || lt == SIZE_MAX || lt == 0) return false; + *out_lt_pos = lt; + *out_args_len = n - lt - 2; /* exclude '<' and '>' */ + return true; +} + +ZEND_API zend_class_entry *zend_try_synthesize_monomorph_by_name( + zend_string *name, uint32_t flags) +{ + size_t lt_pos, args_len; + if (!zend_mp_split_name(name, <_pos, &args_len)) return NULL; + + zend_string *base_name = zend_string_init(ZSTR_VAL(name), lt_pos, 0); + + /* Recursively look up the base class. If the base is itself a monomorph + * name, the lookup hook will be reentrant and synthesize it first. */ + uint32_t lookup_flags = flags | ZEND_FETCH_CLASS_NO_AUTOLOAD; + zend_class_entry *base = zend_lookup_class_ex(base_name, NULL, lookup_flags); + if (!base && !(flags & ZEND_FETCH_CLASS_NO_AUTOLOAD)) { + /* Allow autoload for the base name only, in case it isn't loaded yet. */ + base = zend_lookup_class_ex(base_name, NULL, flags & ~ZEND_FETCH_CLASS_NO_AUTOLOAD); + } + zend_string_release(base_name); + if (!base) return NULL; + if (!base->generic_parameters) { + /* A name like "Plain" where Plain is non-generic does not name an + * existing class. A by-name lookup reports that as "not found" (NULL) + * rather than raising; type arguments in a real type/`new` position are + * rejected at compile time on their own path. */ + return NULL; + } + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(name) + lt_pos + 1, + .end = ZSTR_VAL(name) + ZSTR_LEN(name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + /* A lookup by name (class_exists(), is-a probes, the autoloader) must not + * raise: a monomorph name whose arguments violate the declared bounds simply + * names a class that does not exist. Validate the supplied arguments against + * the base's bounds here and report "not found" (NULL) on violation, instead + * of letting zend_synthesize_monomorph fatal as the `new` / type-declaration + * paths intentionally do. */ + uint32_t bound_check_count = count < base->generic_parameters->count + ? count : base->generic_parameters->count; + for (uint32_t i = 0; i < bound_check_count; i++) { + zend_type bound = base->generic_parameters->parameters[i].bound; + if (ZEND_TYPE_IS_SET(bound) + && zend_check_generic_arg_satisfies_bound(base, args[i], base, bound) != INHERITANCE_SUCCESS) { + for (uint32_t j = 0; j < count; j++) zend_type_release(args[j], false); + efree(args); + return NULL; } - } ZEND_HASH_FOREACH_END(); + } - return overall_status; + zend_class_entry *mono = zend_synthesize_monomorph(base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; } -/* }}} */ -static zend_always_inline bool register_early_bound_ce(zval *delayed_early_binding, zend_string *lcname, zend_class_entry *ce) { - if (delayed_early_binding) { - if (EXPECTED(!(ce->ce_flags & ZEND_ACC_PRELOADED))) { - if (zend_hash_set_bucket_key(EG(class_table), (Bucket *)delayed_early_binding, lcname) != NULL) { - Z_CE_P(delayed_early_binding) = ce; - return true; +/* 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, bool use_arena) +{ + 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 = use_arena + ? zend_arena_alloc(&CG(arena), sizeof(zend_arg_info) * total) + : emalloc(sizeof(zend_arg_info) * total); + memcpy(new_block, orig_block, sizeof(zend_arg_info) * total); + + const HashTable *pre_params = base->generic_types ? base->generic_types->parameters : NULL; + 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; + } + + /* Specialize a BARE FUNCTION_LIKE type-parameter leaf (`T $x`), and also a + * union/intersection of leaves (`T|Other`, `A|B`): substitute the function + * T-refs and, when every member is then ground, install the concrete type so + * RECV/return checks enforce it. A composite carrying a `Box`-style + * NAMED_WITH_ARGS stays erased — 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; + bool is_leaf_union = !is_bare_leaf && pre && ZEND_TYPE_IS_SET(*pre) + && zend_type_is_reifiable_leaf_composite(*pre); + zend_type sub; + bool substitute = is_bare_leaf || is_leaf_union; + if (substitute) { + sub = zend_substitute_function_type_param(*pre, args, arity); + /* A composite that didn't fully ground (an unbound T remains) keeps + * the erased arg_info; a bare leaf always grounds here. */ + if (is_leaf_union && zend_type_contains_type_parameter(sub)) { + substitute = false; } + } + if (substitute) { + /* use_arena false on the AOT/preload path: emalloc, not arena. */ + zend_type_copy_ctor(&sub, use_arena, /* persistent */ false); + new_block[slot].type = sub; } else { - /* If preloading is used, don't replace the existing bucket, add a new one. */ - if (zend_hash_add_ptr(EG(class_table), lcname, ce) != NULL) { - return true; - } + zend_type_copy_ctor(&new_block[slot].type, use_arena, /* 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); } - zend_class_entry *old_ce = zend_hash_find_ptr(EG(class_table), lcname); - ZEND_ASSERT(old_ce); - zend_class_redeclaration_error(E_COMPILE_ERROR, old_ce); - return false; - } - if (zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL) { - return true; } - return false; + + return new_block + (has_return ? 1 : 0); } -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) /* {{{ */ +ZEND_API zend_function *zend_synthesize_function_monomorph( + zend_function *base, const zend_type *args, uint32_t arity) { - inheritance_status status; - zend_class_entry *proto = NULL; - zend_class_entry *orig_linking_class; + 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; + } - if (ce->ce_flags & ZEND_ACC_LINKED) { - ZEND_ASSERT(ce->parent == NULL); - if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ce))) { + /* 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_observer_class_linked_notify(ce, lcname); - return ce; } - uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE; - UPDATE_IS_CACHEABLE(parent_ce); - if (is_cacheable) { - if (zend_inheritance_cache_get && zend_inheritance_cache_add) { - zend_class_entry *ret = zend_inheritance_cache_get(ce, parent_ce, NULL); - if (ret) { - if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ret))) { - return NULL; - } - zend_observer_class_linked_notify(ret, lcname); - return ret; + zend_string *display_name = zend_generic_canonical_class_name( + base->common.function_name, args, arity); + zend_string *lc_name = zend_string_tolower(display_name); + + /* A method's mangled name carries only the method name + type args, so the + * same method name in two classes collides (E::pick<...> and F::pick<...> + * both mangle to pick<...>). Qualify the EG(function_table) key with the + * lowercased declaring scope so distinct classes get distinct monomorphs. + * The "::" can never appear in a free-function monomorph name, so a + * scope-qualified key is never mistaken for a by-name-dispatchable one. */ + if (base->common.scope) { + zend_string *lc_scope = zend_string_tolower(base->common.scope->name); + zend_string *scoped = zend_string_concat3( + ZSTR_VAL(lc_scope), ZSTR_LEN(lc_scope), + "::", 2, + ZSTR_VAL(lc_name), ZSTR_LEN(lc_name)); + zend_string_release(lc_scope); + zend_string_release(lc_name); + lc_name = scoped; + } + + zend_function *existing = zend_hash_find_ptr(EG(function_table), lc_name); + if (existing) { + /* Preload reaches INSTALL via here, not zend_fetch_function: init run_time_cache or cached opcodes hit NULL. */ + if (existing->type == ZEND_USER_FUNCTION) { + zend_init_func_run_time_cache(&existing->op_array); + } + zend_string_release(display_name); + zend_string_release(lc_name); + return existing; + } + + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(&base->op_array, args, arity, true); + + 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_type_arg_table_alloc(tcount); + mono_targs->persisted = true; + for (uint32_t i = 0; i < tcount; i++) { + if (i < arity && ZEND_TYPE_IS_SET(args[i])) { + zend_type owned = args[i]; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + mono_targs->entries[i].owned_type = owned; + zend_string *cname = zend_type_arg_canonical_name(args[i]); + mono_targs->entries[i].name = cname; } - } else { - is_cacheable = 0; } - proto = ce; } - orig_linking_class = CG(current_linking_class); - CG(current_linking_class) = NULL; - status = zend_can_early_bind(ce, parent_ce); - CG(current_linking_class) = orig_linking_class; - if (EXPECTED(status != INHERITANCE_UNRESOLVED)) { - if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { - /* Lazy class loading */ - ce = zend_lazy_class_load(ce); - } else if (ce->ce_flags & ZEND_ACC_FILE_CACHED) { - /* Lazy class loading */ - ce = zend_lazy_class_load(ce); - ce->ce_flags &= ~ZEND_ACC_FILE_CACHED; + { + 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; - if (UNEXPECTED(!register_early_bound_ce(delayed_early_binding, lcname, ce))) { + /* 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_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity) +{ +#if ZEND_USE_ABS_CONST_ADDR + /* The combined opcode+literal buffer below assumes relative literal layout; caller uses shared synth. */ + (void) fn_table; (void) base; (void) args; (void) arity; + return NULL; +#else + if (!fn_table) { + fn_table = EG(function_table); + } + if (!base || base->type != ZEND_USER_FUNCTION) { + return NULL; + } + const zend_generic_parameter_list *params = base->op_array.generic_parameters; + if (!params || params->count == 0) { + return NULL; + } + + zend_type filled[ZEND_GENERIC_MAX_PARAMS]; + uint32_t total = params->count; + if (arity > total) { + return NULL; + } + if (arity < total) { + for (uint32_t i = 0; i < arity; i++) filled[i] = args[i]; + for (uint32_t i = arity; i < total; i++) { + const zend_generic_parameter *p = ¶ms->parameters[i]; + const zend_type *def = ZEND_TYPE_IS_SET(p->default_pre_erasure) + ? &p->default_pre_erasure + : (ZEND_TYPE_IS_SET(p->default_type) ? &p->default_type : NULL); + if (!def) { + return NULL; + } + filled[i] = *def; + } + args = filled; + arity = total; + } + for (uint32_t i = 0; i < arity; i++) { + if (zend_type_contains_type_parameter(args[i])) { return NULL; } + } - orig_linking_class = CG(current_linking_class); - CG(current_linking_class) = is_cacheable ? ce : 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); - bool orig_record_errors = EG(record_errors); + zend_function *existing = zend_hash_find_ptr(fn_table, lc_name); + if (existing) { + zend_string_release(display_name); + zend_string_release(lc_name); + return existing; + } - zend_try{ - CG(zend_lineno) = ce->info.user.line_start; + /* Unlike the shared synth, opcodes+literals are a fresh OWN copy the optimizer rewrites in place. */ + const zend_op_array *src = &base->op_array; + zend_op_array *mono = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); + memcpy(mono, src, sizeof(zend_op_array)); + /* reserved[] memcpy'd from base may point into a transient arena: clear to avoid a dangling pointer. */ + memset(mono->reserved, 0, sizeof(mono->reserved)); - if (!orig_record_errors) { - zend_begin_record_errors(); - } + /* persist efree()s opcodes and _put_free()s arg_info/vars/etc., so each must be an OWN heap alloc. */ + size_t ops_sz = ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op) * src->last, 16); + size_t lit_sz = sizeof(zval) * src->last_literal; + char *buf = emalloc(ops_sz + lit_sz); + memcpy(buf, src->opcodes, ops_sz); + mono->opcodes = (zend_op *) buf; + if (src->last_literal) { + zval *dst_lit = (zval *) (buf + ops_sz); + memcpy(dst_lit, src->literals, lit_sz); + for (uint32_t i = 0; i < src->last_literal; i++) { + Z_TRY_ADDREF(dst_lit[i]); + } + mono->literals = dst_lit; + } -#ifdef ZEND_OPCACHE_SHM_REATTACHMENT - zend_link_hooked_object_iter(ce); -#endif + if (src->last_var) { + mono->vars = emalloc(sizeof(zend_string *) * src->last_var); + for (uint32_t i = 0; i < src->last_var; i++) { + mono->vars[i] = zend_string_copy(src->vars[i]); + } + } else { + mono->vars = NULL; + } + + zend_arg_info *new_arg_info = zend_monomorph_build_arg_info(src, args, arity, /* use_arena */ false); + + zend_type_arg_table *mono_targs = zend_type_arg_table_alloc(total); + mono_targs->persisted = true; + for (uint32_t i = 0; i < total; i++) { + if (i < arity && ZEND_TYPE_IS_SET(args[i])) { + zend_type owned = args[i]; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + mono_targs->entries[i].owned_type = owned; + mono_targs->entries[i].name = zend_type_arg_canonical_name(args[i]); + } + } + { + zend_generic_type_table *gt = emalloc(sizeof(zend_generic_type_table)); + memset(gt, 0, sizeof(*gt)); + /* Deep-copy base's turbofish_args: the copied body references its boxes and persist frees the table (else double-free). */ + if (src->generic_types && src->generic_types->turbofish_args) { + zend_ulong tf_idx; + zend_turbofish_args_entry *tf_entry; + ZEND_HASH_FOREACH_NUM_KEY_PTR(src->generic_types->turbofish_args, tf_idx, tf_entry) { + zend_type box = tf_entry->args_box; + zend_type_copy_ctor(&box, /* use_arena */ false, /* persistent */ false); + zend_generic_type_table_set_turbofish_args(gt, (uint32_t) tf_idx, box); + } ZEND_HASH_FOREACH_END(); + } + gt->persisted = false; + gt->monomorph_type_args = mono_targs; + mono->generic_types = gt; + } + mono->generic_parameters = NULL; /* concrete; and persist would free a shared list */ - zend_do_inheritance_ex(ce, parent_ce, status == INHERITANCE_SUCCESS); - if (parent_ce && parent_ce->num_interfaces) { - zend_do_inherit_interfaces(ce, parent_ce); - } - zend_build_properties_info_table(ce); - if ((ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) == ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) { - zend_verify_abstract_class(ce); - } - zend_inheritance_check_override(ce); - ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)); - ce->ce_flags |= ZEND_ACC_LINKED; + /* persist _put_free's these: own live_range/try_catch, drop doc_comment/attributes. */ + if (src->last_live_range) { + mono->live_range = emalloc(sizeof(zend_live_range) * src->last_live_range); + memcpy(mono->live_range, src->live_range, sizeof(zend_live_range) * src->last_live_range); + } else { + mono->live_range = NULL; + } + if (src->last_try_catch) { + mono->try_catch_array = emalloc(sizeof(zend_try_catch_element) * src->last_try_catch); + memcpy(mono->try_catch_array, src->try_catch_array, sizeof(zend_try_catch_element) * src->last_try_catch); + } else { + mono->try_catch_array = NULL; + } + mono->doc_comment = NULL; + mono->attributes = NULL; - CG(current_linking_class) = orig_linking_class; - } zend_catch { - if (!orig_record_errors) { - EG(record_errors) = false; - zend_free_recorded_errors(); - } - zend_bailout(); - } zend_end_try(); + /* Own the dynamic_func_defs array (persist _put_free's it); the closures stay shared (persist xlat-dedups). */ + if (src->num_dynamic_func_defs) { + mono->dynamic_func_defs = emalloc(sizeof(zend_function *) * src->num_dynamic_func_defs); + memcpy(mono->dynamic_func_defs, src->dynamic_func_defs, + sizeof(zend_function *) * src->num_dynamic_func_defs); + } else { + mono->dynamic_func_defs = NULL; + } - if (is_cacheable) { - HashTable *ht = (HashTable*)ce->inheritance_cache; - zend_class_entry *new_ce; + mono->fn_flags2 |= ZEND_ACC2_MONOMORPH_TYPE_ARGS; - ce->inheritance_cache = NULL; - new_ce = zend_inheritance_cache_add(ce, proto, parent_ce, NULL, ht); - if (new_ce) { - zval *zv = zend_hash_find_known_hash(CG(class_table), lcname); - ce = new_ce; - Z_CE_P(zv) = ce; + mono->refcount = NULL; + mono->fn_flags &= ~(ZEND_ACC_IMMUTABLE | ZEND_ACC_HEAP_RT_CACHE | ZEND_ACC_PRELOADED); + mono->fn_flags |= ZEND_ACC_TRAIT_CLONE; + mono->function_name = zend_string_copy(src->function_name); + /* Own a filename ref: persist consumes one, and base's is shared with EG(included_files) (else double-free). */ + mono->filename = zend_string_copy(src->filename); + if (new_arg_info) { + mono->arg_info = new_arg_info; + + /* Bake each RECV mask to the concrete arg_info and drop TRAIT_CLONE, only if all RECV params are mask-only (no NAMED). */ + bool can_fast_recv = !(mono->fn_flags & ZEND_ACC_VARIADIC); + for (uint32_t i = 0; can_fast_recv && i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode != ZEND_RECV) { + continue; } - if (ht) { - zend_hash_destroy(ht); - FREE_HASHTABLE(ht); + uint32_t an = op->op1.num; + if (an == 0 || an > mono->num_args) { + can_fast_recv = false; + break; + } + zend_type *t = &new_arg_info[an - 1].type; + if (ZEND_TYPE_IS_SET(*t) && !ZEND_TYPE_IS_ONLY_MASK(*t)) { + can_fast_recv = false; + break; } } - - if (!orig_record_errors) { - zend_emit_recorded_errors(); - zend_free_recorded_errors(); + if (can_fast_recv) { + for (uint32_t i = 0; i < mono->last; i++) { + zend_op *op = &mono->opcodes[i]; + if (op->opcode == ZEND_RECV) { + op->op2.num = (uint32_t) ZEND_TYPE_FULL_MASK(new_arg_info[op->op1.num - 1].type); + } + } + mono->fn_flags &= ~ZEND_ACC_TRAIT_CLONE; } + } - if (ZSTR_HAS_CE_CACHE(ce->name)) { - ZSTR_SET_CE_CACHE(ce->name, ce); - } - zend_observer_class_linked_notify(ce, lcname); + mono->static_variables = src->static_variables + ? zend_array_dup(src->static_variables) : NULL; + /* No run_time_cache slot here: persist assigns it; allocating now corrupts the per-request map_ptr table. */ + ZEND_MAP_PTR_INIT(mono->run_time_cache, NULL); + ZEND_MAP_PTR_INIT(mono->static_variables_ptr, NULL); - return ce; + zend_function *mono_fn = (zend_function *) mono; + if (!zend_hash_add_ptr(fn_table, lc_name, mono_fn)) { + existing = zend_hash_find_ptr(fn_table, lc_name); + zend_string_release(display_name); + zend_string_release(lc_name); + return existing ? existing : NULL; } - return NULL; + + zend_string_release(display_name); + zend_string_release(lc_name); + return mono_fn; +#endif +} + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name) +{ + size_t lt_pos, args_len; + 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; +} + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *name) +{ + /* `name` is case-preserved: lowercase only the base part for the table lookup, parse args as-is. */ + size_t lt_pos, args_len; + if (!zend_mp_split_name(name, <_pos, &args_len)) return NULL; + + zend_string *base_part = zend_string_init(ZSTR_VAL(name), lt_pos, 0); + zend_string *base_lc = zend_string_tolower(base_part); + zend_string_release(base_part); + zend_function *base = zend_hash_find_ptr(fn_table, base_lc); + zend_string_release(base_lc); + if (!base || base->type != ZEND_USER_FUNCTION || !base->op_array.generic_parameters) { + return NULL; + } + + zend_monomorph_parser parser = { + .p = ZSTR_VAL(name) + lt_pos + 1, + .end = ZSTR_VAL(name) + ZSTR_LEN(name) - 1, + .error = false, + }; + uint32_t cap = 4, count = 0; + zend_type *args = emalloc(sizeof(zend_type) * cap); + do { + if (count == cap) { cap *= 2; args = erealloc(args, sizeof(zend_type) * cap); } + args[count++] = zend_mp_parse_type(&parser); + if (parser.error) break; + } while (zend_mp_eat(&parser, ',')); + zend_mp_skip_ws(&parser); + if (parser.error || parser.p != parser.end) { + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return NULL; + } + + zend_function *mono = zend_synthesize_specialized_monomorph_into(fn_table, base, args, count); + for (uint32_t i = 0; i < count; i++) zend_type_release(args[i], false); + efree(args); + return mono; } -/* }}} */ diff --git a/Zend/zend_inheritance.h b/Zend/zend_inheritance.h index fdcbd95764b3..4e8e648b9fb7 100644 --- a/Zend/zend_inheritance.h +++ b/Zend/zend_inheritance.h @@ -24,8 +24,34 @@ BEGIN_EXTERN_C() +typedef enum { + INHERITANCE_UNRESOLVED = -1, + INHERITANCE_ERROR = 0, + INHERITANCE_WARNING = 1, + INHERITANCE_SUCCESS = 2, +} zend_inheritance_status; + +typedef struct _zend_inheritance_binding_cache { + const zend_class_entry *ce; + const zend_class_entry *target; + uint32_t arity; + bool present; + bool valid; + zend_type args[ZEND_GENERIC_MAX_PARAMS]; +} zend_inheritance_binding_cache; + ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface); ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *parent_ce, bool checked); +ZEND_API void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent); +ZEND_API zend_inheritance_status zend_check_generic_arg_satisfies_bound( + zend_class_entry *arg_scope, zend_type arg, + zend_class_entry *bound_scope, zend_type bound); +ZEND_API bool zend_get_inheritance_binding_full( + const zend_class_entry *ce, + const zend_class_entry *target, + zend_type *out_args, + uint32_t out_capacity, + uint32_t *out_arity); static zend_always_inline void zend_do_inheritance(zend_class_entry *ce, zend_class_entry *parent_ce) { zend_do_inheritance_ex(ce, parent_ce, 0); @@ -33,22 +59,92 @@ static zend_always_inline void zend_do_inheritance(zend_class_entry *ce, zend_cl ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name, const zend_string *key); +/* Monomorphization: synthesize a generic class application as a real + * class_entry registered in EG(class_table) under the canonical name. The + * synthesized class extends `base` with the supplied type args, inheriting + * all methods/properties/constants with proper substitution. + * + * Idempotent: a second call with the same (base, args) canonicalizing to an + * existing entry returns that entry. Returns NULL on failure (exception will + * be set). The args array is copied; the caller retains ownership. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph( + zend_class_entry *base, const zend_type *args, uint32_t arity); + +/* Same as zend_synthesize_monomorph, but first resolves any TYPE_PARAMETER refs + * in args[] against the currently executing frame's bindings (function-level + * via EX()->type_args; class-level via the lexical class's monomorph descendant). + * Use at runtime `new` sites where the args originate from an op_array side-table + * compiled inside a generic scope and may name enclosing-scope T's by ref. + * Static-build callers with already-resolved args keep using the base. */ +ZEND_API zend_class_entry *zend_synthesize_monomorph_resolved( + zend_class_entry *base, const zend_type *args, uint32_t arity); + +/* For a bare generic class `base`, synthesize (or return the cached) monomorph + * built from the parameters' declared defaults. Returns NULL and throws Error + * if any parameter has no default. If `base` is itself a monomorph (no + * generic_parameters), returns `base` unchanged. Used by ZEND_NEW for the + * `new static()` and dynamic `new $name()` paths. */ +ZEND_API zend_class_entry *zend_get_defaults_monomorph(zend_class_entry *base); + +/* Resolve a naked `new self()` / lexical `new ThisClass()` to the monomorph that + * carries the executing frame's class-level binding (e.g. `new self()` in a + * `C` instance method -> `C`). Returns NULL when no binding is in + * scope. See the definition in zend_inheritance.c. */ +ZEND_API zend_class_entry *zend_resolve_lexical_self_monomorph( + zend_class_entry *lexical, const zend_execute_data *ex); + +/* True when the name has monomorph-canonical shape (contains `<`). The canonical + * encoding for synthesized monomorphs embeds `<...>` in the class name, which is + * invalid in any user-declared class. Use these helpers rather than open-coding + * the memchr check so the encoding stays in one place. */ +static zend_always_inline bool zend_class_name_is_monomorph(const zend_string *name) +{ + return memchr(ZSTR_VAL(name), '<', ZSTR_LEN(name)) != NULL; +} + +static zend_always_inline bool zend_class_is_monomorph(const zend_class_entry *ce) +{ + return zend_class_name_is_monomorph(ce->name); +} + +/* Parses a generic-shaped class-name string ("Box", etc.), looks + * up the base class, and synthesizes the monomorph. Returns NULL if the + * name doesn't have generic shape, the base isn't generic, or parsing fails. + * Used by `zend_lookup_class_ex` to make unserialize, dynamic + * `new $name()`, and `class_exists()` all transparently materialize + * monomorphs on demand. */ +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); + +ZEND_API bool zend_type_is_reifiable_leaf_composite(zend_type t); + +/* 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_synthesize_specialized_monomorph_into( + HashTable *fn_table, zend_function *base, const zend_type *args, uint32_t arity); + +ZEND_API zend_function *zend_synthesize_specialized_monomorph_by_name( + HashTable *fn_table, zend_string *lc_name); + +ZEND_API zend_function *zend_try_synthesize_function_monomorph_by_name(zend_string *lc_name); + void zend_verify_abstract_class(zend_class_entry *ce); 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); void zend_inheritance_check_override(const zend_class_entry *ce); +void zend_check_generic_variance_markers(zend_class_entry *ce); +void zend_check_function_variance_markers(zend_op_array *op_array); ZEND_API extern zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces); ZEND_API extern zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies); -typedef enum { - INHERITANCE_UNRESOLVED = -1, - INHERITANCE_ERROR = 0, - INHERITANCE_WARNING = 1, - INHERITANCE_SUCCESS = 2, -} zend_inheritance_status; - ZEND_API zend_inheritance_status zend_verify_property_hook_variance(const zend_property_info *prop_info, const zend_function *func); ZEND_API ZEND_COLD ZEND_NORETURN void zend_hooked_property_variance_error(const zend_property_info *prop_info); ZEND_API ZEND_COLD ZEND_NORETURN void zend_hooked_property_variance_error_ex(zend_string *value_param_name, zend_string *class_name, zend_string *prop_name); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index b4dda00404ea..96bf9f015e5b 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -50,6 +50,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %destructor { zend_ast_destroy($$); } %destructor { if ($$) zend_string_release_ex($$, 0); } +%precedence PREC_NO_TYPE_ARGS %precedence T_THROW %precedence PREC_ARROW_FUNCTION %precedence T_INCLUDE T_INCLUDE_ONCE T_REQUIRE T_REQUIRE_ONCE @@ -232,6 +233,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_DOLLAR_OPEN_CURLY_BRACES "'${'" %token T_CURLY_OPEN "'{$'" %token T_PAAMAYIM_NEKUDOTAYIM "'::'" +%token T_TURBOFISH "'::<'" %token T_NS_SEPARATOR "'\\'" %token T_ELLIPSIS "'...'" %token T_COALESCE "'??'" @@ -288,6 +290,16 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body %type optional_parameter_list clone_argument_list non_empty_clone_argument_list +%type optional_generic_type_parameter_list generic_type_parameter_list +%type generic_type_parameter_list_inner generic_type_parameter +%type optional_generic_type_parameter_bound optional_generic_type_parameter_default +%type optional_generic_type_argument_list generic_type_argument_list +%type generic_type_argument_list_inner +%type optional_call_type_argument_list call_type_argument_list call_type_argument_list_inner +%type bound_class_name bound_class_name_reference + +%type optional_generic_variance + %type returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers %type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers %type class_modifiers class_modifier anonymous_class_modifiers anonymous_class_modifiers_optional use_type backup_fn_flags @@ -364,10 +376,18 @@ name: ; attribute_decl: - class_name - { $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, NULL); } - | class_name argument_list - { $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, $2); } + class_name optional_call_type_argument_list + { + uint32_t _ga = $2 ? zend_ast_get_list($2)->children : 0; + $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, NULL, $2); + $$->attr = (zend_ast_attr) _ga; + } + | class_name optional_call_type_argument_list argument_list + { + uint32_t _ga = $2 ? zend_ast_get_list($2)->children : 0; + $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, $3, $2); + $$->attr = (zend_ast_attr) _ga; + } ; attribute_group: @@ -551,8 +571,8 @@ catch_list: ; catch_name_list: - class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } - | catch_name_list '|' class_name { $$ = zend_ast_list_add($1, $3); } + bound_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } + | catch_name_list '|' bound_class_name { $$ = zend_ast_list_add($1, $3); } ; optional_variable: @@ -584,10 +604,10 @@ function_name: ; function_declaration_statement: - function returns_ref function_name backup_doc_comment '(' parameter_list ')' return_type + function returns_ref function_name optional_generic_type_parameter_list backup_doc_comment '(' parameter_list ')' return_type backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $13, $1, $4, - zend_ast_get_str($3), $6, NULL, $11, $8, NULL); CG(extra_fn_flags) = $9; } + { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $14, $1, $5, + zend_ast_get_str($3), $7, NULL, $12, $9, NULL, $4); CG(extra_fn_flags) = $10; } ; is_reference: @@ -602,11 +622,11 @@ is_variadic: class_declaration_statement: class_modifiers T_CLASS { $$ = CG(zend_lineno); } - T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $3, $7, zend_ast_get_str($4), $5, $6, $9, NULL, NULL); } + T_STRING optional_generic_type_parameter_list extends_from implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $3, $8, zend_ast_get_str($4), $6, $7, $10, NULL, NULL, $5); } | T_CLASS { $$ = CG(zend_lineno); } - T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $2, $6, zend_ast_get_str($3), $4, $5, $8, NULL, NULL); } + T_STRING optional_generic_type_parameter_list extends_from implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $2, $7, zend_ast_get_str($3), $5, $6, $9, NULL, NULL, $4); } ; class_modifiers: @@ -635,20 +655,20 @@ class_modifier: trait_declaration_statement: T_TRAIT { $$ = CG(zend_lineno); } - T_STRING backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_TRAIT, $2, $4, zend_ast_get_str($3), NULL, NULL, $6, NULL, NULL); } + T_STRING optional_generic_type_parameter_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_TRAIT, $2, $5, zend_ast_get_str($3), NULL, NULL, $7, NULL, NULL, $4); } ; interface_declaration_statement: T_INTERFACE { $$ = CG(zend_lineno); } - T_STRING interface_extends_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $2, $5, zend_ast_get_str($3), NULL, $4, $7, NULL, NULL); } + T_STRING optional_generic_type_parameter_list interface_extends_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, NULL, $4); } ; enum_declaration_statement: T_ENUM { $$ = CG(zend_lineno); } T_STRING enum_backing_type implements_list backup_doc_comment '{' class_statement_list '}' - { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4); } + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4, NULL); } ; enum_backing_type: @@ -668,7 +688,7 @@ enum_case_expr: extends_from: %empty { $$ = NULL; } - | T_EXTENDS class_name { $$ = $2; } + | T_EXTENDS bound_class_name { $$ = $2; } ; interface_extends_list: @@ -830,6 +850,94 @@ optional_type_without_static: | type_expr_without_static { $$ = $1; } ; +optional_generic_type_parameter_list: + %empty { $$ = NULL; } + | generic_type_parameter_list { $$ = $1; } +; + +generic_type_parameter_list: + '<' { CG(type_arg_depth)++; } + generic_type_parameter_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_param_list_size($3); $$ = $3; } +; + +generic_type_parameter_list_inner: + generic_type_parameter + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_TYPE_PARAMETER_LIST, $1); } + | generic_type_parameter_list_inner ',' generic_type_parameter + { $$ = zend_ast_list_add($1, $3); } +; + +generic_type_parameter: + optional_generic_variance T_STRING optional_generic_type_parameter_bound + optional_generic_type_parameter_default + { $$ = zend_ast_create_ex(ZEND_AST_GENERIC_TYPE_PARAMETER, $1, $2, $3, $4); } +; + +optional_generic_variance: + %empty { $$ = 0; } + | '+' { $$ = 1; } + | '-' { $$ = 2; } +; + +optional_generic_type_parameter_bound: + %empty { $$ = NULL; } + | ':' type_expr { $$ = $2; } +; + +optional_generic_type_parameter_default: + %empty { $$ = NULL; } + | '=' type_expr { $$ = $2; } +; + +optional_generic_type_argument_list: + %empty %prec PREC_NO_TYPE_ARGS { $$ = NULL; } + | generic_type_argument_list { $$ = $1; } +; + +generic_type_argument_list: + '<' { CG(type_arg_depth)++; } + generic_type_argument_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_arg_list_size($3); $$ = $3; } +; + +generic_type_argument_list_inner: + type_expr + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, $1); } + | generic_type_argument_list_inner ',' type_expr + { $$ = zend_ast_list_add($1, $3); } +; + +optional_call_type_argument_list: + %empty { $$ = NULL; } + | call_type_argument_list { $$ = $1; } +; + +call_type_argument_list: + T_TURBOFISH { CG(type_arg_depth)++; } + call_type_argument_list_inner possible_comma '>' + { CG(type_arg_depth)--; zend_check_generic_arg_list_size($3); $$ = $3; } +; + +call_type_argument_list_inner: + type_expr + { $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_CALL_TYPE_ARGUMENT_LIST, $1); } + | call_type_argument_list_inner ',' type_expr + { $$ = zend_ast_list_add($1, $3); } +; + +bound_class_name: + class_name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } +; + +bound_class_name_reference: + class_name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } + | new_variable { $$ = $1; } + | '(' expr ')' { $$ = $2; } +; + type_expr: type { $$ = $1; } | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } @@ -839,7 +947,9 @@ type_expr: type: type_without_static { $$ = $1; } - | T_STATIC { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); } + | T_STATIC optional_generic_type_argument_list + { zend_ast *bare = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); + $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, bare, $2) : bare; } ; union_type_element: @@ -872,7 +982,8 @@ type_expr_without_static: type_without_static: T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); } | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); } - | name { $$ = $1; } + | name optional_generic_type_argument_list + { $$ = $2 ? zend_ast_create(ZEND_AST_GENERIC_NAMED_TYPE, $1, $2) : $1; } ; union_type_without_static_element: @@ -995,10 +1106,10 @@ attributed_class_statement: | class_const_modifiers T_CONST type_expr class_const_list ';' { $$ = zend_ast_create(ZEND_AST_CLASS_CONST_GROUP, $4, NULL, $3); $$->attr = $1; } - | method_modifiers function returns_ref identifier backup_doc_comment '(' parameter_list ')' + | method_modifiers function returns_ref identifier optional_generic_type_parameter_list backup_doc_comment '(' parameter_list ')' return_type backup_fn_flags method_body backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $12, $2, $5, - zend_ast_get_str($4), $7, NULL, $11, $9, NULL); CG(extra_fn_flags) = $10; } + { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $13, $2, $6, + zend_ast_get_str($4), $8, NULL, $12, $10, NULL, $5); CG(extra_fn_flags) = $11; } | enum_case { $$ = $1; } ; @@ -1010,8 +1121,8 @@ class_statement: ; class_name_list: - class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } - | class_name_list ',' class_name { $$ = zend_ast_list_add($1, $3); } + bound_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } + | class_name_list ',' bound_class_name { $$ = zend_ast_list_add($1, $3); } ; trait_adaptations: @@ -1164,7 +1275,7 @@ property_hook: optional_parameter_list backup_fn_flags property_hook_body backup_fn_flags { $$ = zend_ast_create_decl( ZEND_AST_PROPERTY_HOOK, $1 | $2 | $9, $5, $4, zend_ast_get_str($3), - $6, NULL, $8, NULL, NULL); + $6, NULL, $8, NULL, NULL, NULL); CG(extra_fn_flags) = $7; } ; @@ -1230,14 +1341,14 @@ anonymous_class: extends_from implements_list backup_doc_comment '{' class_statement_list '}' { zend_ast *decl = zend_ast_create_decl( ZEND_AST_CLASS, ZEND_ACC_ANON_CLASS | $1, $3, $7, NULL, - $5, $6, $9, NULL, NULL); - $$ = zend_ast_create(ZEND_AST_NEW, decl, $4); + $5, $6, $9, NULL, NULL, NULL); + $$ = zend_ast_create(ZEND_AST_NEW, decl, $4, NULL); } ; new_dereferenceable: - T_NEW class_name_reference argument_list - { $$ = zend_ast_create(ZEND_AST_NEW, $2, $3); } + T_NEW class_name_reference optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_NEW, $2, $4, $3); } | T_NEW anonymous_class { $$ = $2; } | T_NEW attributes anonymous_class @@ -1245,8 +1356,8 @@ new_dereferenceable: ; new_non_dereferenceable: - T_NEW class_name_reference - { $$ = zend_ast_create(ZEND_AST_NEW, $2, zend_ast_create_list(0, ZEND_AST_ARG_LIST)); } + T_NEW class_name_reference optional_call_type_argument_list + { $$ = zend_ast_create(ZEND_AST_NEW, $2, zend_ast_create_list(0, ZEND_AST_ARG_LIST), $3); } ; expr: @@ -1263,12 +1374,12 @@ expr: | T_CLONE clone_argument_list { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, $2); + $$ = zend_ast_create(ZEND_AST_CALL, name, $2, NULL); } | T_CLONE expr { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2)); + $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2), NULL); } | variable T_PLUS_EQUAL expr { $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); } @@ -1347,7 +1458,7 @@ expr: { $$ = zend_ast_create(ZEND_AST_GREATER_EQUAL, $1, $3); } | expr T_SPACESHIP expr { $$ = zend_ast_create_binary_op(ZEND_SPACESHIP, $1, $3); } - | expr T_INSTANCEOF class_name_reference + | expr T_INSTANCEOF bound_class_name_reference { $$ = zend_ast_create(ZEND_AST_INSTANCEOF, $1, $3); } | '(' expr ')' { $$ = $2; @@ -1373,7 +1484,7 @@ expr: | T_EXIT ctor_arguments { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_EXIT)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, $2); + $$ = zend_ast_create(ZEND_AST_CALL, name, $2, NULL); } | '@' expr { $$ = zend_ast_create(ZEND_AST_SILENCE, $2); } | scalar { $$ = $1; } @@ -1394,16 +1505,16 @@ expr: inline_function: - function returns_ref backup_doc_comment '(' parameter_list ')' lexical_vars return_type + function returns_ref backup_doc_comment optional_generic_type_parameter_list '(' parameter_list ')' lexical_vars return_type backup_fn_flags '{' inner_statement_list '}' backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $13, $1, $3, + { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $14, $1, $3, NULL, - $5, $7, $11, $8, NULL); CG(extra_fn_flags) = $9; } - | fn returns_ref backup_doc_comment '(' parameter_list ')' return_type + $6, $8, $12, $9, NULL, $4); CG(extra_fn_flags) = $10; } + | fn returns_ref backup_doc_comment optional_generic_type_parameter_list '(' parameter_list ')' return_type T_DOUBLE_ARROW backup_fn_flags backup_lex_pos expr backup_fn_flags - { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12, $1, $3, - NULL, $5, NULL, $11, $7, NULL); - CG(extra_fn_flags) = $9; } + { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $13, $1, $3, + NULL, $6, NULL, $12, $8, NULL, $4); + CG(extra_fn_flags) = $10; } ; fn: @@ -1447,20 +1558,20 @@ lexical_var: ; function_call: - name argument_list - { $$ = zend_ast_create(ZEND_AST_CALL, $1, $2); } + name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_CALL, $1, $3, $2); } | T_READONLY argument_list { zval zv; if (zend_lex_tstring(&zv, $1) == FAILURE) { YYABORT; } - $$ = zend_ast_create(ZEND_AST_CALL, zend_ast_create_zval(&zv), $2); + $$ = zend_ast_create(ZEND_AST_CALL, zend_ast_create_zval(&zv), $2, NULL); } - | class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list - { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); } - | variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list - { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); } - | callable_expr { $$ = CG(zend_lineno); } argument_list { - $$ = zend_ast_create(ZEND_AST_CALL, $1, $3); - $$->lineno = $2; + | class_name T_PAAMAYIM_NEKUDOTAYIM member_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $5, $4); } + | variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $5, $4); } + | callable_expr optional_call_type_argument_list { $$ = CG(zend_lineno); } argument_list { + $$ = zend_ast_create(ZEND_AST_CALL, $1, $4, $2); + $$->lineno = $3; } ; @@ -1571,10 +1682,10 @@ callable_variable: { $$ = zend_ast_create(ZEND_AST_VAR, $1); } | array_object_dereferenceable '[' optional_expr ']' { $$ = zend_ast_create(ZEND_AST_DIM, $1, $3); } - | array_object_dereferenceable T_OBJECT_OPERATOR property_name argument_list - { $$ = zend_ast_create(ZEND_AST_METHOD_CALL, $1, $3, $4); } - | array_object_dereferenceable T_NULLSAFE_OBJECT_OPERATOR property_name argument_list - { $$ = zend_ast_create(ZEND_AST_NULLSAFE_METHOD_CALL, $1, $3, $4); } + | array_object_dereferenceable T_OBJECT_OPERATOR property_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_METHOD_CALL, $1, $3, $5, $4); } + | array_object_dereferenceable T_NULLSAFE_OBJECT_OPERATOR property_name optional_call_type_argument_list argument_list + { $$ = zend_ast_create(ZEND_AST_NULLSAFE_METHOD_CALL, $1, $3, $5, $4); } | function_call { $$ = $1; } ; diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 07f2d44cb5c6..cf6ccfe8bbe7 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1610,6 +1610,10 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN_WITH_STR(T_STRING, 0); } +"::<" { + RETURN_TOKEN(T_TURBOFISH); +} + "::" { RETURN_TOKEN(T_PAAMAYIM_NEKUDOTAYIM); } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 538eff3ea34d..1a7236680d83 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -24,6 +24,7 @@ #include "zend_compile.h" #include "zend_extensions.h" #include "zend_API.h" +#include "zend_inheritance.h" #include "zend_sort.h" #include "zend_constants.h" #include "zend_observer.h" @@ -91,6 +92,9 @@ void init_op_array(zend_op_array *op_array, zend_function_type type, int initial op_array->num_dynamic_func_defs = 0; op_array->dynamic_func_defs = NULL; + op_array->generic_parameters = NULL; + op_array->generic_types = NULL; + ZEND_MAP_PTR_INIT(op_array->run_time_cache, NULL); op_array->cache_size = zend_op_array_extension_handles * sizeof(void*); @@ -118,11 +122,298 @@ ZEND_API void zend_type_release(zend_type type, bool persistent) { if (!ZEND_TYPE_USES_ARENA(type)) { pefree(ZEND_TYPE_LIST(type), persistent); } + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(type); + if (named->name) { + zend_string_release(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_type_release(named->args[i], persistent); + } + pefree(named, persistent); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->name) { + zend_string_release(ref->name); + } + pefree(ref, persistent); } else if (ZEND_TYPE_HAS_NAME(type)) { zend_string_release(ZEND_TYPE_NAME(type)); } } +ZEND_API zend_generic_parameter_list *zend_generic_parameter_list_alloc(uint32_t count, bool persistent) { + ZEND_ASSERT(count > 0); + zend_generic_parameter_list *list = pemalloc(ZEND_GENERIC_PARAMETER_LIST_SIZE(count), persistent); + list->count = count; + list->inferable_mask = 0; + for (uint32_t i = 0; i < count; i++) { + list->parameters[i].name = NULL; + list->parameters[i].variance = 0; + list->parameters[i].bound = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].bound_pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].default_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + list->parameters[i].default_pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + } + return list; +} + +ZEND_API void zend_generic_parameter_list_destroy(zend_generic_parameter_list *list) { + if (!list) { + return; + } + for (uint32_t i = 0; i < list->count; i++) { + zend_generic_parameter *param = &list->parameters[i]; + if (param->name) { + zend_string_release(param->name); + } + zend_type_release(param->bound, /* persistent */ false); + zend_type_release(param->bound_pre_erasure, /* persistent */ false); + zend_type_release(param->default_type, /* persistent */ false); + zend_type_release(param->default_pre_erasure, /* persistent */ false); + } + efree(list); +} + +static void zend_generic_type_table_value_dtor(zval *zv) { + zend_type *type = Z_PTR_P(zv); + zend_type_release(*type, /* persistent */ false); + efree(type); +} + +/* Dtor for turbofish_args entries: each one owns an inline args_box. + * The per-call-site runtime cache that stashes the most recently built + * zend_type_arg_table for this entry lives in the caller op_array's + * runtime cache slot, so it's released by the op_array runtime-cache + * teardown — not here. */ +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); +} + +ZEND_API zend_generic_type_table *zend_generic_type_table_alloc(void) { + zend_generic_type_table *table = ecalloc(1, sizeof(zend_generic_type_table)); + return table; +} + +ZEND_API void zend_generic_type_table_destroy(zend_generic_type_table *table) { + if (!table || table->persisted) { + return; + } + + if (table->return_type) { + zend_type_release(*table->return_type, /* persistent */ false); + efree(table->return_type); + } + if (table->extends) { + zend_type_release(*table->extends, /* persistent */ false); + efree(table->extends); + } + if (table->parameters) { + zend_hash_destroy(table->parameters); + FREE_HASHTABLE(table->parameters); + } + if (table->properties) { + zend_hash_destroy(table->properties); + FREE_HASHTABLE(table->properties); + } + if (table->class_constants) { + zend_hash_destroy(table->class_constants); + FREE_HASHTABLE(table->class_constants); + } + if (table->implements) { + zend_hash_destroy(table->implements); + FREE_HASHTABLE(table->implements); + } + if (table->trait_uses) { + zend_hash_destroy(table->trait_uses); + FREE_HASHTABLE(table->trait_uses); + } + if (table->turbofish_args) { + zend_hash_destroy(table->turbofish_args); + FREE_HASHTABLE(table->turbofish_args); + } + if (table->value_check_plan) { + efree(table->value_check_plan); + } + efree(table); +} + +static HashTable *zend_generic_type_table_ensure_indexed(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_generic_type_table_value_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + +static HashTable *zend_generic_type_table_ensure_turbofish(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_turbofish_args_entry_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + +static HashTable *zend_generic_type_table_ensure_named(HashTable **slot) { + if (!*slot) { + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, 4, NULL, zend_generic_type_table_value_dtor, /* persistent */ false); + *slot = ht; + } + return *slot; +} + +static zend_type *zend_type_box(zend_type type) { + zend_type *boxed = emalloc(sizeof(zend_type)); + *boxed = type; + return boxed; +} + +ZEND_API void zend_generic_type_table_set_return(zend_generic_type_table *t, zend_type type) { + if (t->return_type) { + zend_type_release(*t->return_type, /* persistent */ false); + efree(t->return_type); + } + t->return_type = zend_type_box(type); +} + +ZEND_API void zend_generic_type_table_set_extends(zend_generic_type_table *t, zend_type type) { + if (t->extends) { + zend_type_release(*t->extends, /* persistent */ false); + efree(t->extends); + } + t->extends = zend_type_box(type); +} + +ZEND_API void zend_generic_type_table_set_parameter(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->parameters), idx, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_property(zend_generic_type_table *t, zend_string *name, zend_type type) { + zend_hash_update_ptr(zend_generic_type_table_ensure_named(&t->properties), name, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_class_constant(zend_generic_type_table *t, zend_string *name, zend_type type) { + zend_hash_update_ptr(zend_generic_type_table_ensure_named(&t->class_constants), name, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_implements(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->implements), idx, zend_type_box(type)); +} + +ZEND_API void zend_generic_type_table_set_trait_use(zend_generic_type_table *t, uint32_t idx, zend_type type) { + zend_hash_index_update_ptr(zend_generic_type_table_ensure_indexed(&t->trait_uses), idx, zend_type_box(type)); +} + +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); +} + +/* Monotonic nonce stamped onto every allocated zend_type_arg_table so PIC + * slots can distinguish "same table" from "same address, reused table." 32 + * bits gives ~4 billion allocations between potential rollovers, which is + * fine for in-request scope. */ +static uint32_t zend_type_arg_table_generation_counter = 1; + +ZEND_API zend_type_arg_table *zend_type_arg_table_alloc(uint32_t count) { + zend_type_arg_table *table = emalloc(ZEND_TYPE_ARG_TABLE_SIZE(count)); + table->count = count; + table->generation = zend_type_arg_table_generation_counter++; + table->persisted = false; + table->refcount = 1; + for (uint32_t i = 0; i < count; i++) { + table->entries[i].name = NULL; + table->entries[i].type_ref = NULL; + table->entries[i].owned_type = (zend_type) ZEND_TYPE_INIT_NONE(0); + } + return table; +} + +ZEND_API void zend_type_arg_table_release(zend_type_arg_table *table) { + if (!table || table->refcount == 0 || --table->refcount > 0) { + return; + } + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_string_release(table->entries[i].name); + } + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_type_release(table->entries[i].owned_type, false); + } + } + efree(table); +} + +ZEND_API void zend_type_arg_table_destroy(zend_type_arg_table *table) { + if (!table || table->persisted) { + return; + } + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_string_release(table->entries[i].name); + } + /* type_ref is borrowed — never released here. owned_type is the + * locally-synthesised fallback (inference) and is. */ + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_type_release(table->entries[i].owned_type, /* persistent */ false); + } + } + efree(table); +} + +/* Clone a type-arg table for long-lived capture (e.g., closure capture). All + * entries are materialised into `owned_type` so the resulting table owns its + * type contents and doesn't depend on the source's lifetime. Returned table + * is marked persisted so frame teardown leaves it alone; the holder is + * responsible for clearing persisted and destroying when it's done. */ +ZEND_API zend_type_arg_table *zend_type_arg_table_capture_clone(const zend_type_arg_table *src) { + if (!src) { + return NULL; + } + zend_type_arg_table *dst = zend_type_arg_table_alloc(src->count); + dst->persisted = true; + for (uint32_t i = 0; i < src->count; i++) { + const zend_type_arg_entry *se = &src->entries[i]; + zend_type_arg_entry *de = &dst->entries[i]; + if (se->name) { + de->name = zend_string_copy(se->name); + } + const zend_type *src_type = zend_type_arg_entry_type(se); + if (src_type && ZEND_TYPE_IS_SET(*src_type)) { + zend_type owned = *src_type; + zend_type_copy_ctor(&owned, /* use_arena */ false, /* persistent */ false); + de->owned_type = owned; + } + } + return dst; +} + +/* Always returns the canonical name of the bound type — class names, monomorph + * applications (`Box`), scalars (`int`, `string`), and unions/intersections. + * Callers that only care about class-name dispatch should themselves check + * whether the lookup succeeded. Returns NULL only for unset/empty types. */ +ZEND_API zend_string *zend_type_arg_canonical_name(zend_type type) { + if (!ZEND_TYPE_IS_SET(type)) { + return NULL; + } + return zend_type_to_canonical_string(type); +} + ZEND_API void zend_free_internal_arg_info(zend_internal_function *function, bool persistent) { if (function->arg_info) { @@ -335,6 +626,13 @@ ZEND_API void destroy_zend_class(zval *zv) p++; } } + /* Monomorph args are heap-allocated per request, not relocated into + * the opcache file cache; free them even when the rest of the ce is + * cache-resident. */ + if (ce->generic_type_args) { + zend_type_arg_table_destroy(ce->generic_type_args); + ce->generic_type_args = NULL; + } return; } @@ -375,6 +673,12 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->num_traits > 0) { _destroy_zend_class_traits_info(ce); } + } else if (ce->num_traits > 0) { + uint32_t i; + for (i = 0; i < ce->num_traits; i++) { + zend_string_release_ex(ce->trait_names[i].name, 0); + zend_string_release_ex(ce->trait_names[i].lc_name, 0); + } } if (ce->default_properties_table) { @@ -415,6 +719,18 @@ ZEND_API void destroy_zend_class(zval *zv) } } } + } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + zend_type_release(prop_info->type, /* persistent */ false); + if (prop_info->name) { + zend_string_release_ex(prop_info->name, 0); + } + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop_info->hooks[i]) { + destroy_op_array(&prop_info->hooks[i]->op_array); + } + } + } } } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&ce->properties_info); @@ -441,6 +757,15 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->backed_enum_table) { zend_hash_release(ce->backed_enum_table); } + if (ce->generic_parameters) { + zend_generic_parameter_list_destroy(ce->generic_parameters); + } + if (ce->generic_types) { + zend_generic_type_table_destroy(ce->generic_types); + } + if (ce->generic_type_args) { + zend_type_arg_table_destroy(ce->generic_type_args); + } break; case ZEND_INTERNAL_CLASS: if (ce->doc_comment) { @@ -479,6 +804,14 @@ ZEND_API void destroy_zend_class(zval *zv) zend_hash_release(prop_info->attributes); } free(prop_info); + } else if (prop_info->flags & ZEND_ACC_GENERIC_CLONE) { + /* Cross-class generic clones (e.g., Holder's property + * cloned into the Holder monomorph) take their own + * reference on the borrowed name string in + * do_inherit_property; release it here on the holder's + * destruction so the count balances. The prop_info struct + * itself is arena-allocated and freed in bulk. */ + zend_string_release(prop_info->name); } } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&ce->properties_info); @@ -560,10 +893,100 @@ ZEND_API void zend_destroy_static_vars(zend_op_array *op_array) } } +static void zend_release_transient_monomorph(zend_function *cm) +{ + if (!cm || cm->type != ZEND_USER_FUNCTION + || !(cm->common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + || (cm->common.fn_flags & ZEND_ACC_IMMUTABLE)) { + return; + } + zend_op_array *moa = &cm->op_array; + if (moa->generic_types && moa->generic_types->monomorph_type_args) { + zend_type_arg_table_release(moa->generic_types->monomorph_type_args); + moa->generic_types->monomorph_type_args = NULL; + } + if (moa->arg_info) { + bool has_ret = (moa->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) != 0; + uint32_t total = moa->num_args + (has_ret ? 1 : 0) + + ((moa->fn_flags & ZEND_ACC_VARIADIC) ? 1 : 0); + zend_arg_info *block = moa->arg_info - (has_ret ? 1 : 0); + for (uint32_t a = 0; a < total; a++) { + zend_type_release(block[a].type, 0); + if (block[a].name) { + zend_string_release(block[a].name); + } + if (block[a].doc_comment) { + zend_string_release(block[a].doc_comment); + } + } + moa->arg_info = NULL; + } +} + ZEND_API void destroy_op_array(zend_op_array *op_array) { uint32_t i; + /* 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. + * + * Gate on ZEND_ACC_HEAP_RT_CACHE: SHM/preload op_arrays share the per-request + * map_ptr cache region, already freed by the time they are destroyed — reading + * the stale cache here would be a use-after-free. */ + if ((op_array->fn_flags2 & ZEND_ACC2_HAS_GENERIC_CALL_OPS) + && (op_array->fn_flags & ZEND_ACC_HEAP_RT_CACHE) + && op_array->opcodes && ZEND_MAP_PTR(op_array->run_time_cache)) { + char *cache_buf = (char *) ZEND_MAP_PTR_GET(op_array->run_time_cache); + if (cache_buf) { + for (uint32_t op_idx = 0; op_idx < op_array->last; op_idx++) { + const zend_op *op = &op_array->opcodes[op_idx]; + if ((op->opcode == ZEND_INIT_FCALL_BY_NAME + || op->opcode == ZEND_INIT_NS_FCALL_BY_NAME) + && op->result.num) { + void **cache_slot = (void **) (cache_buf + op->result.num); + zend_release_transient_monomorph((zend_function *) cache_slot[0]); + cache_slot[0] = NULL; + continue; + } + if ((op->opcode == ZEND_VERIFY_GENERIC_ARGUMENTS + || op->opcode == ZEND_INSTALL_GENERIC_ARGS) + && op->op1_type == IS_UNUSED + && 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; + } + zend_release_transient_monomorph((zend_function *) cache_slot[0]); + 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; + zend_type_arg_table_destroy(t); + cache_slot[0] = NULL; + cache_slot[1] = NULL; + } + } + } + } + } + if ((op_array->fn_flags & ZEND_ACC_HEAP_RT_CACHE) && ZEND_MAP_PTR(op_array->run_time_cache)) { efree(ZEND_MAP_PTR(op_array->run_time_cache)); @@ -573,6 +996,14 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) zend_string_release_ex(op_array->function_name, 0); } + if ((op_array->fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + && !(op_array->fn_flags & ZEND_ACC_IMMUTABLE) + && op_array->generic_types + && op_array->generic_types->monomorph_type_args) { + zend_type_arg_table_release(op_array->generic_types->monomorph_type_args); + op_array->generic_types->monomorph_type_args = NULL; + } + if (!op_array->refcount || --(*op_array->refcount) > 0) { return; } @@ -663,6 +1094,12 @@ ZEND_API void destroy_op_array(zend_op_array *op_array) } efree(op_array->dynamic_func_defs); } + if (op_array->generic_parameters) { + zend_generic_parameter_list_destroy(op_array->generic_parameters); + } + if (op_array->generic_types) { + zend_generic_type_table_destroy(op_array->generic_types); + } } static void zend_update_extended_stmts(zend_op_array *op_array) diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index ab8f2c2b54f8..1d1a6d5a3b6c 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -2560,6 +2560,56 @@ ZEND_API bool ZEND_FASTCALL zend_class_implements_interface(const zend_class_ent } /* }}} */ +/* Variance-aware comparison of two siblings monos `a` and `b` of the same + * generic base. Returns true if `a` is a subtype of `b` under the base's + * declared variance markers. Both monos must have generic_type_args populated + * with the same arity as base->generic_parameters. */ +static bool zend_mono_subtype_under_variance(const zend_class_entry *base, + const zend_class_entry *a, const zend_class_entry *b) +{ + if (!base->generic_parameters + || !a->generic_type_args + || !b->generic_type_args) { + return false; + } + uint32_t count = base->generic_parameters->count; + if (a->generic_type_args->count != count + || b->generic_type_args->count != count) { + return false; + } + for (uint32_t i = 0; i < count; i++) { + zend_string *a_name = a->generic_type_args->entries[i].name; + zend_string *b_name = b->generic_type_args->entries[i].name; + if (!a_name || !b_name) { + return false; + } + if (zend_string_equals(a_name, b_name)) { + continue; + } + zend_generic_variance variance = base->generic_parameters->parameters[i].variance; + if (variance == ZEND_GENERIC_VARIANCE_INVARIANT) { + return false; + } + zend_class_entry *a_ce = zend_lookup_class_ex(a_name, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + zend_class_entry *b_ce = zend_lookup_class_ex(b_name, NULL, + ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (!a_ce || !b_ce) { + return false; + } + bool ok; + if (variance == ZEND_GENERIC_VARIANCE_COVARIANT) { + ok = instanceof_function(a_ce, b_ce); + } else { /* CONTRAVARIANT */ + ok = instanceof_function(b_ce, a_ce); + } + if (!ok) { + return false; + } + } + return true; +} + ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *instance_ce, const zend_class_entry *ce) /* {{{ */ { ZEND_ASSERT(instance_ce != ce && "Should have been checked already"); @@ -2576,12 +2626,24 @@ ZEND_API bool ZEND_FASTCALL instanceof_function_slow(const zend_class_entry *ins } return 0; } else { + /* When the target is a monomorph (e.g. Box), the instance may + * be a sibling monomorph of the same base (Box) — siblings via + * the parent chain are not subtypes via `==`, so check the variance + * markers on the shared base. */ + const zend_class_entry *target_base = ce->generic_type_args ? ce->parent : NULL; + const zend_class_entry *walker = instance_ce; while (1) { - instance_ce = instance_ce->parent; - if (instance_ce == ce) { + if (target_base + && walker->parent == target_base + && walker->generic_type_args + && zend_mono_subtype_under_variance(target_base, walker, ce)) { + return 1; + } + walker = walker->parent; + if (walker == ce) { return 1; } - if (instance_ce == NULL) { + if (walker == NULL) { return 0; } } diff --git a/Zend/zend_operators.h b/Zend/zend_operators.h index 27aa4fdb0486..f25b6236e773 100644 --- a/Zend/zend_operators.h +++ b/Zend/zend_operators.h @@ -80,7 +80,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_types.h b/Zend/zend_types.h index 0edc4df37484..188b311dd1c6 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -127,14 +127,42 @@ typedef struct { zend_type types[1]; } zend_type_list; -#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 25 -#define _ZEND_TYPE_MASK ((1u << 25) - 1) +/* Generic type-parameter reference. */ +typedef struct _zend_type_parameter_ref { + zend_string *name; /* type-parameter source name (e.g. "T") */ + uint32_t index; /* position in declaring entity's parameter list */ + uint8_t origin; /* one of zend_generic_origin (see zend_compile.h) */ +} zend_type_parameter_ref; + +/* List of pre-erasure type arguments attached to a named pre-erasure zend_type. */ +typedef struct _zend_type_arguments { + uint32_t count; + zend_type arguments[1]; +} zend_type_arguments; + +/* Pre-erasure named type with type arguments. */ +typedef struct _zend_type_named_with_args { + zend_string *name; /* class name */ + uint32_t name_attr; /* ZEND_NAME_NOT_FQ / ZEND_NAME_FQ / ZEND_NAME_RELATIVE */ + uint32_t count; /* number of type arguments */ + zend_type args[1]; /* flexible array of pre-erasure type arguments */ +} zend_type_named_with_args; + +#define ZEND_TYPE_NAMED_WITH_ARGS_SIZE(count) \ + (sizeof(zend_type_named_with_args) + ((count) - 1) * sizeof(zend_type)) + +#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 26 +#define _ZEND_TYPE_MASK (((1u << 26) - 1) | _ZEND_TYPE_NAMED_WITH_ARGS_BIT) +/* Pre-erasure named type with type arguments. Side-table only. */ +#define _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31) +/* Generic type-parameter reference. */ +#define _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) /* Only one of these bits may be set. */ #define _ZEND_TYPE_NAME_BIT (1u << 24) // Used to signify that type.ptr is not a `zend_string*` but a `const char*`, #define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23) #define _ZEND_TYPE_LIST_BIT (1u << 22) -#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT) +#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT|_ZEND_TYPE_TYPE_PARAMETER_BIT|_ZEND_TYPE_NAMED_WITH_ARGS_BIT) /* For BC behaviour with iterable type */ #define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ @@ -165,6 +193,18 @@ typedef struct { #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) +#define ZEND_TYPE_HAS_TYPE_PARAMETER(t) \ + ((((t).type_mask) & _ZEND_TYPE_TYPE_PARAMETER_BIT) != 0) + +#define ZEND_TYPE_TYPE_PARAMETER(t) \ + ((zend_type_parameter_ref *) (t).ptr) + +#define ZEND_TYPE_HAS_NAMED_WITH_ARGS(t) \ + ((((t).type_mask) & _ZEND_TYPE_NAMED_WITH_ARGS_BIT) != 0) + +#define ZEND_TYPE_NAMED_WITH_ARGS(t) \ + ((zend_type_named_with_args *) (t).ptr) + #define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \ ((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index a0bfad488f85..9be9d25a2b2b 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); @@ -4373,6 +4399,35 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -4491,6 +4546,18 @@ ZEND_VM_COLD_CONST_HANDLER(124, ZEND_VERIFY_RETURN_TYPE, CONST|TMP|VAR|UNUSED|CV ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -4848,7 +4915,7 @@ ZEND_VM_COLD_CONST_HANDLER(108, ZEND_THROW, CONST|TMP|CV, ANY) HANDLE_EXCEPTION(); } -ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT) +ZEND_VM_HANDLER(107, ZEND_CATCH, CONST|UNUSED, JMP_ADDR, LAST_CATCH|CACHE_SLOT) { USE_OPLINE zend_class_entry *ce, *catch_ce; @@ -4859,11 +4926,38 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT) if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (OP1_TYPE == IS_UNUSED) { + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -5742,6 +5836,18 @@ ZEND_VM_HOT_HANDLER(63, ZEND_RECV, NUM, UNUSED) ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -5754,6 +5860,17 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_RECV, op->op2.num == MAY_BE_ANY, ZEND_RECV_NO ZEND_VM_DISPATCH_TO_HELPER(zend_missing_arg_helper); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper, op_1, param); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -6004,6 +6121,68 @@ ZEND_VM_HANDLER(68, ZEND_NEW, UNUSED|CLASS_FETCH|CONST|VAR, UNUSED|CACHE_SLOT, N ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (OP1_TYPE == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (OP1_TYPE == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (OP1_TYPE == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -8105,7 +8284,28 @@ ZEND_VM_C_LABEL(try_instanceof): } } } else if (OP2_TYPE == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { FREE_OP1(); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -8409,6 +8609,13 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, CONST, NUM) zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -8931,6 +9138,249 @@ ZEND_VM_HOT_HANDLER(211, ZEND_TYPE_ASSERT, CONST, ANY, NUM) ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } +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; + 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) { + /* 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(); + } + /* 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; + } + } + } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* 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. 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 + * don't leak. */ + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +ZEND_VM_HANDLER(213, ZEND_INSTALL_GENERIC_ARGS, TMP|UNUSED, UNUSED) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + 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) { + /* 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; + } + } + 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))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT) { USE_OPLINE @@ -8994,6 +9444,17 @@ ZEND_VM_HANDLER(157, ZEND_FETCH_CLASS_NAME, CV|TMP|UNUSED|CLASS_FETCH, ANY) } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cedc735bbb1e..e475449738e7 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1974,6 +1974,35 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -2109,6 +2138,35 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_D ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -2244,6 +2302,35 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -2826,6 +2913,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -4072,7 +4170,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))) { @@ -4080,6 +4188,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); @@ -4157,7 +4266,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); @@ -4167,6 +4290,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); @@ -4341,6 +4465,18 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_R ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -5174,11 +5310,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_CO if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + if (IS_CONST == IS_UNUSED) { + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -5939,6 +6102,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_DECLARE_LAMBD zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -11219,6 +11389,18 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -11397,6 +11579,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_CONS ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_CONST == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_CONST == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -18258,6 +18502,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -19668,7 +19923,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -21322,7 +21598,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -21531,6 +21828,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -21849,7 +22158,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -21994,6 +22324,249 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_YIELD_SPEC_TM ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + 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) { + /* 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(); + } + /* 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; + } + } + } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* 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. 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 + * don't leak. */ + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + 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) { + /* 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; + } + } + 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))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -29579,6 +30152,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -30079,6 +30664,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_VAR_ ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_VAR == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_VAR == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -32764,6 +33411,83 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_F } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CATCH_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_class_entry *ce, *catch_ce; + zend_object *exception; + + SAVE_OPLINE(); + /* Check whether an exception has been thrown, if not, jump over code */ + if (EG(exception) == NULL) { + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + if (IS_UNUSED == IS_UNUSED) { + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } + } + ce = EG(exception)->ce; + +#ifdef HAVE_DTRACE + if (DTRACE_EXCEPTION_CAUGHT_ENABLED()) { + DTRACE_EXCEPTION_CAUGHT((char *)ce->name); + } +#endif /* HAVE_DTRACE */ + + if (ce != catch_ce) { + if (!catch_ce || !instanceof_function(ce, catch_ce)) { + if (opline->extended_value & ZEND_LAST_CATCH) { + zend_rethrow_exception(execute_data); + HANDLE_EXCEPTION(); + } + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + } + + exception = EG(exception); + EG(exception) = NULL; + if (RETURN_VALUE_USED(opline)) { + /* Always perform a strict assignment. There is a reasonable expectation that if you + * write "catch (Exception $e)" then $e will actually be instanceof Exception. As such, + * we should not permit coercion to string here. */ + zval tmp; + ZVAL_OBJ(&tmp, exception); + zend_assign_to_variable(EX_VAR(opline->result.var), &tmp, IS_TMP_VAR, /* strict */ 1); + } else { + OBJ_RELEASE(exception); + } + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -32859,6 +33583,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -36969,6 +37704,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -37127,6 +37874,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NEW_SPEC_UNUS ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_UNUSED == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_UNUSED == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -37311,6 +38120,249 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_YIELD_SPEC_UN ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + 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) { + /* 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(); + } + /* 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; + } + } + } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* 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. 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 + * don't leak. */ + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + 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) { + /* 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; + } + } + 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))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -40862,6 +41914,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_FETCH_CLASS_N } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -44540,7 +45603,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -48361,7 +49445,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -49181,6 +50286,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_VERIFY_RETURN ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -49741,7 +50858,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INSTANCEOF_SP } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -54779,6 +55917,35 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -54914,6 +56081,35 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FCA ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -55049,6 +56245,35 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DO_FC ret = EX_VAR(opline->result.var); } + /* Closures created inside a generic frame carry that frame's T-table + * via zend_vm_init_call_frame. Run the T-ref value-arg check against + * the captured bindings now, before i_init_func_execute_data relocates + * variadic args past the CV slots (which would leave the original arg + * positions UNDEF and silently make every check pass). The cleanup on + * failure matches ZEND_VERIFY_GENERIC_ARGUMENTS — at this point + * call->prev_execute_data is still its zend_vm_init_call_frame value + * (typically NULL), so EX(call) is the right slot to clear. */ + if (UNEXPECTED(call->type_args + && (fbc->common.fn_flags & ZEND_ACC_CLOSURE))) { + SAVE_OPLINE(); + zend_verify_generic_arg_types(call, NULL); + if (UNEXPECTED(EG(exception))) { + zend_vm_stack_free_args(call); + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } else if (ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE) { + OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); + } + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + } + call->prev_execute_data = execute_data; execute_data = call; i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC); @@ -55609,6 +56834,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_N ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); } + /* Origin compiled this parameter as mixed (MAY_BE_ANY), but a generic + * inheritance clone may carry a substituted type in arg_info. Check that + * concrete mask inline; only a mismatch needs the verifying helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zval *param = EX_VAR(opline->result.var); + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -56761,7 +57997,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))) { @@ -56769,6 +58015,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); @@ -56846,7 +58093,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); @@ -56856,6 +58117,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); @@ -57030,6 +58292,18 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_RECV_S ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); } + /* The inline op2.num mask was baked in at the origin's compile time. A + * generic-inheritance clone may carry a tighter substituted arg_info that + * the mask doesn't reflect. Check that concrete arg_info mask inline: an exact + * type match needs no coercion/instanceof (just like a typed RECV), so only a + * mismatch (scalar coercion, class instanceof, union miss) falls to the helper. */ + if (UNEXPECTED(EX(func)->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { + uint32_t _cmask = (uint32_t) ZEND_TYPE_FULL_MASK(EX(func)->op_array.arg_info[arg_num - 1].type); + if (UNEXPECTED(!(_cmask & (1u << Z_TYPE_P(param))))) { + ZEND_VM_DISPATCH_TO_HELPER(zend_verify_recv_arg_type_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_EX param)); + } + } + ZEND_VM_NEXT_OPCODE(); } @@ -57863,11 +59137,38 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_CONST_T if (EG(exception) == NULL) { ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); } - catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); - if (UNEXPECTED(catch_ce == NULL)) { - catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); - - CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + if (IS_CONST == IS_UNUSED) { + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } } ce = EG(exception)->ce; @@ -58628,6 +59929,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_DECLARE_LAMBDA_FUN zend_create_closure(EX_VAR(opline->result.var), func, EX(func)->op_array.scope, called_scope, object); + /* If we're inside a generic frame, snapshot its T-table onto the new + * closure so the closure body resolves outer T-refs against the + * binding in effect at lambda-declaration time. */ + if (UNEXPECTED(EX(type_args) != NULL)) { + zend_closure_capture_type_args(EX_VAR(opline->result.var), EX(type_args)); + } + ZEND_VM_NEXT_OPCODE(); } @@ -63806,6 +65114,18 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIF ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -63984,6 +65304,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_CONST_UNU ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_CONST == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_CONST == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_CONST == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -70845,6 +72227,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -72255,7 +73648,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -73909,7 +75323,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -74018,6 +75453,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -74336,7 +75783,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_TM } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -74481,6 +75949,249 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_YIELD_SPEC_TMP_UNU ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + 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) { + /* 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(); + } + /* 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; + } + } + } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* 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. 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 + * don't leak. */ + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + 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) { + /* 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; + } + } + 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))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_COUNT_SPEC_TMP_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -82066,6 +83777,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -82566,6 +84289,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_VAR_UNUSE ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_VAR == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_VAR == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_VAR == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -85251,6 +87036,83 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FE_FET } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CATCH_SPEC_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_class_entry *ce, *catch_ce; + zend_object *exception; + + SAVE_OPLINE(); + /* Check whether an exception has been thrown, if not, jump over code */ + if (EG(exception) == NULL) { + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + if (IS_UNUSED == IS_UNUSED) { + /* catch (T $e) / catch (Box $e): polymorphic inline cache keyed on + * (type_args generation, called_scope). The generation is a monotonic + * nonce per zend_type_arg_table allocation, so it's ABA-safe across + * calls that reuse the same heap address. NULL type_args maps to + * generation 0 (the counter starts at 1). zend_fetch_class dispatches + * to the right resolver based on the packed sub-type on miss. */ + void **slot = CACHE_ADDR(opline->extended_value & ~ZEND_LAST_CATCH); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + catch_ce = (zend_class_entry*)slot[2]; + } else { + catch_ce = zend_fetch_class(NULL, + opline->op1.num | ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + if (EXPECTED(catch_ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)catch_ce; + } + } + } else { + catch_ce = CACHED_PTR(opline->extended_value & ~ZEND_LAST_CATCH); + if (UNEXPECTED(catch_ce == NULL)) { + catch_ce = zend_fetch_class_by_name(Z_STR_P(RT_CONSTANT(opline, opline->op1)), Z_STR_P(RT_CONSTANT(opline, opline->op1) + 1), ZEND_FETCH_CLASS_NO_AUTOLOAD | ZEND_FETCH_CLASS_SILENT); + + CACHE_PTR(opline->extended_value & ~ZEND_LAST_CATCH, catch_ce); + } + } + ce = EG(exception)->ce; + +#ifdef HAVE_DTRACE + if (DTRACE_EXCEPTION_CAUGHT_ENABLED()) { + DTRACE_EXCEPTION_CAUGHT((char *)ce->name); + } +#endif /* HAVE_DTRACE */ + + if (ce != catch_ce) { + if (!catch_ce || !instanceof_function(ce, catch_ce)) { + if (opline->extended_value & ZEND_LAST_CATCH) { + zend_rethrow_exception(execute_data); + HANDLE_EXCEPTION(); + } + ZEND_VM_JMP_EX(OP_JMP_ADDR(opline, opline->op2), 0); + } + } + + exception = EG(exception); + EG(exception) = NULL; + if (RETURN_VALUE_USED(opline)) { + /* Always perform a strict assignment. There is a reasonable expectation that if you + * write "catch (Exception $e)" then $e will actually be instanceof Exception. As such, + * we should not permit coercion to string here. */ + zval tmp; + ZVAL_OBJ(&tmp, exception); + zend_assign_to_variable(EX_VAR(opline->result.var), &tmp, IS_TMP_VAR, /* strict */ 1); + } else { + OBJ_RELEASE(exception); + } + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} + static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CLONE_SPEC_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -85346,6 +87208,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -89456,6 +91329,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -89614,6 +91499,68 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NEW_SPEC_UNUSED_UN ce = Z_CE_P(EX_VAR(opline->op1.var)); } + /* Naked `new` of a bare generic class that didn't go through the + * compile-time canonical-name rewrite: `new self()`/`new static()`, + * `new $name`, and `new C()` when C was declared at runtime (it uses a trait + * or a parameterized `implements`, so the compiler couldn't see its ce to + * rewrite the name). A monomorph has no generic_parameters, so once the + * rewrite (or the synthesis below) has run, ce is concrete and this block is + * a no-op. Skip when ZEND_VERIFY_GENERIC_ARGUMENTS follows — that opcode + * handles the synthesis-and-swap for the turbofish path. + * + * Resolution, in order: + * - every type parameter has a default -> the defaults monomorph, so all + * naked-new spellings agree on identity (frame-independent; cached); + * - `new self()` -> the monomorph carrying the executing frame's binding, + * e.g. `new self()` in a `C` method yields `C`. `self` names + * "this class with my type arguments", so this is unambiguous and is the + * clone idiom (frame-dependent; not cached). `static` already resolves + * to the called scope before this block; only a `static` that lands on + * the bare generic itself reaches here, with no binding; + * - otherwise it is an error to name a generic class with no type + * arguments and no in-scope binding — the same rule the compiler + * enforces for an early-bound `new C()`. A by-name `new C()` is genuinely + * ambiguous (its type arguments could differ from the frame's, as in a + * `swap(): Pair`), so it must be spelled `new C::<...>()`. Dynamic + * `new $name()` keeps its own diagnostic; everything else gets "type + * parameter X has no default" from zend_get_defaults_monomorph. */ + if (UNEXPECTED(ce->generic_parameters + && (opline + 1)->opcode != ZEND_VERIFY_GENERIC_ARGUMENTS)) { + if (ce->ce_flags & ZEND_ACC_GENERIC_ALL_DEFAULTS) { + ce = zend_synthesize_monomorph(ce, NULL, 0); + if (UNEXPECTED(ce == NULL)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + if (IS_UNUSED == IS_CONST) { + /* Frame-independent: re-point the inline cache at the monomorph + * so later instantiations resolve it directly (the monomorph has + * no generic_parameters, so this block won't fire again). */ + CACHE_PTR(opline->op2.num, ce); + } + } else { + /* No defaults: only `new self()` can be resolved, from the frame. */ + zend_class_entry *mono = (IS_UNUSED == IS_UNUSED + && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF) + ? zend_resolve_lexical_self_monomorph(ce, execute_data) + : NULL; + if (mono) { + ce = mono; + } else { + if (IS_UNUSED == IS_VAR) { + zend_throw_error(NULL, + "Cannot instantiate generic class %s without type arguments " + "via dynamic class name; no defaults declared", + ZSTR_VAL(ce->name)); + } else { + (void) zend_get_defaults_monomorph(ce); + } + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + result = EX_VAR(opline->result.var); if (UNEXPECTED(object_init_ex(result, ce) != SUCCESS)) { ZVAL_UNDEF(result); @@ -89798,6 +91745,249 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_YIELD_SPEC_UNUSED_ ZEND_VM_RETURN(); } +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + uint32_t arity = opline->op2.num; + 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) { + /* 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(); + } + /* 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; + } + } + } + } else { + zval *new_obj = EX_VAR(opline->op1.var); + /* 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. 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 + * don't leak. */ + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + +/* Installs the type-arg table (and, for `new`, swaps the object's class to + * its monomorph) without running the runtime arity + bound checks that + * VERIFY does. Emitted by the compiler when the callee/ce is statically + * known, the turbofish args are concrete, and bounds were validated at + * compile time — so the runtime checks are pure overhead. The value-arg + * type-check (zend_verify_generic_arg_types) still runs since value + * arguments are runtime values. */ +static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + zend_execute_data *call = EX(call); + 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) { + /* 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; + } + } + 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))) { + if (call->type_args && !call->type_args->persisted) { + zend_type_arg_table_destroy(call->type_args); + call->type_args = NULL; + } + zend_vm_stack_free_args(call); + + if (call->func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) { + zend_string_release_ex(call->func->common.function_name, 0); + zend_free_trampoline(call->func); + } + + if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + EX(call) = call->prev_execute_data; + zend_vm_stack_free_call_frame(call); + HANDLE_EXCEPTION(); + } + + ZEND_VM_NEXT_OPCODE(); +} + static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE @@ -93349,6 +95539,17 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_FETCH_CLASS_NAME_S } fetch_type = opline->op1.num; + if (zend_fetch_is_type_param(fetch_type)) { + SAVE_OPLINE(); + zend_class_entry *resolved = zend_resolve_generic_type_param( + zend_unpack_type_param_index(fetch_type), fetch_type); + if (UNEXPECTED(!resolved)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + ZVAL_STR_COPY(EX_VAR(opline->result.var), resolved->name); + ZEND_VM_NEXT_OPCODE(); + } scope = EX(func)->op_array.scope; if (UNEXPECTED(scope == NULL)) { SAVE_OPLINE(); @@ -97027,7 +99228,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_CONST == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -100848,7 +103070,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_VAR == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -101566,6 +103809,18 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_VERIFY_RETURN_TYPE ZVAL_DEREF(retval_ptr); } + /* If the return is a generic T-ref erased to a wider bound, the + * inline ZEND_TYPE_CONTAINS_CODE fast-path may falsely accept (e.g. + * mixed for unbounded T). Consult the reified binding first; the + * erased check still runs after for non-T returns and composite + * shapes that the reified helper leaves untouched. */ + if (UNEXPECTED(execute_data->type_args != NULL)) { + SAVE_OPLINE(); + if (UNEXPECTED(!zend_verify_generic_return_type(execute_data, retval_ptr))) { + HANDLE_EXCEPTION(); + } + } + if (EXPECTED(ZEND_TYPE_CONTAINS_CODE(ret_info->type, Z_TYPE_P(retval_ref)))) { ZEND_VM_NEXT_OPCODE(); } @@ -102126,7 +104381,28 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INSTANCEOF_SPEC_CV } } } else if (IS_UNUSED == IS_UNUSED) { - ce = zend_fetch_class(NULL, opline->op2.num); + /* Polymorphic inline cache for T-ref / deferred-generic fetches: + * key on (type_args generation, called_scope). The generation is + * a monotonic nonce per zend_type_arg_table allocation, so it's + * ABA-safe across calls that reuse the same heap address. NULL + * type_args maps to generation 0 (the counter starts at 1). */ + void **slot = CACHE_ADDR(opline->extended_value); + uintptr_t cur_gen = EX(type_args) ? EX(type_args)->generation : 0; + zend_class_entry *cur_scope = zend_get_called_scope(execute_data); + /* slot[2] != NULL distinguishes a populated entry from an + * uninitialized (all-zeros) slot, which otherwise matches + * cur_gen=0 / cur_scope=NULL and would return a spurious NULL ce. */ + if (EXPECTED(slot[2] != NULL + && (uintptr_t)slot[0] == cur_gen && slot[1] == (void*)cur_scope)) { + ce = (zend_class_entry*)slot[2]; + } else { + ce = zend_fetch_class(NULL, opline->op2.num); + if (EXPECTED(ce)) { + slot[0] = (void*)cur_gen; + slot[1] = (void*)cur_scope; + slot[2] = (void*)ce; + } + } if (UNEXPECTED(ce == NULL)) { @@ -108681,6 +110957,10 @@ ZEND_API void execute_ex(zend_execute_data *ex) (void*)&&ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_LABEL, (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_CATCH_SPEC_CONST_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_CATCH_SPEC_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_THROW_SPEC_CONST_LABEL, (void*)&&ZEND_THROW_SPEC_TMP_LABEL, (void*)&&ZEND_NULL_LABEL, @@ -109273,6 +111553,16 @@ ZEND_API void execute_ex(zend_execute_data *ex) (void*)&&ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_LABEL, (void*)&&ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_LABEL, (void*)&&ZEND_TYPE_ASSERT_SPEC_CONST_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, + (void*)&&ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_LABEL, + (void*)&&ZEND_NULL_LABEL, (void*)&&ZEND_INIT_FCALL_OFFSET_SPEC_CONST_LABEL, (void*)&&ZEND_RECV_NOTYPE_SPEC_LABEL, (void*)&&ZEND_NULL_LABEL, @@ -113005,6 +115295,16 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_YIELD_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_YIELD_SPEC_TMP_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED): + VM_TRACE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) + HYBRID_BREAK(); + HYBRID_CASE(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED): + VM_TRACE(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_COUNT_SPEC_TMP_UNUSED): VM_TRACE(ZEND_COUNT_SPEC_TMP_UNUSED) ZEND_COUNT_SPEC_TMP_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -113810,6 +116110,11 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_CATCH_SPEC_UNUSED): + VM_TRACE(ZEND_CATCH_SPEC_UNUSED) + ZEND_CATCH_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_CATCH_SPEC_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_CLONE_SPEC_UNUSED): VM_TRACE(ZEND_CLONE_SPEC_UNUSED) ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -114110,6 +116415,16 @@ ZEND_API void execute_ex(zend_execute_data *ex) ZEND_YIELD_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); VM_TRACE_OP_END(ZEND_YIELD_SPEC_UNUSED_UNUSED) HYBRID_BREAK(); + HYBRID_CASE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED): + VM_TRACE(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) + HYBRID_BREAK(); + HYBRID_CASE(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED): + VM_TRACE(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); + VM_TRACE_OP_END(ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) + HYBRID_BREAK(); HYBRID_CASE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED): VM_TRACE(ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); @@ -117619,6 +119934,10 @@ void zend_vm_init(void) ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_HANDLER, ZEND_NULL_HANDLER, ZEND_CATCH_SPEC_CONST_HANDLER, + ZEND_NULL_HANDLER, + ZEND_NULL_HANDLER, + ZEND_CATCH_SPEC_UNUSED_HANDLER, + ZEND_NULL_HANDLER, ZEND_THROW_SPEC_CONST_HANDLER, ZEND_THROW_SPEC_TMP_HANDLER, ZEND_NULL_HANDLER, @@ -118211,6 +120530,16 @@ void zend_vm_init(void) ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_HANDLER, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_HANDLER, ZEND_TYPE_ASSERT_SPEC_CONST_HANDLER, + ZEND_NULL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_HANDLER, + ZEND_NULL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_HANDLER, + ZEND_NULL_HANDLER, + ZEND_NULL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_HANDLER, + ZEND_NULL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_HANDLER, + ZEND_NULL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_HANDLER, ZEND_RECV_NOTYPE_SPEC_HANDLER, ZEND_NULL_HANDLER, @@ -121097,6 +123426,10 @@ void zend_vm_init(void) ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, ZEND_CATCH_SPEC_CONST_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_CATCH_SPEC_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, ZEND_THROW_SPEC_CONST_TAILCALL_HANDLER, ZEND_THROW_SPEC_TMP_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, @@ -121689,6 +124022,16 @@ void zend_vm_init(void) ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED_TAILCALL_HANDLER, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST_TAILCALL_HANDLER, ZEND_TYPE_ASSERT_SPEC_CONST_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, + ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED_TAILCALL_HANDLER, + ZEND_NULL_TAILCALL_HANDLER, ZEND_INIT_FCALL_OFFSET_SPEC_CONST_TAILCALL_HANDLER, ZEND_RECV_NOTYPE_SPEC_TAILCALL_HANDLER, ZEND_NULL_TAILCALL_HANDLER, @@ -122657,7 +125000,7 @@ void zend_vm_init(void) 1255, 1256 | SPEC_RULE_OP1, 1261 | SPEC_RULE_OP1, - 3474, + 3488, 1266 | SPEC_RULE_OP1, 1271 | SPEC_RULE_OP1, 1276 | SPEC_RULE_OP2, @@ -122691,7 +125034,7 @@ void zend_vm_init(void) 1559 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1584 | SPEC_RULE_OP1, 1589, - 3474, + 3488, 1590 | SPEC_RULE_OP1, 1595 | SPEC_RULE_OP1 | SPEC_RULE_OP2, 1620 | SPEC_RULE_OP1 | SPEC_RULE_OP2, @@ -122719,155 +125062,155 @@ void zend_vm_init(void) 1959, 1960, 1961 | SPEC_RULE_OP2, - 1966, - 1967 | SPEC_RULE_OP1, - 1972 | SPEC_RULE_OP2, - 1977 | SPEC_RULE_OP1, - 1982 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, - 1992 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2017 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2042 | SPEC_RULE_OP1, - 2047 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2072 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_QUICK_ARG, - 2122 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2147 | SPEC_RULE_OP2, - 2152, - 2153 | SPEC_RULE_OP1, - 2158 | SPEC_RULE_OP1, - 2163, - 2164 | SPEC_RULE_OP1, - 2169 | SPEC_RULE_OP1, - 2174 | SPEC_RULE_OP1, - 2179, - 2180, - 2181 | SPEC_RULE_OP2, - 2186 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, + 1966 | SPEC_RULE_OP1, + 1971 | SPEC_RULE_OP1, + 1976 | SPEC_RULE_OP2, + 1981 | SPEC_RULE_OP1, + 1986 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, + 1996 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2021 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2046 | SPEC_RULE_OP1, + 2051 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2076 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_QUICK_ARG, + 2126 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2151 | SPEC_RULE_OP2, + 2156, + 2157 | SPEC_RULE_OP1, + 2162 | SPEC_RULE_OP1, + 2167, + 2168 | SPEC_RULE_OP1, + 2173 | SPEC_RULE_OP1, + 2178 | SPEC_RULE_OP1, + 2183, + 2184, + 2185 | SPEC_RULE_OP2, 2190 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, 2194 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, - 2198 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2198 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2223 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2223 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2248 | SPEC_RULE_OP1, - 2253, - 2254 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2279, - 2280 | SPEC_RULE_OP1, - 2285, - 2286, - 2287, - 2288, + 2198 | SPEC_RULE_RETVAL | SPEC_RULE_OBSERVER, + 2202 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2202 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2227 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2227 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2252 | SPEC_RULE_OP1, + 2257, + 2258 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2283, + 2284 | SPEC_RULE_OP1, 2289, 2290, 2291, - 2292 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2317, - 2318, - 2319, - 2320 | SPEC_RULE_OP1, - 2325, - 2326 | SPEC_RULE_ISSET, - 2328 | SPEC_RULE_OP2, - 2333, - 2334 | SPEC_RULE_OP1, - 2339 | SPEC_RULE_OBSERVER, - 2341, - 2342 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2367 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, - 2377, - 2378, - 2379, - 2380, - 2381 | SPEC_RULE_OP1, - 2386, - 2387, - 2388 | SPEC_RULE_OP1, - 2393 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2418, - 2419 | SPEC_RULE_OP1, - 2424, - 2425, - 2426, - 2427, + 2292, + 2293, + 2294, + 2295, + 2296 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2321, + 2322, + 2323, + 2324 | SPEC_RULE_OP1, + 2329, + 2330 | SPEC_RULE_ISSET, + 2332 | SPEC_RULE_OP2, + 2337, + 2338 | SPEC_RULE_OP1, + 2343 | SPEC_RULE_OBSERVER, + 2345, + 2346 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2371 | SPEC_RULE_OP1 | SPEC_RULE_OBSERVER, + 2381, + 2382, + 2383, + 2384, + 2385 | SPEC_RULE_OP1, + 2390, + 2391, + 2392 | SPEC_RULE_OP1, + 2397 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2422, + 2423 | SPEC_RULE_OP1, 2428, 2429, 2430, 2431, - 2432 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2457, - 2458, - 2459, - 2460 | SPEC_RULE_OP2, - 2465, - 2466 | SPEC_RULE_OP1, - 2471 | SPEC_RULE_OP1, - 2476 | SPEC_RULE_OP1, - 2481 | SPEC_RULE_OP1, - 2486 | SPEC_RULE_OP1, - 2491, - 2492 | SPEC_RULE_OP1, - 2497 | SPEC_RULE_OP1 | SPEC_RULE_OP2, - 2522 | SPEC_RULE_OP1, - 2527 | SPEC_RULE_OP2, - 2532 | SPEC_RULE_OP1, - 2537 | SPEC_RULE_OP1, - 2542, - 2543, - 2544, - 2545, + 2432, + 2433, + 2434, + 2435, + 2436 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2461, + 2462, + 2463, + 2464 | SPEC_RULE_OP2, + 2469, + 2470 | SPEC_RULE_OP1, + 2475 | SPEC_RULE_OP1, + 2480 | SPEC_RULE_OP1, + 2485 | SPEC_RULE_OP1, + 2490 | SPEC_RULE_OP1, + 2495, + 2496 | SPEC_RULE_OP1, + 2501 | SPEC_RULE_OP1 | SPEC_RULE_OP2, + 2526 | SPEC_RULE_OP1, + 2531 | SPEC_RULE_OP2, + 2536 | SPEC_RULE_OP1, + 2541 | SPEC_RULE_OP1, 2546, - 2547 | SPEC_RULE_OBSERVER, - 2549 | SPEC_RULE_OBSERVER, + 2547, + 2548, + 2549, + 2550, 2551 | SPEC_RULE_OBSERVER, 2553 | SPEC_RULE_OBSERVER, - 2555, - 2556, - 2557, - 2558, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, - 3474, + 2555 | SPEC_RULE_OBSERVER, + 2557 | SPEC_RULE_OBSERVER, + 2559, + 2560, + 2561, + 2562, + 2563 | SPEC_RULE_OP1, + 2568 | SPEC_RULE_OP1, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, + 3488, }; #if 0 #elif (ZEND_VM_KIND == ZEND_VM_KIND_HYBRID) @@ -123060,7 +125403,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2567 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2581 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123068,7 +125411,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2592 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2606 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123076,7 +125419,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2617 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2631 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; if (op->op1_type < op->op2_type) { zend_swap_operands(op); } @@ -123087,17 +125430,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2642 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2656 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2667 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2681 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2692 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 2706 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_MUL: @@ -123108,17 +125451,17 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2717 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2731 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2742 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2756 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2767 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 2781 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_IDENTICAL: @@ -123129,16 +125472,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2792 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2806 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2867 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2881 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3092 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3106 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3098 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3112 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_IDENTICAL: @@ -123149,16 +125492,16 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2942 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2956 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3017 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3031 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0)) { - spec = 3095 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3109 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { - spec = 3103 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; + spec = 3117 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_EQUAL: @@ -123169,12 +125512,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2792 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2806 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2867 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2881 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_NOT_EQUAL: @@ -123185,12 +125528,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 2942 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 2956 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3017 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; + spec = 3031 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; } break; case ZEND_IS_SMALLER: @@ -123198,12 +125541,12 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3108 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3122 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3183 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3197 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_IS_SMALLER_OR_EQUAL: @@ -123211,79 +125554,79 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3258 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3272 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } else if (op1_info == MAY_BE_DOUBLE && op2_info == MAY_BE_DOUBLE) { if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3333 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; + spec = 3347 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH; } break; case ZEND_QM_ASSIGN: if (op1_info == MAY_BE_LONG) { - spec = 3420 | SPEC_RULE_OP1; + spec = 3434 | SPEC_RULE_OP1; } else if (op1_info == MAY_BE_DOUBLE) { - spec = 3425 | SPEC_RULE_OP1; + spec = 3439 | SPEC_RULE_OP1; } else if ((op->op1_type == IS_CONST) ? !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1)) : (!(op1_info & ((MAY_BE_ANY|MAY_BE_UNDEF)-(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE))))) { - spec = 3430 | SPEC_RULE_OP1; + spec = 3444 | SPEC_RULE_OP1; } break; case ZEND_PRE_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3408 | SPEC_RULE_RETVAL; + spec = 3422 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3410 | SPEC_RULE_RETVAL; + spec = 3424 | SPEC_RULE_RETVAL; } break; case ZEND_PRE_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3412 | SPEC_RULE_RETVAL; + spec = 3426 | SPEC_RULE_RETVAL; } else if (op1_info == MAY_BE_LONG) { - spec = 3414 | SPEC_RULE_RETVAL; + spec = 3428 | SPEC_RULE_RETVAL; } break; case ZEND_POST_INC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3416; + spec = 3430; } else if (op1_info == MAY_BE_LONG) { - spec = 3417; + spec = 3431; } break; case ZEND_POST_DEC: if (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG) { - spec = 3418; + spec = 3432; } else if (op1_info == MAY_BE_LONG) { - spec = 3419; + spec = 3433; } break; case ZEND_JMP: if (OP_JMP_ADDR(op, op->op1) > op) { - spec = 2566; + spec = 2580; } break; case ZEND_INIT_FCALL: if (Z_EXTRA_P(RT_CONSTANT(op, op->op2)) != 0) { - spec = 2559; + spec = 2573; } break; case ZEND_RECV: if (op->op2.num == MAY_BE_ANY) { - spec = 2560; + spec = 2574; } break; case ZEND_SEND_VAL: if (op->op1_type == IS_CONST && op->op2_type == IS_UNUSED && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3470; + spec = 3484; } break; case ZEND_SEND_VAR_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3465 | SPEC_RULE_OP1; + spec = 3479 | SPEC_RULE_OP1; } break; case ZEND_FE_FETCH_R: if (op->op2_type == IS_CV && (op1_info & (MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 3472 | SPEC_RULE_RETVAL; + spec = 3486 | SPEC_RULE_RETVAL; } break; case ZEND_FETCH_DIM_R: @@ -123291,22 +125634,22 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t if (op->op1_type == IS_CONST && op->op2_type == IS_CONST) { break; } - spec = 3435 | SPEC_RULE_OP1 | SPEC_RULE_OP2; + spec = 3449 | SPEC_RULE_OP1 | SPEC_RULE_OP2; } break; case ZEND_SEND_VAL_EX: if (op->op2_type == IS_UNUSED && op->op2.num <= MAX_ARG_FLAG_NUM && op->op1_type == IS_CONST && !Z_REFCOUNTED_P(RT_CONSTANT(op, op->op1))) { - spec = 3471; + spec = 3485; } break; case ZEND_SEND_VAR: if (op->op2_type == IS_UNUSED && (op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) == 0) { - spec = 3460 | SPEC_RULE_OP1; + spec = 3474 | SPEC_RULE_OP1; } break; case ZEND_COUNT: if ((op1_info & (MAY_BE_ANY|MAY_BE_UNDEF|MAY_BE_REF)) == MAY_BE_ARRAY) { - spec = 2561 | SPEC_RULE_OP1; + spec = 2575 | SPEC_RULE_OP1; } break; case ZEND_BW_OR: diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h index 6f1595195450..b1ff354531a5 100644 --- a/Zend/zend_vm_handlers.h +++ b/Zend/zend_vm_handlers.h @@ -772,822 +772,827 @@ _(1961, ZEND_SEND_VAR_NO_REF_SPEC_VAR_CONST) \ _(1964, ZEND_SEND_VAR_NO_REF_SPEC_VAR_UNUSED) \ _(1966, ZEND_CATCH_SPEC_CONST) \ - _(1967, ZEND_THROW_SPEC_CONST) \ - _(1968, ZEND_THROW_SPEC_TMP) \ - _(1971, ZEND_THROW_SPEC_CV) \ - _(1972, ZEND_FETCH_CLASS_SPEC_UNUSED_CONST) \ - _(1973, ZEND_FETCH_CLASS_SPEC_UNUSED_TMP) \ - _(1975, ZEND_FETCH_CLASS_SPEC_UNUSED_UNUSED) \ - _(1976, ZEND_FETCH_CLASS_SPEC_UNUSED_CV) \ - _(1977, ZEND_CLONE_SPEC_CONST) \ - _(1978, ZEND_CLONE_SPEC_TMP) \ - _(1980, ZEND_CLONE_SPEC_UNUSED) \ - _(1981, ZEND_CLONE_SPEC_CV) \ - _(1982, ZEND_RETURN_BY_REF_SPEC_CONST) \ - _(1983, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1984, ZEND_RETURN_BY_REF_SPEC_TMP) \ - _(1985, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1986, ZEND_RETURN_BY_REF_SPEC_VAR) \ + _(1969, ZEND_CATCH_SPEC_UNUSED) \ + _(1971, ZEND_THROW_SPEC_CONST) \ + _(1972, ZEND_THROW_SPEC_TMP) \ + _(1975, ZEND_THROW_SPEC_CV) \ + _(1976, ZEND_FETCH_CLASS_SPEC_UNUSED_CONST) \ + _(1977, ZEND_FETCH_CLASS_SPEC_UNUSED_TMP) \ + _(1979, ZEND_FETCH_CLASS_SPEC_UNUSED_UNUSED) \ + _(1980, ZEND_FETCH_CLASS_SPEC_UNUSED_CV) \ + _(1981, ZEND_CLONE_SPEC_CONST) \ + _(1982, ZEND_CLONE_SPEC_TMP) \ + _(1984, ZEND_CLONE_SPEC_UNUSED) \ + _(1985, ZEND_CLONE_SPEC_CV) \ + _(1986, ZEND_RETURN_BY_REF_SPEC_CONST) \ _(1987, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1990, ZEND_RETURN_BY_REF_SPEC_CV) \ + _(1988, ZEND_RETURN_BY_REF_SPEC_TMP) \ + _(1989, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ + _(1990, ZEND_RETURN_BY_REF_SPEC_VAR) \ _(1991, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ - _(1992, ZEND_INIT_METHOD_CALL_SPEC_CONST_CONST) \ - _(1993, ZEND_INIT_METHOD_CALL_SPEC_CONST_TMP) \ - _(1996, ZEND_INIT_METHOD_CALL_SPEC_CONST_CV) \ - _(1997, ZEND_INIT_METHOD_CALL_SPEC_TMP_CONST) \ - _(1998, ZEND_INIT_METHOD_CALL_SPEC_TMP_TMP) \ - _(2001, ZEND_INIT_METHOD_CALL_SPEC_TMP_CV) \ - _(2007, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CONST) \ - _(2008, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_TMP) \ - _(2011, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CV) \ - _(2012, ZEND_INIT_METHOD_CALL_SPEC_CV_CONST) \ - _(2013, ZEND_INIT_METHOD_CALL_SPEC_CV_TMP) \ - _(2016, ZEND_INIT_METHOD_CALL_SPEC_CV_CV) \ - _(2017, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST) \ - _(2018, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_TMP) \ - _(2020, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_UNUSED) \ - _(2021, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CV) \ - _(2027, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CONST) \ - _(2028, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_TMP) \ - _(2030, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_UNUSED) \ - _(2031, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CV) \ - _(2032, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CONST) \ - _(2033, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_TMP) \ - _(2035, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_UNUSED) \ - _(2036, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CV) \ - _(2042, ZEND_ISSET_ISEMPTY_VAR_SPEC_CONST_UNUSED) \ - _(2043, ZEND_ISSET_ISEMPTY_VAR_SPEC_TMP_UNUSED) \ - _(2046, ZEND_ISSET_ISEMPTY_VAR_SPEC_CV_UNUSED) \ - _(2047, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST) \ - _(2048, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMP) \ - _(2051, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV) \ - _(2052, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CONST) \ - _(2053, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_TMP) \ - _(2056, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CV) \ - _(2067, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST) \ - _(2068, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMP) \ - _(2071, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV) \ - _(2072, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ - _(2073, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ - _(2078, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED) \ - _(2079, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED_QUICK) \ - _(2082, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ - _(2083, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ - _(2088, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED) \ - _(2089, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED_QUICK) \ - _(2132, ZEND_SEND_VAR_SPEC_VAR_CONST) \ - _(2135, ZEND_SEND_VAR_SPEC_VAR_UNUSED) \ - _(2142, ZEND_SEND_VAR_SPEC_CV_CONST) \ - _(2145, ZEND_SEND_VAR_SPEC_CV_UNUSED) \ - _(2147, ZEND_INIT_USER_CALL_SPEC_CONST_CONST) \ - _(2148, ZEND_INIT_USER_CALL_SPEC_CONST_TMP) \ - _(2151, ZEND_INIT_USER_CALL_SPEC_CONST_CV) \ - _(2152, ZEND_SEND_ARRAY_SPEC) \ - _(2153, ZEND_SEND_USER_SPEC_CONST) \ - _(2154, ZEND_SEND_USER_SPEC_TMP) \ - _(2157, ZEND_SEND_USER_SPEC_CV) \ - _(2158, ZEND_STRLEN_SPEC_CONST) \ - _(2159, ZEND_STRLEN_SPEC_TMP) \ - _(2162, ZEND_STRLEN_SPEC_CV) \ - _(2163, ZEND_DEFINED_SPEC_CONST) \ - _(2164, ZEND_TYPE_CHECK_SPEC_CONST) \ - _(2165, ZEND_TYPE_CHECK_SPEC_TMP) \ - _(2168, ZEND_TYPE_CHECK_SPEC_CV) \ - _(2169, ZEND_VERIFY_RETURN_TYPE_SPEC_CONST_UNUSED) \ - _(2170, ZEND_VERIFY_RETURN_TYPE_SPEC_TMP_UNUSED) \ - _(2171, ZEND_VERIFY_RETURN_TYPE_SPEC_VAR_UNUSED) \ - _(2172, ZEND_VERIFY_RETURN_TYPE_SPEC_UNUSED_UNUSED) \ - _(2173, ZEND_VERIFY_RETURN_TYPE_SPEC_CV_UNUSED) \ - _(2174, ZEND_FE_RESET_RW_SPEC_CONST) \ - _(2175, ZEND_FE_RESET_RW_SPEC_TMP) \ - _(2176, ZEND_FE_RESET_RW_SPEC_VAR) \ - _(2178, ZEND_FE_RESET_RW_SPEC_CV) \ - _(2179, ZEND_FE_FETCH_RW_SPEC_VAR) \ - _(2180, ZEND_FE_FREE_SPEC_TMPVAR) \ - _(2181, ZEND_INIT_DYNAMIC_CALL_SPEC_CONST) \ - _(2182, ZEND_INIT_DYNAMIC_CALL_SPEC_TMP) \ - _(2185, ZEND_INIT_DYNAMIC_CALL_SPEC_CV) \ - _(2186, ZEND_DO_ICALL_SPEC_RETVAL_UNUSED) \ - _(2187, ZEND_DO_ICALL_SPEC_RETVAL_USED) \ - _(2188, ZEND_DO_ICALL_SPEC_OBSERVER) \ - _(2189, ZEND_DO_ICALL_SPEC_OBSERVER) \ - _(2190, ZEND_DO_UCALL_SPEC_RETVAL_UNUSED) \ - _(2191, ZEND_DO_UCALL_SPEC_RETVAL_USED) \ - _(2192, ZEND_DO_UCALL_SPEC_OBSERVER) \ - _(2193, ZEND_DO_UCALL_SPEC_OBSERVER) \ - _(2194, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED) \ - _(2195, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED) \ - _(2196, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ - _(2197, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ - _(2208, ZEND_PRE_INC_OBJ_SPEC_VAR_CONST) \ - _(2209, ZEND_PRE_INC_OBJ_SPEC_VAR_TMP) \ - _(2212, ZEND_PRE_INC_OBJ_SPEC_VAR_CV) \ - _(2213, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CONST) \ - _(2214, ZEND_PRE_INC_OBJ_SPEC_UNUSED_TMP) \ - _(2217, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CV) \ - _(2218, ZEND_PRE_INC_OBJ_SPEC_CV_CONST) \ - _(2219, ZEND_PRE_INC_OBJ_SPEC_CV_TMP) \ - _(2222, ZEND_PRE_INC_OBJ_SPEC_CV_CV) \ - _(2233, ZEND_POST_INC_OBJ_SPEC_VAR_CONST) \ - _(2234, ZEND_POST_INC_OBJ_SPEC_VAR_TMP) \ - _(2237, ZEND_POST_INC_OBJ_SPEC_VAR_CV) \ - _(2238, ZEND_POST_INC_OBJ_SPEC_UNUSED_CONST) \ - _(2239, ZEND_POST_INC_OBJ_SPEC_UNUSED_TMP) \ - _(2242, ZEND_POST_INC_OBJ_SPEC_UNUSED_CV) \ - _(2243, ZEND_POST_INC_OBJ_SPEC_CV_CONST) \ - _(2244, ZEND_POST_INC_OBJ_SPEC_CV_TMP) \ - _(2247, ZEND_POST_INC_OBJ_SPEC_CV_CV) \ - _(2248, ZEND_ECHO_SPEC_CONST) \ - _(2249, ZEND_ECHO_SPEC_TMP) \ - _(2252, ZEND_ECHO_SPEC_CV) \ - _(2259, ZEND_INSTANCEOF_SPEC_TMP_CONST) \ - _(2261, ZEND_INSTANCEOF_SPEC_TMP_VAR) \ - _(2262, ZEND_INSTANCEOF_SPEC_TMP_UNUSED) \ - _(2274, ZEND_INSTANCEOF_SPEC_CV_CONST) \ - _(2276, ZEND_INSTANCEOF_SPEC_CV_VAR) \ - _(2277, ZEND_INSTANCEOF_SPEC_CV_UNUSED) \ - _(2279, ZEND_GENERATOR_CREATE_SPEC) \ - _(2282, ZEND_MAKE_REF_SPEC_VAR_UNUSED) \ - _(2284, ZEND_MAKE_REF_SPEC_CV_UNUSED) \ - _(2285, ZEND_DECLARE_FUNCTION_SPEC) \ - _(2286, ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST) \ - _(2287, ZEND_DECLARE_CONST_SPEC_CONST_CONST) \ - _(2288, ZEND_DECLARE_CLASS_SPEC_CONST) \ - _(2289, ZEND_DECLARE_CLASS_DELAYED_SPEC_CONST_CONST) \ - _(2290, ZEND_DECLARE_ANON_CLASS_SPEC) \ - _(2291, ZEND_ADD_ARRAY_UNPACK_SPEC) \ - _(2292, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CONST) \ - _(2293, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_TMP) \ - _(2296, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CV) \ - _(2297, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CONST) \ - _(2298, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_TMP) \ - _(2301, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CV) \ - _(2307, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CONST) \ - _(2308, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_TMP) \ - _(2311, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CV) \ - _(2312, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST) \ - _(2313, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_TMP) \ - _(2316, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CV) \ - _(2317, ZEND_HANDLE_EXCEPTION_SPEC) \ - _(2318, ZEND_USER_OPCODE_SPEC) \ - _(2319, ZEND_ASSERT_CHECK_SPEC) \ - _(2320, ZEND_JMP_SET_SPEC_CONST) \ - _(2321, ZEND_JMP_SET_SPEC_TMP) \ - _(2324, ZEND_JMP_SET_SPEC_CV) \ - _(2325, ZEND_UNSET_CV_SPEC_CV_UNUSED) \ - _(2326, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_SET) \ - _(2327, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_EMPTY) \ - _(2328, ZEND_FETCH_LIST_W_SPEC_VAR_CONST) \ - _(2329, ZEND_FETCH_LIST_W_SPEC_VAR_TMP) \ - _(2332, ZEND_FETCH_LIST_W_SPEC_VAR_CV) \ - _(2333, ZEND_SEPARATE_SPEC_VAR_UNUSED) \ - _(2335, ZEND_FETCH_CLASS_NAME_SPEC_TMP) \ - _(2337, ZEND_FETCH_CLASS_NAME_SPEC_UNUSED) \ - _(2338, ZEND_FETCH_CLASS_NAME_SPEC_CV) \ - _(2339, ZEND_CALL_TRAMPOLINE_SPEC) \ - _(2340, ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER) \ - _(2341, ZEND_DISCARD_EXCEPTION_SPEC) \ - _(2342, ZEND_YIELD_SPEC_CONST_CONST) \ - _(2343, ZEND_YIELD_SPEC_CONST_TMP) \ - _(2345, ZEND_YIELD_SPEC_CONST_UNUSED) \ - _(2346, ZEND_YIELD_SPEC_CONST_CV) \ - _(2347, ZEND_YIELD_SPEC_TMP_CONST) \ - _(2348, ZEND_YIELD_SPEC_TMP_TMP) \ - _(2350, ZEND_YIELD_SPEC_TMP_UNUSED) \ - _(2351, ZEND_YIELD_SPEC_TMP_CV) \ - _(2352, ZEND_YIELD_SPEC_VAR_CONST) \ - _(2353, ZEND_YIELD_SPEC_VAR_TMP) \ - _(2355, ZEND_YIELD_SPEC_VAR_UNUSED) \ - _(2356, ZEND_YIELD_SPEC_VAR_CV) \ - _(2357, ZEND_YIELD_SPEC_UNUSED_CONST) \ - _(2358, ZEND_YIELD_SPEC_UNUSED_TMP) \ - _(2360, ZEND_YIELD_SPEC_UNUSED_UNUSED) \ - _(2361, ZEND_YIELD_SPEC_UNUSED_CV) \ - _(2362, ZEND_YIELD_SPEC_CV_CONST) \ - _(2363, ZEND_YIELD_SPEC_CV_TMP) \ - _(2365, ZEND_YIELD_SPEC_CV_UNUSED) \ - _(2366, ZEND_YIELD_SPEC_CV_CV) \ - _(2367, ZEND_GENERATOR_RETURN_SPEC_CONST) \ - _(2368, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2369, ZEND_GENERATOR_RETURN_SPEC_TMP) \ - _(2370, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2371, ZEND_GENERATOR_RETURN_SPEC_VAR) \ + _(1994, ZEND_RETURN_BY_REF_SPEC_CV) \ + _(1995, ZEND_RETURN_BY_REF_SPEC_OBSERVER) \ + _(1996, ZEND_INIT_METHOD_CALL_SPEC_CONST_CONST) \ + _(1997, ZEND_INIT_METHOD_CALL_SPEC_CONST_TMP) \ + _(2000, ZEND_INIT_METHOD_CALL_SPEC_CONST_CV) \ + _(2001, ZEND_INIT_METHOD_CALL_SPEC_TMP_CONST) \ + _(2002, ZEND_INIT_METHOD_CALL_SPEC_TMP_TMP) \ + _(2005, ZEND_INIT_METHOD_CALL_SPEC_TMP_CV) \ + _(2011, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CONST) \ + _(2012, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_TMP) \ + _(2015, ZEND_INIT_METHOD_CALL_SPEC_UNUSED_CV) \ + _(2016, ZEND_INIT_METHOD_CALL_SPEC_CV_CONST) \ + _(2017, ZEND_INIT_METHOD_CALL_SPEC_CV_TMP) \ + _(2020, ZEND_INIT_METHOD_CALL_SPEC_CV_CV) \ + _(2021, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST) \ + _(2022, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_TMP) \ + _(2024, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_UNUSED) \ + _(2025, ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CV) \ + _(2031, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CONST) \ + _(2032, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_TMP) \ + _(2034, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_UNUSED) \ + _(2035, ZEND_INIT_STATIC_METHOD_CALL_SPEC_VAR_CV) \ + _(2036, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CONST) \ + _(2037, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_TMP) \ + _(2039, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_UNUSED) \ + _(2040, ZEND_INIT_STATIC_METHOD_CALL_SPEC_UNUSED_CV) \ + _(2046, ZEND_ISSET_ISEMPTY_VAR_SPEC_CONST_UNUSED) \ + _(2047, ZEND_ISSET_ISEMPTY_VAR_SPEC_TMP_UNUSED) \ + _(2050, ZEND_ISSET_ISEMPTY_VAR_SPEC_CV_UNUSED) \ + _(2051, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST) \ + _(2052, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMP) \ + _(2055, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV) \ + _(2056, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CONST) \ + _(2057, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_TMP) \ + _(2060, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMP_CV) \ + _(2071, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST) \ + _(2072, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMP) \ + _(2075, ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV) \ + _(2076, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ + _(2077, ZEND_SEND_VAL_EX_SPEC_CONST_CONST) \ + _(2082, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED) \ + _(2083, ZEND_SEND_VAL_EX_SPEC_CONST_UNUSED_QUICK) \ + _(2086, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ + _(2087, ZEND_SEND_VAL_EX_SPEC_TMP_CONST) \ + _(2092, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED) \ + _(2093, ZEND_SEND_VAL_EX_SPEC_TMP_UNUSED_QUICK) \ + _(2136, ZEND_SEND_VAR_SPEC_VAR_CONST) \ + _(2139, ZEND_SEND_VAR_SPEC_VAR_UNUSED) \ + _(2146, ZEND_SEND_VAR_SPEC_CV_CONST) \ + _(2149, ZEND_SEND_VAR_SPEC_CV_UNUSED) \ + _(2151, ZEND_INIT_USER_CALL_SPEC_CONST_CONST) \ + _(2152, ZEND_INIT_USER_CALL_SPEC_CONST_TMP) \ + _(2155, ZEND_INIT_USER_CALL_SPEC_CONST_CV) \ + _(2156, ZEND_SEND_ARRAY_SPEC) \ + _(2157, ZEND_SEND_USER_SPEC_CONST) \ + _(2158, ZEND_SEND_USER_SPEC_TMP) \ + _(2161, ZEND_SEND_USER_SPEC_CV) \ + _(2162, ZEND_STRLEN_SPEC_CONST) \ + _(2163, ZEND_STRLEN_SPEC_TMP) \ + _(2166, ZEND_STRLEN_SPEC_CV) \ + _(2167, ZEND_DEFINED_SPEC_CONST) \ + _(2168, ZEND_TYPE_CHECK_SPEC_CONST) \ + _(2169, ZEND_TYPE_CHECK_SPEC_TMP) \ + _(2172, ZEND_TYPE_CHECK_SPEC_CV) \ + _(2173, ZEND_VERIFY_RETURN_TYPE_SPEC_CONST_UNUSED) \ + _(2174, ZEND_VERIFY_RETURN_TYPE_SPEC_TMP_UNUSED) \ + _(2175, ZEND_VERIFY_RETURN_TYPE_SPEC_VAR_UNUSED) \ + _(2176, ZEND_VERIFY_RETURN_TYPE_SPEC_UNUSED_UNUSED) \ + _(2177, ZEND_VERIFY_RETURN_TYPE_SPEC_CV_UNUSED) \ + _(2178, ZEND_FE_RESET_RW_SPEC_CONST) \ + _(2179, ZEND_FE_RESET_RW_SPEC_TMP) \ + _(2180, ZEND_FE_RESET_RW_SPEC_VAR) \ + _(2182, ZEND_FE_RESET_RW_SPEC_CV) \ + _(2183, ZEND_FE_FETCH_RW_SPEC_VAR) \ + _(2184, ZEND_FE_FREE_SPEC_TMPVAR) \ + _(2185, ZEND_INIT_DYNAMIC_CALL_SPEC_CONST) \ + _(2186, ZEND_INIT_DYNAMIC_CALL_SPEC_TMP) \ + _(2189, ZEND_INIT_DYNAMIC_CALL_SPEC_CV) \ + _(2190, ZEND_DO_ICALL_SPEC_RETVAL_UNUSED) \ + _(2191, ZEND_DO_ICALL_SPEC_RETVAL_USED) \ + _(2192, ZEND_DO_ICALL_SPEC_OBSERVER) \ + _(2193, ZEND_DO_ICALL_SPEC_OBSERVER) \ + _(2194, ZEND_DO_UCALL_SPEC_RETVAL_UNUSED) \ + _(2195, ZEND_DO_UCALL_SPEC_RETVAL_USED) \ + _(2196, ZEND_DO_UCALL_SPEC_OBSERVER) \ + _(2197, ZEND_DO_UCALL_SPEC_OBSERVER) \ + _(2198, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED) \ + _(2199, ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED) \ + _(2200, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ + _(2201, ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER) \ + _(2212, ZEND_PRE_INC_OBJ_SPEC_VAR_CONST) \ + _(2213, ZEND_PRE_INC_OBJ_SPEC_VAR_TMP) \ + _(2216, ZEND_PRE_INC_OBJ_SPEC_VAR_CV) \ + _(2217, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CONST) \ + _(2218, ZEND_PRE_INC_OBJ_SPEC_UNUSED_TMP) \ + _(2221, ZEND_PRE_INC_OBJ_SPEC_UNUSED_CV) \ + _(2222, ZEND_PRE_INC_OBJ_SPEC_CV_CONST) \ + _(2223, ZEND_PRE_INC_OBJ_SPEC_CV_TMP) \ + _(2226, ZEND_PRE_INC_OBJ_SPEC_CV_CV) \ + _(2237, ZEND_POST_INC_OBJ_SPEC_VAR_CONST) \ + _(2238, ZEND_POST_INC_OBJ_SPEC_VAR_TMP) \ + _(2241, ZEND_POST_INC_OBJ_SPEC_VAR_CV) \ + _(2242, ZEND_POST_INC_OBJ_SPEC_UNUSED_CONST) \ + _(2243, ZEND_POST_INC_OBJ_SPEC_UNUSED_TMP) \ + _(2246, ZEND_POST_INC_OBJ_SPEC_UNUSED_CV) \ + _(2247, ZEND_POST_INC_OBJ_SPEC_CV_CONST) \ + _(2248, ZEND_POST_INC_OBJ_SPEC_CV_TMP) \ + _(2251, ZEND_POST_INC_OBJ_SPEC_CV_CV) \ + _(2252, ZEND_ECHO_SPEC_CONST) \ + _(2253, ZEND_ECHO_SPEC_TMP) \ + _(2256, ZEND_ECHO_SPEC_CV) \ + _(2263, ZEND_INSTANCEOF_SPEC_TMP_CONST) \ + _(2265, ZEND_INSTANCEOF_SPEC_TMP_VAR) \ + _(2266, ZEND_INSTANCEOF_SPEC_TMP_UNUSED) \ + _(2278, ZEND_INSTANCEOF_SPEC_CV_CONST) \ + _(2280, ZEND_INSTANCEOF_SPEC_CV_VAR) \ + _(2281, ZEND_INSTANCEOF_SPEC_CV_UNUSED) \ + _(2283, ZEND_GENERATOR_CREATE_SPEC) \ + _(2286, ZEND_MAKE_REF_SPEC_VAR_UNUSED) \ + _(2288, ZEND_MAKE_REF_SPEC_CV_UNUSED) \ + _(2289, ZEND_DECLARE_FUNCTION_SPEC) \ + _(2290, ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST) \ + _(2291, ZEND_DECLARE_CONST_SPEC_CONST_CONST) \ + _(2292, ZEND_DECLARE_CLASS_SPEC_CONST) \ + _(2293, ZEND_DECLARE_CLASS_DELAYED_SPEC_CONST_CONST) \ + _(2294, ZEND_DECLARE_ANON_CLASS_SPEC) \ + _(2295, ZEND_ADD_ARRAY_UNPACK_SPEC) \ + _(2296, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CONST) \ + _(2297, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_TMP) \ + _(2300, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CONST_CV) \ + _(2301, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CONST) \ + _(2302, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_TMP) \ + _(2305, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMP_CV) \ + _(2311, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CONST) \ + _(2312, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_TMP) \ + _(2315, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_UNUSED_CV) \ + _(2316, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CONST) \ + _(2317, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_TMP) \ + _(2320, ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_CV_CV) \ + _(2321, ZEND_HANDLE_EXCEPTION_SPEC) \ + _(2322, ZEND_USER_OPCODE_SPEC) \ + _(2323, ZEND_ASSERT_CHECK_SPEC) \ + _(2324, ZEND_JMP_SET_SPEC_CONST) \ + _(2325, ZEND_JMP_SET_SPEC_TMP) \ + _(2328, ZEND_JMP_SET_SPEC_CV) \ + _(2329, ZEND_UNSET_CV_SPEC_CV_UNUSED) \ + _(2330, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_SET) \ + _(2331, ZEND_ISSET_ISEMPTY_CV_SPEC_CV_UNUSED_EMPTY) \ + _(2332, ZEND_FETCH_LIST_W_SPEC_VAR_CONST) \ + _(2333, ZEND_FETCH_LIST_W_SPEC_VAR_TMP) \ + _(2336, ZEND_FETCH_LIST_W_SPEC_VAR_CV) \ + _(2337, ZEND_SEPARATE_SPEC_VAR_UNUSED) \ + _(2339, ZEND_FETCH_CLASS_NAME_SPEC_TMP) \ + _(2341, ZEND_FETCH_CLASS_NAME_SPEC_UNUSED) \ + _(2342, ZEND_FETCH_CLASS_NAME_SPEC_CV) \ + _(2343, ZEND_CALL_TRAMPOLINE_SPEC) \ + _(2344, ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER) \ + _(2345, ZEND_DISCARD_EXCEPTION_SPEC) \ + _(2346, ZEND_YIELD_SPEC_CONST_CONST) \ + _(2347, ZEND_YIELD_SPEC_CONST_TMP) \ + _(2349, ZEND_YIELD_SPEC_CONST_UNUSED) \ + _(2350, ZEND_YIELD_SPEC_CONST_CV) \ + _(2351, ZEND_YIELD_SPEC_TMP_CONST) \ + _(2352, ZEND_YIELD_SPEC_TMP_TMP) \ + _(2354, ZEND_YIELD_SPEC_TMP_UNUSED) \ + _(2355, ZEND_YIELD_SPEC_TMP_CV) \ + _(2356, ZEND_YIELD_SPEC_VAR_CONST) \ + _(2357, ZEND_YIELD_SPEC_VAR_TMP) \ + _(2359, ZEND_YIELD_SPEC_VAR_UNUSED) \ + _(2360, ZEND_YIELD_SPEC_VAR_CV) \ + _(2361, ZEND_YIELD_SPEC_UNUSED_CONST) \ + _(2362, ZEND_YIELD_SPEC_UNUSED_TMP) \ + _(2364, ZEND_YIELD_SPEC_UNUSED_UNUSED) \ + _(2365, ZEND_YIELD_SPEC_UNUSED_CV) \ + _(2366, ZEND_YIELD_SPEC_CV_CONST) \ + _(2367, ZEND_YIELD_SPEC_CV_TMP) \ + _(2369, ZEND_YIELD_SPEC_CV_UNUSED) \ + _(2370, ZEND_YIELD_SPEC_CV_CV) \ + _(2371, ZEND_GENERATOR_RETURN_SPEC_CONST) \ _(2372, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2375, ZEND_GENERATOR_RETURN_SPEC_CV) \ + _(2373, ZEND_GENERATOR_RETURN_SPEC_TMP) \ + _(2374, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ + _(2375, ZEND_GENERATOR_RETURN_SPEC_VAR) \ _(2376, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ - _(2377, ZEND_FAST_CALL_SPEC) \ - _(2378, ZEND_FAST_RET_SPEC) \ - _(2379, ZEND_RECV_VARIADIC_SPEC_UNUSED) \ - _(2380, ZEND_SEND_UNPACK_SPEC) \ - _(2381, ZEND_YIELD_FROM_SPEC_CONST) \ - _(2382, ZEND_YIELD_FROM_SPEC_TMP) \ - _(2385, ZEND_YIELD_FROM_SPEC_CV) \ - _(2386, ZEND_COPY_TMP_SPEC_TMPVAR_UNUSED) \ - _(2387, ZEND_BIND_GLOBAL_SPEC_CV_CONST) \ - _(2388, ZEND_COALESCE_SPEC_CONST) \ - _(2389, ZEND_COALESCE_SPEC_TMP) \ - _(2392, ZEND_COALESCE_SPEC_CV) \ - _(2393, ZEND_SPACESHIP_SPEC_CONST_CONST) \ - _(2394, ZEND_SPACESHIP_SPEC_CONST_TMP) \ - _(2397, ZEND_SPACESHIP_SPEC_CONST_CV) \ - _(2398, ZEND_SPACESHIP_SPEC_TMP_CONST) \ - _(2399, ZEND_SPACESHIP_SPEC_TMP_TMP) \ - _(2402, ZEND_SPACESHIP_SPEC_TMP_CV) \ - _(2413, ZEND_SPACESHIP_SPEC_CV_CONST) \ - _(2414, ZEND_SPACESHIP_SPEC_CV_TMP) \ - _(2417, ZEND_SPACESHIP_SPEC_CV_CV) \ - _(2418, ZEND_FUNC_NUM_ARGS_SPEC_UNUSED_UNUSED) \ - _(2419, ZEND_FUNC_GET_ARGS_SPEC_CONST_UNUSED) \ - _(2422, ZEND_FUNC_GET_ARGS_SPEC_UNUSED_UNUSED) \ - _(2424, ZEND_FETCH_STATIC_PROP_R_SPEC) \ - _(2425, ZEND_FETCH_STATIC_PROP_W_SPEC) \ - _(2426, ZEND_FETCH_STATIC_PROP_RW_SPEC) \ - _(2427, ZEND_FETCH_STATIC_PROP_IS_SPEC) \ - _(2428, ZEND_FETCH_STATIC_PROP_FUNC_ARG_SPEC) \ - _(2429, ZEND_FETCH_STATIC_PROP_UNSET_SPEC) \ - _(2430, ZEND_UNSET_STATIC_PROP_SPEC) \ - _(2431, ZEND_ISSET_ISEMPTY_STATIC_PROP_SPEC) \ - _(2432, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_CONST) \ - _(2433, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2434, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2436, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ - _(2442, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_CONST) \ - _(2443, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2444, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2446, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ - _(2447, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_CONST) \ - _(2448, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2449, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2451, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ - _(2457, ZEND_BIND_LEXICAL_SPEC_TMP_CV) \ - _(2458, ZEND_BIND_STATIC_SPEC_CV) \ - _(2459, ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) \ - _(2460, ZEND_SEND_FUNC_ARG_SPEC_VAR_CONST) \ - _(2463, ZEND_SEND_FUNC_ARG_SPEC_VAR_UNUSED) \ - _(2465, ZEND_ISSET_ISEMPTY_THIS_SPEC_UNUSED_UNUSED) \ - _(2466, ZEND_SWITCH_LONG_SPEC_CONST_CONST) \ - _(2467, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2468, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2470, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ - _(2471, ZEND_SWITCH_STRING_SPEC_CONST_CONST) \ - _(2472, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2473, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2475, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ - _(2476, ZEND_IN_ARRAY_SPEC_CONST_CONST) \ - _(2477, ZEND_IN_ARRAY_SPEC_TMP_CONST) \ - _(2480, ZEND_IN_ARRAY_SPEC_CV_CONST) \ - _(2481, ZEND_COUNT_SPEC_CONST_UNUSED) \ - _(2482, ZEND_COUNT_SPEC_TMP_UNUSED) \ - _(2485, ZEND_COUNT_SPEC_CV_UNUSED) \ - _(2486, ZEND_GET_CLASS_SPEC_CONST_UNUSED) \ - _(2487, ZEND_GET_CLASS_SPEC_TMP_UNUSED) \ - _(2489, ZEND_GET_CLASS_SPEC_UNUSED_UNUSED) \ - _(2490, ZEND_GET_CLASS_SPEC_CV_UNUSED) \ - _(2491, ZEND_GET_CALLED_CLASS_SPEC_UNUSED_UNUSED) \ - _(2492, ZEND_GET_TYPE_SPEC_CONST_UNUSED) \ - _(2493, ZEND_GET_TYPE_SPEC_TMP_UNUSED) \ - _(2496, ZEND_GET_TYPE_SPEC_CV_UNUSED) \ - _(2497, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CONST) \ - _(2498, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_TMP) \ - _(2501, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CV) \ - _(2502, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CONST) \ - _(2503, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_TMP) \ - _(2506, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CV) \ - _(2517, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CONST) \ - _(2518, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_TMP) \ - _(2521, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CV) \ - _(2522, ZEND_MATCH_SPEC_CONST_CONST) \ - _(2523, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2524, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2526, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ - _(2527, ZEND_CASE_STRICT_SPEC_TMP_CONST) \ - _(2528, ZEND_CASE_STRICT_SPEC_TMP_TMP) \ - _(2531, ZEND_CASE_STRICT_SPEC_TMP_CV) \ - _(2532, ZEND_MATCH_ERROR_SPEC_CONST_UNUSED) \ - _(2533, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2534, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2536, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ - _(2537, ZEND_JMP_NULL_SPEC_CONST) \ - _(2538, ZEND_JMP_NULL_SPEC_TMP) \ - _(2541, ZEND_JMP_NULL_SPEC_CV) \ - _(2542, ZEND_CHECK_UNDEF_ARGS_SPEC_UNUSED_UNUSED) \ - _(2543, ZEND_FETCH_GLOBALS_SPEC_UNUSED_UNUSED) \ - _(2544, ZEND_VERIFY_NEVER_TYPE_SPEC_UNUSED_UNUSED) \ - _(2545, ZEND_CALLABLE_CONVERT_SPEC_UNUSED_UNUSED) \ - _(2546, ZEND_BIND_INIT_STATIC_OR_JMP_SPEC_CV) \ - _(2547, ZEND_FRAMELESS_ICALL_0_SPEC_UNUSED_UNUSED) \ - _(2548, ZEND_FRAMELESS_ICALL_0_SPEC_OBSERVER) \ - _(2549, ZEND_FRAMELESS_ICALL_1_SPEC_UNUSED) \ - _(2550, ZEND_FRAMELESS_ICALL_1_SPEC_OBSERVER) \ - _(2551, ZEND_FRAMELESS_ICALL_2_SPEC) \ - _(2552, ZEND_FRAMELESS_ICALL_2_SPEC_OBSERVER) \ - _(2553, ZEND_FRAMELESS_ICALL_3_SPEC) \ - _(2554, ZEND_FRAMELESS_ICALL_3_SPEC_OBSERVER) \ - _(2555, ZEND_JMP_FRAMELESS_SPEC_CONST) \ - _(2556, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED) \ - _(2557, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST) \ - _(2558, ZEND_TYPE_ASSERT_SPEC_CONST) \ - _(2559, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ - _(2560, ZEND_RECV_NOTYPE_SPEC) \ - _(2562, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ - _(2565, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ - _(2566, ZEND_JMP_FORWARD_SPEC) \ - _(2572, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2573, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2574, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2576, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2577, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2578, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2579, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2581, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2587, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2379, ZEND_GENERATOR_RETURN_SPEC_CV) \ + _(2380, ZEND_GENERATOR_RETURN_SPEC_OBSERVER) \ + _(2381, ZEND_FAST_CALL_SPEC) \ + _(2382, ZEND_FAST_RET_SPEC) \ + _(2383, ZEND_RECV_VARIADIC_SPEC_UNUSED) \ + _(2384, ZEND_SEND_UNPACK_SPEC) \ + _(2385, ZEND_YIELD_FROM_SPEC_CONST) \ + _(2386, ZEND_YIELD_FROM_SPEC_TMP) \ + _(2389, ZEND_YIELD_FROM_SPEC_CV) \ + _(2390, ZEND_COPY_TMP_SPEC_TMPVAR_UNUSED) \ + _(2391, ZEND_BIND_GLOBAL_SPEC_CV_CONST) \ + _(2392, ZEND_COALESCE_SPEC_CONST) \ + _(2393, ZEND_COALESCE_SPEC_TMP) \ + _(2396, ZEND_COALESCE_SPEC_CV) \ + _(2397, ZEND_SPACESHIP_SPEC_CONST_CONST) \ + _(2398, ZEND_SPACESHIP_SPEC_CONST_TMP) \ + _(2401, ZEND_SPACESHIP_SPEC_CONST_CV) \ + _(2402, ZEND_SPACESHIP_SPEC_TMP_CONST) \ + _(2403, ZEND_SPACESHIP_SPEC_TMP_TMP) \ + _(2406, ZEND_SPACESHIP_SPEC_TMP_CV) \ + _(2417, ZEND_SPACESHIP_SPEC_CV_CONST) \ + _(2418, ZEND_SPACESHIP_SPEC_CV_TMP) \ + _(2421, ZEND_SPACESHIP_SPEC_CV_CV) \ + _(2422, ZEND_FUNC_NUM_ARGS_SPEC_UNUSED_UNUSED) \ + _(2423, ZEND_FUNC_GET_ARGS_SPEC_CONST_UNUSED) \ + _(2426, ZEND_FUNC_GET_ARGS_SPEC_UNUSED_UNUSED) \ + _(2428, ZEND_FETCH_STATIC_PROP_R_SPEC) \ + _(2429, ZEND_FETCH_STATIC_PROP_W_SPEC) \ + _(2430, ZEND_FETCH_STATIC_PROP_RW_SPEC) \ + _(2431, ZEND_FETCH_STATIC_PROP_IS_SPEC) \ + _(2432, ZEND_FETCH_STATIC_PROP_FUNC_ARG_SPEC) \ + _(2433, ZEND_FETCH_STATIC_PROP_UNSET_SPEC) \ + _(2434, ZEND_UNSET_STATIC_PROP_SPEC) \ + _(2435, ZEND_ISSET_ISEMPTY_STATIC_PROP_SPEC) \ + _(2436, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_CONST) \ + _(2437, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2438, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2440, ZEND_FETCH_CLASS_CONSTANT_SPEC_CONST_TMPVARCV) \ + _(2446, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_CONST) \ + _(2447, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2448, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2450, ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_TMPVARCV) \ + _(2451, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_CONST) \ + _(2452, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2453, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2455, ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUSED_TMPVARCV) \ + _(2461, ZEND_BIND_LEXICAL_SPEC_TMP_CV) \ + _(2462, ZEND_BIND_STATIC_SPEC_CV) \ + _(2463, ZEND_FETCH_THIS_SPEC_UNUSED_UNUSED) \ + _(2464, ZEND_SEND_FUNC_ARG_SPEC_VAR_CONST) \ + _(2467, ZEND_SEND_FUNC_ARG_SPEC_VAR_UNUSED) \ + _(2469, ZEND_ISSET_ISEMPTY_THIS_SPEC_UNUSED_UNUSED) \ + _(2470, ZEND_SWITCH_LONG_SPEC_CONST_CONST) \ + _(2471, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2472, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2474, ZEND_SWITCH_LONG_SPEC_TMPVARCV_CONST) \ + _(2475, ZEND_SWITCH_STRING_SPEC_CONST_CONST) \ + _(2476, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2477, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2479, ZEND_SWITCH_STRING_SPEC_TMPVARCV_CONST) \ + _(2480, ZEND_IN_ARRAY_SPEC_CONST_CONST) \ + _(2481, ZEND_IN_ARRAY_SPEC_TMP_CONST) \ + _(2484, ZEND_IN_ARRAY_SPEC_CV_CONST) \ + _(2485, ZEND_COUNT_SPEC_CONST_UNUSED) \ + _(2486, ZEND_COUNT_SPEC_TMP_UNUSED) \ + _(2489, ZEND_COUNT_SPEC_CV_UNUSED) \ + _(2490, ZEND_GET_CLASS_SPEC_CONST_UNUSED) \ + _(2491, ZEND_GET_CLASS_SPEC_TMP_UNUSED) \ + _(2493, ZEND_GET_CLASS_SPEC_UNUSED_UNUSED) \ + _(2494, ZEND_GET_CLASS_SPEC_CV_UNUSED) \ + _(2495, ZEND_GET_CALLED_CLASS_SPEC_UNUSED_UNUSED) \ + _(2496, ZEND_GET_TYPE_SPEC_CONST_UNUSED) \ + _(2497, ZEND_GET_TYPE_SPEC_TMP_UNUSED) \ + _(2500, ZEND_GET_TYPE_SPEC_CV_UNUSED) \ + _(2501, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CONST) \ + _(2502, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_TMP) \ + _(2505, ZEND_ARRAY_KEY_EXISTS_SPEC_CONST_CV) \ + _(2506, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CONST) \ + _(2507, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_TMP) \ + _(2510, ZEND_ARRAY_KEY_EXISTS_SPEC_TMP_CV) \ + _(2521, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CONST) \ + _(2522, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_TMP) \ + _(2525, ZEND_ARRAY_KEY_EXISTS_SPEC_CV_CV) \ + _(2526, ZEND_MATCH_SPEC_CONST_CONST) \ + _(2527, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2528, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2530, ZEND_MATCH_SPEC_TMPVARCV_CONST) \ + _(2531, ZEND_CASE_STRICT_SPEC_TMP_CONST) \ + _(2532, ZEND_CASE_STRICT_SPEC_TMP_TMP) \ + _(2535, ZEND_CASE_STRICT_SPEC_TMP_CV) \ + _(2536, ZEND_MATCH_ERROR_SPEC_CONST_UNUSED) \ + _(2537, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2538, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2540, ZEND_MATCH_ERROR_SPEC_TMPVARCV_UNUSED) \ + _(2541, ZEND_JMP_NULL_SPEC_CONST) \ + _(2542, ZEND_JMP_NULL_SPEC_TMP) \ + _(2545, ZEND_JMP_NULL_SPEC_CV) \ + _(2546, ZEND_CHECK_UNDEF_ARGS_SPEC_UNUSED_UNUSED) \ + _(2547, ZEND_FETCH_GLOBALS_SPEC_UNUSED_UNUSED) \ + _(2548, ZEND_VERIFY_NEVER_TYPE_SPEC_UNUSED_UNUSED) \ + _(2549, ZEND_CALLABLE_CONVERT_SPEC_UNUSED_UNUSED) \ + _(2550, ZEND_BIND_INIT_STATIC_OR_JMP_SPEC_CV) \ + _(2551, ZEND_FRAMELESS_ICALL_0_SPEC_UNUSED_UNUSED) \ + _(2552, ZEND_FRAMELESS_ICALL_0_SPEC_OBSERVER) \ + _(2553, ZEND_FRAMELESS_ICALL_1_SPEC_UNUSED) \ + _(2554, ZEND_FRAMELESS_ICALL_1_SPEC_OBSERVER) \ + _(2555, ZEND_FRAMELESS_ICALL_2_SPEC) \ + _(2556, ZEND_FRAMELESS_ICALL_2_SPEC_OBSERVER) \ + _(2557, ZEND_FRAMELESS_ICALL_3_SPEC) \ + _(2558, ZEND_FRAMELESS_ICALL_3_SPEC_OBSERVER) \ + _(2559, ZEND_JMP_FRAMELESS_SPEC_CONST) \ + _(2560, ZEND_INIT_PARENT_PROPERTY_HOOK_CALL_SPEC_CONST_UNUSED) \ + _(2561, ZEND_DECLARE_ATTRIBUTED_CONST_SPEC_CONST_CONST) \ + _(2562, ZEND_TYPE_ASSERT_SPEC_CONST) \ + _(2564, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_TMP_UNUSED) \ + _(2566, ZEND_VERIFY_GENERIC_ARGUMENTS_SPEC_UNUSED_UNUSED) \ + _(2569, ZEND_INSTALL_GENERIC_ARGS_SPEC_TMP_UNUSED) \ + _(2571, ZEND_INSTALL_GENERIC_ARGS_SPEC_UNUSED_UNUSED) \ + _(2573, ZEND_INIT_FCALL_OFFSET_SPEC_CONST) \ + _(2574, ZEND_RECV_NOTYPE_SPEC) \ + _(2576, ZEND_COUNT_ARRAY_SPEC_TMP_UNUSED) \ + _(2579, ZEND_COUNT_ARRAY_SPEC_CV_UNUSED) \ + _(2580, ZEND_JMP_FORWARD_SPEC) \ + _(2586, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2587, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2588, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2589, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2591, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2597, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2598, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2599, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2601, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2602, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ - _(2603, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2604, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2606, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2612, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2590, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2591, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2592, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2593, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2595, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2601, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2602, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2603, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2605, ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2611, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2612, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2613, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2614, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2616, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2622, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2623, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2624, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2626, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2627, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2628, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2629, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2631, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2637, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2615, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2616, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2617, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2618, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2620, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2626, ZEND_ADD_LONG_SPEC_TMPVARCV_CONST) \ + _(2627, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2628, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2630, ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2636, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2637, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2638, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2639, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2641, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2643, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2644, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2646, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ - _(2647, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2648, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2649, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2651, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2652, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2653, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2654, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2656, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2662, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2640, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2641, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2642, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2643, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2645, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2651, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2652, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2653, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2655, ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2657, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2658, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2660, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV) \ + _(2661, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2662, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2663, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2664, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2666, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2668, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2669, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2671, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ - _(2672, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2673, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2674, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2676, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2677, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ - _(2678, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2679, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2681, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2687, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2665, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2666, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2667, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2668, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2670, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2676, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2677, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2678, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2680, ZEND_SUB_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2682, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2683, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2685, ZEND_SUB_LONG_SPEC_CONST_TMPVARCV) \ + _(2686, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2687, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2688, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2689, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2691, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2693, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2694, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2696, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(2697, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2698, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2699, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2701, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2702, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2703, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2704, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2706, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2712, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2690, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2691, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2692, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2693, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2695, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2701, ZEND_SUB_LONG_SPEC_TMPVARCV_CONST) \ + _(2702, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2703, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2705, ZEND_SUB_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2707, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2708, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2710, ZEND_SUB_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(2711, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2712, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2713, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2714, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2716, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2722, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2723, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2724, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2726, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2727, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ - _(2728, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2729, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2731, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2737, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2715, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2716, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2717, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2718, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2720, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2726, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2727, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2728, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2730, ZEND_SUB_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2736, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2737, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ _(2738, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2739, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2741, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ - _(2747, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2748, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2749, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2751, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2752, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ - _(2753, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2754, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2756, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2762, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2740, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2741, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2742, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2743, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2745, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2751, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_CONST) \ + _(2752, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2753, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2755, ZEND_MUL_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV) \ + _(2761, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2762, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ _(2763, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2764, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2766, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2772, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2773, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2774, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2776, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2777, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2778, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2779, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2781, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2787, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2765, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2766, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2767, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2768, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2770, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2776, ZEND_MUL_LONG_SPEC_TMPVARCV_CONST) \ + _(2777, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2778, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2780, ZEND_MUL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2786, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2787, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ _(2788, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2789, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2791, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2807, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2808, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2809, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2810, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2811, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2812, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2813, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2814, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2815, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2819, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2820, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2830, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2852, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2853, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2854, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2855, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2856, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2857, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2858, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2859, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2860, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2864, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2865, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2882, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2883, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2884, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2885, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2886, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2887, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2888, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2889, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2890, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2894, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2895, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2905, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2927, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(2928, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2929, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2930, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2931, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2932, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2933, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2934, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2935, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2939, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(2940, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2957, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2958, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2959, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2960, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2961, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2962, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2963, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2964, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2965, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2969, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2970, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2980, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3002, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3003, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3004, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3005, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3006, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3007, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3008, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3009, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3010, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3014, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3015, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3032, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3033, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3034, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3035, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3036, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3037, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3038, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3039, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3040, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3044, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3045, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3055, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3077, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3078, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3079, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3080, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3081, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3082, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3083, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3084, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3085, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3089, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3090, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3092, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3093, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3094, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3095, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ - _(3096, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3097, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3098, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3102, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3103, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ - _(3107, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ - _(3111, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3112, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3113, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3114, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3115, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3116, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3120, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ - _(3121, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3122, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3123, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3124, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3125, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3126, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3127, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3128, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3129, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3130, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3131, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3135, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3136, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3146, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3168, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ - _(3169, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3170, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3171, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3172, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3173, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3174, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3175, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3176, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3180, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3181, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3186, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3187, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3188, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3189, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3190, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3191, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3195, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3196, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3197, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3198, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3199, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3206, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3221, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3243, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3244, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3245, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3246, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3247, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3248, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3249, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3250, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3251, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3255, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3256, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3261, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3262, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3263, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3264, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3265, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3266, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3270, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ - _(3271, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3272, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3273, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3274, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3281, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3296, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3318, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ - _(3319, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3320, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3321, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3322, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3323, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3324, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3325, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3326, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3330, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ - _(3331, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3336, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3337, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3338, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3339, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3340, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3341, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3345, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ - _(3346, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ - _(3347, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ - _(3348, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3349, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3356, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3371, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3393, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ - _(3394, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ - _(3395, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ - _(3396, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3397, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3398, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3399, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3400, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3401, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3405, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ - _(3406, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ - _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ - _(3408, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3409, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3410, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3411, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ - _(3412, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ - _(3413, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ - _(3414, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ - _(3415, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ - _(3416, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3417, ZEND_POST_INC_LONG_SPEC_CV) \ - _(3418, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ - _(3419, ZEND_POST_DEC_LONG_SPEC_CV) \ - _(3420, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ - _(3421, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3422, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3424, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ - _(3425, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ - _(3426, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3427, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3429, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ - _(3430, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ - _(3431, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3432, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3434, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ - _(3436, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3437, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3439, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ - _(3440, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ - _(3441, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3442, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3444, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3445, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ - _(3446, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3447, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3449, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ - _(3455, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ - _(3456, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3457, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3459, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ - _(3462, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ - _(3464, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ - _(3467, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ - _(3469, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ - _(3470, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ - _(3471, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ - _(3472, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ - _(3473, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ - _(3473+1, ZEND_NULL) + _(2790, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2791, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2792, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2793, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2795, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2801, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2802, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2803, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2805, ZEND_MUL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2821, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2822, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2823, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2824, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2825, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2826, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2827, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2828, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2829, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2833, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2834, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2835, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2836, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2837, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2838, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2839, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2840, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2841, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2842, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2843, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2844, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2848, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2849, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2850, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2866, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2867, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2868, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2869, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2870, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2871, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2872, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2873, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2874, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2878, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2879, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2880, ZEND_IS_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2896, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2897, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2898, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2899, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2900, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2901, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2902, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2903, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2904, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2908, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2909, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2910, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2911, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2912, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2913, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2914, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2915, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2916, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2917, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2918, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2919, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2923, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2924, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2925, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2941, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(2942, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2943, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2944, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2945, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2946, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2947, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2948, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2949, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2953, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(2954, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2955, ZEND_IS_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2971, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2972, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2973, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2974, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2975, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2976, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2977, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2978, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2979, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2983, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2984, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2985, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2986, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(2987, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(2988, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(2989, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2990, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2991, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2992, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2993, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(2994, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(2998, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(2999, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3000, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3016, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3017, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3018, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3019, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3020, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3021, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3022, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3023, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3024, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3028, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3029, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3030, ZEND_IS_NOT_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3046, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3047, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3048, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3049, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3050, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3051, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3052, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3053, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3054, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3058, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3059, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3060, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3061, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3062, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3063, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3064, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3065, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3066, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3067, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3068, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3069, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3073, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3074, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3075, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3091, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3092, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3093, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3094, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3095, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3096, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3097, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3098, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3099, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3103, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3104, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3105, ZEND_IS_NOT_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3106, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3107, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3108, ZEND_IS_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3109, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST) \ + _(3110, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3111, ZEND_IS_NOT_IDENTICAL_EMPTY_ARRAY_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3112, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3116, ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3117, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CONST) \ + _(3121, ZEND_IS_NOT_IDENTICAL_NOTHROW_SPEC_CV_CV) \ + _(3125, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3126, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3127, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3128, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3129, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3130, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3134, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV) \ + _(3135, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3136, ZEND_IS_SMALLER_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3137, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3138, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3139, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3140, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3141, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3142, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3143, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3144, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3145, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3149, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3150, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3151, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3152, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3153, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3154, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3155, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3156, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3157, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3158, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3159, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3160, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3164, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3165, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3166, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3182, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST) \ + _(3183, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3184, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3185, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3186, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3187, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3188, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3189, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3190, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3194, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3195, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3196, ZEND_IS_SMALLER_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3200, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3201, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3202, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3203, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3204, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3205, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3209, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3210, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3211, ZEND_IS_SMALLER_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3212, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3213, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3214, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3215, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3216, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3217, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3218, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3219, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3220, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3224, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3225, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3226, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3227, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3228, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3229, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3230, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3231, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3232, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3233, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3234, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3235, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3239, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3240, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3241, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3257, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3258, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3259, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3260, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3261, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3262, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3263, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3264, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3265, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3269, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3270, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3271, ZEND_IS_SMALLER_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3275, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3276, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3277, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3278, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3279, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3280, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3284, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV) \ + _(3285, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3286, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3287, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3288, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3289, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3290, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3291, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3292, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3293, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3294, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3295, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3299, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3300, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3301, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3302, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3303, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3304, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3305, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3306, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3307, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3308, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3309, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3310, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3314, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3315, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3316, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3332, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST) \ + _(3333, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3334, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3335, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3336, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3337, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3338, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3339, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3340, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3344, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV) \ + _(3345, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3346, ZEND_IS_SMALLER_OR_EQUAL_LONG_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3350, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3351, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3352, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3353, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3354, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3355, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3359, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV) \ + _(3360, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPZ) \ + _(3361, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_CONST_TMPVARCV_JMPNZ) \ + _(3362, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3363, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3364, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3365, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3366, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3367, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3368, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3369, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3370, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3374, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3375, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3376, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3377, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3378, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3379, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3380, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3381, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3382, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3383, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3384, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3385, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3389, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3390, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3391, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3407, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST) \ + _(3408, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPZ) \ + _(3409, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_CONST_JMPNZ) \ + _(3410, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3411, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3412, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3413, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3414, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3415, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3419, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV) \ + _(3420, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPZ) \ + _(3421, ZEND_IS_SMALLER_OR_EQUAL_DOUBLE_SPEC_TMPVARCV_TMPVARCV_JMPNZ) \ + _(3422, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3423, ZEND_PRE_INC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3424, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3425, ZEND_PRE_INC_LONG_SPEC_CV_RETVAL_USED) \ + _(3426, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_UNUSED) \ + _(3427, ZEND_PRE_DEC_LONG_NO_OVERFLOW_SPEC_CV_RETVAL_USED) \ + _(3428, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_UNUSED) \ + _(3429, ZEND_PRE_DEC_LONG_SPEC_CV_RETVAL_USED) \ + _(3430, ZEND_POST_INC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3431, ZEND_POST_INC_LONG_SPEC_CV) \ + _(3432, ZEND_POST_DEC_LONG_NO_OVERFLOW_SPEC_CV) \ + _(3433, ZEND_POST_DEC_LONG_SPEC_CV) \ + _(3434, ZEND_QM_ASSIGN_LONG_SPEC_CONST) \ + _(3435, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3436, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3438, ZEND_QM_ASSIGN_LONG_SPEC_TMPVARCV) \ + _(3439, ZEND_QM_ASSIGN_DOUBLE_SPEC_CONST) \ + _(3440, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3441, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3443, ZEND_QM_ASSIGN_DOUBLE_SPEC_TMPVARCV) \ + _(3444, ZEND_QM_ASSIGN_NOREF_SPEC_CONST) \ + _(3445, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3446, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3448, ZEND_QM_ASSIGN_NOREF_SPEC_TMPVARCV) \ + _(3450, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3451, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3453, ZEND_FETCH_DIM_R_INDEX_SPEC_CONST_TMPVARCV) \ + _(3454, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3455, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3456, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3458, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3459, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_CONST) \ + _(3460, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3461, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3463, ZEND_FETCH_DIM_R_INDEX_SPEC_TMPVAR_TMPVARCV) \ + _(3469, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_CONST) \ + _(3470, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3471, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3473, ZEND_FETCH_DIM_R_INDEX_SPEC_CV_TMPVARCV) \ + _(3476, ZEND_SEND_VAR_SIMPLE_SPEC_VAR) \ + _(3478, ZEND_SEND_VAR_SIMPLE_SPEC_CV) \ + _(3481, ZEND_SEND_VAR_EX_SIMPLE_SPEC_VAR_UNUSED) \ + _(3483, ZEND_SEND_VAR_EX_SIMPLE_SPEC_CV_UNUSED) \ + _(3484, ZEND_SEND_VAL_SIMPLE_SPEC_CONST) \ + _(3485, ZEND_SEND_VAL_EX_SIMPLE_SPEC_CONST) \ + _(3486, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_UNUSED) \ + _(3487, ZEND_FE_FETCH_R_SIMPLE_SPEC_VAR_CV_RETVAL_USED) \ + _(3487+1, ZEND_NULL) diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c index 0ece3e6f0c66..84e541d4de06 100644 --- a/Zend/zend_vm_opcodes.c +++ b/Zend/zend_vm_opcodes.c @@ -21,7 +21,7 @@ #include #include -static const char *zend_vm_opcodes_names[212] = { +static const char *zend_vm_opcodes_names[214] = { "ZEND_NOP", "ZEND_ADD", "ZEND_SUB", @@ -234,9 +234,11 @@ static const char *zend_vm_opcodes_names[212] = { "ZEND_INIT_PARENT_PROPERTY_HOOK_CALL", "ZEND_DECLARE_ATTRIBUTED_CONST", "ZEND_TYPE_ASSERT", + "ZEND_VERIFY_GENERIC_ARGUMENTS", + "ZEND_INSTALL_GENERIC_ARGS", }; -static uint32_t zend_vm_opcodes_flags[212] = { +static uint32_t zend_vm_opcodes_flags[214] = { 0x00000000, 0x00000b0b, 0x00000b0b, @@ -449,6 +451,8 @@ static uint32_t zend_vm_opcodes_flags[212] = { 0x01001103, 0x00000303, 0x01000003, + 0x00000101, + 0x00000101, }; ZEND_API const char* ZEND_FASTCALL zend_get_opcode_name(uint8_t opcode) { diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h index 92b46e6628f3..1c4b487196d2 100644 --- a/Zend/zend_vm_opcodes.h +++ b/Zend/zend_vm_opcodes.h @@ -331,7 +331,9 @@ END_EXTERN_C() #define ZEND_INIT_PARENT_PROPERTY_HOOK_CALL 209 #define ZEND_DECLARE_ATTRIBUTED_CONST 210 #define ZEND_TYPE_ASSERT 211 +#define ZEND_VERIFY_GENERIC_ARGUMENTS 212 +#define ZEND_INSTALL_GENERIC_ARGS 213 -#define ZEND_VM_LAST_OPCODE 211 +#define ZEND_VM_LAST_OPCODE 213 #endif diff --git a/bench/bench_reification.php b/bench/bench_reification.php new file mode 100644 index 000000000000..937730796f22 --- /dev/null +++ b/bench/bench_reification.php @@ -0,0 +1,1044 @@ + 0 ? $a_ns / $b_ns : 0; + printf(" %-55s %+9.1f ns/op delta (%.2fx %s vs %s)\n", + $label, $delta, $ratio, $a_name, $b_name); +} + +// -------------------------------------------------------------------------- +// Scenario 1: non-generic baseline — instantiation and method dispatch +// -------------------------------------------------------------------------- +class PlainFoo { public int $x = 0; } +class PlainBox { + public function __construct(public mixed $value = null) {} + public function tick(int $x): int { return $x + 1; } +} + +function plain_new(int $n): void { + for ($i = 0; $i < $n; $i++) { + new PlainFoo(); + } +} +function plain_method(int $n): void { + $b = new PlainBox(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} + +// -------------------------------------------------------------------------- +// Scenario 2: generic-class instantiation +// -------------------------------------------------------------------------- +class GenBox { + public function __construct(public mixed $value = null) {} + public function tick(int $x): int { return $x + 1; } +} +class IntBox extends GenBox {} + +function gen_new_turbofish(int $n): void { + for ($i = 0; $i < $n; $i++) { + new GenBox::(); + } +} +function gen_new_default(int $n): void { + for ($i = 0; $i < $n; $i++) { + new GenBox(); + } +} +function gen_new_extends(int $n): void { + for ($i = 0; $i < $n; $i++) { + new IntBox(); + } +} + +// -------------------------------------------------------------------------- +// Scenario 3: method dispatch on generic-class instance +// -------------------------------------------------------------------------- +function gen_method_on_mono(int $n): void { + $b = new GenBox::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} +function gen_method_on_default(int $n): void { + $b = new GenBox(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { + $acc = $b->tick($acc); + } +} + +// -------------------------------------------------------------------------- +// Scenario 4: T-keyed expressions inside the body +// -------------------------------------------------------------------------- +class Marker {} +class TBox { + public function makeT(): object { return new T(); } + public function classT(): string { return T::class; } + public function isT(object $x): bool { return $x instanceof T; } +} +function tbox_makeT(int $n): void { + $b = new TBox::(); + for ($i = 0; $i < $n; $i++) { + $b->makeT(); + } +} +function tbox_classT(int $n): void { + $b = new TBox::(); + for ($i = 0; $i < $n; $i++) { + $b->classT(); + } +} +function tbox_isT(int $n): void { + $b = new TBox::(); + $m = new Marker(); + for ($i = 0; $i < $n; $i++) { + $b->isT($m); + } +} +function plain_makeT(int $n): void { + for ($i = 0; $i < $n; $i++) { + new Marker(); + } +} +function plain_classT(int $n): void { + for ($i = 0; $i < $n; $i++) { + $_ = Marker::class; + } +} +function plain_isT(int $n): void { + $m = new Marker(); + for ($i = 0; $i < $n; $i++) { + $_ = $m instanceof Marker; + } +} + +// -------------------------------------------------------------------------- +// Scenario 5: inference vs turbofish vs non-generic +// -------------------------------------------------------------------------- +class Foo { public int $tag = 0; } +function makeGen(T $x): T { return $x; } +function makeNonGen(Foo $x): Foo { return $x; } + +function infer_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeGen($f); + } +} +function turbofish_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeGen::($f); + } +} +function plain_call(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeNonGen($f); + } +} + +// -------------------------------------------------------------------------- +// Scenario 6: multi-parameter inference +// -------------------------------------------------------------------------- +class Bar { public int $tag = 0; } +function makeTwoGen(T $x, U $y): T { return $x; } +function makeTwoNonGen(Foo $x, Bar $y): Foo { return $x; } + +function infer_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoGen($f, $b); + } +} +function turbofish_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoGen::($f, $b); + } +} +function plain_two(int $n): void { + $f = new Foo(); + $b = new Bar(); + for ($i = 0; $i < $n; $i++) { + makeTwoNonGen($f, $b); + } +} + +// -------------------------------------------------------------------------- +// Scenario 7: inference + bound conformance check +// -------------------------------------------------------------------------- +function makeBounded(T $x): T { return $x; } + +function infer_bounded(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeBounded($f); + } +} +function turbofish_bounded(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + makeBounded::($f); + } +} + +// -------------------------------------------------------------------------- +// Scenario 8: reified arg-type coercion (callee has T $x and turbofish) +// -------------------------------------------------------------------------- +function takesT(T $x): T { return $x; } +function takesTBounded(T $x): T { return $x; } +function takesPlain(Foo $x): Foo { return $x; } + +function reified_arg_check(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + takesTBounded::($f); + } +} +function plain_arg_check(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + takesPlain($f); + } +} + +// -------------------------------------------------------------------------- +// Scenario 9: forwarded T-refs — `id::($x)` inside a generic wrapper. +// Exercises (a) per-call zend_type_arg_table build via the borrowed-pointer +// chain, (b) zend_check_user_type_slow on the substituted type, and (c) the +// VERIFY path that INSTALL can't elide (T-refs in turbofish). +// -------------------------------------------------------------------------- +function idGen(T $x): T { return $x; } +function nestedGen(U $x): U { return idGen::($x); } +interface Fooable {} +interface Barable {} +class FooBar implements Fooable, Barable {} + +function forwarded_scalar(int $n): void { + for ($i = 0; $i < $n; $i++) { + nestedGen::($i); + } +} +function direct_scalar(int $n): void { + for ($i = 0; $i < $n; $i++) { + idGen::($i); + } +} +function forwarded_class(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + nestedGen::($f); + } +} +function direct_class(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { + idGen::($f); + } +} +function forwarded_union(int $n): void { + for ($i = 0; $i < $n; $i++) { + nestedGen::($i); + } +} +function forwarded_intersection(int $n): void { + $fb = new FooBar(); + for ($i = 0; $i < $n; $i++) { + nestedGen::($fb); + } +} + +// -------------------------------------------------------------------------- +// Scenario 10: generic instanceof/catch in a hot inner loop — exercises the +// polymorphic inline cache on the IS_UNUSED FETCH_CLASS path. The first +// iteration misses; subsequent iterations within the same frame hit on a +// pointer compare against (type_args generation, called_scope). +// -------------------------------------------------------------------------- +class Container {} + +function instanceof_t_hot_loop(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof Container) { + $count++; + } + } + return $count; +} +function instanceof_concrete_hot_loop(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof Container) { + $count++; + } + } + return $count; +} + +function bench_instanceof_t(int $n): void { + $items = [new Container::(), new Container::(), new Foo()]; + /* outer N drives the inner-loop count via a fixed batch — keeps the + * PIC slot warm across iterations of the body. */ + instanceof_t_hot_loop::(array_fill(0, $n, $items[0])); +} +function bench_instanceof_concrete(int $n): void { + $items = [new Container::(), new Container::(), new Foo()]; + instanceof_concrete_hot_loop(array_fill(0, $n, $items[0])); +} + +// -------------------------------------------------------------------------- +// Driver +// -------------------------------------------------------------------------- +echo "PHP ", PHP_VERSION, " (", PHP_BINARY, ")\n"; +echo "Reified bound-erased generics benchmark\n"; + +section("1. Non-generic baseline"); +$plain_new_ns = bench("plain new Foo()", 'plain_new'); +$plain_method_ns = bench("plain \$box->tick()", 'plain_method'); + +section("2. Generic-class instantiation"); +$gen_tf_ns = bench("new GenBox::()", 'gen_new_turbofish'); +$gen_def_ns = bench("new GenBox() (default T=int)", 'gen_new_default'); +$gen_ext_ns = bench("new IntBox() (extends GenBox)", 'gen_new_extends'); +compare("delta: GenBox vs PlainFoo", $gen_tf_ns, "gen", $plain_new_ns, "plain"); +compare("delta: GenBox (default) vs PlainFoo", $gen_def_ns, "gen", $plain_new_ns, "plain"); +compare("delta: IntBox extends vs PlainFoo", $gen_ext_ns, "gen", $plain_new_ns, "plain"); + +section("3. Method dispatch on generic-class receiver"); +$gen_mono_method_ns = bench("GenBox->tick()", 'gen_method_on_mono'); +$gen_default_method_ns = bench("GenBox(default)->tick()", 'gen_method_on_default'); +compare("delta: GenBox->tick vs PlainBox->tick", $gen_mono_method_ns, "gen", $plain_method_ns, "plain"); + +section("4. T-keyed body expressions"); +$tbox_makeT_ns = bench("TBox->makeT()", 'tbox_makeT'); +$plain_makeT_ns = bench("new Marker() direct", 'plain_makeT'); +compare("delta: new T() vs new Marker()", $tbox_makeT_ns, "T", $plain_makeT_ns, "direct"); +$tbox_classT_ns = bench("TBox->classT()", 'tbox_classT'); +$plain_classT_ns = bench("Marker::class direct", 'plain_classT'); +compare("delta: T::class vs Marker::class", $tbox_classT_ns, "T", $plain_classT_ns, "direct"); +$tbox_isT_ns = bench("TBox->isT()", 'tbox_isT'); +$plain_isT_ns = bench("\$x instanceof Marker direct", 'plain_isT'); +compare("delta: instanceof T vs instanceof Marker", $tbox_isT_ns, "T", $plain_isT_ns, "direct"); + +section("5. Inference vs turbofish vs non-generic call"); +$infer_ns = bench("makeGen(\$x) — inference", 'infer_call'); +$turbo_ns = bench("makeGen::(\$x) — turbofish", 'turbofish_call'); +$plain_fn_ns = bench("makeNonGen(\$x) — no generics", 'plain_call'); +compare("delta: inference vs plain", $infer_ns, "infer", $plain_fn_ns, "plain"); +compare("delta: turbofish vs plain", $turbo_ns, "turbo", $plain_fn_ns, "plain"); +compare("delta: inference vs turbofish", $infer_ns, "infer", $turbo_ns, "turbo"); + +section("6. Multi-parameter inference"); +$infer2_ns = bench("makeTwoGen(\$f,\$b) — infer T,U", 'infer_two'); +$turbo2_ns = bench("makeTwoGen:: — turbofish", 'turbofish_two'); +$plain2_ns = bench("makeTwoNonGen(\$f,\$b)", 'plain_two'); +compare("delta: infer T,U vs plain", $infer2_ns, "infer2", $plain2_ns, "plain"); +compare("delta: turbofish T,U vs plain", $turbo2_ns, "turbo2", $plain2_ns, "plain"); + +section("7. Inference + bound check"); +$infer_bd_ns = bench("makeBounded(\$x) — infer + bound", 'infer_bounded'); +$turbo_bd_ns = bench("makeBounded::(\$x) — turbo+bd", 'turbofish_bounded'); +compare("delta: infer+bound vs infer (no bound)", $infer_bd_ns, "ibnd", $infer_ns, "infer"); +compare("delta: turbo+bound vs turbo (no bound)", $turbo_bd_ns, "tbnd", $turbo_ns, "turbo"); + +section("8. Reified arg-type coercion"); +$reified_arg_ns = bench("takesTBounded::(\$f)", 'reified_arg_check'); +$plain_arg_ns = bench("takesPlain(\$f)", 'plain_arg_check'); +compare("delta: reified arg check vs plain typed arg", $reified_arg_ns, "reif", $plain_arg_ns, "plain"); + +section("9. Forwarded T-refs (nested generic calls)"); +$fwd_scalar_ns = bench("nestedGen::(\$i) — forwarded scalar", 'forwarded_scalar'); +$dir_scalar_ns = bench("idGen::(\$i) — direct scalar", 'direct_scalar'); +compare("delta: forwarded scalar vs direct scalar", $fwd_scalar_ns, "fwd", $dir_scalar_ns, "dir"); +$fwd_class_ns = bench("nestedGen::(\$f) — forwarded class", 'forwarded_class'); +$dir_class_ns = bench("idGen::(\$f) — direct class", 'direct_class'); +compare("delta: forwarded class vs direct class", $fwd_class_ns, "fwd", $dir_class_ns, "dir"); +$fwd_union_ns = bench("nestedGen:: — forwarded union", 'forwarded_union', ITERS_LIGHT); +$fwd_inter_ns = bench("nestedGen:: — forwarded ix", 'forwarded_intersection', ITERS_LIGHT); +compare("delta: forwarded union vs forwarded scalar", $fwd_union_ns, "uni", $fwd_scalar_ns, "scl"); + +section("10. Generic instanceof in hot inner loop (PIC)"); +$inst_t_ns = bench("instanceof Container (PIC warm)", 'bench_instanceof_t'); +$inst_concrete_ns = bench("instanceof Container (cache slot)", 'bench_instanceof_concrete'); +compare("delta: instanceof T vs instanceof bare", $inst_t_ns, "T", $inst_concrete_ns, "bare"); + +// -------------------------------------------------------------------------- +// Scenario 11: depth scaling for nested monomorph instantiation. +// All monomorphs are synthesized + cached on first use; the hot loop is +// supposed to hit the class-table lookup at pointer-compare speed regardless +// of nesting depth. If the cost grows with depth in the hot path, something +// is being recomputed per call (canonical-name string build, arg-table +// rebuild, etc.). +// -------------------------------------------------------------------------- +class L1 {} +class L2 {} +class L3 {} +class L4 {} + +function deep_new_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::(); } +} +function deep_new_d2(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>(); } +} +function deep_new_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>>(); } +} +function deep_new_d4(int $n): void { + for ($i = 0; $i < $n; $i++) { new L1::>>>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 12: composite type arguments (union, intersection, DNF). +// Canonicalization sorts the alternatives, so equivalent shapes converge on +// the same monomorph name. +// -------------------------------------------------------------------------- +interface Ifc1 {} +interface Ifc2 {} +class Holder1 {} + +function comp_concrete(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_union(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_intersection(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::(); } +} +function comp_dnf(int $n): void { + for ($i = 0; $i < $n; $i++) { new Holder1::<(Ifc1&Ifc2)|int>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 13: bounded inner parameter — the `A>` pattern where +// the inner C declares its own bound, so synthesizing the outer monomorph +// triggers a bound-conformance check on the inner application. +// -------------------------------------------------------------------------- +class FBound {} +class FSub extends FBound {} +class WithBound {} +class WithoutBound {} + +function nested_bounded_only(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithBound::(); } +} +function nested_outer_around_bounded(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithoutBound::>(); } +} +function nested_intersection_with_bounded(int $n): void { + for ($i = 0; $i < $n; $i++) { new WithoutBound::>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 14: instanceof on deeply-nested monomorphs. The polymorphic +// inline cache should make depth irrelevant after warmup — the slot just +// caches the resolved ce. +// -------------------------------------------------------------------------- +function deep_instanceof_d1(int $n): void { + $box = new L1::(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1; } +} +function deep_instanceof_d3(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1>>; } +} +function deep_instanceof_d3_mismatch(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1>>; } +} +function deep_instanceof_d3_bare(int $n): void { + $box = new L1::>>(); + for ($i = 0; $i < $n; $i++) { $_ = $box instanceof L1; } +} + +section("11. Depth scaling — nested monomorph `new`"); +$d1_ns = bench("new L1 (depth 1)", 'deep_new_d1'); +$d2_ns = bench("new L1> (depth 2)", 'deep_new_d2'); +$d3_ns = bench("new L1>> (depth 3)", 'deep_new_d3'); +$d4_ns = bench("new L1>>> (depth 4)", 'deep_new_d4'); +compare("delta: depth 2 vs depth 1", $d2_ns, "d2", $d1_ns, "d1"); +compare("delta: depth 3 vs depth 1", $d3_ns, "d3", $d1_ns, "d1"); +compare("delta: depth 4 vs depth 1", $d4_ns, "d4", $d1_ns, "d1"); + +section("12. Composite type-argument shapes"); +$cc_ns = bench("new Holder1", 'comp_concrete'); +$cu_ns = bench("new Holder1", 'comp_union'); +$ci_ns = bench("new Holder1", 'comp_intersection'); +$cd_ns = bench("new Holder1<(Ifc1&Ifc2)|int>", 'comp_dnf'); +compare("delta: union vs concrete", $cu_ns, "uni", $cc_ns, "cnc"); +compare("delta: intersection vs concrete", $ci_ns, "ix", $cc_ns, "cnc"); +compare("delta: DNF vs concrete", $cd_ns, "dnf", $cc_ns, "cnc"); + +section("13. Bounded inner parameter (`A>` pattern)"); +$nb_ns = bench("new WithBound (just the inner)", 'nested_bounded_only'); +$nob_ns = bench("new WithoutBound>", 'nested_outer_around_bounded'); +$nix_ns = bench("new WithoutBound>", 'nested_intersection_with_bounded'); +compare("delta: outer wraps bounded vs inner alone", $nob_ns, "out", $nb_ns, "inner"); +compare("delta: intersection w/bound vs outer alone", $nix_ns, "ix", $nob_ns, "out"); + +section("14. Deep `instanceof` (PIC vs depth)"); +$di1_ns = bench("instanceof L1 (depth 1)", 'deep_instanceof_d1'); +$di3_ns = bench("instanceof L1>> (depth 3, match)", 'deep_instanceof_d3'); +$dim_ns = bench("instanceof L1>> (depth 3, mismatch)", 'deep_instanceof_d3_mismatch'); +$dib_ns = bench("instanceof L1 (bare, hits ancestor)", 'deep_instanceof_d3_bare'); +compare("delta: depth 3 match vs depth 1 match", $di3_ns, "d3", $di1_ns, "d1"); +compare("delta: depth 3 mismatch vs depth 3 match", $dim_ns, "mis", $di3_ns, "mat"); +compare("delta: bare ancestor vs depth 3 match", $dib_ns, "bare", $di3_ns, "d3"); + +// -------------------------------------------------------------------------- +// Scenario 15: deep parameter type check at RECV. The arg_info type is now +// the canonical monomorph (`Box>>`), so RECV's instanceof_function +// resolves and enforces the full identity — passing the wrong monomorph +// rejects, not silently accepts. After warmup the class-lookup cache makes +// the check the same speed as a non-generic typed param. +// -------------------------------------------------------------------------- +class DPlain {} +class DBox {} + +function takes_plain(DPlain $x): void {} +function takes_d1(DBox $x): void {} +function takes_d2(DBox> $x): void {} +function takes_d3(DBox>> $x): void {} +function takes_d4(DBox>>> $x): void {} + +function bench_param_plain(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { takes_plain($v); } +} +function bench_param_d1(int $n): void { + $v = new DBox::(); + for ($i = 0; $i < $n; $i++) { takes_d1($v); } +} +function bench_param_d2(int $n): void { + $v = new DBox::>(); + for ($i = 0; $i < $n; $i++) { takes_d2($v); } +} +function bench_param_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { takes_d3($v); } +} +function bench_param_d4(int $n): void { + $v = new DBox::>>>(); + for ($i = 0; $i < $n; $i++) { takes_d4($v); } +} + +section("15. Deep parameter type check (RECV reifies the full monomorph)"); +$p_pl_ns = bench("f(DPlain \$x) (non-generic baseline)", 'bench_param_plain'); +$p_d1_ns = bench("f(DBox \$x) (depth 1)", 'bench_param_d1'); +$p_d2_ns = bench("f(DBox> \$x) (depth 2)", 'bench_param_d2'); +$p_d3_ns = bench("f(DBox>> \$x) (depth 3)", 'bench_param_d3'); +$p_d4_ns = bench("f(DBox>>> \$x) (depth 4)", 'bench_param_d4'); +compare("delta: deep d1 vs plain", $p_d1_ns, "d1", $p_pl_ns, "plain"); +compare("delta: deep d4 vs plain", $p_d4_ns, "d4", $p_pl_ns, "plain"); +compare("delta: deep d4 vs deep d1", $p_d4_ns, "d4", $p_d1_ns, "d1"); + +// -------------------------------------------------------------------------- +// Scenario 16: inference from a deep-typed value. The compiler can't +// destructure `Box` into a T-binding at the call site (only bare T +// params are inferable), but `id(T $x)` does pick up the value's full +// runtime monomorph as T. Compare to explicit turbofish with the same deep +// type, and to the equivalent non-generic call. +// -------------------------------------------------------------------------- +function id_gen(T $x): T { return $x; } + +function bench_infer_d1(int $n): void { + $v = new DBox::(); + for ($i = 0; $i < $n; $i++) { id_gen($v); } +} +function bench_infer_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { id_gen($v); } +} +function bench_turbofish_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { id_gen::>>>($v); } +} +function bench_plain_pass(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { takes_plain($v); } +} + +section("16. Inference / turbofish with deep value"); +$inf1_ns = bench("id(DBox) — inferred", 'bench_infer_d1'); +$inf3_ns = bench("id(DBox>>) — inferred", 'bench_infer_d3'); +$tf3_ns = bench("id::>>>(\$v) — turbofish", 'bench_turbofish_d3'); +$plp_ns = bench("takes_plain(\$plain) — non-generic ref", 'bench_plain_pass'); +compare("delta: inferred d1 vs plain", $inf1_ns, "infD1", $plp_ns, "plain"); +compare("delta: inferred d3 vs plain", $inf3_ns, "infD3", $plp_ns, "plain"); +compare("delta: turbofish d3 vs inferred d3", $tf3_ns, "tf", $inf3_ns, "inf"); + +// -------------------------------------------------------------------------- +// Scenario 17: method dispatch on a deep generic receiver. The receiver's +// class is the monomorph (e.g., DReceiver>>); the method lookup +// goes through the monomorph's function table. Hot loops with the same +// receiver class should pay zero depth penalty after warmup. +// -------------------------------------------------------------------------- +class DReceiver { + public function tick(int $x): int { return $x + 1; } +} +class DReceiverPlain { + public function tick(int $x): int { return $x + 1; } +} + +function bench_recv_plain(int $n): void { + $r = new DReceiverPlain(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} +function bench_recv_d1(int $n): void { + $r = new DReceiver::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} +function bench_recv_d3(int $n): void { + $r = new DReceiver::>>(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->tick($acc); } +} + +section("17. Method dispatch on a deep generic receiver"); +$mr_pl_ns = bench("DReceiverPlain->tick() (non-generic)", 'bench_recv_plain'); +$mr_d1_ns = bench("DReceiver->tick() (depth 1)", 'bench_recv_d1'); +$mr_d3_ns = bench("DReceiver>>->tick() (depth 3)", 'bench_recv_d3'); +compare("delta: receiver d1 vs plain", $mr_d1_ns, "d1", $mr_pl_ns, "plain"); +compare("delta: receiver d3 vs d1", $mr_d3_ns, "d3", $mr_d1_ns, "d1"); + +// -------------------------------------------------------------------------- +// Scenario 18: pass-through with deep param + deep return. Two RECV checks +// per call (param + return), each resolving a deep monomorph class. +// -------------------------------------------------------------------------- +function passthrough_plain(DPlain $x): DPlain { return $x; } +function passthrough_d3(DBox>> $x): DBox>> { return $x; } + +function bench_passthrough_plain(int $n): void { + $v = new DPlain(); + for ($i = 0; $i < $n; $i++) { passthrough_plain($v); } +} +function bench_passthrough_d3(int $n): void { + $v = new DBox::>>(); + for ($i = 0; $i < $n; $i++) { passthrough_d3($v); } +} + +section("18. Pass-through (param + return both deep)"); +$pt_pl_ns = bench("DPlain f(DPlain \$x): DPlain", 'bench_passthrough_plain'); +$pt_d3_ns = bench("DBox>> f(DBox>> \$x): same", 'bench_passthrough_d3'); +compare("delta: deep passthrough vs plain passthrough", $pt_d3_ns, "d3", $pt_pl_ns, "plain"); + +// ============================================================================ +// PATHOLOGICAL — looking for super-linear blowup. +// Each scenario varies one structural dimension while holding the others +// constant. If the cost grows worse than linearly in that dimension, the +// implementation has a problem worth flagging. +// ============================================================================ + +// -------------------------------------------------------------------------- +// Scenario 19: canonical-name traversal depth. Building "Box>>" +// touches every layer of the tree. Linear depth → linear cost is the goal. +// -------------------------------------------------------------------------- +class CB {} +function cn_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::(); } +} +function cn_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>(); } +} +function cn_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>>>(); } +} +function cn_d8(int $n): void { + for ($i = 0; $i < $n; $i++) { new CB::>>>>>>>(); } +} + +// -------------------------------------------------------------------------- +// Scenario 20: wide DNF — many alternatives in a union of intersections. +// Pre-erasure value check loops over alternatives; worst case is O(width). +// -------------------------------------------------------------------------- +interface DA {} interface DB {} interface DC {} interface DD {} +interface DE {} interface DF {} interface DG {} interface DH {} +class Hits implements DA, DB, DC, DD, DE, DF, DG, DH {} +function dnf_w2(T $x): T { return $x; } +function dnf_w4(T $x): T { return $x; } +function dnf_w2_call(int $n): void { + $v = new Hits(); + for ($i = 0; $i < $n; $i++) { dnf_w2($v); } +} +function dnf_w4_call(int $n): void { + $v = new Hits(); + for ($i = 0; $i < $n; $i++) { dnf_w4($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 21: diamond inheritance binding chain. D extends C>> +// resolution. Bench the act of constructing instances through the chain. +// -------------------------------------------------------------------------- +class CL1 {} +class CL2 extends CL1 {} +class CL3 extends CL2 {} +class CL4 extends CL3 {} +class CL5 extends CL4 {} +function chain_d2(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL2::(); } +} +function chain_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL3::(); } +} +function chain_d4(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL4::(); } +} +function chain_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new CL5::(); } +} + +// -------------------------------------------------------------------------- +// Scenario 22: polymorphic inline cache thrashing. The PIC at instanceof/ +// catch keys on (type_args generation, called_scope). A hot loop that +// switches the binding per iteration should miss the cache every time, +// turning O(1) into an O(class-table-lookup) cost per iteration. +// -------------------------------------------------------------------------- +class PicBox {} +function pic_stable(array $items): int { + $count = 0; + foreach ($items as $item) { + if ($item instanceof PicBox) { $count++; } + } + return $count; +} +/* Outer loop alternates the T binding per call — every entry into the inner + * loop sees a different type_args generation, so the PIC slot mismatches + * and re-resolves on the first item. The inner loop then runs warm. */ +function bench_pic_stable_int(int $n): void { + $items = array_fill(0, 16, new PicBox::()); + for ($i = 0; $i < $n; $i++) { + pic_stable::($items); + } +} +function bench_pic_alternating(int $n): void { + $items = array_fill(0, 16, new PicBox::()); + /* call alternates T between int and string — each call's args_box has + * a different cache key, so the per-call type_arg_table differs and + * the inner-loop PIC must re-resolve at least once per call. */ + for ($i = 0; $i < $n; $i++) { + if ($i & 1) { + pic_stable::($items); + } else { + pic_stable::($items); + } + } +} + +// -------------------------------------------------------------------------- +// Scenario 23: bound-conformance check on deeply-nested args. Each level of +// nesting may trigger an additional recursive call into the variance/bound +// machinery. +// -------------------------------------------------------------------------- +class BX {} +function bnd_d1(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::(); } +} +function bnd_d3(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::>>(); } +} +function bnd_d5(int $n): void { + for ($i = 0; $i < $n; $i++) { new BX::>>>>(); } +} + +section("19. Canonical-name traversal depth"); +$cn1_ns = bench("new CB (depth 1)", 'cn_d1'); +$cn3_ns = bench("new CB>> (depth 3)", 'cn_d3'); +$cn5_ns = bench("new CB>>>> (depth 5)", 'cn_d5'); +$cn8_ns = bench("new CB<...x8 levels...> (depth 8)", 'cn_d8'); +compare("delta: d3 vs d1 (3x deeper)", $cn3_ns, "d3", $cn1_ns, "d1"); +compare("delta: d5 vs d1 (5x deeper)", $cn5_ns, "d5", $cn1_ns, "d1"); +compare("delta: d8 vs d1 (8x deeper)", $cn8_ns, "d8", $cn1_ns, "d1"); + +section("20. Wide DNF bound — value check across union of intersections"); +$dnf2_ns = bench("dnf<(DA&DB)|(DC&DD)>(Hits) — 2 alts", 'dnf_w2_call'); +$dnf4_ns = bench("dnf<(DA&DB)|(DC&DD)|(DE&DF)|(DG&DH)> — 4", 'dnf_w4_call'); +compare("delta: 4 alts vs 2 alts (2x wider)", $dnf4_ns, "w4", $dnf2_ns, "w2"); + +section("21. Inheritance binding chain depth"); +$ch2_ns = bench("new CL2 (chain depth 2)", 'chain_d2'); +$ch3_ns = bench("new CL3 (chain depth 3)", 'chain_d3'); +$ch4_ns = bench("new CL4 (chain depth 4)", 'chain_d4'); +$ch5_ns = bench("new CL5 (chain depth 5)", 'chain_d5'); +compare("delta: chain 3 vs chain 2", $ch3_ns, "c3", $ch2_ns, "c2"); +compare("delta: chain 5 vs chain 2", $ch5_ns, "c5", $ch2_ns, "c2"); + +section("22. PIC thrashing (alternating T)"); +$pic_st_ns = bench("instanceof PicBox — stable T (PIC warm)", 'bench_pic_stable_int', ITERS_LIGHT); +$pic_alt_ns = bench("instanceof PicBox — alternating T", 'bench_pic_alternating', ITERS_LIGHT); +compare("delta: alternating vs stable", $pic_alt_ns, "alt", $pic_st_ns, "stable"); + +section("23. Bound-conformance depth"); +$bd1_ns = bench("new BX (depth 1, bound: object)", 'bnd_d1'); +$bd3_ns = bench("new BX>> (depth 3)", 'bnd_d3'); +$bd5_ns = bench("new BX<...x5 nested...> (depth 5)", 'bnd_d5'); +compare("delta: d3 vs d1 (3x deeper)", $bd3_ns, "d3", $bd1_ns, "d1"); +compare("delta: d5 vs d1 (5x deeper)", $bd5_ns, "d5", $bd1_ns, "d1"); + +// -------------------------------------------------------------------------- +// Scenario 24: inference cost isolation — inferred call vs turbofish vs plain. +// The commenter's concern: "inference is expensive". Inference is ~10 lines: +// peek at Z_OBJCE_P(arg)->name for each unbound inferable slot. No +// unification, no constraint-solving, no new compiler phase. +// -------------------------------------------------------------------------- + +// 24a: single-param inference with different class hierarchy depths +class Shallow {} +class Mid extends Shallow {} +class Deep extends Mid {} + +function infer_id(T $x): T { return $x; } +function plain_id(object $x): object { return $x; } + +function bench_infer_shallow(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { infer_id($v); } +} +function bench_infer_deep_hierarchy(int $n): void { + $v = new Deep(); + for ($i = 0; $i < $n; $i++) { infer_id($v); } +} +function bench_turbofish_shallow(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { infer_id::($v); } +} +function bench_plain_object(int $n): void { + $v = new Shallow(); + for ($i = 0; $i < $n; $i++) { plain_id($v); } +} + +// 24b: multi-param inference — the "int and float, T could be int|float" worry. +// In this implementation, each parameter maps to exactly one T slot. +// Scalars are NOT inferred (only objects). No ambiguity exists. +class TypeA {} +class TypeB {} +class TypeC {} +class TypeD {} + +function infer_two_params(T $a, U $b): T { return $a; } +function infer_three_params(T $a, U $b, V $c): T { return $a; } +function infer_four_params(T $a, U $b, V $c, W $d): T { return $a; } +function plain_four_params(TypeA $a, TypeB $b, TypeC $c, TypeD $d): TypeA { return $a; } + +function bench_infer_2param(int $n): void { + $a = new TypeA(); $b = new TypeB(); + for ($i = 0; $i < $n; $i++) { infer_two_params($a, $b); } +} +function bench_infer_3param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); + for ($i = 0; $i < $n; $i++) { infer_three_params($a, $b, $c); } +} +function bench_infer_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { infer_four_params($a, $b, $c, $d); } +} +function bench_turbofish_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { infer_four_params::($a, $b, $c, $d); } +} +function bench_plain_4param(int $n): void { + $a = new TypeA(); $b = new TypeB(); $c = new TypeC(); $d = new TypeD(); + for ($i = 0; $i < $n; $i++) { plain_four_params($a, $b, $c, $d); } +} + +// -------------------------------------------------------------------------- +// Scenario 25: inference through inheritance chains — the "expensive when +// nested in inheritance chains" worry. The monomorph for GenChild is +// synthesized once and cached in the class table. Inference reads the +// already-resolved ce->name from the object — it doesn't walk chains. +// -------------------------------------------------------------------------- +class GenBase { + public function id(T $x): T { return $x; } +} +class GenMid extends GenBase {} +class GenChild extends GenMid {} +class GenGrandchild extends GenChild {} + +function infer_from_chain(T $x): T { return $x; } + +function bench_infer_chain_d1(int $n): void { + $v = new GenBase::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d2(int $n): void { + $v = new GenMid::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d3(int $n): void { + $v = new GenChild::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} +function bench_infer_chain_d4(int $n): void { + $v = new GenGrandchild::(); + for ($i = 0; $i < $n; $i++) { infer_from_chain($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 26: inferred call inside a generic function (forwarding). This is +// the nested case: `outer` calls `inner(u)` which infers T=U from the +// runtime value. The inner call can't cache (inference depends on runtime +// value), but the work per call is still just a ce->name read. +// -------------------------------------------------------------------------- +function inner_infer(T $x): T { return $x; } +function outer_calls_inferred(U $x): U { return inner_infer($x); } +function outer_calls_turbofish(U $x): U { return inner_infer::($x); } +function outer_plain(Foo $x): Foo { return $x; } + +function bench_nested_infer(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_calls_inferred::($f); } +} +function bench_nested_turbofish(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_calls_turbofish::($f); } +} +function bench_nested_plain(int $n): void { + $f = new Foo(); + for ($i = 0; $i < $n; $i++) { outer_plain($f); } +} + +// -------------------------------------------------------------------------- +// Scenario 27: bounded inference — the callee has `T : SomeBase`, so after +// inferring T from the value's class, the bound must also be checked. This +// is the same covariance check as turbofish, not an extra cost of inference. +// -------------------------------------------------------------------------- +class Animal {} +class Dog extends Animal {} +class Puppy extends Dog {} + +function infer_bounded_obj(T $x): T { return $x; } +function turbofish_bounded_obj(Animal $x): Animal { return $x; } + +function bench_infer_bounded_shallow(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj($v); } +} +function bench_infer_bounded_deep(int $n): void { + $v = new Puppy(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj($v); } +} +function bench_turbofish_bounded_equiv(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { infer_bounded_obj::($v); } +} +function bench_plain_bounded_equiv(int $n): void { + $v = new Dog(); + for ($i = 0; $i < $n; $i++) { turbofish_bounded_obj($v); } +} + +// -------------------------------------------------------------------------- +// Scenario 28: inference with monomorphised receiver in inheritance chain. +// The object's ce->name is already the resolved monomorph name (e.g. +// "GenChild"). Inference just copies that string — no chain walking. +// -------------------------------------------------------------------------- +function bench_infer_mono_receiver_d1(int $n): void { + $r = new GenBase::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->id($acc); } +} +function bench_infer_mono_receiver_d4(int $n): void { + $r = new GenGrandchild::(); + $acc = 0; + for ($i = 0; $i < $n; $i++) { $acc = $r->id($acc); } +} + +section("24. Inference cost isolation"); +$is_ns = bench("infer_id(\$shallow) — inferred, flat class", 'bench_infer_shallow'); +$idh_ns = bench("infer_id(\$deep) — inferred, 3-deep class", 'bench_infer_deep_hierarchy'); +$ts_ns = bench("infer_id::(\$v) — turbofish equiv", 'bench_turbofish_shallow'); +$po_ns = bench("plain_id(\$v) — non-generic", 'bench_plain_object'); +compare("delta: inferred vs turbofish", $is_ns, "inf", $ts_ns, "tf"); +compare("delta: inferred vs plain", $is_ns, "inf", $po_ns, "plain"); +compare("delta: deep hierarchy class vs shallow", $idh_ns, "deep", $is_ns, "shallow"); + +section("24b. Multi-param inference scaling"); +$i2_ns = bench("infer_two_params(\$a,\$b) — 2 params inferred", 'bench_infer_2param'); +$i3_ns = bench("infer_three_params(\$a,\$b,\$c) — 3 params inferred", 'bench_infer_3param'); +$i4_ns = bench("infer_four_params(\$a...\$d) — 4 params inferred", 'bench_infer_4param'); +$t4_ns = bench("turbofish::(\$a...) — 4 params turbofish", 'bench_turbofish_4param'); +$p4_ns = bench("plain_four_params(\$a...\$d) — non-generic", 'bench_plain_4param'); +compare("delta: 4-param infer vs 2-param infer", $i4_ns, "4p", $i2_ns, "2p"); +compare("delta: 4-param infer vs 4-param turbo", $i4_ns, "inf", $t4_ns, "tf"); +compare("delta: 4-param infer vs 4-param plain", $i4_ns, "inf", $p4_ns, "plain"); + +section("25. Inference through inheritance chains"); +$ic1_ns = bench("infer_from_chain(GenBase) — chain depth 1", 'bench_infer_chain_d1'); +$ic2_ns = bench("infer_from_chain(GenMid) — chain depth 2", 'bench_infer_chain_d2'); +$ic3_ns = bench("infer_from_chain(GenChild) — chain depth 3", 'bench_infer_chain_d3'); +$ic4_ns = bench("infer_from_chain(GenGrandchild) — chain depth 4", 'bench_infer_chain_d4'); +compare("delta: chain d2 vs chain d1", $ic2_ns, "d2", $ic1_ns, "d1"); +compare("delta: chain d4 vs chain d1", $ic4_ns, "d4", $ic1_ns, "d1"); + +section("26. Nested generic call with inner inference"); +$ni_ns = bench("outer → inner inferred — inner infers", 'bench_nested_infer'); +$nt_ns = bench("outer → inner:: turbofish — inner forwarded",'bench_nested_turbofish'); +$np_ns = bench("outer_plain(\$f) — non-generic", 'bench_nested_plain'); +compare("delta: nested infer vs nested turbo", $ni_ns, "inf", $nt_ns, "tf"); +compare("delta: nested infer vs plain", $ni_ns, "inf", $np_ns, "plain"); +compare("delta: nested turbo vs plain", $nt_ns, "tf", $np_ns, "plain"); + +section("27. Bounded inference (covariance check)"); +$ibs_ns = bench("infer_bounded(Dog) — shallow subtype", 'bench_infer_bounded_shallow'); +$ibd_ns = bench("infer_bounded(Puppy) — deeper subtype", 'bench_infer_bounded_deep'); +$tbe_ns = bench("infer_bounded::(Dog) — turbofish+bound", 'bench_turbofish_bounded_equiv'); +$pbe_ns = bench("plain_bounded(Dog) — non-generic typed", 'bench_plain_bounded_equiv'); +compare("delta: infer+bound vs turbofish+bound", $ibs_ns, "inf", $tbe_ns, "tf"); +compare("delta: infer+bound vs plain typed", $ibs_ns, "inf", $pbe_ns, "plain"); +compare("delta: deeper subtype vs shallow subtype", $ibd_ns, "deep", $ibs_ns, "shallow"); + +section("28. Method dispatch on monomorphised inheritance chain receiver"); +$mrd1_ns = bench("GenBase->id() — chain depth 1", 'bench_infer_mono_receiver_d1'); +$mrd4_ns = bench("GenGrandchild->id() — chain depth 4", 'bench_infer_mono_receiver_d4'); +compare("delta: chain d4 receiver vs chain d1", $mrd4_ns, "d4", $mrd1_ns, "d1"); + +$opcache_loaded = extension_loaded('Zend OPcache'); +$opcache_on = $opcache_loaded + && function_exists('opcache_get_status') + && (bool) (opcache_get_status(false)['opcache_enabled'] ?? false); +$jit_on = false; +if ($opcache_on) { + $status = opcache_get_status(false); + /* 'enabled' = JIT is built into this opcache; 'on' = JIT is active at runtime */ + $jit_on = (bool) ($status['jit']['on'] ?? false); +} +$opcache_label = $opcache_on + ? ('enabled' . ($jit_on ? ' + JIT' : ', JIT off')) + : ($opcache_loaded ? 'loaded but disabled (opcache.enable_cli=0)' : 'not loaded'); +echo "\nNote: opcache is $opcache_label. Same build, same interpreter\n"; +echo " path across all scenarios — deltas between rows are the signal.\n"; diff --git a/docs/source/core/generics.rst b/docs/source/core/generics.rst new file mode 100644 index 000000000000..569759694313 --- /dev/null +++ b/docs/source/core/generics.rst @@ -0,0 +1,361 @@ +############### + Generic types +############### + +PHP supports generic type parameters on classes, interfaces, traits, functions, methods, closures, +and arrow functions. They are *reified*, but reified along two different axes depending on the +declaring entity: + +- **Class-like generics are monomorphised.** Each distinct argument tuple at a generic class, + interface, or trait synthesises a separate ``zend_class_entry`` — ``Box`` and + ``Box`` are distinct classes whose canonical names are spelled out + (``"Box"``, ``"Box"``) and registered in ``EG(class_table)``. The monomorph extends + (or implements, or uses) the base class with the substitution baked in. ``get_class()``, + ``var_dump``, and ``$x instanceof Box`` all see the canonical mono. + +- **Function- and method-level generics are reified per call frame.** Each call gets a fresh + ``zend_type_arg_table`` on ``EX(type_args)`` mapping parameter index to bound class name. There + is no persistent ``id`` function entry — function-level monomorphs would grow without + bound and require a lot of bookkeeping, so the binding lives on the frame and is consulted on + demand. Closures and generators preserve the table so it survives suspension. + +A parameter's **bound** is the type-check the engine uses on ordinary parameter / return / +property slots: ``arg_info::type``, ``zend_property_info::type``, and ``zend_class_constant::type`` +carry the bound, and the existing runtime handlers (parameter coercion, return verification, +property writes) walk them as if no generics were involved. The bound is also the answer to "what +can a caller actually pass at this slot", which is why Reflection's ``ReflectionType::getName()`` +and ``__toString()`` continue to return the bound's text. Reflection exposes the reified shape +separately through ``ReflectionNamedType::getGenericArguments()`` and +``ReflectionTypeParameterReference``. + +The reified shape is held on a side table keyed by entity slot. The runtime reads it at a small +number of well-defined points: + +- monomorph synthesis (``zend_synthesize_monomorph``), +- turbofish argument validation (``ZEND_VERIFY_GENERIC_ARGUMENTS``), +- deferred class lookups for ``instanceof Box`` / ``catch (Box $e)`` (the new + ``ZEND_FETCH_CLASS_GENERIC_DEFERRED`` sub-type), +- bare ``T``-ref resolution for ``instanceof T`` / ``catch (T $e)`` / ``new T()`` / + ``T::method()``, +- inheritance linking, where a parent's reified type is substituted with the child's bindings, +- Reflection. + +*********************** + Reified type carriers +*********************** + +``zend_type`` carries the reified shapes generics need on side-table slots. Two bits in +``type_mask`` distinguish them: + +.. code:: c + + #define _ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) /* t.ptr is a zend_type_parameter_ref * */ + #define _ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31) /* t.ptr is a zend_type_named_with_args * */ + +When ``_ZEND_TYPE_TYPE_PARAMETER_BIT`` is set, ``t.ptr`` is a reference to a declared parameter; +when ``_ZEND_TYPE_NAMED_WITH_ARGS_BIT`` is set, ``t.ptr`` is a generic application like +``Foo``: + +.. code:: c + + typedef struct _zend_type_parameter_ref { + zend_string *name; /* "T" */ + uint32_t index; /* position in the parameter list */ + uint8_t origin; /* CLASS_LIKE or FUNCTION_LIKE */ + } zend_type_parameter_ref; + + typedef struct _zend_type_named_with_args { + zend_string *name; /* "Foo", or NULL for synthetic turbofish carriers */ + uint32_t name_attr; + uint32_t count; + zend_type args[1]; /* flexible array */ + } zend_type_named_with_args; + +The ``origin`` field on a ``T``-ref tells you which scope to resolve ``index`` in. NAMED_WITH_ARGS +values with ``name == NULL`` are synthetic carriers that hold the args at a turbofish site — +they're not type expressions, just a payload. Both payloads only appear in side-table slots, are +heap-allocated, and are released through ``zend_type_release``. + +********************** + Per-entity metadata +********************** + +Both ``zend_class_entry`` and ``zend_op_array`` carry two pointer fields for generic information, +both ``NULL`` on entities that don't use generics: + +.. code:: c + + zend_generic_parameter_list *generic_parameters; /* the entity's own declaration */ + zend_generic_type_table *generic_types; /* reified shapes for every other slot */ + +``generic_parameters`` is the entity's own declaration list (a flexible-array struct allocated +through ``zend_generic_parameter_list_alloc``). Each parameter carries two views of its bound and +default: the ``bound`` / ``default_type`` slots hold the form the ordinary type-check machinery +sees (a class name or scalar mask, with ``T``-refs resolved to their bounds), and the +``..._pre_erasure`` slots (the C field names are kept from earlier scaffolding) hold the reified +shape when it differs from the bound view: + +.. code:: c + + typedef struct _zend_generic_parameter { + zend_string *name; + zend_generic_variance variance; /* INVARIANT, COVARIANT, CONTRAVARIANT */ + zend_type bound; /* type-check view; NONE if unbounded */ + zend_type bound_pre_erasure; /* NONE if same as bound */ + zend_type default_type; + zend_type default_pre_erasure; + } zend_generic_parameter; + +``generic_types`` collects every other slot where a reified type might need to be looked up: + +.. code:: c + + typedef struct _zend_generic_type_table { + zend_type *return_type; + zend_type *extends; + HashTable *parameters; /* parameter index -> zend_type * */ + HashTable *properties; /* zend_string * -> zend_type * */ + HashTable *class_constants; + HashTable *implements; /* implements index -> zend_type * */ + HashTable *trait_uses; /* trait-use index -> zend_type * */ + HashTable *turbofish_args; /* args_id -> zend_type * */ + } zend_generic_type_table; + +Every slot is independently NULLable. The table is allocated lazily by +``zend_generic_get_or_create_class_table`` / ``..._op_array_table`` on first use, and individual +slots stay ``NULL`` whenever the reified form would be byte-equal to the bound view. You populate +slots through the ``zend_generic_type_table_set_*`` family; each setter takes ownership of the +``zend_type`` you hand it. + +The ``turbofish_args`` table is keyed by an ``args_id`` rather than the opline's position in the +bytecode array. Optimizer passes reorder, insert, and delete opcodes; the id is stamped into the +opline itself (either ``extended_value`` for ``ZEND_VERIFY_GENERIC_ARGUMENTS`` or the packed +``op.num`` for ``ZEND_FETCH_CLASS_GENERIC_DEFERRED``), so the id survives reordering. + +Attributes carry their turbofish on two fields on ``zend_attribute``: + +.. code:: c + + uint8_t generic_arity; + zend_type *generic_args; /* NAMED_WITH_ARGS holding turbofish args */ + +****************** + Compile-time flow +****************** + +When the compiler reaches a generic declaration, ``zend_compile_generic_type_parameter_list`` +builds the parameter list and pushes a linked-list scope entry. ``T``-ref resolution walks the +chain through ``zend_generic_lookup``. The scope entry's ``self_compiling`` field points at the +parameter whose bound or default is currently being compiled, which is what lets the compiler +reject ``class A`` (top-level self-reference) while accepting +``class A>`` (the inner ``T`` is nested under another generic). + +Every type expression carrying a ``T`` is compiled twice: once as the bound-view ``zend_type`` on +the entity's ordinary slot (``arg_info::type``, ``zend_property_info::type``), and once in +reified form on the matching ``generic_types`` slot. When the bound is a list type (union, +intersection, or DNF), the bound copy is built through ``zend_arena_deep_copy_type_list`` so +nested type lists are not aliased between ``param->bound`` and the consuming ``arg_info``. + +For a turbofish at a call or ``new`` site, ``zend_emit_verify_generic_arguments`` compiles the +args into a NAMED_WITH_ARGS payload, stores it in ``op_array->generic_types->turbofish_args`` +under a fresh ``args_id``, and emits a ``ZEND_VERIFY_GENERIC_ARGUMENTS`` opcode with +``op2.num = arity`` and ``extended_value = args_id``. The opcode sits between the call's +``INIT_*`` (or ``ZEND_NEW``) and its ``DO_*``, so the call frame is set up when the handler runs. + +For a generic named type in expression position — ``instanceof Box``, ``catch (Box $e)``, +``new Box::()`` — ``zend_compile_class_ref`` does compile-time canonicalisation when the +args are fully concrete (no ``T``-refs): it produces the canonical name string and emits an +``IS_CONST`` operand, so the lookup is the same constant-cache hit any other class reference +gets. When the args contain ``T``-refs the resolution has to wait for the call frame's bindings; +see *Deferred generic class resolution* below. + +****************************** + Variance and static context +****************************** + +After a class or function with non-invariant generic parameters compiles, +``zend_check_generic_variance_markers`` and ``zend_check_function_variance_markers`` walk every +position where a ``T`` can appear (signatures, properties, hook signatures, the type-args at +``extends``/``implements``/``use``, bounds, defaults). Both go through ``zend_variance_walk``, +which dispatches each ``T``-ref to the matching parameter list by ``ref->origin``. + +Polarity composes through nested generics: outer × slot, with INVARIANT absorbing. Method return +types are covariant, parameters contravariant; readonly and get-only-hooked properties covariant, +set-only-hooked contravariant, r/w and get+set hooked invariant; bounds and defaults always +invariant. Constructors are exempt. Violations produce ``Type parameter T declared covariant +(+T) cannot appear in contravariant position``. + +The static-context check is structural: ``zend_check_class_origin_in_static_context`` rejects any +class-origin ``T``-ref that resolves while ``CG(in_static_member_type)`` is set. The flag is set +in ``zend_compile_func_decl`` around the signature of a static method and in +``zend_compile_prop_decl`` around the type of a static property — a static member has no instance, +so an instance-bound parameter has no binding to consult. + +****************** + Runtime handlers +****************** + +``ZEND_VERIFY_GENERIC_ARGUMENTS`` +--------------------------------- + +The handler lives in ``zend_vm_def.h``. It looks up the captured args via +``zend_generic_get_turbofish_args`` and dispatches to ``zend_check_generic_call_arguments`` +(call) or ``zend_check_generic_new_arguments`` (instantiation). Both validate arity against the +callee's ``generic_parameters``, then walk the carrier and call +``zend_check_generic_arg_satisfies_bound`` for each ``(arg, parameter)`` pair. Mismatches throw +``ArgumentCountError`` or ``TypeError``; on exception, teardown releases the call frame, the +trampoline name if any, and ``$this`` if held. + +When validation passes, the handler installs the bindings: for calls, +``zend_build_generic_call_type_args`` builds the ``zend_type_arg_table`` and assigns it to +``call->type_args``; for instantiation, the canonical monomorph is synthesised (or fetched from +the class table) and ``object_init_ex`` runs against it. + +Bare ``T``-ref resolution +------------------------- + +``instanceof T`` / ``catch (T $e)`` / ``new T()`` / ``T::method()`` emit an ``IS_UNUSED`` operand +whose ``op.num`` is packed by ``zend_pack_type_param_fetch`` (sub-type +``ZEND_FETCH_CLASS_TYPE_PARAM`` or ``..._TYPE_PARAM_CLASS``, parameter index in the high bits). +``zend_resolve_generic_type_param`` does the lookup at runtime: + +- Function/method-level: ``EX(type_args)->names[index]`` is the canonical class name supplied + for that parameter at the call site. +- Class-level: walk from the called scope upward until you reach a direct child of the lexical + scope — that child *is* the monomorph that owns the binding, on ``ce->generic_type_args``. + +If the table doesn't have a binding for that slot, the resolver falls back to the parameter's +declared bound. ``zend_type_arg_canonical_name`` writes the full canonical name into the table +(including scalars), and the resolver retries via ``zend_fetch_class_by_name`` silently — a +scalar name like ``"int"`` simply fails to resolve as a class and triggers the bound fallback, +which is fine for the cases where the bound has a class name. + +Deferred generic class resolution +--------------------------------- + +``instanceof Box`` and ``catch (Box $e)`` — generic named types whose argument list still +contains ``T``-refs at compile time — can't be canonicalised statically because the binding for +``T`` only exists per call frame. The compiler: + +1. Compiles the reified ``zend_type`` (``Box`` as a NAMED_WITH_ARGS with a T-ref child). +2. Stashes it in the op_array's ``generic_types->turbofish_args`` under a fresh ``args_id``. +3. Emits an ``IS_UNUSED`` operand whose ``op.num`` packs ``ZEND_FETCH_CLASS_GENERIC_DEFERRED`` + and the ``args_id`` (via ``zend_pack_generic_deferred_fetch``). + +At runtime, ``zend_fetch_class`` dispatches on the sub-type to +``zend_resolve_deferred_generic_class``, which: + +1. Reads the boxed type from the executing op_array's ``turbofish_args`` table. +2. Recursively walks the NAMED_WITH_ARGS, substituting each ``T``-ref against the current + frame's bindings (function-level via ``EX(type_args)``, class-level via the same walk as + bare ``T``-ref resolution). +3. Builds the canonical name string (``"Box"``, ``"Outer>"``, etc.). +4. Hands it to ``zend_fetch_class_by_name``, which finds the existing monomorph or synthesises + one through ``zend_try_synthesize_monomorph_by_name``. + +``ZEND_INSTANCEOF`` and ``ZEND_CATCH`` already accept ``IS_UNUSED`` operands and route through +``zend_fetch_class``; the deferred dispatch is just a new sub-type case there. + +************************ + Inheritance and linking +************************ + +When a child extends a generic ancestor or uses a generic trait, the linker substitutes the +inherited prototype's reified types with the child's bindings before the variance check runs. +``zend_get_inheritance_binding`` returns the *direct* binding (the args at ce's own +``extends``/``implements``/``use`` clause); for ``extends`` it matches by pointer when +``ZEND_ACC_RESOLVED_PARENT`` is set and by name otherwise, since the override check fires before +parent resolution on the deferred-obligation path. ``zend_get_inheritance_binding_full`` walks +the parent chain and direct interfaces, composing each link through +``zend_substitute_leaf_type_param`` and looking the parent up by name if it isn't yet resolved. + +``zend_substitute_leaf_type_param`` replaces a class-scope ``T``-ref with ``args[T->index]`` and +propagates the nullable bit, so ``?T`` with ``T = int`` substitutes to ``?int``. Other modifier +bits are not propagated; non-leaf carriers (NAMED_WITH_ARGS, lists) are returned unchanged. +``zend_substitute_proto_type`` is the entry point used by ``zend_do_perform_implementation_check``; +it returns the unsubstituted fallback when ce isn't generic, when the binding lookup fails, or +when substitution would yield another ``T``-ref because the inheriting class forwards the +parameter. ``zend_get_function_declaration`` takes an optional ``subst_ce``: when present, each +printed type runs through ``zend_substitute_proto_type`` so the LSP error message shows the +substituted parent signature rather than the bound view. + +*************************** + Member substitution sites +*************************** + +Substitution is applied at five sites. Each clones the parent's ``zend_property_info`` or +``zend_function``, allocates a fresh ``arg_info`` in the child's arena, and shares body opcodes +with the parent via refcount. Clones keep ``ce`` set to the parent's defining class so +``zend_opcode.c``'s teardown does not double-release shared fields. + +- ``do_inherit_property`` — property type on a class extending a generic parent. +- Property hook signatures — the ``get`` return slot and the ``set`` value-parameter slot. +- ``zend_do_traits_property_binding`` — trait property type, with the using class's binding. +- ``zend_add_trait_method`` — trait method signature, via ``zend_substitute_trait_method_arg_info``. +- ``do_inherit_method`` — non-overridden inherited method signature, via + ``zend_maybe_substitute_inherited_method``. + +Body opcodes are not re-emitted: ``VERIFY_*`` opcodes were laid down at the parent's compile time +against the unsubstituted view, so the child observes the substituted signature on the clone but +the parent's original opcodes inside. + +Diamond detection is in ``zend_validate_generic_diamond_bindings``. It runs *before* +``ce->parent`` is set and ``ZEND_ACC_RESOLVED_PARENT`` is established; the side-table accessors +bypass the resolved-parent gate, so detection sees the bindings directly. For each direct binding +source of ce (parent plus each ``implements`` entry), the check composes the ce-to-source binding +with ``zend_get_inheritance_binding_full`` for every generic target reachable from source. +Records are stored in a transient ``HashTable`` keyed by target ``zend_class_entry *``; +``zend_diamond_record_or_check`` compares only arity. Differing arity is structurally +inconsistent and fires ``zend_error_noreturn`` with both source paths. Differing args at matching +arity are admitted at this stage and resolved downstream — the interface-level merge synthesises +a use-site-variance-aware contract on the inheriting interface, and per-path LSP verifies any +concrete implementer against each substituted parent prototype. + +``zend_check_generic_link_bounds`` validates each supplied arg against the corresponding target +parameter's bound. When the supplied arg is a leaf class-scope ``T``-ref of ce, the effective arg +type is ce's own bound on that parameter — "ce's bound on Y must satisfy target's bound on T". An +unbounded child parameter cannot be forwarded into a bounded ancestor slot. + +*********************** + Reflection visibility +*********************** + +Reflection is structured so that the *bound view* answers "what can a caller actually pass at +this slot" — the most common question — while the *reified shape* is reachable for tooling that +needs it: + +- ``ReflectionType::__toString()`` and ``ReflectionNamedType::getName()`` return the bound's + text (``Foo`` for ``T : Foo``, ``mixed`` for unbounded ``T``, ``Container`` for + ``Container``). This matches what an ordinary parameter / return / property slot enforces + at runtime. +- ``ReflectionNamedType::hasGenericArguments()`` / ``getGenericArguments()`` return the type + arguments of a generic application as ``ReflectionType`` instances in source order. +- ``ReflectionTypeParameterReference`` appears inside reified type expressions only — as an + element of ``getGenericArguments()``, as a bound, as a default, or as a nested arg. It is the + reflection type you see when a ``T``-ref shows up inside another type, not on a parameter + whose declared type *is* ``T``. +- ``ReflectionFunctionAbstract::getGenericParameters()`` and ``ReflectionClass::getGenericParameters()`` + return the entity's own declaration list. +- ``ReflectionClass::getGenericArgumentsFor{ParentClass,ParentInterface,UsedTrait}()`` returns + the args supplied at the class's own ``extends`` / ``implements`` / ``use`` clause. + +****************************** + Opcache, optimizer, and JIT +****************************** + +Opcache persistence walks the side tables from ``zend_persist_generic_type_table``: scalar slots +(``return_type``, ``extends``) get duped into SHM via ``zend_shared_memdup_put_free`` and walked +by ``zend_persist_type``; hash slots go through ``zend_persist_generic_type_table_ht``. Calc-side +mirrors. ``zend_persist_attributes`` persists ``zend_attribute->generic_args``. ``zend_persist_type`` +handles NAMED_WITH_ARGS values whose ``name`` is ``NULL``. + +``zend_try_inline_call`` (``Zend/Optimizer/optimize_func_calls.c``) skips its inline transform +when a ``ZEND_VERIFY_GENERIC_ARGUMENTS`` opcode sits between ``INIT_FCALL`` and ``DO_FCALL``: the +verify handler dereferences ``EX(call)->func``, so NOP-ing ``INIT_FCALL`` while leaving the verify +in place would walk an unrelated frame. + +Under JIT, ``ZEND_VERIFY_GENERIC_ARGUMENTS`` falls through to the interpreter. The handler is +cold, so the dispatch cost is dwarfed by the type-comparison work. Code that doesn't use +turbofish doesn't emit the opcode, and the surrounding ``INIT_*`` and ``ZEND_NEW`` JIT +specializations are unaffected. The deferred ``instanceof`` / ``catch`` lookup likewise falls +through to ``zend_fetch_class`` like any other ``IS_UNUSED`` class operand. diff --git a/docs/source/index.rst b/docs/source/index.rst index 21e2526f47f6..219264fec13d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ :hidden: core/data-structures/index + core/generics .. toctree:: :caption: Miscellaneous diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..3555158b1da1 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4371,7 +4371,11 @@ static void preload_fix_trait_op_array(zend_op_array *op_array) } const zend_op_array *orig_op_array = zend_shared_alloc_get_xlat_entry(op_array->refcount); - ZEND_ASSERT(orig_op_array && "Must be in xlat table"); + if (!orig_op_array) { + /* Monomorph clones reuse ZEND_ACC_TRAIT_CLONE but have no original; re-sync would clobber their substituted arg_info. */ + ZEND_ASSERT(zend_class_is_monomorph(op_array->scope)); + return; + } zend_string *function_name = op_array->function_name; zend_class_entry *scope = op_array->scope; @@ -4435,6 +4439,15 @@ static void preload_optimize(zend_persistent_script *script) zend_optimize_script(&script->script, ZCG(accel_directives).optimization_level, ZCG(accel_directives).opt_debug_level); zend_accel_finalize_delayed_early_binding_list(script); + /* Loop to fixpoint over the transitive closure of reachable instantiations. */ + for (int aot_round = 0; aot_round < 32; aot_round++) { + if (zend_aot_monomorphize_script(&script->script, + ZCG(accel_directives).optimization_level) == 0) { + break; + } + } + zend_aot_upgrade_dispatch_to_ucall(&script->script); + ZEND_HASH_MAP_FOREACH_PTR(&script->script.class_table, ce) { preload_fix_trait_methods(ce); } ZEND_HASH_FOREACH_END(); diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index fbbfab6b243c..4ddad984ff7d 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -2933,6 +2933,18 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op } } } + /* Same teardown gap as in the tracing path: the inline CV loop + * skips zend_free_compiled_variables, so a generic-call frame's + * EX(type_args) needs an explicit destroy here. */ + if (!left_frame) { + if (!zend_jit_leave_frame(&ctx)) { + goto jit_failure; + } + left_frame = true; + } + if (!zend_jit_free_type_args(&ctx)) { + goto jit_failure; + } if (!zend_jit_leave_func(&ctx, op_array, NULL, MAY_BE_ANY, left_frame, NULL, NULL, (ssa->cfg.flags & ZEND_FUNC_INDIRECT_VAR_ACCESS) != 0, 1)) { goto jit_failure; @@ -3303,6 +3315,25 @@ int zend_jit_op_array(zend_op_array *op_array, zend_script *script) return FAILURE; } + /* Reified-generic monomorphs are shallow copies of their base op_array that + * SHARE the opcode buffer (see zend_synthesize_function_monomorph). The JIT + * keys compiled code and patched opline handlers on opcode addresses and + * bakes op_array-specific constants (arg_info for RECV / VERIFY_RETURN_TYPE, + * inferred operand types, cached literals). Those are valid for exactly one + * arg_info layout, but every monomorph that shares these opcodes carries a + * DIFFERENT substituted layout — so compiling the base (or any one + * monomorph) corrupts its siblings: a sibling's call enters a trace baked + * for a different binding and the wrong type is enforced. The interpreter + * avoids this through the TRAIT_CLONE slow-path RECV; the JIT has no + * equivalent. Until per-monomorph JIT state exists, keep monomorphizable + * generics and their monomorphs interpreted. */ + if (op_array->generic_parameters + || (op_array->fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS) + || (op_array->scope && op_array->scope->generic_parameters)) { + ZEND_SET_FUNC_INFO(op_array, NULL); + return SUCCESS; + } + if (JIT_G(trigger) == ZEND_JIT_ON_FIRST_EXEC) { zend_jit_op_array_extension *jit_extension; zend_op *opline = op_array->opcodes; diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index cf43d3ad840f..0e214a7350e3 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -8442,14 +8442,7 @@ static int zend_jit_isset_isempty_cv(zend_jit_ctx *jit, const zend_op *opline, u return 1; } -/* copy of hidden zend_closure */ -typedef struct _zend_closure { - zend_object std; - zend_function func; - zval this_ptr; - zend_class_entry *called_scope; - zif_handler orig_internal_handler; -} zend_closure; +/* zend_closure struct now exposed in zend_closures.h */ static int zend_jit_stack_check(zend_jit_ctx *jit, const zend_op *opline, uint32_t used_stack) { @@ -8744,6 +8737,12 @@ static int zend_jit_push_call_frame(zend_jit_ctx *jit, const zend_op *opline, co // JIT: ZEND_CALL_NUM_ARGS(call) = num_args; ir_STORE(jit_CALL(rx, This.u2.num_args), ir_CONST_U32(opline->extended_value)); + // JIT: call->type_args = NULL; + // Mirrors zend_vm_init_call_frame; without this the field is stack + // garbage and zend_vm_stack_free_call_frame_ex dereferences it on + // frame teardown (segfault on the exception / undef-prop path). + ir_STORE(jit_CALL(rx, type_args), IR_NULL); + return 1; } @@ -11039,6 +11038,23 @@ static int zend_jit_free_cvs(zend_jit_ctx *jit) return 1; } +/* The inline CV-free fast path that callers use when last_var <= 100 frees + * each CV individually and bypasses zend_free_compiled_variables, which is + * where the interpreter teardown destroys EX(type_args). Without this helper + * the table allocated by VERIFY_GENERIC_ARGUMENTS / INSTALL_GENERIC_ARGS + * leaks on every JIT'd generic call return — and for inference calls each + * leaked table also carries refcounted owned_type fragments, so the rate is + * fast enough to OOM tight loops in seconds. */ +static int zend_jit_free_type_args(zend_jit_ctx *jit) +{ + ir_ref ta = ir_LOAD_A(jit_EX(type_args)); + ir_ref if_ta = ir_IF(ta); + ir_IF_TRUE_cold(if_ta); + ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_type_arg_table_destroy), ta); + ir_MERGE_WITH_EMPTY_FALSE(if_ta); + return 1; +} + static int zend_jit_free_cv(zend_jit_ctx *jit, uint32_t info, uint32_t var) { if (info & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) { diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 024a5d0e194d..d5d1f5b4238b 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -5646,6 +5646,20 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer, } } } + /* The inline CV-free loop above bypasses the + * zend_free_compiled_variables path that destroys + * EX(type_args), so a generic-call frame with a + * non-cached (frame-owned) type-arg table would + * leak it on every return. Mirror the slow path. */ + if (!left_frame) { + if (!zend_jit_leave_frame(&ctx)) { + goto jit_failure; + } + left_frame = 1; + } + if (!zend_jit_free_type_args(&ctx)) { + goto jit_failure; + } if (!zend_jit_leave_func(&ctx, op_array, opline, op1_info, left_frame, p + 1, &zend_jit_traces[ZEND_JIT_TRACE_NUM], (op_array_ssa->cfg.flags & ZEND_FUNC_INDIRECT_VAR_ACCESS) != 0, may_throw)) { diff --git a/ext/opcache/tests/opt/jmp_001.phpt b/ext/opcache/tests/opt/jmp_001.phpt index d5491dfe1ab4..c061d336c4b0 100644 --- a/ext/opcache/tests/opt/jmp_001.phpt +++ b/ext/opcache/tests/opt/jmp_001.phpt @@ -24,10 +24,11 @@ $_main: 0000 RETURN int(1) test: - ; (lines=4, args=0, vars=1, tmps=1) + ; (lines=5, args=0, vars=1, tmps=1) ; (after optimizer) ; %s:2-6 0000 INIT_FCALL_BY_NAME 0 string("test2") -0001 T1 = DO_FCALL_BY_NAME -0002 CV0($var) = QM_ASSIGN T1 -0003 RETURN CV0($var) +0001 VERIFY_GENERIC_ARGUMENTS +0002 T1 = DO_FCALL_BY_NAME +0003 CV0($var) = QM_ASSIGN T1 +0004 RETURN CV0($var) diff --git a/ext/opcache/tests/pipe_optimizations.phpt b/ext/opcache/tests/pipe_optimizations.phpt index 5115eaff59a2..018fbe79f42a 100644 --- a/ext/opcache/tests/pipe_optimizations.phpt +++ b/ext/opcache/tests/pipe_optimizations.phpt @@ -45,8 +45,8 @@ $_main: 0004 SEND_VAL int(5) 1 0005 T2 = DO_UCALL 0006 INIT_METHOD_CALL 1 CV0($o) string("foo") -0007 SEND_VAL_EX T2 1 -0008 T2 = DO_FCALL +0007 SEND_VAL T2 1 +0008 T2 = DO_UCALL 0009 INIT_STATIC_METHOD_CALL 1 string("Other") string("bar") 0010 SEND_VAL T2 1 0011 T2 = DO_UCALL @@ -57,7 +57,6 @@ $_main: 0016 RETURN int(1) LIVE RANGES: 2: 0001 - 0002 (new) - 2: 0009 - 0010 (tmp/var) _test1: ; (lines=4, args=1, vars=1, tmps=%d) diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index af59b9b2c34a..a6cd2379c2a1 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -253,6 +253,12 @@ static void zend_file_cache_serialize_attribute(zval *zv, static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_script *script, void *buf); +static void zend_file_cache_serialize_type( + zend_type *type, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf); + +static void zend_file_cache_unserialize_type( + zend_type *type, zend_class_entry *scope, zend_persistent_script *script, void *buf); + static void *zend_file_cache_serialize_interned(zend_string *str, zend_file_cache_metainfo *info) { @@ -468,11 +474,40 @@ static void zend_file_cache_serialize_attribute(zval *zv, SERIALIZE_STR(attr->args[i].name); zend_file_cache_serialize_zval(&attr->args[i].value, script, info, buf); } + + if (attr->generic_args) { + SERIALIZE_PTR(attr->generic_args); + zend_type *t = attr->generic_args; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } } static void zend_file_cache_serialize_type( zend_type *type, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + SERIALIZE_PTR(ref); + ZEND_TYPE_SET_PTR(*type, ref); + UNSERIALIZE_PTR(ref); + SERIALIZE_STR(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + SERIALIZE_PTR(named); + ZEND_TYPE_SET_PTR(*type, named); + UNSERIALIZE_PTR(named); + SERIALIZE_STR(named->name); + for (uint32_t i = 0; i < named->count; i++) { + zend_file_cache_serialize_type(&named->args[i], script, info, buf); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); SERIALIZE_PTR(list); @@ -490,6 +525,128 @@ static void zend_file_cache_serialize_type( } } +static void zend_file_cache_serialize_generic_type_entry( + zval *zv, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + SERIALIZE_PTR(Z_PTR_P(zv)); + zend_type *boxed = Z_PTR_P(zv); + UNSERIALIZE_PTR(boxed); + zend_file_cache_serialize_type(boxed, script, info, buf); +} + +/* Serialise a turbofish_args entry — same shape as a boxed type from the + * file's perspective but the buffer slot is the entry struct (currently a + * single args_box; the per-call-site runtime cache that pairs with it lives + * in the caller op_array's runtime cache slot, not here). */ +static void zend_file_cache_serialize_turbofish_args_entry( + zval *zv, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + SERIALIZE_PTR(Z_PTR_P(zv)); + 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( + HashTable **ht_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + HashTable *ht; + SERIALIZE_PTR(*ht_ptr); + ht = *ht_ptr; + UNSERIALIZE_PTR(ht); + zend_file_cache_serialize_hash(ht, script, info, buf, zend_file_cache_serialize_generic_type_entry); +} + +static void zend_file_cache_serialize_turbofish_args_ht( + HashTable **ht_ptr, zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + HashTable *ht; + SERIALIZE_PTR(*ht_ptr); + ht = *ht_ptr; + UNSERIALIZE_PTR(ht); + zend_file_cache_serialize_hash(ht, script, info, buf, zend_file_cache_serialize_turbofish_args_entry); +} + +static void zend_file_cache_serialize_generic_parameter_list( + zend_generic_parameter_list **list_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_generic_parameter_list *list; + SERIALIZE_PTR(*list_ptr); + list = *list_ptr; + UNSERIALIZE_PTR(list); + for (uint32_t i = 0; i < list->count; i++) { + SERIALIZE_STR(list->parameters[i].name); + zend_file_cache_serialize_type(&list->parameters[i].bound, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].bound_pre_erasure, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].default_type, script, info, buf); + zend_file_cache_serialize_type(&list->parameters[i].default_pre_erasure, script, info, buf); + } +} + +static void zend_file_cache_serialize_generic_type_table( + zend_generic_type_table **table_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_generic_type_table *table; + SERIALIZE_PTR(*table_ptr); + table = *table_ptr; + UNSERIALIZE_PTR(table); + if (table->return_type) { + SERIALIZE_PTR(table->return_type); + zend_type *t = table->return_type; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } + if (table->extends) { + SERIALIZE_PTR(table->extends); + zend_type *t = table->extends; + UNSERIALIZE_PTR(t); + zend_file_cache_serialize_type(t, script, info, buf); + } + if (table->parameters) { + zend_file_cache_serialize_generic_type_table_ht(&table->parameters, script, info, buf); + } + if (table->properties) { + zend_file_cache_serialize_generic_type_table_ht(&table->properties, script, info, buf); + } + if (table->class_constants) { + zend_file_cache_serialize_generic_type_table_ht(&table->class_constants, script, info, buf); + } + if (table->implements) { + zend_file_cache_serialize_generic_type_table_ht(&table->implements, script, info, buf); + } + if (table->trait_uses) { + zend_file_cache_serialize_generic_type_table_ht(&table->trait_uses, script, info, buf); + } + if (table->turbofish_args) { + zend_file_cache_serialize_turbofish_args_ht(&table->turbofish_args, script, info, buf); + } +} + +/* Serialise a monomorph's generic_type_args. The owned strings (names) and + * owned_type payloads are inline within the table — type_ref is a borrowed + * pointer that we rebuild from ce->generic_types on the unserialise side, so + * we just NULL it out here. */ +static void zend_file_cache_serialize_type_arg_table( + zend_type_arg_table **table_ptr, zend_persistent_script *script, + zend_file_cache_metainfo *info, void *buf) +{ + zend_type_arg_table *table; + SERIALIZE_PTR(*table_ptr); + table = *table_ptr; + UNSERIALIZE_PTR(table); + for (uint32_t i = 0; i < table->count; i++) { + SERIALIZE_STR(table->entries[i].name); + table->entries[i].type_ref = NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_file_cache_serialize_type(&table->entries[i].owned_type, script, info, buf); + } + } +} + static void zend_file_cache_serialize_op_array(zend_op_array *op_array, zend_persistent_script *script, zend_file_cache_metainfo *info, @@ -700,6 +857,14 @@ static void zend_file_cache_serialize_op_array(zend_op_array *op_arra SERIALIZE_PTR(op_array->try_catch_array); SERIALIZE_PTR(op_array->prototype); SERIALIZE_PTR(op_array->prop_info); + + if (op_array->generic_parameters) { + zend_file_cache_serialize_generic_parameter_list(&op_array->generic_parameters, script, info, buf); + } + + if (op_array->generic_types) { + zend_file_cache_serialize_generic_type_table(&op_array->generic_types, script, info, buf); + } } } @@ -831,6 +996,18 @@ static void zend_file_cache_serialize_class(zval *zv, SERIALIZE_STR(ce->info.user.filename); SERIALIZE_STR(ce->doc_comment); SERIALIZE_ATTRIBUTES(ce->attributes); + if (ce->generic_parameters) { + zend_file_cache_serialize_generic_parameter_list(&ce->generic_parameters, script, info, buf); + } + + if (ce->generic_types) { + zend_file_cache_serialize_generic_type_table(&ce->generic_types, script, info, buf); + } + + if (ce->generic_type_args) { + zend_file_cache_serialize_type_arg_table(&ce->generic_type_args, script, info, buf); + } + zend_file_cache_serialize_hash(&ce->properties_info, script, info, buf, zend_file_cache_serialize_prop_info); if (ce->properties_info_table) { @@ -1384,11 +1561,36 @@ static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_scri UNSERIALIZE_STR(attr->args[i].name); zend_file_cache_unserialize_zval(&attr->args[i].value, script, buf); } + + if (attr->generic_args) { + UNSERIALIZE_PTR(attr->generic_args); + zend_file_cache_unserialize_type(attr->generic_args, NULL, script, buf); + } } static void zend_file_cache_unserialize_type( zend_type *type, zend_class_entry *scope, zend_persistent_script *script, void *buf) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + UNSERIALIZE_PTR(ref); + ZEND_TYPE_SET_PTR(*type, ref); + UNSERIALIZE_STR(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + UNSERIALIZE_PTR(named); + ZEND_TYPE_SET_PTR(*type, named); + UNSERIALIZE_STR(named->name); + for (uint32_t i = 0; i < named->count; i++) { + zend_file_cache_unserialize_type(&named->args[i], scope, script, buf); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); UNSERIALIZE_PTR(list); @@ -1410,6 +1612,149 @@ static void zend_file_cache_unserialize_type( } } +static zend_class_entry *zend_file_cache_generic_unserialize_scope = NULL; + +static void zend_file_cache_unserialize_generic_type_entry( + zval *zv, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(Z_PTR_P(zv)); + zend_file_cache_unserialize_type( + (zend_type *) Z_PTR_P(zv), zend_file_cache_generic_unserialize_scope, script, buf); +} + +static void zend_file_cache_unserialize_turbofish_args_entry( + zval *zv, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(Z_PTR_P(zv)); + 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( + HashTable **ht_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*ht_ptr); + HashTable *ht = *ht_ptr; + zend_class_entry *prev = zend_file_cache_generic_unserialize_scope; + zend_file_cache_generic_unserialize_scope = scope; + zend_file_cache_unserialize_hash(ht, script, buf, zend_file_cache_unserialize_generic_type_entry, NULL); + zend_file_cache_generic_unserialize_scope = prev; +} + +static void zend_file_cache_unserialize_turbofish_args_ht( + HashTable **ht_ptr, zend_class_entry *scope, zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*ht_ptr); + HashTable *ht = *ht_ptr; + zend_class_entry *prev = zend_file_cache_generic_unserialize_scope; + zend_file_cache_generic_unserialize_scope = scope; + zend_file_cache_unserialize_hash(ht, script, buf, zend_file_cache_unserialize_turbofish_args_entry, NULL); + zend_file_cache_generic_unserialize_scope = prev; +} + +static void zend_file_cache_unserialize_generic_parameter_list( + zend_generic_parameter_list **list_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*list_ptr); + zend_generic_parameter_list *list = *list_ptr; + for (uint32_t i = 0; i < list->count; i++) { + UNSERIALIZE_STR(list->parameters[i].name); + zend_file_cache_unserialize_type(&list->parameters[i].bound, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].bound_pre_erasure, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].default_type, scope, script, buf); + zend_file_cache_unserialize_type(&list->parameters[i].default_pre_erasure, scope, script, buf); + } +} + +static void zend_file_cache_unserialize_generic_type_table( + zend_generic_type_table **table_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*table_ptr); + zend_generic_type_table *table = *table_ptr; + if (table->return_type) { + UNSERIALIZE_PTR(table->return_type); + zend_file_cache_unserialize_type(table->return_type, scope, script, buf); + } + + if (table->extends) { + UNSERIALIZE_PTR(table->extends); + zend_file_cache_unserialize_type(table->extends, scope, script, buf); + } + + if (table->parameters) { + zend_file_cache_unserialize_generic_type_table_ht(&table->parameters, scope, script, buf); + } + + if (table->properties) { + zend_file_cache_unserialize_generic_type_table_ht(&table->properties, scope, script, buf); + } + + if (table->class_constants) { + zend_file_cache_unserialize_generic_type_table_ht(&table->class_constants, scope, script, buf); + } + + if (table->implements) { + zend_file_cache_unserialize_generic_type_table_ht(&table->implements, scope, script, buf); + } + + if (table->trait_uses) { + zend_file_cache_unserialize_generic_type_table_ht(&table->trait_uses, scope, script, buf); + } + + 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` + * to point into the now-unserialized ce->generic_types side table — same + * logic as zend_persist_mono_binding_nwa in zend_persist.c. */ +static void zend_file_cache_unserialize_type_arg_table( + zend_type_arg_table **table_ptr, zend_class_entry *scope, + zend_persistent_script *script, void *buf) +{ + UNSERIALIZE_PTR(*table_ptr); + zend_type_arg_table *table = *table_ptr; + const zend_type_named_with_args *new_nwa = NULL; + if (scope && scope->generic_types) { + if (scope->generic_types->extends + && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*scope->generic_types->extends)) { + new_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*scope->generic_types->extends); + } else { + HashTable *tables[] = { scope->generic_types->implements, + scope->generic_types->trait_uses }; + for (size_t k = 0; k < sizeof(tables) / sizeof(tables[0]); k++) { + if (!tables[k] || zend_hash_num_elements(tables[k]) == 0) continue; + zval *zv; + ZEND_HASH_FOREACH_VAL(tables[k], zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (boxed && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + new_nwa = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + } + break; + } ZEND_HASH_FOREACH_END(); + if (new_nwa) break; + } + } + } + for (uint32_t i = 0; i < table->count; i++) { + UNSERIALIZE_STR(table->entries[i].name); + table->entries[i].type_ref = + (new_nwa && i < new_nwa->count) ? &new_nwa->args[i] : NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_file_cache_unserialize_type(&table->entries[i].owned_type, scope, script, buf); + } + } +} + static void zend_file_cache_unserialize_op_array(zend_op_array *op_array, zend_persistent_script *script, void *buf) @@ -1596,6 +1941,16 @@ static void zend_file_cache_unserialize_op_array(zend_op_array *op_arr UNSERIALIZE_PTR(op_array->try_catch_array); UNSERIALIZE_PTR(op_array->prototype); UNSERIALIZE_PTR(op_array->prop_info); + + if (op_array->generic_parameters) { + zend_file_cache_unserialize_generic_parameter_list(&op_array->generic_parameters, + (op_array->fn_flags & ZEND_ACC_CLOSURE) ? NULL : op_array->scope, script, buf); + } + + if (op_array->generic_types) { + zend_file_cache_unserialize_generic_type_table(&op_array->generic_types, + (op_array->fn_flags & ZEND_ACC_CLOSURE) ? NULL : op_array->scope, script, buf); + } } } @@ -1720,6 +2075,18 @@ static void zend_file_cache_unserialize_class(zval *zv, UNSERIALIZE_STR(ce->info.user.filename); UNSERIALIZE_STR(ce->doc_comment); UNSERIALIZE_ATTRIBUTES(ce->attributes); + if (ce->generic_parameters) { + zend_file_cache_unserialize_generic_parameter_list(&ce->generic_parameters, ce, script, buf); + } + + if (ce->generic_types) { + zend_file_cache_unserialize_generic_type_table(&ce->generic_types, ce, script, buf); + } + + if (ce->generic_type_args) { + zend_file_cache_unserialize_type_arg_table(&ce->generic_type_args, ce, script, buf); + } + zend_file_cache_unserialize_hash(&ce->properties_info, script, buf, zend_file_cache_unserialize_prop_info, NULL); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index c06452e6acf2..3c41bffd75d2 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -85,6 +85,7 @@ typedef void (*zend_persist_func_t)(zval*); static void zend_persist_zval(zval *z); static void zend_persist_op_array(zval *zv); +static void zend_persist_type(zend_type *type); static const uint32_t uninitialized_bucket[-HT_MIN_MASK] = {HT_INVALID_IDX, HT_INVALID_IDX}; @@ -321,6 +322,11 @@ static HashTable *zend_persist_attributes(HashTable *attributes) zend_persist_zval(©->args[i].value); } + if (copy->generic_args) { + copy->generic_args = zend_shared_memdup_put_free(copy->generic_args, sizeof(zend_type)); + zend_persist_type(copy->generic_args); + } + ZVAL_PTR(v, copy); } ZEND_HASH_FOREACH_END(); @@ -359,6 +365,38 @@ uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name) } static void zend_persist_type(zend_type *type) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + ref = zend_shared_memdup_put_free(ref, sizeof(*ref)); + zend_accel_store_interned_string(ref->name); + ZEND_TYPE_SET_PTR(*type, ref); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + /* Compile-time NWAs can be structurally shared across the script + * (e.g. two `extends Foo, ...>` clauses end up referencing + * the same `Box` payload). Use the xlat-aware variant so the + * second visit short-circuits to the already-persisted SHM copy + * instead of memdup'ing from the now-freed heap source. */ + zend_type_named_with_args *prev = zend_shared_alloc_get_xlat_entry(named); + if (prev) { + ZEND_TYPE_SET_PTR(*type, prev); + return; + } + named = zend_shared_memdup_put_free(named, ZEND_TYPE_NAMED_WITH_ARGS_SIZE(named->count)); + if (named->name) { + zend_accel_store_interned_string(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_persist_type(&named->args[i]); + } + + ZEND_TYPE_SET_PTR(*type, named); + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { zend_type_list *list = ZEND_TYPE_LIST(*type); if (ZEND_TYPE_USES_ARENA(*type) || zend_accel_in_shm(list)) { @@ -376,6 +414,17 @@ static void zend_persist_type(zend_type *type) { zend_persist_type(single_type); continue; } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*single_type)) { + zend_persist_type(single_type); + continue; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*single_type)) { + zend_persist_type(single_type); + continue; + } + if (ZEND_TYPE_HAS_NAME(*single_type)) { zend_string *type_name = ZEND_TYPE_NAME(*single_type); zend_accel_store_interned_string(type_name); @@ -387,6 +436,252 @@ static void zend_persist_type(zend_type *type) { } ZEND_TYPE_FOREACH_END(); } +static zend_generic_parameter_list *zend_persist_generic_parameter_list(zend_generic_parameter_list *list) +{ + if (!list) { + return NULL; + } + + zend_generic_parameter_list *persisted = zend_shared_memdup_put_free( + list, + ZEND_GENERIC_PARAMETER_LIST_SIZE(list->count) + ); + + for (uint32_t i = 0; i < persisted->count; i++) { + zend_generic_parameter *param = &persisted->parameters[i]; + zend_accel_store_interned_string(param->name); + zend_persist_type(¶m->bound); + zend_persist_type(¶m->bound_pre_erasure); + zend_persist_type(¶m->default_type); + zend_persist_type(¶m->default_pre_erasure); + } + + return persisted; +} + +static HashTable *zend_persist_generic_type_table_ht(HashTable *ht) +{ + zend_hash_persist(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + zend_type *boxed = Z_PTR_P(v); + zend_type *copy = zend_shared_memdup_put_free(boxed, sizeof(zend_type)); + zend_persist_type(copy); + Z_PTR_P(v) = copy; + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + zend_accel_store_interned_string(p->key); + } + zend_type *boxed = Z_PTR(p->val); + zend_type *copy = zend_shared_memdup_put_free(boxed, sizeof(zend_type)); + zend_persist_type(copy); + Z_PTR(p->val) = copy; + } ZEND_HASH_FOREACH_END(); + } + HashTable *ptr = zend_shared_memdup_put_free(ht, sizeof(HashTable)); + GC_SET_REFCOUNT(ptr, 2); + GC_TYPE_INFO(ptr) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + 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 + * here, so the persisted SHM copy carries no per-process state. */ +static HashTable *zend_persist_turbofish_args_ht(HashTable *ht) +{ + zend_hash_persist(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + 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 { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + zend_accel_store_interned_string(p->key); + } + 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(); + } + HashTable *ptr = zend_shared_memdup_put_free(ht, sizeof(HashTable)); + GC_SET_REFCOUNT(ptr, 2); + GC_TYPE_INFO(ptr) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + return ptr; +} + +/* Locate the post-persistence NWA payload that holds this monomorph's + * bindings — `extends` for class monos, the first `implements` entry for + * interface monos, the first `trait_uses` entry for trait monos. Returns + * NULL when the ce isn't a monomorph (no generic_types side table). */ +static const zend_type_named_with_args *zend_persist_mono_binding_nwa(const zend_class_entry *ce) +{ + if (!ce->generic_types) { + return NULL; + } + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + return ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + } + HashTable *tables[] = { ce->generic_types->implements, ce->generic_types->trait_uses }; + for (size_t k = 0; k < sizeof(tables) / sizeof(tables[0]); k++) { + if (!tables[k] || zend_hash_num_elements(tables[k]) == 0) continue; + zval *zv; + ZEND_HASH_FOREACH_VAL(tables[k], zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (boxed && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + return ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + } + break; + } ZEND_HASH_FOREACH_END(); + } + return NULL; +} + +/* Persist a monomorph's generic_type_args. Each entry's `type_ref` was + * borrowed from a heap NWA payload that has just been relocated into SHM + * as part of the surrounding ce->generic_types; rebuild type_ref against + * the SHM payload so it stays valid after the heap source is freed. */ +static zend_type_arg_table *zend_persist_type_arg_table( + zend_type_arg_table *table, const zend_class_entry *ce) +{ + if (!table) { + return NULL; + } + /* ce == NULL: function-level table with concrete owned_type entries, no binding to rebind against. */ + const zend_type_named_with_args *new_nwa = ce ? zend_persist_mono_binding_nwa(ce) : NULL; + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + zend_accel_store_interned_string(table->entries[i].name); + } + table->entries[i].type_ref = + (new_nwa && i < new_nwa->count) ? &new_nwa->args[i] : NULL; + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_persist_type(&table->entries[i].owned_type); + } + } + zend_type_arg_table *persisted = zend_shared_memdup_put_free( + table, ZEND_TYPE_ARG_TABLE_SIZE(table->count)); + persisted->persisted = true; + return persisted; +} + +static zend_generic_type_table *zend_persist_generic_type_table(zend_generic_type_table *table) +{ + if (!table) { + return NULL; + } + + zend_generic_type_table *persisted = zend_shared_memdup_put_free(table, sizeof(*table)); + persisted->persisted = true; + if (persisted->return_type) { + persisted->return_type = zend_shared_memdup_put_free(persisted->return_type, sizeof(zend_type)); + zend_persist_type(persisted->return_type); + } + + if (persisted->extends) { + persisted->extends = zend_shared_memdup_put_free(persisted->extends, sizeof(zend_type)); + zend_persist_type(persisted->extends); + } + + if (persisted->parameters) { + persisted->parameters = zend_persist_generic_type_table_ht(persisted->parameters); + } + + if (persisted->properties) { + persisted->properties = zend_persist_generic_type_table_ht(persisted->properties); + } + + if (persisted->class_constants) { + persisted->class_constants = zend_persist_generic_type_table_ht(persisted->class_constants); + } + + if (persisted->implements) { + persisted->implements = zend_persist_generic_type_table_ht(persisted->implements); + } + + if (persisted->trait_uses) { + persisted->trait_uses = zend_persist_generic_type_table_ht(persisted->trait_uses); + } + + if (persisted->turbofish_args) { + persisted->turbofish_args = zend_persist_turbofish_args_ht(persisted->turbofish_args); + } + + if (persisted->monomorph_type_args) { + persisted->monomorph_type_args = + zend_persist_type_arg_table(persisted->monomorph_type_args, NULL); + } + + /* Precompute the value-check plan into SHM (relocated via the persist arena, + * which also works in file_cache mode where no SHM segment is locked). */ + if (persisted->parameters) { + 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; +} + static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_script* main_persistent_script) { zend_op *persist_ptr; @@ -456,8 +751,31 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { arg_info--; } - arg_info = zend_shared_alloc_get_xlat_entry(arg_info); - ZEND_ASSERT(arg_info != NULL); + zend_arg_info *xlat_arg_info = zend_shared_alloc_get_xlat_entry(arg_info); + if (xlat_arg_info) { + arg_info = xlat_arg_info; + } else { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + arg_info = zend_shared_memdup_put(arg_info, sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + zend_accel_store_interned_string(arg_info[i].name); + } + + zend_persist_type(&arg_info[i].type); + if (arg_info[i].doc_comment) { + zend_accel_store_interned_string(arg_info[i].doc_comment); + } + } + } if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { arg_info++; } @@ -492,6 +810,16 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc op_array->dynamic_func_defs = zend_shared_alloc_get_xlat_entry(op_array->dynamic_func_defs); ZEND_ASSERT(op_array->dynamic_func_defs != NULL); } + if (op_array->generic_parameters) { + zend_generic_parameter_list *xlat_params = zend_shared_alloc_get_xlat_entry(op_array->generic_parameters); + ZEND_ASSERT(xlat_params != NULL); + op_array->generic_parameters = xlat_params; + } + if (op_array->generic_types) { + zend_generic_type_table *xlat_types = zend_shared_alloc_get_xlat_entry(op_array->generic_types); + ZEND_ASSERT(xlat_types != NULL); + op_array->generic_types = xlat_types; + } ZCG(mem) = (void*)((char*)ZCG(mem) + ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist(op_array, ZCG(mem)))); return; } @@ -503,6 +831,47 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc if (op_array->scope && !(op_array->fn_flags & ZEND_ACC_CLOSURE) && (op_array->scope->ce_flags & ZEND_ACC_CACHED)) { + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_accel_in_shm(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + zend_arg_info *xlat_arg_info = zend_shared_alloc_get_xlat_entry(arg_info); + if (xlat_arg_info) { + arg_info = xlat_arg_info; + } else { + arg_info = zend_shared_memdup_put(arg_info, sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + zend_accel_store_interned_string(arg_info[i].name); + } + + zend_persist_type(&arg_info[i].type); + if (arg_info[i].doc_comment) { + zend_accel_store_interned_string(arg_info[i].doc_comment); + } + } + } + + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info++; + } + + op_array->arg_info = arg_info; + } + } + return; } @@ -699,6 +1068,14 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc } } + if (op_array->generic_parameters) { + op_array->generic_parameters = zend_persist_generic_parameter_list(op_array->generic_parameters); + } + + if (op_array->generic_types) { + op_array->generic_types = zend_persist_generic_type_table(op_array->generic_types); + } + ZCG(mem) = (void*)((char*)ZCG(mem) + ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist(op_array, ZCG(mem)))); } @@ -867,6 +1244,62 @@ static zend_property_info *zend_persist_property_info(zend_property_info *prop) return prop; } +static zend_property_info *zend_persist_substituted_property_info(zend_property_info *prop) +{ + zend_class_entry *ce; + prop = zend_shared_memdup_put(prop, sizeof(zend_property_info)); + ce = zend_shared_alloc_get_xlat_entry(prop->ce); + if (ce) { + prop->ce = ce; + } + + if (prop->name) { + zend_string *xlat_name = zend_shared_alloc_get_xlat_entry(prop->name); + if (xlat_name) { + prop->name = xlat_name; + } + } + + if (prop->doc_comment) { + zend_string *xlat_doc = zend_shared_alloc_get_xlat_entry(prop->doc_comment); + if (xlat_doc) { + prop->doc_comment = xlat_doc; + } + } + + if (prop->attributes) { + HashTable *xlat_attrs = zend_shared_alloc_get_xlat_entry(prop->attributes); + if (xlat_attrs) { + prop->attributes = xlat_attrs; + } + } + + if (prop->prototype) { + const zend_property_info *xlat_proto = zend_shared_alloc_get_xlat_entry(prop->prototype); + if (xlat_proto) { + prop->prototype = xlat_proto; + } + } + + zend_persist_type(&prop->type); + if (prop->hooks) { + prop->hooks = zend_shared_memdup_put(prop->hooks, ZEND_PROPERTY_HOOK_STRUCT_SIZE); + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + zend_op_array *hook = zend_persist_class_method(&prop->hooks[i]->op_array, prop->ce); + const zend_property_info *new_prop_info = zend_shared_alloc_get_xlat_entry(hook->prop_info); + if (new_prop_info) { + hook->prop_info = new_prop_info; + } + + prop->hooks[i] = (zend_function *) hook; + } + } + } + + return prop; +} + static void zend_persist_class_constant(zval *zv) { const zend_class_constant *orig_c = Z_PTR_P(zv); @@ -1002,9 +1435,11 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) if (prop->ce == orig_ce) { Z_PTR(p->val) = zend_persist_property_info(prop); } else { - prop = zend_shared_alloc_get_xlat_entry(prop); - if (prop) { - Z_PTR(p->val) = prop; + zend_property_info *xlat_prop = zend_shared_alloc_get_xlat_entry(prop); + if (xlat_prop) { + Z_PTR(p->val) = xlat_prop; + } else if (prop->flags & ZEND_ACC_GENERIC_CLONE) { + Z_PTR(p->val) = zend_persist_substituted_property_info(prop); } else { /* This can happen if preloading is used and we inherit a property from an * internal class. In that case we should keep pointing to the internal @@ -1066,6 +1501,18 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) ce->attributes = zend_persist_attributes(ce->attributes); } + if (ce->generic_parameters) { + ce->generic_parameters = zend_persist_generic_parameter_list(ce->generic_parameters); + } + + if (ce->generic_types) { + ce->generic_types = zend_persist_generic_type_table(ce->generic_types); + } + + if (ce->generic_type_args) { + ce->generic_type_args = zend_persist_type_arg_table(ce->generic_type_args, ce); + } + if (ce->num_interfaces && !(ce->ce_flags & ZEND_ACC_LINKED)) { uint32_t i = 0; diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 9ff37079193b..d4558c16eab9 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -46,6 +46,7 @@ static void zend_persist_zval_calc(zval *z); static void zend_persist_op_array_calc(const zval *zv); +static void zend_persist_type_calc(zend_type *type); static void zend_hash_persist_calc(const HashTable *ht) { @@ -191,12 +192,37 @@ static void zend_persist_attributes_calc(HashTable *attributes) } zend_persist_zval_calc(&attr->args[i].value); } + + if (attr->generic_args) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(attr->generic_args); + } } ZEND_HASH_FOREACH_END(); } } static void zend_persist_type_calc(zend_type *type) { + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*type)) { + zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(*type); + ADD_SIZE(sizeof(*ref)); + ADD_INTERNED_STRING(ref->name); + return; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*type)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*type); + ADD_SIZE(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(named->count)); + if (named->name) { + ADD_INTERNED_STRING(named->name); + } + for (uint32_t i = 0; i < named->count; i++) { + zend_persist_type_calc(&named->args[i]); + } + + return; + } + if (ZEND_TYPE_HAS_LIST(*type)) { ADD_SIZE(ZEND_TYPE_LIST_SIZE(ZEND_TYPE_LIST(*type)->num_types)); } @@ -207,6 +233,17 @@ static void zend_persist_type_calc(zend_type *type) zend_persist_type_calc(single_type); continue; } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(*single_type)) { + zend_persist_type_calc(single_type); + continue; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(*single_type)) { + zend_persist_type_calc(single_type); + continue; + } + if (ZEND_TYPE_HAS_NAME(*single_type)) { zend_string *type_name = ZEND_TYPE_NAME(*single_type); ADD_INTERNED_STRING(type_name); @@ -215,6 +252,172 @@ static void zend_persist_type_calc(zend_type *type) } ZEND_TYPE_FOREACH_END(); } +static void zend_persist_generic_parameter_list_calc(zend_generic_parameter_list *list) +{ + if (!list) { + return; + } + + ADD_SIZE(ZEND_GENERIC_PARAMETER_LIST_SIZE(list->count)); + for (uint32_t i = 0; i < list->count; i++) { + ADD_INTERNED_STRING(list->parameters[i].name); + zend_persist_type_calc(&list->parameters[i].bound); + zend_persist_type_calc(&list->parameters[i].bound_pre_erasure); + zend_persist_type_calc(&list->parameters[i].default_type); + zend_persist_type_calc(&list->parameters[i].default_pre_erasure); + } +} + +static void zend_persist_generic_type_table_ht_calc(HashTable *ht) +{ + zend_hash_persist_calc(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc((zend_type *) Z_PTR_P(v)); + } ZEND_HASH_FOREACH_END(); + } else { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + ADD_INTERNED_STRING(p->key); + } + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc((zend_type *) Z_PTR(p->val)); + } ZEND_HASH_FOREACH_END(); + } + 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. */ +static void zend_persist_turbofish_args_ht_calc(HashTable *ht) +{ + zend_hash_persist_calc(ht); + if (HT_IS_PACKED(ht)) { + zval *v; + ZEND_HASH_PACKED_FOREACH_VAL(ht, v) { + ADD_SIZE(sizeof(zend_turbofish_args_entry)); + 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; + ZEND_HASH_MAP_FOREACH_BUCKET(ht, p) { + if (p->key) { + ADD_INTERNED_STRING(p->key); + } + ADD_SIZE(sizeof(zend_turbofish_args_entry)); + 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)); +} + +static void zend_persist_type_arg_table_calc(zend_type_arg_table *table); + +static void zend_persist_generic_type_table_calc(zend_generic_type_table *table) +{ + if (!table) { + return; + } + + ADD_SIZE(sizeof(*table)); + if (table->return_type) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(table->return_type); + } + + if (table->extends) { + ADD_SIZE(sizeof(zend_type)); + zend_persist_type_calc(table->extends); + } + + if (table->parameters) { + zend_persist_generic_type_table_ht_calc(table->parameters); + } + + if (table->properties) { + zend_persist_generic_type_table_ht_calc(table->properties); + } + + if (table->class_constants) { + zend_persist_generic_type_table_ht_calc(table->class_constants); + } + + if (table->implements) { + zend_persist_generic_type_table_ht_calc(table->implements); + } + + if (table->trait_uses) { + zend_persist_generic_type_table_ht_calc(table->trait_uses); + } + + if (table->turbofish_args) { + zend_persist_turbofish_args_ht_calc(table->turbofish_args); + } + + if (table->monomorph_type_args) { + zend_persist_type_arg_table_calc(table->monomorph_type_args); + } + + /* Mirror the value-check plan reservation in zend_persist_generic_type_table. */ + if (table->parameters) { + uint32_t cnt = zend_count_generic_value_checks(table->parameters); + 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) +{ + if (!table) { + return; + } + ADD_SIZE(ZEND_TYPE_ARG_TABLE_SIZE(table->count)); + for (uint32_t i = 0; i < table->count; i++) { + if (table->entries[i].name) { + ADD_INTERNED_STRING(table->entries[i].name); + } + if (ZEND_TYPE_IS_SET(table->entries[i].owned_type)) { + zend_persist_type_calc(&table->entries[i].owned_type); + } + } +} + static void zend_persist_op_array_calc_ex(zend_op_array *op_array) { if (op_array->function_name) { @@ -231,6 +434,36 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) if (zend_shared_alloc_get_xlat_entry(op_array->opcodes)) { /* already stored */ ADD_SIZE(ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist_calc(op_array))); + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_shared_alloc_get_xlat_entry(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + ADD_SIZE(sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + ADD_INTERNED_STRING(arg_info[i].name); + } + + zend_persist_type_calc(&arg_info[i].type); + if (arg_info[i].doc_comment) { + ADD_INTERNED_STRING(arg_info[i].doc_comment); + } + } + } + } + return; } } @@ -238,6 +471,36 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) if (op_array->scope && !(op_array->fn_flags & ZEND_ACC_CLOSURE) && (op_array->scope->ce_flags & ZEND_ACC_CACHED)) { + if (op_array->arg_info) { + zend_arg_info *arg_info = op_array->arg_info; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + arg_info--; + } + + if (!zend_accel_in_shm(arg_info) && !zend_shared_alloc_get_xlat_entry(arg_info)) { + uint32_t num_args = op_array->num_args; + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + num_args++; + } + + if (op_array->fn_flags & ZEND_ACC_VARIADIC) { + num_args++; + } + + ADD_SIZE(sizeof(zend_arg_info) * num_args); + for (uint32_t i = 0; i < num_args; i++) { + if (arg_info[i].name) { + ADD_INTERNED_STRING(arg_info[i].name); + } + + zend_persist_type_calc(&arg_info[i].type); + if (arg_info[i].doc_comment) { + ADD_INTERNED_STRING(arg_info[i].doc_comment); + } + } + } + } + return; } @@ -344,6 +607,9 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array) } } + zend_persist_generic_parameter_list_calc(op_array->generic_parameters); + zend_persist_generic_type_table_calc(op_array->generic_types); + ADD_SIZE(ZEND_ALIGNED_SIZE(zend_extensions_op_array_persist_calc(op_array))); } @@ -420,6 +686,20 @@ static void zend_persist_property_info_calc(zend_property_info *prop) } } +static void zend_persist_substituted_property_info_calc(zend_property_info *prop) +{ + ADD_SIZE(sizeof(zend_property_info)); + zend_persist_type_calc(&prop->type); + if (prop->hooks) { + ADD_SIZE(ZEND_PROPERTY_HOOK_STRUCT_SIZE); + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + zend_persist_class_method_calc(&prop->hooks[i]->op_array); + } + } + } +} + static void zend_persist_class_constant_calc(const zval *zv) { zend_class_constant *c = Z_PTR_P(zv); @@ -503,6 +783,9 @@ void zend_persist_class_entry_calc(zend_class_entry *ce) ADD_INTERNED_STRING(p->key); if (prop->ce == ce) { zend_persist_property_info_calc(prop); + } else if ((prop->flags & ZEND_ACC_GENERIC_CLONE) && !zend_shared_alloc_get_xlat_entry(prop)) { + zend_shared_alloc_register_xlat_entry(prop, prop); + zend_persist_substituted_property_info_calc(prop); } } ZEND_HASH_FOREACH_END(); @@ -537,6 +820,10 @@ void zend_persist_class_entry_calc(zend_class_entry *ce) zend_persist_attributes_calc(ce->attributes); } + zend_persist_generic_parameter_list_calc(ce->generic_parameters); + zend_persist_generic_type_table_calc(ce->generic_types); + zend_persist_type_arg_table_calc(ce->generic_type_args); + if (ce->num_interfaces) { uint32_t i; diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index a208741b1590..0e4feebe1045 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -45,6 +45,7 @@ #include "zend_closures.h" #include "zend_generators.h" #include "zend_extensions.h" +#include "zend_inheritance.h" #include "zend_builtin_functions.h" #include "zend_smart_str.h" #include "zend_enum.h" @@ -102,6 +103,9 @@ PHPAPI zend_class_entry *reflection_enum_unit_case_ptr; PHPAPI zend_class_entry *reflection_enum_backed_case_ptr; PHPAPI zend_class_entry *reflection_fiber_ptr; PHPAPI zend_class_entry *reflection_constant_ptr; +PHPAPI zend_class_entry *reflection_generic_type_parameter_ptr; +PHPAPI zend_class_entry *reflection_type_parameter_reference_ptr; +PHPAPI zend_class_entry *reflection_generic_variance_ptr; PHPAPI zend_class_entry *reflection_property_hook_type_ptr; #define GET_REFLECTION_OBJECT() do { \ @@ -120,6 +124,19 @@ PHPAPI zend_class_entry *reflection_property_hook_type_ptr; target = intern->ptr; \ } while (0) +/* When reflecting a bare generic class, swap in the defaults monomorph (same + * contract as `new ClassName()`); throws and returns from the caller if any + * type parameter has no default. Reflecting an existing monomorph leaves + * `ce` unchanged. */ +#define REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce) do { \ + if ((ce)->generic_parameters) { \ + (ce) = zend_get_defaults_monomorph(ce); \ + if (UNEXPECTED((ce) == NULL)) { \ + RETURN_THROWS(); \ + } \ + } \ +} while (0) + /* {{{ Object structure */ /* Struct for properties */ @@ -140,10 +157,132 @@ typedef struct _parameter_reference { /* Struct for type hints */ typedef struct _type_reference { zend_type type; + bool owns_type; /* Whether to use backwards compatible null representation */ bool legacy_behavior; + /* Optional pre-erasure form for generic-aware reflection. */ + zend_type pre_erasure; + bool owns_pre_erasure; + /* Declaring entity for type-parameter resolution. */ + zend_class_entry *declaring_class; + zend_function *declaring_fn; } type_reference; +static zend_type reflection_type_copy(zend_type type); + +static zend_type_list *reflection_type_list_copy(const zend_type_list *old_list) +{ + size_t size = ZEND_TYPE_LIST_SIZE(old_list->num_types); + zend_type_list *new_list = emalloc(size); + new_list->num_types = old_list->num_types; + + for (uint32_t i = 0; i < old_list->num_types; i++) { + new_list->types[i] = reflection_type_copy(old_list->types[i]); + } + + return new_list; +} + +static zend_type_named_with_args *reflection_named_with_args_copy( + const zend_type_named_with_args *old_named) +{ + zend_type_named_with_args *new_named = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(old_named->count)); + new_named->name = old_named->name ? zend_string_copy(old_named->name) : NULL; + new_named->name_attr = old_named->name_attr; + new_named->count = old_named->count; + + for (uint32_t i = 0; i < old_named->count; i++) { + new_named->args[i] = reflection_type_copy(old_named->args[i]); + } + + return new_named; +} + +static zend_type_parameter_ref *reflection_type_parameter_ref_copy( + const zend_type_parameter_ref *old_ref) +{ + zend_type_parameter_ref *new_ref = emalloc(sizeof(zend_type_parameter_ref)); + *new_ref = *old_ref; + if (new_ref->name) { + zend_string_addref(new_ref->name); + } + return new_ref; +} + +static zend_type reflection_type_copy(zend_type type) +{ + zend_type result = type; + + if (ZEND_TYPE_HAS_LIST(type)) { + ZEND_TYPE_SET_LIST(result, reflection_type_list_copy(ZEND_TYPE_LIST(type))); + ZEND_TYPE_FULL_MASK(result) &= ~_ZEND_TYPE_ARENA_BIT; + } else if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + ZEND_TYPE_SET_PTR(result, reflection_named_with_args_copy(ZEND_TYPE_NAMED_WITH_ARGS(type))); + } else if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + ZEND_TYPE_SET_PTR(result, reflection_type_parameter_ref_copy(ZEND_TYPE_TYPE_PARAMETER(type))); + } else if (ZEND_TYPE_HAS_NAME(type)) { + zend_string_addref(ZEND_TYPE_NAME(type)); + } + + return result; +} + +static zend_type reflection_type_substitute_class_params( + zend_type type, const zend_type *args, uint32_t arity) +{ + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + const zend_type_parameter_ref *ref = ZEND_TYPE_TYPE_PARAMETER(type); + if (ref->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE && ref->index < arity) { + zend_type result = reflection_type_copy(args[ref->index]); + if (ZEND_TYPE_FULL_MASK(type) & _ZEND_TYPE_NULLABLE_BIT) { + ZEND_TYPE_FULL_MASK(result) |= _ZEND_TYPE_NULLABLE_BIT; + } + return result; + } + } + + if (ZEND_TYPE_HAS_LIST(type)) { + zend_type result = type; + const zend_type_list *old_list = ZEND_TYPE_LIST(type); + size_t size = ZEND_TYPE_LIST_SIZE(old_list->num_types); + zend_type_list *new_list = emalloc(size); + new_list->num_types = old_list->num_types; + for (uint32_t i = 0; i < old_list->num_types; i++) { + new_list->types[i] = reflection_type_substitute_class_params( + old_list->types[i], args, arity); + } + ZEND_TYPE_SET_LIST(result, new_list); + ZEND_TYPE_FULL_MASK(result) &= ~_ZEND_TYPE_ARENA_BIT; + return result; + } + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + zend_type result = type; + const zend_type_named_with_args *old_named = ZEND_TYPE_NAMED_WITH_ARGS(type); + zend_type_named_with_args *new_named = emalloc(ZEND_TYPE_NAMED_WITH_ARGS_SIZE(old_named->count)); + new_named->name = old_named->name ? zend_string_copy(old_named->name) : NULL; + new_named->name_attr = old_named->name_attr; + new_named->count = old_named->count; + for (uint32_t i = 0; i < old_named->count; i++) { + new_named->args[i] = reflection_type_substitute_class_params( + old_named->args[i], args, arity); + } + ZEND_TYPE_SET_PTR(result, new_named); + return result; + } + + return reflection_type_copy(type); +} + +/* Struct for generic type-parameter reflection. Points back at a parameter slot + * inside the declaring entity's zend_generic_parameter_list. Lifetime is bound + * to the declaring class_entry / op_array. */ +typedef struct _generic_parameter_reference { + zend_generic_parameter *param; /* points into a zend_generic_parameter_list */ + uint32_t index; /* position in the list */ + zval declaring; /* zval-wrapped ReflectionClass / ReflectionFunctionAbstract */ +} generic_parameter_reference; + /* Struct for attributes */ typedef struct _attribute_reference { HashTable *attributes; @@ -162,7 +301,8 @@ typedef enum { REF_TYPE_TYPE, REF_TYPE_PROPERTY, REF_TYPE_CLASS_CONSTANT, - REF_TYPE_ATTRIBUTE + REF_TYPE_ATTRIBUTE, + REF_TYPE_GENERIC_PARAMETER } reflection_type_t; /* Struct for reflection objects */ @@ -242,9 +382,14 @@ static void reflection_free_objects_storage(zend_object *object) /* {{{ */ case REF_TYPE_TYPE: { type_reference *type_ref = intern->ptr; - if (ZEND_TYPE_HAS_NAME(type_ref->type)) { + if (type_ref->owns_type) { + zend_type_release(type_ref->type, /* persistent */ false); + } else if (ZEND_TYPE_HAS_NAME(type_ref->type)) { zend_string_release(ZEND_TYPE_NAME(type_ref->type)); } + if (type_ref->owns_pre_erasure) { + zend_type_release(type_ref->pre_erasure, /* persistent */ false); + } efree(type_ref); break; } @@ -262,6 +407,12 @@ static void reflection_free_objects_storage(zend_object *object) /* {{{ */ efree(intern->ptr); break; } + case REF_TYPE_GENERIC_PARAMETER: { + generic_parameter_reference *ref = intern->ptr; + zval_ptr_dtor(&ref->declaring); + efree(ref); + break; + } case REF_TYPE_GENERATOR: case REF_TYPE_FIBER: case REF_TYPE_CLASS_CONSTANT: @@ -1493,11 +1644,69 @@ static reflection_type_kind get_type_kind(zend_type type) { return NAMED_TYPE; } -/* {{{ reflection_type_factory */ -static void reflection_type_factory(zend_type type, zval *object, bool legacy_behavior) +static zend_class_entry *reflection_resolve_declaring_class(zend_class_entry *ce) +{ + return (ce && ce->generic_parameters) ? ce : NULL; +} + +static void reflection_resolve_fn_context( + zend_function *fptr, + zend_class_entry **declaring_class_out, + zend_function **declaring_fn_out) +{ + *declaring_class_out = NULL; + *declaring_fn_out = NULL; + if (fptr->type != ZEND_USER_FUNCTION) { + return; + } + + const zend_op_array *op = &fptr->op_array; + if (op->generic_parameters) { + *declaring_fn_out = fptr; + } + + *declaring_class_out = reflection_resolve_declaring_class(op->scope); +} + +/* {{{ reflection_type_factory_ex */ +static void reflection_type_factory_ex_impl( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn, + bool copy_type, + bool copy_pre_erasure) { reflection_object *intern; type_reference *reference; + + if (ZEND_TYPE_HAS_NAMED_WITH_ARGS(type)) { + ZEND_ASSERT(!ZEND_TYPE_HAS_NAMED_WITH_ARGS(pre_erasure)); + pre_erasure = type; + copy_pre_erasure = copy_type; + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(type); + type = (zend_type) ZEND_TYPE_INIT_CLASS(named->name, 0, 0); + copy_type = false; + } + + if (ZEND_TYPE_HAS_TYPE_PARAMETER(type)) { + object_init_ex(object, reflection_type_parameter_reference_ptr); + intern = Z_REFLECTION_P(object); + reference = (type_reference*) emalloc(sizeof(type_reference)); + reference->type = copy_type ? reflection_type_copy(type) : type; + reference->owns_type = copy_type; + reference->legacy_behavior = false; + reference->pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + reference->owns_pre_erasure = false; + reference->declaring_class = declaring_class; + reference->declaring_fn = declaring_fn; + intern->ptr = reference; + intern->ref_type = REF_TYPE_TYPE; + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(type); + ZVAL_STR_COPY(reflection_prop_name(object), tp->name); + return; + } + reflection_type_kind type_kind = get_type_kind(type); bool is_mixed = ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY; bool is_only_null = (ZEND_TYPE_PURE_MASK(type) == MAY_BE_NULL && !ZEND_TYPE_IS_COMPLEX(type)); @@ -1517,8 +1726,14 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be intern = Z_REFLECTION_P(object); reference = (type_reference*) emalloc(sizeof(type_reference)); - reference->type = type; + reference->type = copy_type ? reflection_type_copy(type) : type; + reference->owns_type = copy_type; reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed && !is_only_null; + reference->pre_erasure = copy_pre_erasure && ZEND_TYPE_IS_SET(pre_erasure) + ? reflection_type_copy(pre_erasure) : pre_erasure; + reference->owns_pre_erasure = copy_pre_erasure && ZEND_TYPE_IS_SET(pre_erasure); + reference->declaring_class = declaring_class; + reference->declaring_fn = declaring_fn; intern->ptr = reference; intern->ref_type = REF_TYPE_TYPE; @@ -1527,12 +1742,37 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be * do this for the top-level type, as resolutions inside type lists will be * fully visible to us (we'd have to do a fully copy of the type if we wanted * to prevent that). */ - if (ZEND_TYPE_HAS_NAME(type)) { + if (!copy_type && ZEND_TYPE_HAS_NAME(type)) { zend_string_addref(ZEND_TYPE_NAME(type)); } } + +static void reflection_type_factory_ex( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn) +{ + reflection_type_factory_ex_impl(type, object, legacy_behavior, pre_erasure, + declaring_class, declaring_fn, false, false); +} + +static void reflection_type_factory_ex_copy( + zend_type type, zval *object, bool legacy_behavior, + zend_type pre_erasure, + zend_class_entry *declaring_class, + zend_function *declaring_fn) +{ + reflection_type_factory_ex_impl(type, object, legacy_behavior, pre_erasure, + declaring_class, declaring_fn, true, ZEND_TYPE_IS_SET(pre_erasure)); +} /* }}} */ +static void reflection_type_factory(zend_type type, zval *object, bool legacy_behavior) +{ + reflection_type_factory_ex(type, object, legacy_behavior, (zend_type) ZEND_TYPE_INIT_NONE(0), NULL, NULL); +} + /* {{{ reflection_function_factory */ static void reflection_function_factory(zend_function *function, zval *closure_object, zval *object) { @@ -2802,7 +3042,23 @@ ZEND_METHOD(ReflectionParameter, getType) if (!ZEND_TYPE_IS_SET(param->arg_info->type)) { RETURN_NULL(); } - reflection_type_factory(param->arg_info->type, return_value, true); + + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *declaring_class; + zend_function *declaring_fn; + reflection_resolve_fn_context(param->fptr, &declaring_class, &declaring_fn); + if (param->fptr->type == ZEND_USER_FUNCTION) { + zend_op_array *op = ¶m->fptr->op_array; + if (op->generic_types && op->generic_types->parameters) { + zend_type *boxed = zend_hash_index_find_ptr(op->generic_types->parameters, param->offset); + if (boxed) { + pre_erasure = *boxed; + } + } + } + + reflection_type_factory_ex(param->arg_info->type, return_value, true, + pre_erasure, declaring_class, declaring_fn); } /* }}} */ @@ -3674,7 +3930,19 @@ ZEND_METHOD(ReflectionFunctionAbstract, getReturnType) RETURN_NULL(); } - reflection_type_factory(fptr->common.arg_info[-1].type, return_value, true); + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *declaring_class; + zend_function *declaring_fn; + reflection_resolve_fn_context(fptr, &declaring_class, &declaring_fn); + if (fptr->type == ZEND_USER_FUNCTION) { + zend_op_array *op = &fptr->op_array; + if (op->generic_types && op->generic_types->return_type) { + pre_erasure = *op->generic_types->return_type; + } + } + + reflection_type_factory_ex(fptr->common.arg_info[-1].type, return_value, true, + pre_erasure, declaring_class, declaring_fn); } /* }}} */ @@ -5028,6 +5296,8 @@ ZEND_METHOD(ReflectionClass, newInstance) GET_REFLECTION_OBJECT_PTR(ce); + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { return; } @@ -5082,6 +5352,8 @@ ZEND_METHOD(ReflectionClass, newInstanceWithoutConstructor) RETURN_THROWS(); } + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + object_init_ex(return_value, ce); } /* }}} */ @@ -5105,6 +5377,8 @@ ZEND_METHOD(ReflectionClass, newInstanceArgs) argc = zend_hash_num_elements(args); } + REFLECTION_RESOLVE_GENERIC_DEFAULTS(ce); + if (UNEXPECTED(object_init_ex(return_value, ce) != SUCCESS)) { return; } @@ -6422,7 +6696,17 @@ ZEND_METHOD(ReflectionProperty, getType) RETURN_NULL(); } - reflection_type_factory(ref->prop->type, return_value, true); + zend_type pre_erasure = (zend_type) ZEND_TYPE_INIT_NONE(0); + zend_class_entry *ce = ref->prop->ce; + if (ce && ce->generic_types && ce->generic_types->properties) { + zend_type *boxed = zend_hash_find_ptr(ce->generic_types->properties, ref->prop->name); + if (boxed) { + pre_erasure = *boxed; + } + } + + reflection_type_factory_ex(ref->prop->type, return_value, true, + pre_erasure, reflection_resolve_declaring_class(ce), NULL); } /* }}} */ @@ -7620,6 +7904,13 @@ ZEND_METHOD(ReflectionAttribute, newInstance) } } + if (attr->data->generic_arity > 0) { + zend_check_generic_new_arguments(ce, attr->data->generic_arity, attr->data->generic_args); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } + } + zval obj; if (SUCCESS != zend_get_attribute_object(&obj, ce, attr->data, attr->scope, attr->filename)) { @@ -8168,6 +8459,750 @@ ZEND_METHOD(ReflectionConstant, __toString) RETURN_STR(smart_str_extract(&str)); } +ZEND_METHOD(ReflectionGenericTypeParameter, __construct) +{ + zend_throw_exception(reflection_exception_ptr, + "Cannot directly instantiate ReflectionGenericTypeParameter", 0); +} + +ZEND_METHOD(ReflectionTypeParameterReference, __construct) +{ + zend_throw_exception(reflection_exception_ptr, + "Cannot directly instantiate ReflectionTypeParameterReference", 0); +} + + +static void reflection_generic_type_parameter_factory( + zend_generic_parameter *param, uint32_t index, zval *declaring, zval *object) +{ + reflection_object *intern; + generic_parameter_reference *reference; + + object_init_ex(object, reflection_generic_type_parameter_ptr); + intern = Z_REFLECTION_P(object); + reference = emalloc(sizeof(generic_parameter_reference)); + reference->param = param; + reference->index = index; + ZVAL_COPY(&reference->declaring, declaring); + intern->ptr = reference; + intern->ref_type = REF_TYPE_GENERIC_PARAMETER; + + ZVAL_STR_COPY(reflection_prop_name(object), param->name); +} + +static void reflection_build_generic_parameters_array( + zend_generic_parameter_list *list, zval *declaring, zval *return_value) +{ + if (!list) { + array_init(return_value); + return; + } + array_init_size(return_value, list->count); + for (uint32_t i = 0; i < list->count; i++) { + zval entry; + reflection_generic_type_parameter_factory(&list->parameters[i], i, declaring, &entry); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +ZEND_METHOD(ReflectionFunctionAbstract, isGeneric) +{ + reflection_object *intern; + zend_function *fptr; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(fptr); + + if (fptr->type != ZEND_USER_FUNCTION) { + RETURN_FALSE; + } + RETURN_BOOL(fptr->op_array.generic_parameters != NULL); +} + +ZEND_METHOD(ReflectionFunctionAbstract, getGenericParameters) +{ + reflection_object *intern; + zend_function *fptr; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(fptr); + + if (fptr->type != ZEND_USER_FUNCTION) { + array_init(return_value); + return; + } + reflection_build_generic_parameters_array(fptr->op_array.generic_parameters, ZEND_THIS, return_value); +} + +ZEND_METHOD(ReflectionClass, isGeneric) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + RETURN_BOOL(ce->generic_parameters != NULL); +} + +ZEND_METHOD(ReflectionClass, getGenericParameters) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + reflection_build_generic_parameters_array(ce->generic_parameters, ZEND_THIS, return_value); +} + +#define GET_GENERIC_PARAMETER_REFERENCE(intern, param_ref) \ + do { \ + (intern) = Z_REFLECTION_P(getThis()); \ + if ((intern)->ptr == NULL) { \ + zend_throw_error(NULL, "Internal error: Failed to retrieve the type parameter reference"); \ + RETURN_THROWS(); \ + } \ + (param_ref) = (generic_parameter_reference *) (intern)->ptr; \ + } while (0) + +ZEND_METHOD(ReflectionGenericTypeParameter, getName) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_STR_COPY(ref->param->name); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getPosition) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_LONG((zend_long) ref->index); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getVariance) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + const char *case_name; + switch (ref->param->variance) { + case 1: case_name = "Covariant"; break; + case 2: case_name = "Contravariant"; break; + default: case_name = "Invariant"; break; + } + zend_object *case_obj = zend_enum_get_case_cstr(reflection_generic_variance_ptr, case_name); + if (!case_obj) { + zend_throw_error(NULL, "Internal error: ReflectionGenericVariance enum case not found"); + RETURN_THROWS(); + } + ZVAL_OBJ_COPY(return_value, case_obj); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, hasBound) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_BOOL(ZEND_TYPE_IS_SET(ref->param->bound)); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getBound) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + if (!ZEND_TYPE_IS_SET(ref->param->bound)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Type parameter %s has no bound", ZSTR_VAL(ref->param->name)); + RETURN_THROWS(); + } + zend_class_entry *declaring_class = NULL; + zend_function *declaring_fn = NULL; + if (Z_TYPE(ref->declaring) == IS_OBJECT) { + zend_object *obj = Z_OBJ(ref->declaring); + if (instanceof_function(obj->ce, reflection_class_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_class = decl_intern->ptr; + } else if (instanceof_function(obj->ce, reflection_function_abstract_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_fn = decl_intern->ptr; + } + } + zend_type primary = ZEND_TYPE_IS_SET(ref->param->bound_pre_erasure) + ? ref->param->bound_pre_erasure : ref->param->bound; + reflection_type_factory_ex(primary, return_value, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), declaring_class, declaring_fn); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, hasDefault) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + RETURN_BOOL(ZEND_TYPE_IS_SET(ref->param->default_type)); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getDefault) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + if (!ZEND_TYPE_IS_SET(ref->param->default_type)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Type parameter %s has no default", ZSTR_VAL(ref->param->name)); + RETURN_THROWS(); + } + + zend_class_entry *declaring_class = NULL; + zend_function *declaring_fn = NULL; + if (Z_TYPE(ref->declaring) == IS_OBJECT) { + zend_object *obj = Z_OBJ(ref->declaring); + if (instanceof_function(obj->ce, reflection_class_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_class = decl_intern->ptr; + } else if (instanceof_function(obj->ce, reflection_function_abstract_ptr)) { + reflection_object *decl_intern = reflection_object_from_obj(obj); + declaring_fn = decl_intern->ptr; + } + } + + zend_type primary = ZEND_TYPE_IS_SET(ref->param->default_pre_erasure) + ? ref->param->default_pre_erasure : ref->param->default_type; + reflection_type_factory_ex(primary, return_value, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), declaring_class, declaring_fn); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, getDeclaringEntity) +{ + reflection_object *intern; + generic_parameter_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + ZVAL_COPY(return_value, &ref->declaring); +} + +ZEND_METHOD(ReflectionGenericTypeParameter, __toString) +{ + reflection_object *intern; + generic_parameter_reference *ref; + smart_str str = {0}; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_GENERIC_PARAMETER_REFERENCE(intern, ref); + + switch (ref->param->variance) { + case 1: + smart_str_appendc(&str, '+'); + break; + case 2: + smart_str_appendc(&str, '-'); + break; + default: + break; + } + smart_str_append(&str, ref->param->name); + if (ZEND_TYPE_IS_SET(ref->param->bound)) { + zend_string *bound = zend_type_to_string(ref->param->bound); + smart_str_appends(&str, " : "); + smart_str_append(&str, bound); + zend_string_release(bound); + } + if (ZEND_TYPE_IS_SET(ref->param->default_type)) { + zend_string *def = zend_type_to_string(ref->param->default_type); + smart_str_appends(&str, " = "); + smart_str_append(&str, def); + zend_string_release(def); + } + RETURN_NEW_STR(smart_str_extract(&str)); +} + +ZEND_METHOD(ReflectionTypeParameterReference, getName) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + zend_throw_error(NULL, "Type parameter reference has no parameter"); + RETURN_THROWS(); + } + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + RETURN_STR_COPY(tp->name); +} + +ZEND_METHOD(ReflectionTypeParameterReference, getTypeParameter) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + zend_throw_error(NULL, "Type parameter reference has no parameter"); + RETURN_THROWS(); + } + + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + + zend_generic_parameter_list *list = NULL; + zval declaring_zv; + ZVAL_UNDEF(&declaring_zv); + + if (tp->origin == ZEND_GENERIC_ORIGIN_CLASS_LIKE) { + if (!ref->declaring_class) { + zend_throw_error(NULL, + "Type parameter reference has no declaring class context"); + RETURN_THROWS(); + } + + list = ref->declaring_class->generic_parameters; + zend_reflection_class_factory(ref->declaring_class, &declaring_zv); + } else { + if (!ref->declaring_fn) { + zend_throw_error(NULL, + "Type parameter reference has no declaring function context"); + RETURN_THROWS(); + } + + list = ref->declaring_fn->op_array.generic_parameters; + if (ref->declaring_fn->op_array.scope) { + reflection_method_factory(ref->declaring_fn->op_array.scope, + ref->declaring_fn, NULL, &declaring_zv); + } else { + reflection_function_factory(ref->declaring_fn, NULL, &declaring_zv); + } + } + + if (!list || tp->index >= list->count) { + zval_ptr_dtor(&declaring_zv); + zend_throw_error(NULL, + "Type parameter index %u out of range for declaring entity", tp->index); + RETURN_THROWS(); + } + + reflection_generic_type_parameter_factory( + &list->parameters[tp->index], tp->index, &declaring_zv, return_value); + zval_ptr_dtor(&declaring_zv); +} + +ZEND_METHOD(ReflectionTypeParameterReference, allowsNull) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_TRUE; +} + +ZEND_METHOD(ReflectionTypeParameterReference, __toString) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_TYPE_PARAMETER(ref->type)) { + RETURN_EMPTY_STRING(); + } + zend_type_parameter_ref *tp = ZEND_TYPE_TYPE_PARAMETER(ref->type); + RETURN_STR_COPY(tp->name); +} + +ZEND_METHOD(ReflectionNamedType, hasGenericArguments) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + RETURN_BOOL(ZEND_TYPE_HAS_NAMED_WITH_ARGS(ref->pre_erasure)); +} + +ZEND_METHOD(ReflectionNamedType, getGenericArguments) +{ + reflection_object *intern; + type_reference *ref; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ref); + + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(ref->pre_erasure)) { + array_init(return_value); + return; + } + + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(ref->pre_erasure); + array_init_size(return_value, named->count); + for (uint32_t i = 0; i < named->count; i++) { + zval entry; + reflection_type_factory_ex_copy(named->args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + ref->declaring_class, ref->declaring_fn); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +static void reflection_build_args_list_ex(zval *return_value, const zend_type *args, + uint32_t count, zend_class_entry *declaring_class, bool copy_types) +{ + array_init_size(return_value, count); + for (uint32_t i = 0; i < count; i++) { + zval entry; + if (copy_types) { + reflection_type_factory_ex_copy(args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + declaring_class, NULL); + } else { + reflection_type_factory_ex(args[i], &entry, false, + (zend_type) ZEND_TYPE_INIT_NONE(0), + declaring_class, NULL); + } + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + } +} + +static void reflection_build_args_list(zval *return_value, const zend_type *args, + uint32_t count, zend_class_entry *declaring_class) +{ + reflection_build_args_list_ex(return_value, args, count, declaring_class, false); +} + +static void reflection_build_named_args_list(zval *return_value, const zend_type *boxed, + zend_class_entry *declaring_class) +{ + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + reflection_build_args_list(return_value, named->args, named->count, declaring_class); +} + +static void reflection_type_array_release(zend_type *args, uint32_t arity) +{ + for (uint32_t i = 0; i < arity; i++) { + zend_type_release(args[i], /* persistent */ false); + } +} + +static zend_class_entry *reflection_find_interface_by_name( + zend_class_entry *ce, zend_string *name) +{ + if (!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { + return NULL; + } + + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (ce->interfaces[i] && zend_string_equals_ci(ce->interfaces[i]->name, name)) { + return ce->interfaces[i]; + } + } + + return NULL; +} + +static bool reflection_get_direct_inheritance_binding( + zend_class_entry *ce, zend_class_entry *target, + const zend_type **out_args, uint32_t *out_arity) +{ + if (!ce->generic_types) { + return false; + } + + if (ce->generic_types->extends && ZEND_TYPE_HAS_NAMED_WITH_ARGS(*ce->generic_types->extends)) { + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*ce->generic_types->extends); + bool matches = (ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) + ? ce->parent == target + : (named->name && target->name && zend_string_equals_ci(named->name, target->name)); + + if (matches) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } + + if (ce->generic_types->implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(ce->generic_types->implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + continue; + } + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (named->name && zend_string_equals_ci(named->name, target->name)) { + *out_args = named->args; + *out_arity = named->count; + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +static void reflection_append_binding( + zval *return_value, const zend_type *args, uint32_t arity, + const zend_type *ce_args, uint32_t ce_arity, + zend_class_entry *declaring_class) +{ + zval entry; + + if (!ce_args || arity == 0) { + reflection_build_args_list_ex(&entry, args, arity, declaring_class, true); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + return; + } + + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * arity, use_heap); + for (uint32_t i = 0; i < arity; i++) { + mapped[i] = reflection_type_substitute_class_params(args[i], ce_args, ce_arity); + } + reflection_build_args_list_ex(&entry, mapped, arity, declaring_class, true); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &entry); + reflection_type_array_release(mapped, arity); + free_alloca(mapped, use_heap); +} + +static void reflection_collect_interface_bindings( + zval *return_value, zend_class_entry *ce, zend_class_entry *ancestor, + const zend_type *ce_args, uint32_t ce_arity, + zend_class_entry *declaring_class) +{ + if ((ce->ce_flags & ZEND_ACC_RESOLVED_PARENT) && ce->parent && ce->parent != ancestor) { + const zend_type *parent_args; + uint32_t parent_arity; + if (reflection_get_direct_inheritance_binding(ce, ce->parent, &parent_args, &parent_arity)) { + if (ce_args && parent_arity > 0) { + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * parent_arity, use_heap); + for (uint32_t i = 0; i < parent_arity; i++) { + mapped[i] = reflection_type_substitute_class_params( + parent_args[i], ce_args, ce_arity); + } + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, mapped, parent_arity, declaring_class); + reflection_type_array_release(mapped, parent_arity); + free_alloca(mapped, use_heap); + } else { + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, parent_args, parent_arity, declaring_class); + } + } else { + reflection_collect_interface_bindings( + return_value, ce->parent, ancestor, NULL, 0, declaring_class); + } + } + + HashTable *generic_implements = ce->generic_types ? ce->generic_types->implements : NULL; + if (generic_implements) { + zval *zv; + ZEND_HASH_FOREACH_VAL(generic_implements, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + if (!ZEND_TYPE_HAS_NAMED_WITH_ARGS(*boxed)) { + continue; + } + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + + if (named->name && zend_string_equals_ci(named->name, ancestor->name)) { + reflection_append_binding(return_value, named->args, named->count, + ce_args, ce_arity, declaring_class); + continue; + } + + zend_class_entry *intermediate = named->name + ? reflection_find_interface_by_name(ce, named->name) : NULL; + if (!intermediate || intermediate == ancestor) { + continue; + } + + if (ce_args && named->count > 0) { + ALLOCA_FLAG(use_heap) + zend_type *mapped = (zend_type *) do_alloca(sizeof(zend_type) * named->count, use_heap); + for (uint32_t i = 0; i < named->count; i++) { + mapped[i] = reflection_type_substitute_class_params( + named->args[i], ce_args, ce_arity); + } + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, mapped, named->count, declaring_class); + reflection_type_array_release(mapped, named->count); + free_alloca(mapped, use_heap); + } else { + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, named->args, named->count, declaring_class); + } + } ZEND_HASH_FOREACH_END(); + } + + if (ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES) { + uint32_t parent_count = ce->parent ? ce->parent->num_interfaces : 0; + for (uint32_t i = parent_count; i < ce->num_interfaces; i++) { + zend_class_entry *intermediate = ce->interfaces[i]; + 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; + } + + bool is_transitive = false; + for (uint32_t j = parent_count; j < i && !is_transitive; j++) { + zend_class_entry *earlier = ce->interfaces[j]; + if (!earlier) continue; + for (uint32_t k = 0; k < earlier->num_interfaces; k++) { + if (earlier->interfaces[k] == intermediate) { + is_transitive = true; + break; + } + } + } + if (is_transitive) { + continue; + } + + reflection_collect_interface_bindings( + return_value, intermediate, ancestor, NULL, 0, declaring_class); + } + } +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentClass) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + + bool has_parent = (ce->ce_flags & ZEND_ACC_LINKED) + ? ce->parent != NULL + : ce->parent_name != NULL; + if (!has_parent) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Class %s has no parent class", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (ce->generic_types && ce->generic_types->extends) { + reflection_build_named_args_list(return_value, ce->generic_types->extends, ce); + return; + } + RETURN_EMPTY_ARRAY(); +} + +static bool reflection_try_build_named_args_from_table(HashTable *ht, zend_class_entry *ce, zend_string *name, zval *return_value) +{ + if (!ht) { + return false; + } + + zval *zv; + ZEND_HASH_FOREACH_VAL(ht, zv) { + zend_type *boxed = (zend_type *) Z_PTR_P(zv); + zend_type_named_with_args *named = ZEND_TYPE_NAMED_WITH_ARGS(*boxed); + if (zend_string_equals_ci(named->name, name)) { + reflection_build_named_args_list(return_value, boxed, ce); + return true; + } + } ZEND_HASH_FOREACH_END(); + return false; +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentInterface) +{ + reflection_object *intern; + zend_class_entry *ce; + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + GET_REFLECTION_OBJECT_PTR(ce); + + bool is_ancestor = false; + zend_class_entry *ancestor = NULL; + if (ce->ce_flags & ZEND_ACC_LINKED) { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (zend_string_equals_ci(ce->interfaces[i]->name, name)) { + is_ancestor = true; + ancestor = ce->interfaces[i]; + break; + } + } + } else { + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + if (zend_string_equals_ci(ce->interface_names[i].name, name)) { + is_ancestor = true; + break; + } + } + } + if (!is_ancestor) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "%s is not an ancestor interface of %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + array_init(return_value); + if (ancestor) { + reflection_collect_interface_bindings(return_value, ce, ancestor, NULL, 0, ce); + } +} + +ZEND_METHOD(ReflectionClass, getGenericArgumentsForUsedTrait) +{ + reflection_object *intern; + zend_class_entry *ce; + zend_string *name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(name) + ZEND_PARSE_PARAMETERS_END(); + GET_REFLECTION_OBJECT_PTR(ce); + + HashTable *ht = ce->generic_types ? ce->generic_types->trait_uses : NULL; + if (reflection_try_build_named_args_from_table(ht, ce, name, return_value)) { + return; + } + + bool is_used = false; + for (uint32_t i = 0; i < ce->num_traits; i++) { + if (zend_string_equals_ci(ce->trait_names[i].name, name)) { + is_used = true; + break; + } + } + if (!is_used) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "%s is not a trait used by %s", ZSTR_VAL(name), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + RETURN_EMPTY_ARRAY(); +} + PHP_MINIT_FUNCTION(reflection) /* {{{ */ { memcpy(&reflection_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); @@ -8273,6 +9308,16 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ reflection_property_hook_type_ptr = register_class_PropertyHookType(); + reflection_generic_variance_ptr = register_class_ReflectionGenericVariance(); + + reflection_generic_type_parameter_ptr = register_class_ReflectionGenericTypeParameter(reflector_ptr); + reflection_generic_type_parameter_ptr->create_object = reflection_objects_new; + reflection_generic_type_parameter_ptr->default_object_handlers = &reflection_object_handlers; + + reflection_type_parameter_reference_ptr = register_class_ReflectionTypeParameterReference(reflection_type_ptr); + reflection_type_parameter_reference_ptr->create_object = reflection_objects_new; + reflection_type_parameter_reference_ptr->default_object_handlers = &reflection_object_handlers; + REFLECTION_G(key_initialized) = false; return SUCCESS; diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..f6f7dacb00ed 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -115,6 +115,11 @@ public function hasTentativeReturnType(): bool {} public function getTentativeReturnType(): ?ReflectionType {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function isGeneric(): bool {} + + /** @return list */ + public function getGenericParameters(): array {} } class ReflectionFunction extends ReflectionFunctionAbstract @@ -436,6 +441,46 @@ public function getNamespaceName(): string {} public function getShortName(): string {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function isGeneric(): bool {} + + /** @return list */ + public function getGenericParameters(): array {} + + /** + * Returns the type arguments this class supplies at the parent-class extends + * site, in source order. Returns an empty array if the extends clause + * specified no type arguments. + * + * @return list + * @throws ReflectionException if this class has no parent class + */ + public function getGenericArgumentsForParentClass(): array {} + + /** + * Returns every generic argument set this class supplies for the named + * ancestor interface, in inheritance traversal order. The outer list + * contains one entry per binding: a class may bind the same generic + * interface multiple times either directly (`implements Foo, + * Foo`) or transitively through different inheritance paths. + * Each inner list holds the type arguments for that binding in source + * order. Returns an empty outer list when no generic bindings to the + * ancestor exist. + * + * @return list> + * @throws ReflectionException if $name is not an ancestor interface + */ + public function getGenericArgumentsForParentInterface(string $name): array {} + + /** + * Returns the type arguments this class supplies at the use site for trait + * $name, in source order. Returns an empty array if no type arguments were + * specified at the use site. + * + * @return list + * @throws ReflectionException if $name is not a directly-used trait + */ + public function getGenericArgumentsForUsedTrait(string $name): array {} } class ReflectionObject extends ReflectionClass @@ -741,6 +786,11 @@ public function getName(): string {} /** @tentative-return-type */ public function isBuiltin(): bool {} + + public function hasGenericArguments(): bool {} + + /** @return list */ + public function getGenericArguments(): array {} } class ReflectionUnionType extends ReflectionType @@ -753,6 +803,66 @@ class ReflectionIntersectionType extends ReflectionType public function getTypes(): array {} } +/** + * @strict-properties + * @not-serializable + */ +final class ReflectionTypeParameterReference extends ReflectionType +{ + public string $name; + + private function __construct() {} + + public function getName(): string {} + + public function getTypeParameter(): ReflectionGenericTypeParameter {} + + public function allowsNull(): bool {} + + public function __toString(): string {} +} + +enum ReflectionGenericVariance +{ + case Invariant; + case Covariant; + case Contravariant; +} + +/** + * @strict-properties + * @not-serializable + */ +final class ReflectionGenericTypeParameter implements Reflector +{ + public string $name; + + private function __construct() {} + + /** @implementation-alias ReflectionClass::__clone */ + private function __clone(): void {} + + public function getName(): string {} + + public function getPosition(): int {} + + public function getVariance(): ReflectionGenericVariance {} + + public function hasBound(): bool {} + + /** @throws ReflectionException if this type parameter has no bound; check hasBound() first */ + public function getBound(): ReflectionType {} + + public function hasDefault(): bool {} + + /** @throws ReflectionException if this type parameter has no default; check hasDefault() first */ + public function getDefault(): ReflectionType {} + + public function getDeclaringEntity(): ReflectionClass|ReflectionFunctionAbstract {} + + public function __toString(): string {} +} + /** @not-serializable */ class ReflectionExtension implements Reflector { diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..2f73dbda3866 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 + * Stub hash: e21013562d5d9f87a8707afdf579f799a858dba3 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -87,6 +87,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunctionAbstract ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionFunctionAbstract_isGeneric arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionFunctionAbstract_getGenericParameters arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionFunction___construct, 0, 0, 1) ZEND_ARG_OBJ_TYPE_MASK(0, function, Closure, MAY_BE_STRING, NULL) ZEND_END_ARG_INFO() @@ -367,6 +371,18 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionClass_getAttributes arginfo_class_ReflectionFunctionAbstract_getAttributes +#define arginfo_class_ReflectionClass_isGeneric arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionClass_getGenericParameters arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + +#define arginfo_class_ReflectionClass_getGenericArgumentsForParentClass arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionClass_getGenericArgumentsForUsedTrait arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionObject___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) ZEND_END_ARG_INFO() @@ -581,10 +597,51 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionNamedType_isBuiltin arginfo_class_ReflectionFunctionAbstract_inNamespace +#define arginfo_class_ReflectionNamedType_hasGenericArguments arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionNamedType_getGenericArguments arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables + #define arginfo_class_ReflectionUnionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables #define arginfo_class_ReflectionIntersectionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionTypeParameterReference___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionTypeParameterReference_getName arginfo_class_ReflectionFunction___toString + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionTypeParameterReference_getTypeParameter, 0, 0, ReflectionGenericTypeParameter, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionTypeParameterReference_allowsNull arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionTypeParameterReference___toString arginfo_class_ReflectionFunction___toString + +#define arginfo_class_ReflectionGenericTypeParameter___construct arginfo_class_ReflectionTypeParameterReference___construct + +#define arginfo_class_ReflectionGenericTypeParameter___clone arginfo_class_ReflectionFunctionAbstract___clone + +#define arginfo_class_ReflectionGenericTypeParameter_getName arginfo_class_ReflectionFunction___toString + +#define arginfo_class_ReflectionGenericTypeParameter_getPosition arginfo_class_ReflectionGenerator_getExecutingLine + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionGenericTypeParameter_getVariance, 0, 0, ReflectionGenericVariance, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter_hasBound arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionGenericTypeParameter_getBound, 0, 0, ReflectionType, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter_hasDefault arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionGenericTypeParameter_getDefault arginfo_class_ReflectionGenericTypeParameter_getBound + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_ReflectionGenericTypeParameter_getDeclaringEntity, 0, 0, ReflectionClass|ReflectionFunctionAbstract, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionGenericTypeParameter___toString arginfo_class_ReflectionFunction___toString + #define arginfo_class_ReflectionExtension___clone arginfo_class_ReflectionFunctionAbstract___clone ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionExtension___construct, 0, 0, 1) @@ -641,8 +698,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionReference___clone arginfo_class_ReflectionFunctionAbstract___clone -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionReference___construct, 0, 0, 0) -ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionReference___construct arginfo_class_ReflectionTypeParameterReference___construct #define arginfo_class_ReflectionAttribute_getName arginfo_class_ReflectionFunction___toString @@ -659,7 +715,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionAttribute___clone arginfo_class_ReflectionFunctionAbstract___clone -#define arginfo_class_ReflectionAttribute___construct arginfo_class_ReflectionReference___construct +#define arginfo_class_ReflectionAttribute___construct arginfo_class_ReflectionTypeParameterReference___construct #define arginfo_class_ReflectionEnum___construct arginfo_class_ReflectionClass___construct @@ -768,6 +824,8 @@ ZEND_METHOD(ReflectionFunctionAbstract, getReturnType); ZEND_METHOD(ReflectionFunctionAbstract, hasTentativeReturnType); ZEND_METHOD(ReflectionFunctionAbstract, getTentativeReturnType); ZEND_METHOD(ReflectionFunctionAbstract, getAttributes); +ZEND_METHOD(ReflectionFunctionAbstract, isGeneric); +ZEND_METHOD(ReflectionFunctionAbstract, getGenericParameters); ZEND_METHOD(ReflectionFunction, __construct); ZEND_METHOD(ReflectionFunction, __toString); ZEND_METHOD(ReflectionFunction, isAnonymous); @@ -863,6 +921,11 @@ ZEND_METHOD(ReflectionClass, inNamespace); ZEND_METHOD(ReflectionClass, getNamespaceName); ZEND_METHOD(ReflectionClass, getShortName); ZEND_METHOD(ReflectionClass, getAttributes); +ZEND_METHOD(ReflectionClass, isGeneric); +ZEND_METHOD(ReflectionClass, getGenericParameters); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentClass); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForParentInterface); +ZEND_METHOD(ReflectionClass, getGenericArgumentsForUsedTrait); ZEND_METHOD(ReflectionObject, __construct); ZEND_METHOD(ReflectionProperty, __construct); ZEND_METHOD(ReflectionProperty, __toString); @@ -948,8 +1011,25 @@ ZEND_METHOD(ReflectionType, allowsNull); ZEND_METHOD(ReflectionType, __toString); ZEND_METHOD(ReflectionNamedType, getName); ZEND_METHOD(ReflectionNamedType, isBuiltin); +ZEND_METHOD(ReflectionNamedType, hasGenericArguments); +ZEND_METHOD(ReflectionNamedType, getGenericArguments); ZEND_METHOD(ReflectionUnionType, getTypes); ZEND_METHOD(ReflectionIntersectionType, getTypes); +ZEND_METHOD(ReflectionTypeParameterReference, __construct); +ZEND_METHOD(ReflectionTypeParameterReference, getName); +ZEND_METHOD(ReflectionTypeParameterReference, getTypeParameter); +ZEND_METHOD(ReflectionTypeParameterReference, allowsNull); +ZEND_METHOD(ReflectionTypeParameterReference, __toString); +ZEND_METHOD(ReflectionGenericTypeParameter, __construct); +ZEND_METHOD(ReflectionGenericTypeParameter, getName); +ZEND_METHOD(ReflectionGenericTypeParameter, getPosition); +ZEND_METHOD(ReflectionGenericTypeParameter, getVariance); +ZEND_METHOD(ReflectionGenericTypeParameter, hasBound); +ZEND_METHOD(ReflectionGenericTypeParameter, getBound); +ZEND_METHOD(ReflectionGenericTypeParameter, hasDefault); +ZEND_METHOD(ReflectionGenericTypeParameter, getDefault); +ZEND_METHOD(ReflectionGenericTypeParameter, getDeclaringEntity); +ZEND_METHOD(ReflectionGenericTypeParameter, __toString); ZEND_METHOD(ReflectionExtension, __construct); ZEND_METHOD(ReflectionExtension, __toString); ZEND_METHOD(ReflectionExtension, getName); @@ -1048,6 +1128,8 @@ static const zend_function_entry class_ReflectionFunctionAbstract_methods[] = { ZEND_ME(ReflectionFunctionAbstract, hasTentativeReturnType, arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunctionAbstract, getTentativeReturnType, arginfo_class_ReflectionFunctionAbstract_getTentativeReturnType, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunctionAbstract, getAttributes, arginfo_class_ReflectionFunctionAbstract_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunctionAbstract, isGeneric, arginfo_class_ReflectionFunctionAbstract_isGeneric, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunctionAbstract, getGenericParameters, arginfo_class_ReflectionFunctionAbstract_getGenericParameters, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1161,6 +1243,11 @@ static const zend_function_entry class_ReflectionClass_methods[] = { ZEND_ME(ReflectionClass, getNamespaceName, arginfo_class_ReflectionClass_getNamespaceName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getShortName, arginfo_class_ReflectionClass_getShortName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getAttributes, arginfo_class_ReflectionClass_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isGeneric, arginfo_class_ReflectionClass_isGeneric, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericParameters, arginfo_class_ReflectionClass_getGenericParameters, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForParentClass, arginfo_class_ReflectionClass_getGenericArgumentsForParentClass, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForParentInterface, arginfo_class_ReflectionClass_getGenericArgumentsForParentInterface, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, getGenericArgumentsForUsedTrait, arginfo_class_ReflectionClass_getGenericArgumentsForUsedTrait, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1274,6 +1361,8 @@ static const zend_function_entry class_ReflectionType_methods[] = { static const zend_function_entry class_ReflectionNamedType_methods[] = { ZEND_ME(ReflectionNamedType, getName, arginfo_class_ReflectionNamedType_getName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionNamedType, isBuiltin, arginfo_class_ReflectionNamedType_isBuiltin, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionNamedType, hasGenericArguments, arginfo_class_ReflectionNamedType_hasGenericArguments, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionNamedType, getGenericArguments, arginfo_class_ReflectionNamedType_getGenericArguments, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -1287,6 +1376,30 @@ static const zend_function_entry class_ReflectionIntersectionType_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_ReflectionTypeParameterReference_methods[] = { + ZEND_ME(ReflectionTypeParameterReference, __construct, arginfo_class_ReflectionTypeParameterReference___construct, ZEND_ACC_PRIVATE) + ZEND_ME(ReflectionTypeParameterReference, getName, arginfo_class_ReflectionTypeParameterReference_getName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, getTypeParameter, arginfo_class_ReflectionTypeParameterReference_getTypeParameter, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, allowsNull, arginfo_class_ReflectionTypeParameterReference_allowsNull, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionTypeParameterReference, __toString, arginfo_class_ReflectionTypeParameterReference___toString, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_ReflectionGenericTypeParameter_methods[] = { + ZEND_ME(ReflectionGenericTypeParameter, __construct, arginfo_class_ReflectionGenericTypeParameter___construct, ZEND_ACC_PRIVATE) + ZEND_RAW_FENTRY("__clone", zim_ReflectionClass___clone, arginfo_class_ReflectionGenericTypeParameter___clone, ZEND_ACC_PRIVATE, NULL, NULL) + ZEND_ME(ReflectionGenericTypeParameter, getName, arginfo_class_ReflectionGenericTypeParameter_getName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getPosition, arginfo_class_ReflectionGenericTypeParameter_getPosition, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getVariance, arginfo_class_ReflectionGenericTypeParameter_getVariance, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, hasBound, arginfo_class_ReflectionGenericTypeParameter_hasBound, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getBound, arginfo_class_ReflectionGenericTypeParameter_getBound, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, hasDefault, arginfo_class_ReflectionGenericTypeParameter_hasDefault, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getDefault, arginfo_class_ReflectionGenericTypeParameter_getDefault, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, getDeclaringEntity, arginfo_class_ReflectionGenericTypeParameter_getDeclaringEntity, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionGenericTypeParameter, __toString, arginfo_class_ReflectionGenericTypeParameter___toString, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static const zend_function_entry class_ReflectionExtension_methods[] = { ZEND_RAW_FENTRY("__clone", zim_ReflectionClass___clone, arginfo_class_ReflectionExtension___clone, ZEND_ACC_PRIVATE, NULL, NULL) ZEND_ME(ReflectionExtension, __construct, arginfo_class_ReflectionExtension___construct, ZEND_ACC_PUBLIC) @@ -1808,6 +1921,48 @@ static zend_class_entry *register_class_ReflectionIntersectionType(zend_class_en return class_entry; } +static zend_class_entry *register_class_ReflectionTypeParameterReference(zend_class_entry *class_entry_ReflectionType) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "ReflectionTypeParameterReference", class_ReflectionTypeParameterReference_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_ReflectionType, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + zval property_name_default_value; + ZVAL_UNDEF(&property_name_default_value); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + + return class_entry; +} + +static zend_class_entry *register_class_ReflectionGenericVariance(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("ReflectionGenericVariance", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Invariant", NULL); + + zend_enum_add_case_cstr(class_entry, "Covariant", NULL); + + zend_enum_add_case_cstr(class_entry, "Contravariant", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_ReflectionGenericTypeParameter(zend_class_entry *class_entry_Reflector) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "ReflectionGenericTypeParameter", class_ReflectionGenericTypeParameter_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Reflector); + + zval property_name_default_value; + ZVAL_UNDEF(&property_name_default_value); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_NAME), &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + + return class_entry; +} + static zend_class_entry *register_class_ReflectionExtension(zend_class_entry *class_entry_Reflector) { zend_class_entry ce, *class_entry; diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..43cec2cd9936 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,18 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: e21013562d5d9f87a8707afdf579f799a858dba3 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_H +#define ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H */ +typedef enum zend_enum_ReflectionGenericVariance { + ZEND_ENUM_ReflectionGenericVariance_Invariant = 1, + ZEND_ENUM_ReflectionGenericVariance_Covariant = 2, + ZEND_ENUM_ReflectionGenericVariance_Contravariant = 3, +} zend_enum_ReflectionGenericVariance; + +#endif /* ZEND_PHP_REFLECTION_DECL_e21013562d5d9f87a8707afdf579f799a858dba3_H */ diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e91741..01415d08aacb 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -30,7 +30,7 @@ Class [ class ReflectionClass implements Stringable, Refle Property [ public string $name ] } - - Methods [64] { + - Methods [69] { Method [ private method __clone ] { - Parameters [0] { @@ -514,5 +514,42 @@ Class [ class ReflectionClass implements Stringable, Refle } - Return [ array ] } + + Method [ public method isGeneric ] { + + - Parameters [0] { + } + - Return [ bool ] + } + + Method [ public method getGenericParameters ] { + + - Parameters [0] { + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForParentClass ] { + + - Parameters [0] { + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForParentInterface ] { + + - Parameters [1] { + Parameter #0 [ string $name ] + } + - Return [ array ] + } + + Method [ public method getGenericArgumentsForUsedTrait ] { + + - Parameters [1] { + Parameter #0 [ string $name ] + } + - Return [ array ] + } } } diff --git a/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt index 8ba243a503bd..e591c9b2f091 100644 --- a/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt @@ -8,7 +8,7 @@ $ext = new ReflectionExtension('reflection'); var_dump($ext->getClasses()); ?> --EXPECTF-- -array(26) { +array(29) { ["ReflectionException"]=> object(ReflectionClass)#%d (1) { ["name"]=> @@ -139,4 +139,19 @@ array(26) { ["name"]=> string(16) "PropertyHookType" } + ["ReflectionGenericVariance"]=> + object(ReflectionEnum)#%d (1) { + ["name"]=> + string(25) "ReflectionGenericVariance" + } + ["ReflectionGenericTypeParameter"]=> + object(ReflectionClass)#%d (1) { + ["name"]=> + string(30) "ReflectionGenericTypeParameter" + } + ["ReflectionTypeParameterReference"]=> + object(ReflectionClass)#%d (1) { + ["name"]=> + string(32) "ReflectionTypeParameterReference" + } } diff --git a/ext/reflection/tests/generics/ancestor_args_composite.phpt b/ext/reflection/tests/generics/ancestor_args_composite.phpt new file mode 100644 index 000000000000..dbd45bd29c6a --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_composite.phpt @@ -0,0 +1,19 @@ +--TEST-- +Reflection: composite (union/intersection) type args inside extends/implements +--FILE-- + {} + +class C extends Pair {} + +$args = (new ReflectionClass('C'))->getGenericArgumentsForParentClass(); +echo "count: ", count($args), "\n"; +echo "[0] kind: ", get_class($args[0]), "\n"; +echo "[1] kind: ", get_class($args[1]), "\n"; +?> +--EXPECT-- +count: 2 +[0] kind: ReflectionUnionType +[1] kind: ReflectionIntersectionType diff --git a/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt b/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt new file mode 100644 index 000000000000..0135912d4cf5 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_duplicate_bindings.phpt @@ -0,0 +1,64 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface lists every binding when the same interface is bound more than once +--FILE-- + {} +interface Bar {} +interface Baz extends Bar {} + +interface DA<+T> {} +interface I1 extends DA {} +interface I2 extends DA {} + +class Box {} + +class DirectDup implements Foo, Foo {} + +class MixedDup implements Bar, Baz {} + +class ParentDup implements Foo, Foo {} +class ChildDup extends ParentDup {} + +class NestedDup implements Foo>, Foo> {} + +class DiamondVar implements I1, I2 {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static function (ReflectionType $t): string { + if ($t instanceof ReflectionNamedType && $t->hasGenericArguments()) { + return $t->getName() . '<' . implode(', ', array_map( + static fn(ReflectionType $inner): string => $inner->getName(), + $t->getGenericArguments(), + )) . '>'; + } + return $t->getName(); + }, + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $ancestor): void { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($ancestor); + printf("%-12s -> %-5s = %s (count=%d)\n", $cls, $ancestor, render($bindings), count($bindings)); +} + +show('DirectDup', 'Foo'); +show('MixedDup', 'Bar'); +show('ChildDup', 'Foo'); +show('NestedDup', 'Foo'); +show('DiamondVar', 'DA'); + +class Single implements Foo {} +show('Single', 'Foo'); +?> +--EXPECT-- +DirectDup -> Foo = [[string], [int]] (count=2) +MixedDup -> Bar = [[string], [int]] (count=2) +ChildDup -> Foo = [[bool], [float]] (count=2) +NestedDup -> Foo = [[Box], [Box]] (count=2) +DiamondVar -> DA = [[int], [string]] (count=2) +Single -> Foo = [[int]] (count=1) diff --git a/ext/reflection/tests/generics/ancestor_args_nested.phpt b/ext/reflection/tests/generics/ancestor_args_nested.phpt new file mode 100644 index 000000000000..0cc893d12a4d --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_nested.phpt @@ -0,0 +1,41 @@ +--TEST-- +Reflection: nested generic arguments inside extends/implements/use are preserved +--FILE-- + {} +class Pair {} +interface I {} +trait T1 { public function noop(): void {} } + +class WithNested + extends Pair, string> + implements I> +{ + use T1>; +} + +$rc = new ReflectionClass('WithNested'); + +// Nested in extends +$args = $rc->getGenericArgumentsForParentClass(); +echo "parent[0]: ", $args[0]->getName(); +echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n"; +echo "parent[1]: ", $args[1]->getName(), "\n"; + +// Nested in implements +$bindings = $rc->getGenericArgumentsForParentInterface('I'); +echo "I bindings: ", count($bindings), "\n"; +echo "I[0][0]: ", $bindings[0][0]->getName(); +echo "<", $bindings[0][0]->getGenericArguments()[0]->getName(), ">\n"; + +// Nested in use +$args = $rc->getGenericArgumentsForUsedTrait('T1'); +echo "T1[0]: ", $args[0]->getName(); +echo "<", $args[0]->getGenericArguments()[0]->getName(), ">\n"; +?> +--EXPECT-- +parent[0]: Box +parent[1]: string +I bindings: 1 +I[0][0]: Box +T1[0]: Box diff --git a/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt b/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt new file mode 100644 index 000000000000..361f93a39019 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_nested_transitive.phpt @@ -0,0 +1,49 @@ +--TEST-- +Reflection: transitive parent interface arguments substitute nested type parameters +--FILE-- + {} +interface Root {} +interface PairRoot {} +interface Mid extends Root> {} +interface Flip extends PairRoot, Box> {} + +class Concrete implements Mid {} +class Forwarded implements Mid {} +class Reordered implements Flip {} + +function show(string $class, string $interface): void { + $bindings = (new ReflectionClass($class))->getGenericArgumentsForParentInterface($interface); + echo "$class/$interface (", count($bindings), " binding)\n"; + foreach ($bindings as $args) { + foreach ($args as $arg) { + echo " ", $arg->getName(); + if ($arg instanceof ReflectionNamedType && $arg->hasGenericArguments()) { + echo "<", implode(", ", array_map( + static fn(ReflectionType $type): string => $type->getName(), + $arg->getGenericArguments(), + )), ">"; + } + echo "\n"; + } + } +} + +show(Concrete::class, Root::class); +show(Forwarded::class, Root::class); +show(Reordered::class, PairRoot::class); + +$forwardedBindings = (new ReflectionClass(Forwarded::class))->getGenericArgumentsForParentInterface(Root::class); +$forwardedInner = $forwardedBindings[0][0]->getGenericArguments()[0]; +echo "Forwarded nested parameter owner: ", + $forwardedInner->getTypeParameter()->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +Concrete/Root (1 binding) + Box +Forwarded/Root (1 binding) + Box +Reordered/PairRoot (1 binding) + Box + Box +Forwarded nested parameter owner: Forwarded diff --git a/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt b/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt new file mode 100644 index 000000000000..b446b8b01789 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_no_generics_class.phpt @@ -0,0 +1,47 @@ +--TEST-- +Reflection: classes with no generic content — empty array when ancestor without args, throw when not an ancestor +--FILE-- +getGenericArgumentsForParentClass(); + echo $cls, ": parent=", json_encode($p); + } catch (ReflectionException $e) { + echo $cls, ": parent=throw(", $e->getMessage(), ")"; + } + + try { + $i = $rc->getGenericArgumentsForParentInterface('IPlain'); + echo " iface=", json_encode($i); + } catch (ReflectionException $e) { + echo " iface=throw(", $e->getMessage(), ")"; + } + + try { + $t = $rc->getGenericArgumentsForUsedTrait('TPlain'); + echo " trait=", json_encode($t); + } catch (ReflectionException $e) { + echo " trait=throw(", $e->getMessage(), ")"; + } + + echo "\n"; +} + +foreach (['Plain', 'WithParent', 'WithIface', 'WithTrait'] as $cls) { + show($cls); +} +?> +--EXPECT-- +Plain: parent=throw(Class Plain has no parent class) iface=throw(IPlain is not an ancestor interface of Plain) trait=throw(TPlain is not a trait used by Plain) +WithParent: parent=[] iface=throw(IPlain is not an ancestor interface of WithParent) trait=throw(TPlain is not a trait used by WithParent) +WithIface: parent=throw(Class WithIface has no parent class) iface=[] trait=throw(TPlain is not a trait used by WithIface) +WithTrait: parent=throw(Class WithTrait has no parent class) iface=throw(IPlain is not an ancestor interface of WithTrait) trait=[] diff --git a/ext/reflection/tests/generics/ancestor_args_parent_class.phpt b/ext/reflection/tests/generics/ancestor_args_parent_class.phpt new file mode 100644 index 000000000000..16a3f48264cf --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_parent_class.phpt @@ -0,0 +1,39 @@ +--TEST-- +Reflection: getGenericArgumentsForParentClass returns args from extends clause, throws when no parent +--FILE-- + {} +class B {} + +class WithArgs extends A {} +class NoArgs extends A {} +class NoParent {} +class Multi extends B {} + +$cases = [ + 'WithArgs' => ['string'], + 'NoArgs' => [], + 'NoParent' => 'throw', + 'Multi' => ['int', 'float'], +]; + +foreach ($cases as $cls => $want) { + try { + $args = (new ReflectionClass($cls))->getGenericArgumentsForParentClass(); + } catch (ReflectionException $e) { + echo $cls, ": ", $want === 'throw' ? "throw OK ({$e->getMessage()})" : "FAIL (unexpected throw)", "\n"; + continue; + } + if ($want === 'throw') { + echo $cls, ": FAIL (expected throw)\n"; + } else { + $got = array_map(fn($t) => $t->getName(), $args); + echo $cls, ": ", $got === $want ? "OK" : ("FAIL got " . implode(",", $got)), "\n"; + } +} +?> +--EXPECT-- +WithArgs: OK +NoArgs: OK +NoParent: throw OK (Class NoParent has no parent class) +Multi: OK diff --git a/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt b/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt new file mode 100644 index 000000000000..7d561cc24f08 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_parent_interface.phpt @@ -0,0 +1,59 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface returns every binding to the ancestor interface; throws when not ancestor +--FILE-- + {} +interface J {} +interface K1 extends I {} +interface Forward extends J {} + +class WithArgs implements I {} +class WithoutArgs implements I {} +class NotImplements {} +class Multi implements I, J {} +class ViaK1 implements K1 {} +class ViaForward implements Forward {} +class ChildWithArgs extends WithArgs {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(',', array_map( + static fn(ReflectionType $t): string => $t->getName(), + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $iface): void { + try { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($iface); + } catch (ReflectionException $e) { + echo "$cls/$iface: throw ({$e->getMessage()})\n"; + return; + } + echo "$cls/$iface: ", render($bindings), "\n"; +} + +show('WithArgs', 'I'); +show('WithArgs', 'i'); // case insensitive +show('WithoutArgs', 'I'); +show('NotImplements', 'I'); +show('Multi', 'I'); +show('Multi', 'J'); +show('K1', 'I'); +show('ViaK1', 'I'); +show('ViaForward', 'J'); +show('ChildWithArgs', 'I'); +?> +--EXPECT-- +WithArgs/I: [[int,string]] +WithArgs/i: [[int,string]] +WithoutArgs/I: [] +NotImplements/I: throw (I is not an ancestor interface of NotImplements) +Multi/I: [[bool,float]] +Multi/J: [[string]] +K1/I: [[int,string]] +ViaK1/I: [[int,string]] +ViaForward/J: [[bool]] +ChildWithArgs/I: [[int,string]] diff --git a/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt b/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt new file mode 100644 index 000000000000..95e4a8839da4 --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_transitive_paths.phpt @@ -0,0 +1,77 @@ +--TEST-- +Reflection: getGenericArgumentsForParentInterface composes substitutions through transitive paths and exposes every diamond binding +--FILE-- + {} +interface Mid extends Root {} +interface DeepRoot {} +interface DeepMid extends DeepRoot {} +interface DeepLeaf extends DeepMid {} + +class FwdGeneric implements Mid {} + +class ConcreteThroughGeneric implements Mid {} + +class DeepFwd implements DeepLeaf {} + +class WithIfaceArgs implements Root {} +class ChildOfBound extends WithIfaceArgs {} +class GrandchildOfBound extends ChildOfBound {} + +class ForwardingChild extends WithIfaceArgs {} + +class BaseViaMid implements Mid {} +class ChildViaMid extends BaseViaMid {} + +interface Leaf extends Mid {} + +interface DA<+T> {} +interface I1 extends DA {} +interface I2 extends DA {} +class Diamond implements I1, I2 {} + +function render(array $bindings): string { + return '[' . implode(', ', array_map( + static fn(array $b): string => '[' . implode(', ', array_map( + static fn(ReflectionType $t): string => (string) $t, + $b, + )) . ']', + $bindings, + )) . ']'; +} + +function show(string $cls, string $ancestor): void { + $bindings = (new ReflectionClass($cls))->getGenericArgumentsForParentInterface($ancestor); + printf("%-30s -> %-10s = %s\n", $cls, $ancestor, render($bindings)); +} + +show('FwdGeneric', 'Root'); +show('FwdGeneric', 'Mid'); +show('ConcreteThroughGeneric', 'Root'); +show('DeepFwd', 'DeepRoot'); +show('DeepFwd', 'DeepMid'); +show('DeepFwd', 'DeepLeaf'); +show('ChildOfBound', 'Root'); +show('GrandchildOfBound', 'Root'); +show('ForwardingChild', 'Root'); +show('ChildViaMid', 'Root'); +show('ChildViaMid', 'Mid'); +show('Leaf', 'Root'); +show('Diamond', 'DA'); + +?> +--EXPECT-- +FwdGeneric -> Root = [[U]] +FwdGeneric -> Mid = [[U]] +ConcreteThroughGeneric -> Root = [[int]] +DeepFwd -> DeepRoot = [[U]] +DeepFwd -> DeepMid = [[U]] +DeepFwd -> DeepLeaf = [[U]] +ChildOfBound -> Root = [[int]] +GrandchildOfBound -> Root = [[int]] +ForwardingChild -> Root = [[V]] +ChildViaMid -> Root = [[string]] +ChildViaMid -> Mid = [[string]] +Leaf -> Root = [[bool]] +Diamond -> DA = [[int], [string]] diff --git a/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt b/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt new file mode 100644 index 000000000000..c75cdf2a359c --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_type_param_refs.phpt @@ -0,0 +1,26 @@ +--TEST-- +Reflection: type-parameter references inside extends/implements args resolve correctly +--FILE-- + {} +class Holder implements Container {} + +$rc = new ReflectionClass('Holder'); +$bindings = $rc->getGenericArgumentsForParentInterface('Container'); + +echo "bindings: ", count($bindings), "\n"; +echo "args in binding 0: ", count($bindings[0]), "\n"; +echo "class: ", get_class($bindings[0][0]), "\n"; +echo "name: ", $bindings[0][0]->getName(), "\n"; + +$param = $bindings[0][0]->getTypeParameter(); +echo "type param: ", $param->getName(), "\n"; +echo "declaring: ", $param->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +bindings: 1 +args in binding 0: 1 +class: ReflectionTypeParameterReference +name: T +type param: T +declaring: Holder diff --git a/ext/reflection/tests/generics/ancestor_args_used_trait.phpt b/ext/reflection/tests/generics/ancestor_args_used_trait.phpt new file mode 100644 index 000000000000..322e0066061d --- /dev/null +++ b/ext/reflection/tests/generics/ancestor_args_used_trait.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reflection: getGenericArgumentsForUsedTrait returns args from use clause; throws when trait not used +--FILE-- + { public T $val; } +trait Pair { public K $k; public V $v; } +trait Plain { public int $x; } + +class WithArgs { use Holder; } +class NoArgs { use Holder; } +class NoTrait {} +class Multi { use Holder, Pair; } +class Combo { use Holder, Plain; } + +function show(string $cls, string $tr): void { + try { + $args = (new ReflectionClass($cls))->getGenericArgumentsForUsedTrait($tr); + } catch (ReflectionException $e) { + echo "$cls/$tr: throw ({$e->getMessage()})\n"; + return; + } + if (!$args) { + echo "$cls/$tr: []\n"; + return; + } + echo "$cls/$tr: ", implode(",", array_map(fn($t)=>$t->getName(), $args)), "\n"; +} + +show('WithArgs', 'Holder'); +show('NoArgs', 'Holder'); +show('NoTrait', 'Holder'); +show('Multi', 'Holder'); +show('Multi', 'Pair'); +show('Combo', 'Holder'); +show('Combo', 'Plain'); +show('Combo', 'holder'); // case insensitive +?> +--EXPECT-- +WithArgs/Holder: string +NoArgs/Holder: [] +NoTrait/Holder: throw (Holder is not a trait used by NoTrait) +Multi/Holder: bool +Multi/Pair: int,float +Combo/Holder: string +Combo/Plain: [] +Combo/holder: string diff --git a/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt b/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt new file mode 100644 index 000000000000..829cdaaa8d2f --- /dev/null +++ b/ext/reflection/tests/generics/arrow_fn_get_generic_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: getGenericParameters on arrow function +--FILE-- +(T $x): T => $x; +$r = new ReflectionFunction($f); +echo $r->isGeneric() ? "gen\n" : "not\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +echo $r->getGenericParameters()[0]->getBound()->getName(), "\n"; +?> +--EXPECT-- +gen +T +int diff --git a/ext/reflection/tests/generics/class_isgeneric_false.phpt b/ext/reflection/tests/generics/class_isgeneric_false.phpt new file mode 100644 index 000000000000..52f5ce8130d1 --- /dev/null +++ b/ext/reflection/tests/generics/class_isgeneric_false.phpt @@ -0,0 +1,9 @@ +--TEST-- +Reflection: ReflectionClass::isGeneric() returns false for non-generic class +--FILE-- +isGeneric()); +?> +--EXPECT-- +bool(false) diff --git a/ext/reflection/tests/generics/class_isgeneric_true.phpt b/ext/reflection/tests/generics/class_isgeneric_true.phpt new file mode 100644 index 000000000000..82eaeaec2247 --- /dev/null +++ b/ext/reflection/tests/generics/class_isgeneric_true.phpt @@ -0,0 +1,9 @@ +--TEST-- +Reflection: ReflectionClass::isGeneric() returns true for generic class +--FILE-- + {} +var_dump((new ReflectionClass('G'))->isGeneric()); +?> +--EXPECT-- +bool(true) diff --git a/ext/reflection/tests/generics/closure_get_generic_params.phpt b/ext/reflection/tests/generics/closure_get_generic_params.phpt new file mode 100644 index 000000000000..8a133191fe72 --- /dev/null +++ b/ext/reflection/tests/generics/closure_get_generic_params.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: getGenericParameters on closure +--FILE-- +(T $x): T { return $x; }; +$r = new ReflectionFunction($cl); +echo $r->isGeneric() ? "gen\n" : "not\n"; +echo count($r->getGenericParameters()), "\n"; +echo $r->getGenericParameters()[0]->getName(), "\n"; +echo $r->getGenericParameters()[0]->getBound()->getName(), "\n"; +?> +--EXPECT-- +gen +1 +T +object diff --git a/ext/reflection/tests/generics/composite_bound.phpt b/ext/reflection/tests/generics/composite_bound.phpt new file mode 100644 index 000000000000..c28ae671d2ad --- /dev/null +++ b/ext/reflection/tests/generics/composite_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: composite (union) bound returned +--FILE-- +(T $x): T { return $x; } +$p = (new ReflectionFunction('f'))->getGenericParameters()[0]; +$b = $p->getBound(); +echo get_class($b), "\n"; +?> +--EXPECT-- +ReflectionUnionType diff --git a/ext/reflection/tests/generics/composite_type_arg.phpt b/ext/reflection/tests/generics/composite_type_arg.phpt new file mode 100644 index 000000000000..a482246b9306 --- /dev/null +++ b/ext/reflection/tests/generics/composite_type_arg.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: composite type as generic argument +--FILE-- + {} +function f(Pair $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +$args = $pt->getGenericArguments(); +echo get_class($args[0]), "\n"; +echo get_class($args[1]), "\n"; +?> +--EXPECT-- +ReflectionUnionType +ReflectionIntersectionType diff --git a/ext/reflection/tests/generics/function_isgeneric.phpt b/ext/reflection/tests/generics/function_isgeneric.phpt new file mode 100644 index 000000000000..93998269ea55 --- /dev/null +++ b/ext/reflection/tests/generics/function_isgeneric.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: ReflectionFunction::isGeneric() +--FILE-- +(): void {} +function plain(): void {} +var_dump((new ReflectionFunction('gen'))->isGeneric()); +var_dump((new ReflectionFunction('plain'))->isGeneric()); +?> +--EXPECT-- +bool(true) +bool(false) diff --git a/ext/reflection/tests/generics/get_generic_parameters_count.phpt b/ext/reflection/tests/generics/get_generic_parameters_count.phpt new file mode 100644 index 000000000000..2f9341e4eace --- /dev/null +++ b/ext/reflection/tests/generics/get_generic_parameters_count.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: getGenericParameters returns correct count +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +echo count($ps), "\n"; +echo $ps[0]->getName(), "\n"; +echo $ps[1]->getName(), "\n"; +echo $ps[2]->getName(), "\n"; +?> +--EXPECT-- +3 +X +Y +Z diff --git a/ext/reflection/tests/generics/get_generic_parameters_empty.phpt b/ext/reflection/tests/generics/get_generic_parameters_empty.phpt new file mode 100644 index 000000000000..987c0ef16e15 --- /dev/null +++ b/ext/reflection/tests/generics/get_generic_parameters_empty.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: getGenericParameters on non-generic returns empty array +--FILE-- +getGenericParameters()); +var_dump((new ReflectionFunction('f'))->getGenericParameters()); +?> +--EXPECT-- +array(0) { +} +array(0) { +} diff --git a/ext/reflection/tests/generics/method_get_generic_params.phpt b/ext/reflection/tests/generics/method_get_generic_params.phpt new file mode 100644 index 000000000000..72f019cde580 --- /dev/null +++ b/ext/reflection/tests/generics/method_get_generic_params.phpt @@ -0,0 +1,16 @@ +--TEST-- +Reflection: method type parameters separate from class type parameters +--FILE-- + { + public function f(): void {} +} +$rc = new ReflectionClass('C'); +$class_ps = $rc->getGenericParameters(); +$method_ps = $rc->getMethod('f')->getGenericParameters(); +echo count($class_ps), ": ", $class_ps[0]->getName(), "\n"; +echo count($method_ps), ": ", $method_ps[0]->getName(), "\n"; +?> +--EXPECT-- +1: X +1: U diff --git a/ext/reflection/tests/generics/method_isgeneric.phpt b/ext/reflection/tests/generics/method_isgeneric.phpt new file mode 100644 index 000000000000..ad2014844029 --- /dev/null +++ b/ext/reflection/tests/generics/method_isgeneric.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: ReflectionMethod::isGeneric() +--FILE-- +(): void {} + public function plain(): void {} +} +var_dump((new ReflectionClass('C'))->getMethod('gen')->isGeneric()); +var_dump((new ReflectionClass('C'))->getMethod('plain')->isGeneric()); +?> +--EXPECT-- +bool(true) +bool(false) diff --git a/ext/reflection/tests/generics/named_type_get_name_erased.phpt b/ext/reflection/tests/generics/named_type_get_name_erased.phpt new file mode 100644 index 000000000000..91a12296d97c --- /dev/null +++ b/ext/reflection/tests/generics/named_type_get_name_erased.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: ReflectionNamedType::getName() returns the reified name with type arguments +--FILE-- + {} +function f(Box $x): Box { return $x; } +$r = new ReflectionFunction('f'); +echo $r->getParameters()[0]->getType()->getName(), "\n"; +echo $r->getReturnType()->getName(), "\n"; +?> +--EXPECT-- +Box +Box diff --git a/ext/reflection/tests/generics/named_type_has_args.phpt b/ext/reflection/tests/generics/named_type_has_args.phpt new file mode 100644 index 000000000000..d3e18b456259 --- /dev/null +++ b/ext/reflection/tests/generics/named_type_has_args.phpt @@ -0,0 +1,24 @@ +--TEST-- +Reflection: hasGenericArguments and getGenericArguments +--FILE-- + {} +function f(Box $x): Box { return $x; } +$r = new ReflectionFunction('f'); +$pt = $r->getParameters()[0]->getType(); +var_dump($pt->hasGenericArguments()); +$args = $pt->getGenericArguments(); +echo count($args), "\n"; +echo $args[0]->getName(), "\n"; + +$rt = $r->getReturnType(); +var_dump($rt->hasGenericArguments()); +$args2 = $rt->getGenericArguments(); +echo $args2[0]->getName(), "\n"; +?> +--EXPECT-- +bool(true) +1 +int +bool(true) +string diff --git a/ext/reflection/tests/generics/named_type_no_args.phpt b/ext/reflection/tests/generics/named_type_no_args.phpt new file mode 100644 index 000000000000..281e29060444 --- /dev/null +++ b/ext/reflection/tests/generics/named_type_no_args.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: hasGenericArguments returns false when no arguments +--FILE-- +getParameters()[0]->getType()->hasGenericArguments()); +var_dump($r->getReturnType()->hasGenericArguments()); +?> +--EXPECT-- +bool(false) +bool(false) diff --git a/ext/reflection/tests/generics/nested_args.phpt b/ext/reflection/tests/generics/nested_args.phpt new file mode 100644 index 000000000000..ba10c52ccfb1 --- /dev/null +++ b/ext/reflection/tests/generics/nested_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reflection: nested generic arguments +--FILE-- + {} +function f(Box> $x): void {} +$pt = (new ReflectionFunction('f'))->getParameters()[0]->getType(); +echo $pt->getName(), "\n"; +$inner = $pt->getGenericArguments()[0]; +echo $inner->getName(), "\n"; +$inmost = $inner->getGenericArguments()[0]; +echo $inmost->getName(), "\n"; +?> +--EXPECT-- +Box> +Box +int diff --git a/ext/reflection/tests/generics/parameter_constructor_throws.phpt b/ext/reflection/tests/generics/parameter_constructor_throws.phpt new file mode 100644 index 000000000000..d33d9de36190 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_constructor_throws.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: cannot instantiate ReflectionGenericTypeParameter directly +--FILE-- +newInstanceWithoutConstructor()->__construct(); + echo "no error\n"; +} catch (Throwable $e) { + echo "error: ", get_class($e), "\n"; +} +?> +--EXPECT-- +error: ReflectionException diff --git a/ext/reflection/tests/generics/parameter_get_bound.phpt b/ext/reflection/tests/generics/parameter_get_bound.phpt new file mode 100644 index 000000000000..c20c07ac2294 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_bound.phpt @@ -0,0 +1,18 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getBound() returns the bound type or throws when no bound +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +var_dump($ps[0]->hasBound()); +try { + $ps[0]->getBound(); +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} +echo $ps[1]->getBound()->getName(), "\n"; +?> +--EXPECT-- +bool(false) +Type parameter X has no bound +object diff --git a/ext/reflection/tests/generics/parameter_get_declaring_class.phpt b/ext/reflection/tests/generics/parameter_get_declaring_class.phpt new file mode 100644 index 000000000000..006bb99922d6 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_declaring_class.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: getDeclaringEntity() returns the declaring class +--FILE-- + {} +$p = (new ReflectionClass('A'))->getGenericParameters()[0]; +$de = $p->getDeclaringEntity(); +echo get_class($de), "\n"; +echo $de->getName(), "\n"; +?> +--EXPECT-- +ReflectionClass +A diff --git a/ext/reflection/tests/generics/parameter_get_declaring_function.phpt b/ext/reflection/tests/generics/parameter_get_declaring_function.phpt new file mode 100644 index 000000000000..3ed7cfdc7625 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_declaring_function.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: getDeclaringEntity() returns the declaring function +--FILE-- +(): void {} +$p = (new ReflectionFunction('f'))->getGenericParameters()[0]; +$de = $p->getDeclaringEntity(); +echo get_class($de), "\n"; +echo $de->getName(), "\n"; +?> +--EXPECT-- +ReflectionFunction +f diff --git a/ext/reflection/tests/generics/parameter_get_position.phpt b/ext/reflection/tests/generics/parameter_get_position.phpt new file mode 100644 index 000000000000..babf13acbd9c --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_position.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getPosition() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +foreach ($ps as $p) { + echo $p->getName(), ": ", $p->getPosition(), "\n"; +} +?> +--EXPECT-- +X: 0 +Y: 1 +Z: 2 diff --git a/ext/reflection/tests/generics/parameter_get_variance.phpt b/ext/reflection/tests/generics/parameter_get_variance.phpt new file mode 100644 index 000000000000..61d39c1d802c --- /dev/null +++ b/ext/reflection/tests/generics/parameter_get_variance.phpt @@ -0,0 +1,13 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::getVariance() +--FILE-- + {} +foreach ((new ReflectionClass('A'))->getGenericParameters() as $p) { + echo $p->getName(), ": ", $p->getVariance()->name, "\n"; +} +?> +--EXPECT-- +X: Covariant +Y: Contravariant +Z: Invariant diff --git a/ext/reflection/tests/generics/parameter_has_bound.phpt b/ext/reflection/tests/generics/parameter_has_bound.phpt new file mode 100644 index 000000000000..2e1732073de3 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_has_bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +Reflection: ReflectionGenericTypeParameter::hasBound() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +echo $ps[0]->getName(), ": ", var_export($ps[0]->hasBound(), true), "\n"; +echo $ps[1]->getName(), ": ", var_export($ps[1]->hasBound(), true), "\n"; +?> +--EXPECT-- +X: false +Y: true diff --git a/ext/reflection/tests/generics/parameter_has_default.phpt b/ext/reflection/tests/generics/parameter_has_default.phpt new file mode 100644 index 000000000000..6e09af852391 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_has_default.phpt @@ -0,0 +1,20 @@ +--TEST-- +Reflection: hasDefault() and getDefault() +--FILE-- + {} +$ps = (new ReflectionClass('A'))->getGenericParameters(); +var_dump($ps[0]->hasDefault()); +try { + $ps[0]->getDefault(); +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($ps[1]->hasDefault()); +echo $ps[1]->getDefault()->getName(), "\n"; +?> +--EXPECT-- +bool(false) +Type parameter X has no default +bool(true) +int diff --git a/ext/reflection/tests/generics/parameter_to_string.phpt b/ext/reflection/tests/generics/parameter_to_string.phpt new file mode 100644 index 000000000000..60c441816533 --- /dev/null +++ b/ext/reflection/tests/generics/parameter_to_string.phpt @@ -0,0 +1,14 @@ +--TEST-- +Reflection: __toString on type parameter +--FILE-- + {} +foreach ((new ReflectionClass('A'))->getGenericParameters() as $p) { + echo $p, "\n"; +} +?> +--EXPECT-- +X +Y : object +Z = int ++W : object = stdClass diff --git a/ext/reflection/tests/generics/property_type_args.phpt b/ext/reflection/tests/generics/property_type_args.phpt new file mode 100644 index 000000000000..d201813592c9 --- /dev/null +++ b/ext/reflection/tests/generics/property_type_args.phpt @@ -0,0 +1,17 @@ +--TEST-- +Reflection: property type arguments via Reflection +--FILE-- + {} +class Holder { + public Box $b; +} +$rt = (new ReflectionClass('Holder'))->getProperty('b')->getType(); +echo $rt->getName(), "\n"; +echo count($rt->getGenericArguments()), "\n"; +echo $rt->getGenericArguments()[0]->getName(), "\n"; +?> +--EXPECT-- +Box +1 +int diff --git a/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt b/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt new file mode 100644 index 000000000000..642a5b7a79d3 --- /dev/null +++ b/ext/reflection/tests/generics/type_parameter_ref_get_param.phpt @@ -0,0 +1,19 @@ +--TEST-- +Reflection: ReflectionTypeParameterReference::getTypeParameter() +--FILE-- + {} +class Outer { + public Box $b; +} +$rt = (new ReflectionClass('Outer'))->getProperty('b')->getType(); +$ref = $rt->getGenericArguments()[0]; +$param = $ref->getTypeParameter(); +echo get_class($param), "\n"; +echo $param->getName(), "\n"; +echo $param->getDeclaringEntity()->getName(), "\n"; +?> +--EXPECT-- +ReflectionGenericTypeParameter +T +Outer diff --git a/ext/reflection/tests/generics/type_parameter_reference.phpt b/ext/reflection/tests/generics/type_parameter_reference.phpt new file mode 100644 index 000000000000..3ff5ae7d50e4 --- /dev/null +++ b/ext/reflection/tests/generics/type_parameter_reference.phpt @@ -0,0 +1,21 @@ +--TEST-- +Reflection: ReflectionTypeParameterReference appears inside getGenericArguments +--FILE-- + {} +class Outer { + public Box $b; +} +$rp = (new ReflectionClass('Outer'))->getProperty('b'); +$rt = $rp->getType(); +echo $rt->getName(), "\n"; +$args = $rt->getGenericArguments(); +echo count($args), "\n"; +echo get_class($args[0]), "\n"; +echo $args[0]->getName(), "\n"; +?> +--EXPECT-- +Box +1 +ReflectionTypeParameterReference +T diff --git a/ext/reflection/tests/generics/variance_enum.phpt b/ext/reflection/tests/generics/variance_enum.phpt new file mode 100644 index 000000000000..f05241526adb --- /dev/null +++ b/ext/reflection/tests/generics/variance_enum.phpt @@ -0,0 +1,18 @@ +--TEST-- +Reflection: ReflectionGenericVariance is a unit enum exposing three cases +--FILE-- +name, "\n"; +echo ReflectionGenericVariance::Covariant->name, "\n"; +echo ReflectionGenericVariance::Contravariant->name, "\n"; +var_dump(ReflectionGenericVariance::Invariant instanceof BackedEnum); +var_dump(ReflectionGenericVariance::Invariant === ReflectionGenericVariance::Invariant); +var_dump(ReflectionGenericVariance::Invariant === ReflectionGenericVariance::Covariant); +?> +--EXPECT-- +Invariant +Covariant +Contravariant +bool(false) +bool(true) +bool(false) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 484cb5aa8fc9..09c52bff0cf5 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -17,6 +17,7 @@ #include "php_incomplete_class.h" #include "zend_portability.h" #include "zend_exceptions.h" +#include "zend_inheritance.h" /* {{{ reference-handling for unserializer: var_* */ #define VAR_ENTRIES_MAX 1018 /* 1024 - offsetof(php_unserialize_data, entries) / sizeof(void*) */ @@ -1195,7 +1196,8 @@ object ":" uiv ":" ["] { lc_name = zend_string_tolower(class_name); if(!unserialize_allowed_class(lc_name, var_hash)) { zend_string_release_ex(lc_name, 0); - if (!zend_is_valid_class_name(class_name)) { + if (!zend_is_valid_class_name(class_name) + && !zend_class_name_is_monomorph(class_name)) { zend_string_release_ex(class_name, 0); return 0; } @@ -1220,7 +1222,13 @@ object ":" uiv ":" ["] { break; } - if (!ZSTR_HAS_CE_CACHE(class_name) && !zend_is_valid_class_name(class_name)) { + /* Canonical monomorph names ("Box") contain characters that + * zend_is_valid_class_name() rejects. zend_lookup_class_ex() has a + * dedicated monomorphization hook for these names, so accept them + * here and let the lookup either synthesize the monomorph or fail. */ + if (!ZSTR_HAS_CE_CACHE(class_name) + && !zend_is_valid_class_name(class_name) + && !zend_class_name_is_monomorph(class_name)) { zend_string_release_ex(lc_name, 0); zend_string_release_ex(class_name, 0); return 0; diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index 87b15b8bb345..38f5c5910876 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -166,6 +166,7 @@ char *get_token_type_name(int token_type) case T_DOLLAR_OPEN_CURLY_BRACES: return "T_DOLLAR_OPEN_CURLY_BRACES"; case T_CURLY_OPEN: return "T_CURLY_OPEN"; case T_PAAMAYIM_NEKUDOTAYIM: return "T_DOUBLE_COLON"; + case T_TURBOFISH: return "T_TURBOFISH"; case T_NS_SEPARATOR: return "T_NS_SEPARATOR"; case T_ELLIPSIS: return "T_ELLIPSIS"; case T_COALESCE: return "T_COALESCE"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 57c8edad8acb..e3dffb5e8979 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -717,6 +717,11 @@ * @cvalue T_PAAMAYIM_NEKUDOTAYIM */ const T_PAAMAYIM_NEKUDOTAYIM = UNKNOWN; +/** + * @var int + * @cvalue T_TURBOFISH + */ +const T_TURBOFISH = UNKNOWN; /** * @var int * @cvalue T_NS_SEPARATOR diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index b82842ede0f1..4c5f81240ae5 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit tokenizer_data.stub.php instead. - * Stub hash: c5235344b7c651d27c2c33c90696a418a9c96837 */ + * Stub hash: 34e6a9cb933770d8434820f2e7b567a05ed95c9e */ static void register_tokenizer_data_symbols(int module_number) { @@ -146,6 +146,7 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_DOLLAR_OPEN_CURLY_BRACES", T_DOLLAR_OPEN_CURLY_BRACES, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_CURLY_OPEN", T_CURLY_OPEN, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_PAAMAYIM_NEKUDOTAYIM", T_PAAMAYIM_NEKUDOTAYIM, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_TURBOFISH", T_TURBOFISH, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_NS_SEPARATOR", T_NS_SEPARATOR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_ELLIPSIS", T_ELLIPSIS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_COALESCE", T_COALESCE, CONST_PERSISTENT);