[PHP-DEV] [RFC] [Discussion] Bound-Erased Generic Types

- RFC: PHP: rfc:bound_erased_generic_types
- Implementation: PHP RFC: Bound-Erased Generic Types by azjezz · Pull Request #21969 · php/php-src · GitHub

Hello Internals,

Status update: RFC is now at v0.22.

Two changes are worth flagging.

1. Substituted runtime contracts are now enforced end-to-end on
inherited members.

A child class binding a generic parent now enforces the substituted
parameter and return types at runtime at every site the substitution
touches: method and constructor entry (including defaults and
per-element variadics), return values, backed property storage,
property hook get/set signatures, and trait-imported method and
property types. Enforcement is uniform regardless of the parent's
bound, a child binding T to int on Box<T : mixed> rejects non-int
values everywhere, not just where the parent's bound happened to be
stricter.

This collapses the "body bytecode is not recompiled per substitution"
caveat from earlier drafts. The specific case the previous Limitations
text called out as observable (a virtual hook returning a hardcoded
value of the wrong substituted type) now throws an error. The
Limitations section is down to three bullets: type-argument erasure
(Box<int> accepts a Box<string> because the runtime check is
instanceof Box), turbofish doesn't tighten parameters, and
method-level T-bound resolves to the bound rather than the
per-instance instantiation.

2. Reflection: plural ancestor-binding getter.

`ReflectionClass::getGenericArgumentsForParentInterface()` now returns
`list<list<ReflectionType>>`. A class binding the same generic
interface multiple times, gets every binding as a separate outer-list
entry. The previous singular shape silently dropped all but one. (
e.g. `class Bar implements Foo<int>, Foo<string>`)

Cheers,
Seifeddine.

Hi Rob

I'm curious how you'd fix it. The fix has to be either (a) per-child bytecode specialization (which the RFC explicitly says it doesn't do), (b) runtime indirection at every entry (which adds dispatch cost the RFC doesn't acknowledge), or (c) something else not described. The architecture section makes a specific claim about where parameter checks happen; the bug-fix commitment implies something different.

it's option (b), with measured cost. landed in v0.22.

The mechanism: when `class IntBox extends Box<int>` links, the engine
produces a substituted clone of each inherited method that has `T` in
its signature. The clone shares the parent's bytecode but carries its
own `arg_info` with `T` substituted to `int`. That part predated this
change (It existed for reflections). What changed is that the runtime
check sites now read from the clone's `arg_info` rather than from
compile-time-baked values.

Cost: non-generic code pays nothing at the bake/strip sites, the gates
check `op_array->generic_types`, which is NULL for non-generic
functions. The only cross-cutting change is `RECV_INIT`'s
verify-after-default, which adds one helper call per default usage;
the helper short-circuits for already-validated defaults. Generic code
pays one helper dispatch per `T`-bearing parameter per call and one
extra verify opcode per `T`-bearing return path. Bench drift is within
the ±0.1% band documented in the Performance section.

Cheers,
Seifeddine.

On Tue, 12 May 2026 at 10:07, Seifeddine Gmati azjezz@carthage.software wrote:

Hello Internals,

I’d like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

Thanks,
Seifeddine.

This is just from the perspective of a static analysis tool author, but I hope it’s instructive:

Currently every return, param type and property type requires two fields: the type in the signature and the type in the docblock.

These can sometimes be different types, and each static analysis tool has its own way of dealing with the discrepancy. Furthermore, people will sometimes omit the docblock type definition on child class definitions of those methods, and static analysis tools need their own way to handle those discrepancies too.

When I ported Psalm to run on Hack code (whose type structure mirrors this RFC) I was able to remove all of that code, which made the implementation much more straightforward and memory-efficient.

I understand that improving the static analysis data model should not, on its own, be a reason to merge, but I just wanted to relate how much easier this model makes things.

Best wishes,

Matt

On Sun, 10 May 2026, Seifeddine Gmati wrote:

