On Mon, 08 Jun 2026 15:13:11 +0000, Rob Landers 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.
> >
> > - RFC: https://wiki.php.net/rfc/bound_erased_generic_types
> > - Implementation: https://github.com/php/php-src/pull/21969
> >
> > 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. 👋
>
> — Rob
Hi Rob,
Assuming that both reified and bound-erased generics hit the same lines
of code, and thus that the exception example you make will/can be fixed
in the bound-erased version, why not offer both versions? Would
developers not choose to run tests, CI/CD, web development and cli
applications with the reified generics enabled, while in web production
they might choose to run with the faster bound-erased mode?
Regards,
Frederik