Re: [RFC] [Discussion] Bound-Erased Generic Types
| From: | Rob Landers | Date: | Mon, 08 Jun 2026 15:13:11 +0000 |
| Subject: | Re: [RFC] [Discussion] Bound-Erased Generic Types | ||
| References: | 1 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-131170@lists.php.net to get a copy of this message | ||
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