I'd like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

- RFC: PHP: rfc:bound_erased_generic_types
- Implementation: PHP RFC: Bound-Erased Generic Types by azjezz · Pull Request #21969 · php/php-src · GitHub

I have just read through this well-put-together RFC again. It however
does not change my opinion that I don't believe that having types
declared, but not generally enforced, is a good idea.

I have had several conversions with PHP users over the last few weeks,
and although many would like generics, they were almost exclusively
confused when it became clear these types weren't enforced.

It would create another paradigm for PHP users, different from
everywhere else where types are defined they are enforced.

I therefore do not believe this is a way forward, and await what the
result of Compile time generics: yay or nay? — The PHP Foundation — Supporting, Advancing, and Developing the PHP Language is
going to turn out to be.

with kind regards,
Derick

--
https://derickrethans.nl | https://xdebug.org | https://dram.io

Author of Xdebug. Like it? Consider supporting me: Xdebug: Support

mastodon: @derickr@phpc.social @xdebug@phpc.social

Hi Derick,

Il 25/05/2026 17:28, Derick Rethans ha scritto:

On Sun, 10 May 2026, Seifeddine Gmati wrote:

I'd like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

- RFC: PHP: rfc:bound_erased_generic_types
- Implementation: PHP RFC: Bound-Erased Generic Types by azjezz · Pull Request #21969 · php/php-src · GitHub

I have just read through this well-put-together RFC again. It however
does not change my opinion that I don't believe that having types
declared, but not generally enforced, is a good idea.

I have had several conversions with PHP users over the last few weeks,
and although many would like generics, they were almost exclusively
confused when it became clear these types weren't enforced.

It would create another paradigm for PHP users, different from
everywhere else where types are defined they are enforced.

I therefore do not believe this is a way forward, and await what the
result of Compile time generics: yay or nay? — The PHP Foundation — Supporting, Advancing, and Developing the PHP Language is
going to turn out to be.

FWIW, I share the same feelings.

Cheers
--
Matteo Beccati

On Mon, 25 May 2026 at 11:29, Derick Rethans <derick@php.net> wrote:

I therefore do not believe this is a way forward, and await what the
result of https://thephp.foundation/blog/2025/08/05/compile-generics/ is
going to turn out to be.

That blog post is a year old, and the top comment on the attached Reddit thread captures my view on it:

The problem with such an approach is that it locks generics to be reified. If at some point PHP realizes it’s not possible or feasible to implement reified generics for everything else (new construct, methods, compound types etc), then PHP will likely never implement erased generics for those missing features, or generics in general in PHP - due to backwards compatibility promise and/or consistency, and PHP will be stuck forever with half-baked feature that, arguably, doesn’t even cover 50% of it’s usage (although the article states it’s 80%, I doubt that’s true. In my experience generics in functions, methods as well as new instantiations account for more than half of all usages, if not much more).

I would much rather have “full” support of erased generics, than a <50% support of reified generics that will never get above that 50%.

I would add to that: in 2004 adding runtime-checked parameter types at function boundaries made sense because there was no other widely-accepted way of checking those contracts. Adding checks at function boundaries made failures much easier to debug. It would take more than a decade before open-source static analysis tools began to be widely adopted by PHP developers — tools that can discover such bugs far earlier in the development process.

In 2026 runtime-checked types don’t make sense in all circumstances, not when static analysis tools can discover far more bugs, far earlier and faster, than runtime checks can ever hope to. The majority of new interpreted code being written today is in languages with erased generics.

I feel that the decisions made in 2004 should not dominate decision-making today.

On 25 May 2026 20:26:41 BST, Matthew Brown <matthewmatthew@gmail.com> wrote:
>> Compile time generics: yay or nay? — The PHP Foundation — Supporting, Advancing, and Developing the PHP Language is

That blog post is a year old, and the top comment on the attached Reddit
thread captures my view on it:

