Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e10b034
Bound-Erased Generic Types Implementation
azjezz May 10, 2026
87b40a2
fix deriving concrete type enforcement
withinboredom May 19, 2026
69ca60d
monomorphize class generics
withinboredom May 19, 2026
c1a1cf2
reify method/functions
withinboredom May 19, 2026
9e1631d
more reification
withinboredom May 19, 2026
056dce3
add benchmarks
withinboredom May 19, 2026
93b104a
finish instanceof/catch impl
withinboredom May 20, 2026
fe13b67
documentation
withinboredom May 20, 2026
e7a12e7
get opcache working-ish
withinboredom May 20, 2026
187aa1f
optimize turbofish
withinboredom May 20, 2026
1c9d824
so many fixes...
withinboredom May 22, 2026
6e6eb89
fix synthesis inside generic methods
withinboredom May 23, 2026
c54f218
fix inheritance chaining
withinboredom May 23, 2026
24c47e1
cache resolved monomorph per call site for new X::<...>
henderkes Jun 15, 2026
754368e
Cache non-turbofish tables, turbofish lookups, concrete monomorphs
henderkes Jun 15, 2026
78b0d3d
Monomorphize generic function/method calls (reified generics)
henderkes Jun 16, 2026
b691fee
fix tests
henderkes Jun 16, 2026
1f95b73
add more benchmarks
withinboredom Jun 17, 2026
21fd39e
opcache: don't JIT reified-generic monomorphs (shared-opcode aliasing)
withinboredom Jun 17, 2026
bfbbe37
Zend: erase nested generic type argument when substituting a bare T leaf
withinboredom Jun 17, 2026
13d7ac8
Zend: by-name monomorph lookup must not raise on invalid type arguments
withinboredom Jun 17, 2026
96cc361
fix inference passing through
withinboredom Jun 18, 2026
c21b571
prevent monomorphed methods from colliding with unrelated classes tha…
withinboredom Jun 18, 2026
60a7eac
fix union of T|null
withinboredom Jun 18, 2026
5d527a6
fix new bare() and return an error when types cannot be determined --…
withinboredom Jun 18, 2026
11e7493
fold union/intersection with return types
withinboredom Jun 19, 2026
5e28858
handle inference for scalar values
withinboredom Jun 19, 2026
9076fd1
make new static::<> a compile time error
withinboredom Jun 19, 2026
1692bfd
preload inference and compile time monomorphization without explicit …
henderkes Jun 18, 2026
81c5215
fix CI
henderkes Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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::<int>()`) at call sites; and
type arguments on named types (`Box<int>`, `array<K, V>`, `iterable<T>`,
`self<T>`, `static<T>`, `parent<T>`). 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
Expand Down
69 changes: 69 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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<T : object> {
public T $value;
public function get(): T { return $this->value; }
}

function id<T>(T $x): T { return $x; }

Call sites accept turbofish type arguments (`Box::<int>::new()`,
`id::<int>(7)`); use sites accept type arguments on named types
(`Box<int>`, `array<int, string>`, `iterable<T>`, `self<T>`,
`static<T>`, `parent<T>`). Recursive bounds (`T : Comparable<T>`)
are supported. Anonymous classes cannot declare type parameters.

Generics are reified along two axes. Class-like generics are
monomorphised: each distinct argument tuple (`Box<int>`,
`Box<string>`) synthesises a separate class entry whose canonical
name is spelled out (`"Box<int>"`), registered in the class table,
and visible through `get_class()`, `var_dump`, and
`instanceof Box<int>`. Function- and method-level generics are
reified per call frame: each call carries its own type-argument
table, with no persistent `id<int>` 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<T>` / `catch (Box<T> $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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>", "Map<string,Foo<int>>"). 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<T>`,
`catch (Box<T> $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
Expand Down
24 changes: 24 additions & 0 deletions Zend/Optimizer/compact_literals.c
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>: 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:
Expand All @@ -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
Expand All @@ -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:
Expand Down
Loading
Loading