Re: [RFC] [Discussion] Bound-Erased Generic Types
| From: | Rob Landers | Date: | Mon, 18 May 2026 10:29:18 +0000 |
| Subject: | Re: [RFC] [Discussion] Bound-Erased Generic Types | ||
| References: | 1 2 3 4 5 6 7 8 9 | Groups: | php.internals |
| Request: | Send a blank email to internals+get-130933@lists.php.net to get a copy of this message | ||
On Mon, May 18, 2026, at 01:36, Seifeddine Gmati wrote:
> Hi Rob,
>
> The RFC enforces type-system structure at four places: declaration-site syntax and variance
> soundness, link-time inheritance arity and bound conformance, link-time parametric LSP (including
> the diamond-merge case I described), and runtime turbofish arity and bounds. The structural
> enforcement is substantial: the variance declared at the parameter
> (
+T, -T, invariant by default)
> constrains how the parameter can be used, parametric LSP substitutes the bindings into method
> signatures at inheritance points, and turbofish forces explicit type arguments at call sites
> requiring disambiguation.
>
> What's not enforced is parametric flow analysis: tracking how a
> T-typed value flows through a method's body, narrowing it
> through control flow, and inferring its concrete type at a use site. That's the layer where
> tools currently diverge most, and you're right that this RFC doesn't directly resolve
> those disagreements.
>
> But the RFC does change the *shape* of the inference-disagreement problem in a way that helps.
> Today, tools have to guess what new Collection([1]) means because
> there's no syntax to express the user's intent. The guesses differ. Once turbofish exists,
> tools can do two things they can't do now:
>
> 1. Simplify inference to only handle the unambiguous cases (where the type is clearly
> determinable from context and conventions), and emit a warning or error when inference would
> otherwise have to guess.
> 2. Recommend turbofish at sites where the user's intent is ambiguous, so the user can
> disambiguate explicitly with new Collection::<int>([1]).
>
> That moves the dev-UX problem from "different tools silently produce different inferred
> types" to "tools agree that the type is ambiguous, recommend turbofish, and the user
> disambiguates." The convergence point becomes the user's annotation, not the tool's
> heuristic. As Mago's maintainer I can tell you we'd lean into this shift hard. Our current
> inference heuristics are messy precisely because there's no other option; the moment turbofish
> exists, we'd simplify them to "be sure or ask the user."I'd expect PHPStan and
> Psalm to land in similar places eventually too.
>
> > Which means Box<A&B> works either way you want it to work. It could mean A|B or
> > A&B; the engine happily accepts either reading.
>
> Disagree. The reading of Box<A&B> is determined by
> Box's variance declaration. If Box<+T> declares T as
> covariant, then Box<A&B> is a subtype of
> Box<A> and Box<B>
> (intersection narrows for covariant positions). If Box<-T>
> declares T as contravariant, the relationship inverts:
> Box<A&B> is a supertype, and a
> Box<A> can be passed where
> Box<A&B> is expected. If T is invariant (the default),
> neither relationship holds and the engine treats Box<A&B>
> as a distinct type from Box<A> and
> Box<B>.
>
> So the engine has a definite reading; it just depends on the variance declared at Box. The
> interpretation isn't ambiguous, it's compositional. Tools that disagreed today on what
> Box<A&B> means would have to agree once variance is
> declared in syntax, because the variance is then a property of the language, not a tool-specific
> interpretation.
>
> I'd grant that for the common case of invariant T (which is the default and what most
> generic code will use) the type argument is opaque relative to subtyping, for example,
> Box<A&B> is not a subtype of
> Box<A> or Box<B>. That
> opacity is the bound-erasure trade. But it's a determinate opacity, not a "happily accepts
> either reading" one.
>
> Cheers,
> Seifeddine.
Hi Seifeddine,
I'm a bit confused. In your earlier email you said that tools currently can't converge on
simple generics correctness (and in your last email admitted even your own tool was incorrect), but
say that this will force them to converge. If they've not been able to do so already, I have
little faith that they will in the future. In fact, if someone could get them in a room together to
converge, maybe we wouldn't need this RFC at all.
In regards to my Box<A&B> example. My apologies. I was collapsing two different
perspectives into one instead of being clear. I'm not talking about the engine; I think your
RFC is clear there. I'm talking about new users or users not using any tooling to tell them
they've got an error. T-typed positions inside Box are checked against T's bound, not
against A&B. So Box<A&B>'s internal value flow is observationally identical to
Box<A|B>'s, or Box<mixed>'s -- all accept and produce values satisfying the
bound. The static subtyping relationship is real and enforced at link-time parametric LSP. It's
invisible at runtime.
I understand this is the entire point of "type erasure", but it's a real problem for
new developers using PHP for the first time. A developer reasoning at the value level gets no engine
feedback distinguishing intersection from union from arbitrary supertype.
Further, the runtime is silent in cases the RFC documents as enforced. The Limitations section says:
"the inherited or trait-imported function's signature is substituted on the child -
reflection sees the substituted types, the link-time variance check uses them, and entry-side type
checks inside the body read the substituted parameter types." Tested on your PR branch:
class Box<T> {
public function test(T $v): T {
return $v;
}
}
class IntBox extends Box<int> {}
$b = new IntBox();
echo $b->test("hello"); // prints "hello"
Reflection reports int for the parameter type on IntBox::test, confirming
the metadata is substituted. But the entry-side check accepts the string, contradicting the same
sentence's claim that those checks read substituted parameter types. The implementation does
the metadata half and skips the runtime-enforcement half, and the runtime-enforcement half is the
part that would catch the bug. The RFC text and the implementation disagree.
One thing I want to call out: the RFC template specifically asks you to address PHP's
"wide range of users" and consider "the consequences of making the learning curve
steeper with every new feature." The RFC engages with that question for developers already
inside the @template ecosystem by formalizing existing practice, shortening code, reducing
two-parallel-type-systems cognitive load. What I don't see addressed anywhere is the new-user
case: a developer who's never seen generics encounters <T> in production code, writes
their own, runs it, gets no runtime feedback when their value-level intuition diverges from the
underlying expectations without using a tool. The IntBox case above demonstrates the gap
empirically: the developer writes what looks like reasonable code, refreshes, and the output looks
correct despite being wrong by the engine's own claimed semantics. The template's
procedural ask is engagement with the full picture; what's been delivered is engagement with
the favorable half and redirection throughout this thread to narrow the audience to only "the
@template ecosystem".
I would vote "no" on this RFC as-is; it is too niche. It claims it is not niche with
"200k files" using @template, on github, focused on some of the important frameworks for
PHP, but not all of them. 200k files is a drop in the bucket when you've worked on php repos
spanning millions of files. It doesn't address ALL php developers and from my example above,
seems to be actively harmful to new developers.
— Rob