The problem with such an approach is that it locks generics to be

reified.

I think the two proposals have a lot more overlap than their framing suggests. Both block us from having completely erased generics, because they include *some* reification/monomorphization, but nothing in Gina/Larry's blog post would actually block Seifeddine's proposal as far as I can see.

Specifically, if you take everything Seifeddine has implemented, but 1) restrict generic declarations to only interfaces and abstract classes; and 2) remove the "turbofish" syntax completely; then you end up basically with what the blog post suggests.

The main difference of opinion is what to do with those "missing" parts:

- Larry & Gina proposed just leaving then forbidden until we decide how to implement them
- Seifeddine proposed locking in the syntax with some partial checking, but mostly just reflection support

In 2026 runtime-checked types don't make sense in all circumstances, not
when static analysis tools can discover far more bugs, far earlier and
faster, than runtime checks can ever hope to.

Elsewhere on this thread, I've put forward some thoughts about having a specific syntax distinction between runtime-checked type information, and SA/reflection-only type information. I'd be interested in your thoughts on that approach.

Rowan Tommins
[IMSoP]

On Tue, May 26, 2026, at 8:12 AM, Rowan Tommins [IMSoP] wrote:

On 25 May 2026 20:26:41 BST, Matthew Brown <matthewmatthew@gmail.com> wrote:
>> Compile time generics: yay or nay? — The PHP Foundation — Supporting, Advancing, and Developing the PHP Language is

That blog post is a year old, and the top comment on the attached Reddit
thread captures my view on it:

The problem with such an approach is that it locks generics to be

reified.

I think the two proposals have a lot more overlap than their framing
suggests. Both block us from having completely erased generics, because
they include *some* reification/monomorphization, but nothing in
Gina/Larry's blog post would actually block Seifeddine's proposal as
far as I can see.

Specifically, if you take everything Seifeddine has implemented, but 1)
restrict generic declarations to only interfaces and abstract classes;
and 2) remove the "turbofish" syntax completely; then you end up
basically with what the blog post suggests.

The main difference of opinion is what to do with those "missing" parts:

- Larry & Gina proposed just leaving then forbidden until we decide how
to implement them
- Seifeddine proposed locking in the syntax with some partial checking,
but mostly just reflection support

I would agree. Seif's proposal is effectively a superset of what Gina was working on, and should it pass, it would include all of the functionality of Gina's proposal and then some.

--Larry Garfield

On Tue, 26 May 2026 at 15:18, Larry Garfield <larry@garfieldtech.com> wrote:

On Tue, May 26, 2026, at 8:12 AM, Rowan Tommins [IMSoP] wrote:

On 25 May 2026 20:26:41 BST, Matthew Brown <matthewmatthew@gmail.com> wrote:

https://thephp.foundation/blog/2025/08/05/compile-generics/ is

That blog post is a year old, and the top comment on the attached Reddit
thread captures my view on it:

The problem with such an approach is that it locks generics to be
reified.

I think the two proposals have a lot more overlap than their framing
suggests. Both block us from having completely erased generics, because
they include some reification/monomorphization, but nothing in
Gina/Larry’s blog post would actually block Seifeddine’s proposal as
far as I can see.

Specifically, if you take everything Seifeddine has implemented, but 1)
restrict generic declarations to only interfaces and abstract classes;
and 2) remove the “turbofish” syntax completely; then you end up
basically with what the blog post suggests.

The main difference of opinion is what to do with those “missing” parts:

  • Larry & Gina proposed just leaving then forbidden until we decide how
    to implement them
  • Seifeddine proposed locking in the syntax with some partial checking,
    but mostly just reflection support

I would agree. Seif’s proposal is effectively a superset of what Gina was working on, and should it pass, it would include all of the functionality of Gina’s proposal and then some.

–Larry Garfield

Yes it’s a superset of the syntax, and leaves the door open to some runtime checks being added in the future, per you & Gina’s proposal.

