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