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 >