On Sunday, 10 May 2026 at 21:05, Seifeddine Gmati <azjezz@carthage.software> wrote:

Hello Internals,

I'd like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

- RFC: PHP: rfc:bound_erased_generic_types
- Implementation: PHP RFC: Bound-Erased Generic Types by azjezz · Pull Request #21969 · php/php-src · GitHub

As you already know from off list discussion I will vote against this RFC.
I've laid my arguments in a blog post on my website. [1]

The only thing I take offence to is saying my attempt/proposal has stalled.
Me not actively working on it 24/7 doesn't mean it stalled, and I had started working on it earlier in the year before this RFC landed, or I was aware you were working on something.

Best regards,

Gina P. Banyard

[1] Opinion about the recent “Bound-Erased Generic Types” RFC

On Tue, 2 Jun 2026 at 11:15, Gina P. Banyard internals@gpb.moe wrote:

As you already know from off list discussion I will vote against this RFC.
I’ve laid my arguments in a blog post on my website. [1]

The only thing I take offence to is saying my attempt/proposal has stalled.
Me not actively working on it 24/7 doesn’t mean it stalled, and I had started working on it earlier in the year before this RFC landed, or I was aware you were working on something.

Best regards,

Gina P. Banyard

[1] https://gpb.moe/blog/opinion-bound-erased-generics.html

Thanks for this article! It’s a very thorough set of complaints.

A few central assertions jumped out to me:

assuming this survey had a representative sample of PHP users … and that nobody uses both PHPStan and Psalm at the same time … we would get at best 44% of users employing SA tools. A far cry from the 90% number I’m constantly told

In that same survey [1], 68% of respondents say they use PhpStorm, and static analysis is one of the main reasons people use PhpStorm.

That easily gets us to >80% of survey respondents using some sort of static analysis tool in their flow, accounting for people using more than one.

Indeed, the majority of static analysis tools introduced additional atomic types that do not exist within PHP — class-string, non-empty-list, positive-int, numeric …

I want to say — as the person who came up with most of those — that some of the docblock types do go a little overboard. But the generic ones are on the money.

Best wishes,

Matt

[1] https://blog.jetbrains.com/phpstorm/2025/10/state-of-php-2025/#most-used-ide-or-editor

I really enjoyed your article about the generics RFC. Your arguments all make sense and are well built and argued. These arguments are against having bound-erased generic types and they are completely valid. The RFC presents arguments for having bound-erased generic types in the languages and they are also completely valid.

It’s good to point out and stress the point that the proposed generics are not completely erased, they are still completely accessible in the reflection (which could be taken advantage of by frameworks and apps to deduplicate some information in the code) and they are bound-erased, which means that some type-checking (which can be done cheaply) will still be performed.

I’d also like to point out a few places in currently shipping PHP version where you can put types in native syntax and they are not checked at runtime, meaning completely wrong and non-existent types are silently being accepted or skipped. I didn’t have to think hard about them, I’ve been using these examples for years at the beginning of my presentations about PHPStan and what errors in code it detects in contrast to PHP which doesn’t detect them.

  • Foo::class - PHP will happily create literal string ‘Foo’ out of this even if class Foo does not exist.

  • catch (FooException) - PHP will not tell you you’re trying to catch an exception class (or interface) that does not exist (or can’t be a subtype of Throwable), it will silently skip this catch block.

  • Using a non-existent class as a property/parameter/return type. PHP will not error when loading the function or class with the method in question, but it will error on anything you will try to pass to it.

  • Using a trait name as a property/parameter/return type. PHP does not report an error on the declaration either, but it will error on anything you will try to pass to it. Including a class that uses the trait. Proof: https://3v4l.org/vH9Bn#v

So it’s not completely unprecedented. Sure, these examples might also surprise a lot of developers writing PHP every day, but they’re there.

···

Ondřej Mirtes

