Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
fc488ae
feat(zend): add lex and grammar for generic syntax
azjezz May 6, 2026
289c98a
feat(zend): add AST kinds, decl arity, and action wiring for generics
azjezz May 6, 2026
916724f
feat(zend): implement generic scope, bound erasure, and runtime metadata
azjezz May 6, 2026
121b283
feat(opcache): persist generic metadata through SHM and cache
azjezz May 6, 2026
6f035be
feat(reflection): expose generic parameters and bounds via Reflection…
azjezz May 6, 2026
aad39c2
feat(zend, opcache, reflection): complete pre-erasure side table and …
azjezz May 6, 2026
72fc1a8
feat(zend): allow generic type arguments on array, iterable, self, st…
azjezz May 6, 2026
ff25af2
test(zend, reflection): add generics test suite and fix uncovered sem…
azjezz May 6, 2026
6cedb00
docs: add generics in NEWS, UPGRADING, and UPGRADING.INTERNALS
azjezz May 6, 2026
633542a
test(zend): cover whitespace-free bound+default with type args (>> sp…
azjezz May 6, 2026
d5f2c86
feat(zend, reflection): expose direct-ancestor type arguments on Refl…
azjezz May 6, 2026
148db61
feat(zend): reject required type parameter after an optional one
azjezz May 6, 2026
a183975
feat(zend): resolve type-param vs type-name collisions via shadowing,…
azjezz May 6, 2026
f57fd72
fix(zend): drop const-discarding qualifiers and unused grammar nonter…
azjezz May 6, 2026
e90f95a
fix(opcache, reflection): plug two memory leaks in generics persisten…
azjezz May 6, 2026
6f44436
feat(zend): allow turbofish on attribute declarations
azjezz May 6, 2026
66d770d
refactor(zend): address review feedback
azjezz May 6, 2026
255ea4b
refactor(reflection): throw on absent bound/default and non-ancestor …
azjezz May 6, 2026
9aefc00
test(reflection): update fixture for non-nullable generic-args returns
azjezz May 6, 2026
60b10e0
feat(zend): cap generic type parameters and arguments at 255
azjezz May 7, 2026
70217b9
feat(zend, reflection): enforce generic-arg arity at call sites and i…
azjezz May 7, 2026
8632b26
feat(zend): treat missing type args on inheritance as arity 0
azjezz May 7, 2026
7040b6d
feat(zend, opcache, reflection): bound checks and parametric substitu…
azjezz May 8, 2026
18fe173
fix(zend): typo
azjezz May 8, 2026
75a5a11
fix(zend): allow ?T and unions containing T when T erases to mixed
azjezz May 8, 2026
38ae91a
fix(zend): point T-aware diagnostic at T-ref erasure in intersection …
azjezz May 8, 2026
ff229d3
fix(zend): support scope-free covariant type checks for generic bounds
azjezz May 9, 2026
d58e97b
fix(opcache): preserve substituted arg_info on inherited methods
azjezz May 9, 2026
6324b5a
fix(opcache): persist substituted property hooks and skip SHM destroy
azjezz May 9, 2026
6ccab81
fix(opcache): only persist parametric-LSP property clones, not intern…
azjezz May 9, 2026
45cccb4
fix(opcache): remap generic_types and generic_parameters in xlat-shor…
azjezz May 9, 2026
3ed5dae
fix(zend, opcache): refcount strings
azjezz May 9, 2026
f46d716
fix(zend, opcache): use a property flag to identify generic clones
azjezz May 9, 2026
b003cc1
fix(zend): use-after-free?
azjezz May 9, 2026
2ef32b5
fix(opcache): plug generic_args/turbofish_args gaps and serialize_gen…
azjezz May 10, 2026
d00a1a3
perf(zend): cache inheritance binding once per (ce, target) within a …
azjezz May 10, 2026
57c9c5b
perf(zend): right-size generic-binding buffers to actual target arity
azjezz May 10, 2026
2211395
refactor(zend): drop unused forward decl for zend_validate_generic_in…
azjezz May 10, 2026
f596068
fix(opcache): remap clone prop name/doc_comment/attributes after pare…
azjezz May 10, 2026
31dd6b7
refactor(zend): extract zend_clone_arg_info_block helper for substitu…
azjezz May 10, 2026
3e0412d
refactor(reflection): extract declaring-context helpers, add fast-pat…
azjezz May 10, 2026
f8088c8
fix(zend, opcache): track generic_type_table persistence with explici…
azjezz May 10, 2026
08265fc
perf(optimizer): skip non-clone hooked properties before HT lookup
azjezz May 10, 2026
a183f10
perf(zend): lazy lc_name in generic-scope lookup, drop redundant push…
azjezz May 10, 2026
6f353c7
fix(zend): reject usage of instance type parameters in static context
azjezz May 10, 2026
50956ba
fix(zend): update expected error message in tests
azjezz May 10, 2026
7b4a15d
fix(zend): substitute generic type-args through extends, preserve nul…
azjezz May 10, 2026
c54ea59
fix(zend): allocate erased list-typed bound on the compile arena to m…
azjezz May 10, 2026
a52ce5f
feat(zend): enforce generic type-parameter variance at class declaration
azjezz May 10, 2026
22015f0
fix(zend): deep-copy nested type lists when erasing a generic bound
azjezz May 10, 2026
178be40
feat(zend): enforce variance markers on function-origin type parameters
azjezz May 10, 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
51 changes: 51 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,31 @@ 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.

At runtime each type parameter is replaced by its declared bound
(or `mixed` when unbounded, or when the bound is invalid in the
target position, e.g. `callable` on a property), and type
arguments are discarded. Pre-erasure metadata is preserved on
functions, methods, and class entries and is exposed through
Reflection so that PHP-based static-analysis tools can consume
generics without re-parsing source.
. It is now possible to use reference assign on WeakMap without the key
needing to be present beforehand.

Expand Down Expand Up @@ -236,6 +261,23 @@ 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 (pre-erasure form);
ReflectionNamedType::getName() continues to return the erased name.

- Intl:
. `grapheme_strrev()` returns strrev for grapheme cluster unit.
Expand All @@ -262,6 +304,15 @@ PHP 8.6 UPGRADE NOTES
. Openssl\Session
RFC: https://wiki.php.net/rfc/tls_session_resumption

- Reflection:
. ReflectionGenericTypeParameter (final, instances obtained via
ReflectionClass::getGenericParameters() and
ReflectionFunctionAbstract::getGenericParameters()).
. ReflectionTypeParameterReference (extends ReflectionType, appears only
inside pre-erasure 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
48 changes: 48 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,54 @@ PHP 8.6 INTERNALS UPGRADE NOTES
zend_fci_consumed_arg(), which allows moving a selected callback argument
instead of copying it in zend_call_function(). Currently only a single
consumed argument is supported.
. Added support for runtime-bound-checked 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 pre-erasure 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 only the erased form.
. 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 pre-erasure 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().
. 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
45 changes: 45 additions & 0 deletions Zend/Optimizer/optimize_func_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,46 @@ static void zend_delete_call_instructions(const zend_op_array *op_array, zend_op
}
}

/* 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:
if (call == 0) {
return false;
}
ZEND_FALLTHROUGH;
case ZEND_NEW:
case ZEND_INIT_DYNAMIC_CALL:
case ZEND_INIT_USER_CALL:
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:
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;
Expand All @@ -97,6 +137,11 @@ 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. */
return;
}

