On Mon, May 11, 2026, at 4:17 PM, Seifeddine Gmati wrote:
> Hi Larry,
>
> Thank you for your review.
>
>> As you note, this RFC is a little more than erased generics; it does provide
>> a little validation, and reflection support (in addition to a standardized
>> syntax that's much more understandable than the current de facto standard
>> docblock syntax). That's not nothing, and is a marginal improvement over the
>> status quo. The question is whether that is enough of a benefit, and what
>> future improvements it makes easier/harder.
>
> I'd argue this RFC makes future improvements strictly easier, not
> harder. see https://wiki.php.net/rfc/bound_erased_generic_types#future_scope.
> The work this RFC does (syntax, parsing, compile time enforcement,
> reflection) is baseline infrastructure that any generics
> implementation in PHP will need regardless of approach. Landing it now
> gives us a foundation to build on.
>
>> Technically, anyone using Symfony or Laravel is "using generics" in that
>> Symfony and Laravel have generic doc-types on their code. That doesn't imply
>> that everyone building a site with Symfony or Laravel is regularly running
>> PHPStan to verify those, or adding their own doc-types to match it.
>
> I disagree here. Someone using a Laravel collection class without
> engaging with its `@template` annotations isn't "using generics",
> they're consuming a typed API and ignoring the type parameters. That
> pattern continues unchanged under this RFC: a user can call
> `$collection->map(...)` without ever writing or reading a generic type
> argument. The only place behavior changes is inheritance: someone who
> extends or implements a generic class is now required to provide type
> arguments (e.g. `class MyCollection extends Collection<int>`), and
> *that* get enforced at both compile time and runtime, because concrete
> type arguments get substituted into method signatures.
>
>> Where this becomes a land-mine is less the production deploys today, but
>> that future improvements become BC breaks. [...] My fear is that people who
>> don't use SA tools will write code on top of someone else's generic code,
>> not care that their types are buggy, not notice, and then we start enforcing
>> it in the future and their code breaks.
>
> This is the right concern to raise, and I think it's addressable
> without needing the "your types are wrong, not covered by BC" escape
> hatch you propose below.
>
> The principle: as long as we don't change the semantics of existing
> syntax, no future improvement introduces a BC break. Reified generics,
> for example, can be added later as opt-in, just like how HackLang did
> it (https://docs.hhvm.com/hack/reified-generics/reified-generics-migration/).
> A class or function would need to declare `#[ReifiedGenerics]` (or
> similar) to opt into reified semantics; everything else continues to
> behave exactly as the bound-erased model specifies. Library and
> framework authors then choose the strictness-vs-performance tradeoff
> that fits their use case, and existing code never breaks because the
> default behavior never changes.
>
> So the "BC only for sloppy code" risk you describe doesn't
> materialize, sloppy code today stays sloppy tomorrow at exactly the
> same level.
My concern is something like this:
function foo<T>(T $val): bool {}
// With this RFC, this will compile and run, and maybe error inside foo() in
oddball ways, or not.
foo::<int>('beep');
Because the type param isn't enforced. So someone is going to write code that
bad, guaranteed. (This is PHP, after all.)
Now fast forward a few years, and we figure out a way to performantly enforce
that check at runtime and turn that function call into a call-site TypeError.
I would consider that a good improvement to the language. However, it would
also mean that the previous mismatched line would now generate an error where
it didn't before. And the author is going to get up in arms about how "PHP is
breaking my code and destroying the language why can't they respect BC" and so
on and so on, because we've seen that movie several times now.
But what I would absolutely not want to see is someone arguing that "well we
can't start enforcing that type at runtime, because someone *might* have stupid
code." Or, even worse, #[ParamTypeMismatchesThatsOk] as an attribute on the
function to opt-out of enforcing it, the way we did for return types on magic
methods. I would consider both of those to be Very Bad(tm) outcomes.
So the question for me is how do we set it up, both technically and
politically, so that if/when we figure out how to enforce that we can do so
without creating another "boo hoo you broke my code" round of blog posts, as
those are quite bad for PHP's image.
>> Another option, if we want to take the stance that SA is The Solution(tm) to
>> generics, is to ship an actual first-party SA tool with PHP.
>
> I want to push back on this strongly. speaking as Mago's author for a moment.
>
> The existing SA tools (PHPStan, Psalm, Mago) aren't just type
> checkers. Type checking is part of what they do, but they also handle
> a much larger surface: types PHP itself has no notion of
> (`positive-int`, `non-empty-lowercase-string`, `class-string<Foo>`,
> `list{1, 3, "hello"}`, and 100s more, see
> https://carthage-software.github.io/suffete/universe/elements.html),
> plus code quality rules, security analysis, dead code detection, and
> so on. A first-party tool would need to compete on that whole surface
> to be useful, anything narrower would disappoint the audience that
> actually wants SA.
>
> That leaves two options, neither good:
>
> 1. A weaker tool than what exists. This doesn't serve the audience
> that wants real SA, and users will rightly be frustrated that PHP
> shipped something less capable than third-party alternatives.
>
> 2. A tool competitive with existing ones. This is probably a year or
> two long C project requiring a dedicated team, on top of the PHP core
> team's existing scope. As someone who built a full toolchain in Rust
> from scratsh following Psalm/Hakana's footsteps, I can tell you this
> is substantially harder than it looks, and it would be *considerably*
> harder in our case than it was for me, for two reasons:
>
> 2.1. Mago is written in Rust, which is a more productive language
> for this kind of tool and has the ecosystem to match. Building the
> equivalent in C, against PHP's existing engine constraints, means a
> much higher cost per feature, per bug fix, per refactor.
>
> 2.2. Mago isn't part of PHP. If we get variance wrong, we ship a
> fix the same day. sometimes twice a day, sometimes once a week. We
> aren't bound by the php-src release cycle, and we iterate as soon as
> we have a fix. A first-party SA tool inherits PHP's release cadence:
> fixed-cadence releases, feature freeze windows, stability guarantees.
> For a tool whose value depends on keeping up with framework patterns,
> library conventions, and emerging idioms, that release shape is wrong.
>
> Beyond the engineering cost, there's a parser problem: PHP's current
> parser doesn't produce the full CST that an SA tool needs. You'd need
> a new parser, a static name resolver, a static reflection system that
> doesn't execute files to extract type information, and so on, a
> substantial reimplementation of analysis infrastructure in C, which
> the community already has high-quality versions of in Rust and PHP.
>
> In short: shipping a first-party SA tool would be a large, probably
> multi-year, investment for a result that's almost certainly worse than
> the tools the community has already built. I don't think it's a good
> use of PHP's resources, and I'd argue strongly against it.
In general, several of those issues are entirely self-created, and PHP can
easily resolve them.
1. Nothing says we can't provide a first-party SA tool that's written in Rust
instead of C.
2. Nothing says a first-party SA tool must follow the exact same cadence as the
engine. Making improvements to it at the same time as a .z release of the
engine is completely reasonable; it would be purely an administrative decision
to do that or not.
First-party doesn't have to mean "in the php-src C code directories." Though
leveraging some parts of those as externs could certainly make sense.
(Joking: Should we just ship Mago with PHP? :-) )
>> I agree with Bob that the +/- syntax is very confusing. I would much rather
>> use in/out, as seen in Kotlin and C#, which are both vastly more
>> self-documenting and match what some of our peer languages are doing.
>
> No strong preference here. I went with `+`/`-` because that's what
> HackLang uses and it's familiar to me, but `in`/`out` is fine if
> that's the consensus. Happy to switch if enough people prefer the
> keyword form.
Kotlin and C# have several orders of magnitude more users, so the "familiarity"
argument goes firmly in that direction.
>> An interesting quirk I found in my previous research is that languages that
>> use : for inheritance use : for bounding generics. Languages that use
>> "extends" for inheritance also use "extends" for bounding generics. PHP uses
>> "extends", so that pattern would suggest we use "class Box<T extends
>> FooBar>" rather than :.
>
> I'd disagree on this one. `class A<T extends C> extends B` reads
> awkwardly to me, especially when the bound is scalar or a union eg.,
> `class A<T extends int|string> extends B`, is rough to parse visually.
> The colon form keeps the type-parameter bound visually distinct from
> the class's own extension relationship, which I think is the more
> important consistency to optimize for.
I don't disagree with you here. As I said, it's more that I want to bring it
up and make sure it's an explicit decision to use a different signifier than
everyone else.
>> I fully expect "turbofish" to result in all kinds of slide shenanigans at
>> conferences. At least on my slides. :-)
>
> It hasn't been a problem in Rust, and I don't think it will be in PHP
> either. (Looking forward to the slides though)
At what point did I say slide shenanigans are a problem? :-)
>> I'd be completely OK with lowering the argument cap from 127, too.
>
> that makes sense in principle, but assuming you mean the
> actual-parameter cap rather than the type-parameter cap, that's a
> separate RFC and would itself be a BC break. Worth doing eventually,
> just not as part of this one.
No no, I meant the type argument cap.
function foo<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>(/* */) {}
That's 16 type arguments, an order of magnitude less than the cap, and I'll
already say any such code should be rejected on sight as bonkers. So reserving
more than one bit for future flags seems completely fine to me, and perhaps
advisable.
>> Why is ReflectionGenericVariance backed? I don't see what the ints add.
>
> It's backed because the values match what the engine uses internally.
> no strong preference though, i can switch it to a unit enum if that
> reads better.
I'd probably omit it, but if it's kept, the RFC should at least include that
explanation.
>> OK, having read the full RFC, it seems to be sort of "mostly-erased"
>> generics. There is notably more enforcement than the term "erased" would
>> imply. [...] It looks like, in practice, basically everything on the
>> declaration side is enforced; it's just the call side that is unenforced. Is
>> that an approximately accurate summary?
>
> Approximately, yes, declaration-side enforcement is substantial,
> call-side type arguments are erased. The proposal is closer to Java's
> model than Python's, but I want to keep the "erased" terminology
> because the defining property, type arguments not available at
> runtime, is what distinguishes this from reified generics.
> "Bound-erased" is the precise term: bounds are enforced where they can
> be (declaration sites, substituted method signatures), type arguments
> themselves are erased at use sites. That said, your point about
> expectations is fair, I'll consider moving the enforcement section
> higher in the RFC to set those expectations earlier.
And explicitly calling out something like "despite the name, this is
mostly-enforced generics." Or something like that, which seems more accurate.
Would this be enforceable?
class Collection<T> {
public function add(T $val): void {}
}
$c = new Collection<int>();
$c->add('string'); // Error, or would this be allowed at runtime?
My gut sense is that is the most common place where it would come up;
function/method generics are going to be a lot less common than class generics,
so that would be the place to ensure we get as much enforcement as we can.
>> If I understand correctly, this RFC would allow for foo(Collection<int> $c)
>> {}, but wouldn't actually enforce it at runtime. Within the function,
>> `instanceof Collection` would still work, but there's no `instanceof
>> Collection<int>` alternative. Am I reading that correctly?
>
> yes. that's exactly the erasure behavior. the type argument isn't
> available at runtime, so `instanceof Collection<int>` is the same as
> `instanceof Collection`. If you pass a `Collection<string>` to a
> function expecting `Collection<int>`, runtime accepts it because both
> are `Collection` at runtime; the mismatch is caught by SA, not by the
> engine.
>
>> I think at this point I am still skeptical, but warming to it, and could be
>> convinced. But more convincing is needed. And lunch, which I think I need
>> after reading all of this. :-)
>
> Hopefully the above moves you a bit further along. Please let me know
> if you have more questions or concerns.
>
> Cheers,
> Seifeddine.
Two additional questions:
- With reflection, would it be possible to tell what a given object was
instantiated with, or only the class? new
ReflectionObject($listOfInts)->getTypeParam() => ReflectionType(int)? Or is
that also the erased part?
- I assume that dynamically specifying the type parameter is also right-out,
yes?
function make(string $className, int $count) {
$c = new Foo<$className>();
foreach ($count as $i) {
$c->add(new $className());
}
return $c;
}
--Larry Garfield