I just remembered one more place: attributes. They’re not validated until something tries to newInstance them via reflection. Which might be never. They can be invalid for all kinds of reasons - nonexistent class, class that isn’t an attribute, targeting wrong target. Proof: https://3v4l.org/Pmai6#v

···

Ondřej Mirtes

On 4 June 2026 11:58:46 BST, "Ondřej Mirtes" <ondrej@mirtes.cz> wrote:

On 4. 6. 2026 at 12:29:42, Ondřej Mirtes <ondrej@mirtes.cz> wrote:

’d also like to point out a few places in currently shipping PHP version
where you can put types in native syntax and they are not checked at
runtime, meaning completely wrong and non-existent types are silently being
accepted or skipped.

I just remembered one more place: attributes.

Interestingly, all of these examples effectively make the *opposite* check to what Seifeddine is proposing: the *name* is unchecked, but the *value* compared to it is always rejected as not matching.

In "function foo(Nonesuch $x) {}", there is no complaint that "Nonesuch" is not a valid class, but the function can't be called because no parameter matches.

In the proposed "function foo(Container<Nonesuch> $x)", the bounds checking will require Nonesuch to exist, but the function can be called with any instance of Collection. The function would be callable even if an instance of Nonesuch was impossible to create (an interface with no implementations, or a final private constructor, etc).

None of your examples breaks the invariant "if I mark a value as requiring a particular type, I can be sure that reading that value will give me a value of that type".

The only marginal case would be parameters to attributes, which can be read through reflection without running the constructor, and therefore without validating the types specified in that constructor. But that's in keeping with the power of reflection to bypass the language's normal invariants, e.g. newInstanceWithoutConstructor, setAccessible, etc

So I do think what Seifeddine is proposing is unprecedented in that sense.

Rowan Tommins
[IMSoP]

On Wed, 3 Jun 2026 at 18:49, Matthew Brown <matthewmatthew@gmail.com> wrote:

On Tue, 2 Jun 2026 at 11:15, Gina P. Banyard internals@gpb.moe wrote:

As you already know from off list discussion I will vote against this RFC.
I’ve laid my arguments in a blog post on my website. [1]

The only thing I take offence to is saying my attempt/proposal has stalled.
Me not actively working on it 24/7 doesn’t mean it stalled, and I had started working on it earlier in the year before this RFC landed, or I was aware you were working on something.

Best regards,

Gina P. Banyard

[1] https://gpb.moe/blog/opinion-bound-erased-generics.html

Also: the article also dedicates a long section to the potential for transpilers.

I believe a transpiler would fail to gain ground for a few reasons:

  • The parser would need to keep up with the upstream parser, like nikic/php-parser does
  • But it could not share the same source code as nikic/php-parser — it would need to be a fork, as the code would no longer be PHP
  • To provide accurate error positions, static analysis tools would need to support the “enhanced” ASTs directly (which would mean they could no longer use nikic/php-parser)
  • Source maps. Source maps everywhere.

And then there’s the trust issue which you reference in your article. Trust is incredibly important when developers choose to migrate all their source code to a new system. Microsoft’s TypeScript has earned that trust over the course of a decade with incredible tooling that has cost that company tens of millions of dollars to produce and market. That effort is not trivial to reproduce, nor would the vast majority of PHP repos likely deem it worthwhile.

OTOH if this proposal is accepted everyone those same audiences get generics without any effort on their part. I believe that’s why there is strong community support for this proposal.

On Tue, 2 Jun 2026 at 16:13, Gina P. Banyard internals@gpb.moe wrote:

On Sunday, 10 May 2026 at 21:05, Seifeddine Gmati azjezz@carthage.software wrote:

Hello Internals,

I’d like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

As you already know from off list discussion I will vote against this RFC.
I’ve laid my arguments in a blog post on my website. [1]

The only thing I take offence to is saying my attempt/proposal has stalled.
Me not actively working on it 24/7 doesn’t mean it stalled, and I had started working on it earlier in the year before this RFC landed, or I was aware you were working on something.