for (i = 0; i < num_args; i++) {
/* Don't inline functions with by-reference arguments. This would require
* correct handling of INDIRECT arguments. */
Expand Down
33 changes: 33 additions & 0 deletions Zend/Optimizer/zend_optimizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1734,12 +1734,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();
Expand Down
18 changes: 18 additions & 0 deletions Zend/tests/generics/declaration/default_satisfies_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Declaration: type-parameter default that satisfies its bound is accepted
--FILE--
<?php
class A {}
class B extends A {}

class Box<T : A = B> {}
function f<T : A = B>(): void {}
trait Tr<T : A = B> {}
interface I<T : A = B> {}

new Box;
f();
echo "OK\n";
?>
--EXPECT--
OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: class type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
class Box<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: function type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
function id<T : Animal = int>(): void {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: interface type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
interface I<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: trait type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
trait Holder<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Declaration: default checked against intersection bound when types are concrete
--FILE--
<?php
class Box<T : Traversable & Countable = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Traversable&Countable in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/generics/declaration/default_with_union_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Declaration: default checked against union bound when types are concrete
--FILE--
<?php
class Box<T : int | string = float> {}
?>
--EXPECTF--
Fatal error: Default float for type parameter T does not satisfy its bound string|int in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Bound error message: declaration-time default-vs-bound error renders NAMED_WITH_ARGS in the bound
--FILE--
<?php
interface Comparable<T> {}
class Box<T : Comparable<T> = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Comparable<T> in %s on line %d
17 changes: 17 additions & 0 deletions Zend/tests/generics/declaration/no_leak_dnf_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Generics: declaring a class with a DNF-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}
interface C {}

class Holder<T: (A&B)|C> {
public function take(T $x): void {}
public function get(): T {}
}

echo "ok\n";
?>
--EXPECT--
ok
15 changes: 15 additions & 0 deletions Zend/tests/generics/declaration/no_leak_intersection_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Generics: declaring a class with an intersection-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}

class Holder<T: A&B> {
public function take(T $x): void {}
}

echo "ok\n";
?>
--EXPECT--
ok
15 changes: 15 additions & 0 deletions Zend/tests/generics/declaration/no_leak_union_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Generics: declaring a class with a union-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}

class Holder<T: A|B> {
public function take(T $x): void {}
}

echo "ok\n";
?>
--EXPECT--
ok
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Variance: +T on an arrow function in parameter position is rejected
--FILE--
<?php
$f = fn<+T>(T $x): T => $x;
?>
--EXPECTF--
Fatal error: Type parameter T declared covariant (+T) cannot appear in contravariant position in %s on line %d
Loading
Loading