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 <[email protected]> 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
>

Reply via email to