Best regards,

Gina P. Banyard

[1] https://gpb.moe/blog/opinion-bound-erased-generics.html

Hi Gina,

Thanks for the blog post. Even though we’ll disagree on the conclusion, the arguments are well-laid-out and worth engaging with on their merits. Matt and Ondřej have responded to most of the substantive points already, and I’d echo what they said.

One small clarification on the “stalled” language: I didn’t mean (and didn’t intend to imply) anything about the people doing the work, or the effort they were putting in. I’m well aware that everyone working on this is doing it on top of other commitments, and progress at the pace people can sustain is the only realistic pace there is. What I meant by “stalled” was specifically the public RFC process: the 2016 RFC remained in Draft, no implementation reached a vote, and the 2024 work continued but didn’t produce a filed RFC. That’s a statement about the procedural milestones, not the engineering work or the people doing it. I should have phrased it more precisely. Apologies for the imprecision.

I appreciate the work you have been doing on compile-time generics for class-likes, and the RFC’s Future Scope section explicitly notes that the two approaches compose. Whatever this RFC’s outcome, that work continues to be valuable.

Cheers,
Seifeddine.

Hi Seifeddine,

But count me out for opt-in enforcement. We either enforce it or we don't. [...] If
call-site enforcement can be made cheap enough to include, it's cheap enough to *always*
include.

The premise here is that runtime enforcement can be made cheap enough
in the foreseeable future. That's the universal-reification path, and
no implementation has yet demonstrated it works under PHP's
compilation model. Until we have a solution, "always enforce" isn't a
deferrable engineering choice, it's a constraint we currently can't
meet. If someone produces a viable design, the path forward is either
an opt-in mechanism (using the #[ReifiedGenerics] attribute, the reify
keyword, or the declare directive) or a complete switch to reified
generics, which would involve a breaking change (BC break). What I
won't commit to is "we'll just make it cheap enough later" when the
engineers who have tried haven't been able to.

Even if runtime enforcement can never be made cheap enough, I believe it's
a good thing for type erased generics' syntax to clearly express the
inconsistency with regular runtime checked type hints. Runtime type
checking is a form of safety offered by PHP, and by default safety options
should be opt-out, not opt-in. Even for best practice developers using
static analysis, seeing 'erased' explicitly in the syntax might be just the gentle
reminder we need while debugging a real-world edge case.

The word 'reified' means nothing to most people other than language
designers, and whether PHP's generics are reified or monomorphised is an
implementation detail developers need not know about. `#[TypeErased]` has
a much better chance of being understood by developers as type unchecked
when they come across code using it.

In summary:
- If reified generics never happen, and our code is forever left with erased keywords/attributes, that's still helpful.
- If reified generics do happen, we get the choice to remove our erased keywords/attributes, that's ideal.
- The only bad scenario is reified generics do happen performantly but we have to opt-in to them everywhere.

Cheers,
Luke

On 03/06/2026 23:49, Matthew Brown wrote:

> assuming this survey had a representative sample of PHP users ... and that nobody uses both PHPStan and Psalm at the same time ... we would get at best 44% of users employing SA tools. A far cry from the 90% number I’m constantly told

In that same survey [1], 68% of respondents say they use PhpStorm, and static analysis is one of the /main /reasons people use PhpStorm.

I don't think this is equivalent. PhpStorm only complains if it can prove a type is wrong; and in my experience, there are plenty of people who happily ignore the errors it does flag.

To replace the safety of run-time checks, you need a tool that only allows code into production if it can prove that no type errors would happen.

That's a different question from "who would a nice syntax for generics be useful for" - I think it would be *useful* even for people coding in a very simple editor, because it makes the code more expressive. But it would be considerably *less useful* if that extra information couldn't actually be trusted because nothing enforced it.

--
Rowan Tommins
[IMSoP]

On Sun, May 10, 2026, at 21:02, Seifeddine Gmati wrote:

