Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5167313
feat(zend): add lex and grammar for generic syntax
azjezz May 6, 2026
25fe011
feat(zend): add AST kinds, decl arity, and action wiring for generics
azjezz May 6, 2026
0c0095f
feat(zend): implement generic scope, bound erasure, and runtime metadata
azjezz May 6, 2026
04db944
feat(opcache): persist generic metadata through SHM and cache
azjezz May 6, 2026
6bde68d
feat(reflection): expose generic parameters and bounds via Reflection…
azjezz May 6, 2026
841bfe9
feat(zend, opcache, reflection): complete pre-erasure side table and …
azjezz May 6, 2026
bd868b6
feat(zend): allow generic type arguments on array, iterable, self, st…
azjezz May 6, 2026
332e214
test(zend, reflection): add generics test suite and fix uncovered sem…
azjezz May 6, 2026
5bedf6f
docs: add generics in NEWS, UPGRADING, and UPGRADING.INTERNALS
azjezz May 6, 2026
e674e30
test(zend): cover whitespace-free bound+default with type args (>> sp…
azjezz May 6, 2026
e833b03
feat(zend, reflection): expose direct-ancestor type arguments on Refl…
azjezz May 6, 2026
987a7eb
feat(zend): reject required type parameter after an optional one
azjezz May 6, 2026
8d4cbe4
feat(zend): resolve type-param vs type-name collisions via shadowing,…
azjezz May 6, 2026
1487315
fix(zend): drop const-discarding qualifiers and unused grammar nonter…
azjezz May 6, 2026
6ab30dc
fix(opcache, reflection): plug two memory leaks in generics persisten…
azjezz May 6, 2026
006bfdc
feat(zend): allow turbofish on attribute declarations
azjezz May 6, 2026
b9a9e0f
refactor(zend): address review feedback
azjezz May 6, 2026
a200bb8
refactor(reflection): throw on absent bound/default and non-ancestor …
azjezz May 6, 2026
1e0f1a9
test(reflection): update fixture for non-nullable generic-args returns
azjezz May 6, 2026
416232c
feat(zend): cap generic type parameters and arguments at 255
azjezz May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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 @@ -127,6 +127,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 @@ -232,6 +257,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 @@ -258,6 +300,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 @@ -103,6 +103,54 @@ PHP 8.6 INTERNALS UPGRADE NOTES
. The deprecated Z_IMMUTABLE(), Z_IMMUTABLE_P(), Z_OPT_IMMUTABLE(), and
Z_OPT_IMMUTABLE_P() macros have been removed. Check for
IS_ARRAY && !REFCOUNTED directly.
. 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
10 changes: 10 additions & 0 deletions Zend/tests/generics/erasure/arrow_fn_erasure.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Erasure: arrow function with type parameters erases to bound
--FILE--
<?php
$f = fn<T : int>(T $x): T => $x;
$r = new ReflectionFunction($f);
echo $r->getParameters()[0]->getType()->__toString(), "\n";
?>
--EXPECT--
int
12 changes: 12 additions & 0 deletions Zend/tests/generics/erasure/bound_object.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Erasure: T : object erases to object
--FILE--
<?php
function id<T : object>(T $x): T { return $x; }
$r = new ReflectionFunction('id');
echo $r->getParameters()[0]->getType()->__toString(), "\n";
echo $r->getReturnType()->__toString(), "\n";
?>
--EXPECT--
object
object
13 changes: 13 additions & 0 deletions Zend/tests/generics/erasure/bound_to_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Erasure: T : Foo erases to Foo
--FILE--
<?php
class Foo {}
function id<T : Foo>(T $x): T { return $x; }
$r = new ReflectionFunction('id');
echo $r->getParameters()[0]->getType()->getName(), "\n";
echo $r->getReturnType()->getName(), "\n";
?>
--EXPECT--
Foo
Foo
18 changes: 18 additions & 0 deletions Zend/tests/generics/erasure/builtin_array_erasure.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Erasure: array<...> erases to plain array at runtime
--FILE--
<?php
function f(array<int> $x): array<string, int> { return $x; }
$r = new ReflectionFunction('f');
$pt = $r->getParameters()[0]->getType();
echo $pt->getName(), "\n";
$rt = $r->getReturnType();
echo $rt->getName(), "\n";

f([1, 2, 3]);
echo "ok\n";
?>
--EXPECT--
array
array
ok
10 changes: 10 additions & 0 deletions Zend/tests/generics/erasure/closure_erasure.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Erasure: closure with type parameters erases to bound
--FILE--
<?php
$f = function <T : object>(T $x): T { return $x; };
$r = new ReflectionFunction($f);
echo $r->getParameters()[0]->getType()->__toString(), "\n";
?>
--EXPECT--
object
11 changes: 11 additions & 0 deletions Zend/tests/generics/erasure/composite_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
Erasure: composite bound erased
--FILE--
<?php
class A {}
function f<T : A|int>(T $x): T { return $x; }
$rt = (new ReflectionFunction('f'))->getReturnType();
echo get_class($rt), "\n";
?>
--EXPECT--
ReflectionUnionType
20 changes: 20 additions & 0 deletions Zend/tests/generics/erasure/erased_signature_match.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Erasure: generic signature matches hand-erased equivalent
--FILE--
<?php
class Foo {}

class Generic<T : Foo> {
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)
13 changes: 13 additions & 0 deletions Zend/tests/generics/erasure/extends_args_discarded.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Erasure: extends type arguments don't change runtime parent
--FILE--
<?php
class Base<T> {}
class Derived extends Base<int> {}
echo (new ReflectionClass('Derived'))->getParentClass()->getName(), "\n";
$d = new Derived;
var_dump($d instanceof Base);
?>
--EXPECT--
Base
bool(true)
13 changes: 13 additions & 0 deletions Zend/tests/generics/erasure/generic_class_param.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Erasure: generic class parameter type erased to bound
--FILE--
<?php
interface Iface {}
class Box<T : Iface> {
public function set(T $x): void {}
}
$rm = (new ReflectionClass('Box'))->getMethod('set');
echo $rm->getParameters()[0]->getType()->getName(), "\n";
?>
--EXPECT--
Iface
10 changes: 10 additions & 0 deletions Zend/tests/generics/erasure/get_class_returns_erased.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Erasure: get_class returns the erased class name
--FILE--
<?php
class Box<T> {}
$b = new Box::<int>;
echo get_class($b), "\n";
?>
--EXPECT--
Box
14 changes: 14 additions & 0 deletions Zend/tests/generics/erasure/instanceof_args_discarded.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Erasure: instanceof type arguments discarded at runtime
--FILE--
<?php
class C {}
$c = new C;
var_dump($c instanceof C);
var_dump($c instanceof C<int>);
var_dump($c instanceof C<string, int>);
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
13 changes: 13 additions & 0 deletions Zend/tests/generics/erasure/method_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Erasure: method-level type parameter erased
--FILE--
<?php
class Stringable2 {}
class C {
public function m<U : Stringable2>(U $x): U { return $x; }
}
$rt = (new ReflectionClass('C'))->getMethod('m')->getReturnType();
echo $rt->getName(), "\n";
?>
--EXPECT--
Stringable2
19 changes: 19 additions & 0 deletions Zend/tests/generics/erasure/method_inheritance_erased.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
Erasure: child overriding generic parent method works on erased signature
--FILE--
<?php
class Animal {}
class Parent2 {
public function f<T : Animal>(T $x): T { return $x; }
}
class Child extends Parent2 {
public function f<U : Animal>(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
13 changes: 13 additions & 0 deletions Zend/tests/generics/erasure/named_args_stripped.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Erasure: type arguments stripped from named types in runtime view
--FILE--
<?php
class Container {}
function f(Container<int> $x): Container<string> { return $x; }
$r = new ReflectionFunction('f');
echo $r->getParameters()[0]->getType()->getName(), "\n";
echo $r->getReturnType()->getName(), "\n";
?>
--EXPECT--
Container
Container
16 changes: 16 additions & 0 deletions Zend/tests/generics/erasure/property_type_erased.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Erasure: generic property type erased to bound
--FILE--
<?php
class Foo {}
class Box<T : Foo> {
public T $value;
}
$rp = (new ReflectionClass('Box'))->getProperty('value');
$rt = $rp->getType();
echo get_class($rt), "\n";
echo $rt->getName(), "\n";
?>
--EXPECT--
ReflectionNamedType
Foo
21 changes: 21 additions & 0 deletions Zend/tests/generics/erasure/runtime_bound_check.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Erasure: bound is enforced at runtime
--FILE--
<?php
class Animal {}
class Dog extends Animal {}

function f<T : Animal>(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
Loading
Loading