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