Hello Internals,

I’d like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

Thanks,
Seifeddine.

Hello internals,

For those not in discord, I spent nearly a week attempting to implement reified generics on top of this branch to see how challenging it would be.

I have a working implementation: https://github.com/php/php-src/compare/master…bottledcode:php-src:reify

Disclosure: AI assisted in tests and memory leak bug hunts.

The Approach

Classes are monomorphized and similar to Gina’s substitution approach – a monomorphized class shares as much memory as the templated class as possible, mainly holding its substitutions. This happens during runtime, only when it cannot be done at compile time (which is mostly already handled by this branch).

Functions/methods get bound in call frames and share the substituted types from their parent (class, outer closure, etc).

The Result

Given:

class Box<T : object> {
  public function __construct(public T $value) {}
  public function get(): T { return $this->value; }
}

The following work as expected:

$b = new Box::<DateTime>(new DateTime());
$b instanceof Box<DateTime>; // true
$b instanceof Box<Request>; // false

Whereas the following will generate errors at runtime:

$b = new Box::<DateTime>(new Response);
$b = new Box::<int>('password 123');

Performance

Importantly, for code that does not use generics: there is no impact (generics specific code is skipped).

For a call (either new or a function/method call) that does use generics, the call takes up to ~2x a non-generic call. By using Seif’s PSL library as a test, and converting it to generics, we were able to show that benchmarks go about 1.3-1.5x slower when comparerd to no type checking on the original (ie, mixed).

Of note, when doing manual type checking with mixed, the performance cost of generics is roughly the same. That means:

function foo<T>(T $val): T {
  return $val;
}

function bar(mixed $val): mixed {
  if (is_int($val)) { return $val; }
  throw new Exception();
}

These two functions will have roughly the same performance, with some jitter depending on the complexity of the type being checked.

I think this is important to point out: if checked generics are “about as fast as manual checks”, then it behooves us to go for checked generics and not erased generics, which would force everyone to type check manually. Whether that happens in this same RFC or a later one is an open question.

Related Bug

One other issue discovered while working on this branch, the following doesn’t behave as written with erased generics:

try {
  do_http_call();
} catch(HttpError<NotFound> $e) {
  // ignore
} catch(HttpError<Forbidden> $e) {
  // alert: api key has been revoked
}

With erased generics, the latter is never hit as it is erased to just HttpError … no warning, no error … it doesn’t work as written.

To me, that is a massive footgun. Checked generics work as written.

Limited Inference

Secondly, I was able to get limited inference “for free” with this approach. This works:

class Foo { public string $kind = 'foo'; }
class Bar { public string $kind = 'bar'; }

function kind<T : object>(T $x): string {
  return T::class;
}

echo kind(new Foo) . "\n";
echo kind(new Bar) . "\n";
// outputs:
// Foo
// Bar

I would appreciate it if others could review the code to verify my results and implementation. Assuming it is soound, I believe this should resolve issues people have been raising with partially erased generics and give us sufficiently complete generics.

Whether to write a new RFC or merge into a single RFC is still being discussed in discord and open for discussion here.

PS. My normal email address is broken, so this is a new email address on this list. :waving_hand:

— Rob

Hi Rob

Very interesting. If this is what it takes to get enough people on board, then so be it.

On a personal note, but don’t let this prevent you from further exploring this option: I hope that at some point we get a way to opt out of runtime type checks (generics or types in general). A 2x performance penalty is a serious issue.

To reiterate what I’ve said before: generics aren’t a runtime tool. Their value comes from static analysis and reflection for meta programming, and I see no reason why generic code should be type-checked at runtime when it has already been type-checked before. As both a PHP user and a representative for one of PHP’s most used static analysers, I want three things: a proper spec, proper syntax, and proper reflection. All these problems are solved with the current RFC; without runtime type checking, and without performance penalties.

Maybe this is just a necessary process for PHP to go through; and who knows, in a couple of years, practical experience will have shown and convinced enough people that it’s unnecessary. I’ll happily deal with the performance overhead and will continue to hope for an opt-out mechanism in the future.

Thanks for the work and effort, really interesting!

Brent

On Mon, Jun 8, 2026 at 5:17 PM Rob Landers <rob@getswytch.com> wrote:

On Sun, May 10, 2026, at 21:02, Seifeddine Gmati wrote:

Hello Internals,

I’d like to start the discussion on a new RFC adding bound-erased
generics types to PHP.

Generic type parameters can be declared on classes, interfaces,
traits, functions, methods, closures, and arrow functions, with
bounds, defaults, and variance markers. Type parameters erase to their
bound at runtime; the pre-erasure form is preserved for Reflection and
consumed by static analyzers.

Thanks,
Seifeddine.

Hello internals,

For those not in discord, I spent nearly a week attempting to implement reified generics on top of this branch to see how challenging it would be.

I have a working implementation: https://github.com/php/php-src/compare/master…bottledcode:php-src:reify

Disclosure: AI assisted in tests and memory leak bug hunts.

The Approach

Classes are monomorphized and similar to Gina’s substitution approach – a monomorphized class shares as much memory as the templated class as possible, mainly holding its substitutions. This happens during runtime, only when it cannot be done at compile time (which is mostly already handled by this branch).

Functions/methods get bound in call frames and share the substituted types from their parent (class, outer closure, etc).

The Result

Given:

class Box<T : object> {
  public function __construct(public T $value) {}
  public function get(): T { return $this->value; }
}

The following work as expected:

$b = new Box::<DateTime>(new DateTime());
$b instanceof Box<DateTime>; // true
$b instanceof Box<Request>; // false

Whereas the following will generate errors at runtime:

$b = new Box::<DateTime>(new Response);
$b = new Box::<int>('password 123');

Performance

Importantly, for code that does not use generics: there is no impact (generics specific code is skipped).

For a call (either new or a function/method call) that does use generics, the call takes up to ~2x a non-generic call. By using Seif’s PSL library as a test, and converting it to generics, we were able to show that benchmarks go about 1.3-1.5x slower when comparerd to no type checking on the original (ie, mixed).

Of note, when doing manual type checking with mixed, the performance cost of generics is roughly the same. That means:

function foo<T>(T $val): T {
  return $val;
}

function bar(mixed $val): mixed {
  if (is_int($val)) { return $val; }
  throw new Exception();
}

These two functions will have roughly the same performance, with some jitter depending on the complexity of the type being checked.

I think this is important to point out: if checked generics are “about as fast as manual checks”, then it behooves us to go for checked generics and not erased generics, which would force everyone to type check manually. Whether that happens in this same RFC or a later one is an open question.

Related Bug

One other issue discovered while working on this branch, the following doesn’t behave as written with erased generics:

try {
  do_http_call();
} catch(HttpError<NotFound> $e) {
  // ignore
} catch(HttpError<Forbidden> $e) {
  // alert: api key has been revoked
}

With erased generics, the latter is never hit as it is erased to just HttpError … no warning, no error … it doesn’t work as written.

To me, that is a massive footgun. Checked generics work as written.

Limited Inference

Secondly, I was able to get limited inference “for free” with this approach. This works:

class Foo { public string $kind = 'foo'; }
class Bar { public string $kind = 'bar'; }

function kind<T : object>(T $x): string {
  return T::class;
}

echo kind(new Foo) . "\n";
echo kind(new Bar) . "\n";
// outputs:
// Foo
// Bar

I would appreciate it if others could review the code to verify my results and implementation. Assuming it is soound, I believe this should resolve issues people have been raising with partially erased generics and give us sufficiently complete generics.

Whether to write a new RFC or merge into a single RFC is still being discussed in discord and open for discussion here.

PS. My normal email address is broken, so this is a new email address on this list. :waving_hand:

— Rob