Re: [RFC] [Discussion] Bound-Erased Generic Types

From: Date: Sat, 16 May 2026 21:32:30 +0000
Subject: Re: [RFC] [Discussion] Bound-Erased Generic Types
References: 1 2 3 4 5 6  Groups: php.internals 
Request: Send a blank email to internals+get-130917@lists.php.net to get a copy of this message
Hi Rowan, The parts this RFC enforces (generic declaration syntax, type-parameter resolution, inheritance arity, bound conformance, parametric LSP, variance soundness, and turbofish arity) are exactly where static analysis tools currently disagree most. For evidence that these inconsistencies are real, you can look at the issue trackers for Mago, PHPStan, or Psalm. Even basic syntax varies across tools today: some use @template T of X while others use @template T as X. Defaults and inference are also major categories of disagreement. For example, new Collection([1]) could be inferred as several different types (Collection<1>, Collection<int>, Collection<scalar>, Collection<mixed>, Collection<numeric>); as a maintainer, there isn't always a "correct" answer. Native syntax like new Collection::<int>([1]) makes user intent explicit, forcing tools to converge on the same answer. The disagreement isn't limited to edge cases or inference. Even fundamental correctness questions divide the tools. Consider this diamond pattern: ```php /** @template T */ interface Example { /** @param T $v @return T */ public function produce(mixed $v): mixed; } /** @extends Example<A> */ interface AExample extends Example {} /** @extends Example<B> */ interface BExample extends Example {} class X implements AExample, BExample { /** @param A|B $v @return A */ #[Override] public function produce(mixed $v): A { /* ... */ } } ``` The correct return type here is A & B. The implementer must satisfy both AExample::produce(A): A and BExample::produce(B): B, which forces the parameter to be A | B (LSP widening on contravariant position) and the return to be A & B (LSP narrowing on covariant position to the intersection). Anything narrower violates one parent contract: returning just A breaks the BExample contract; returning just B breaks the AExample contract. Mago and PHPStan both accept A and B as return types: both are wrong. Psalm rejects A but accepts B: also wrong, and inconsistently so. Three tools exhibit three different incorrect behaviors on the same pattern. Under this RFC, the diamond-merge rule rejects both at compile time and forces the implementer to declare A & B, which is the only sound choice. - https://mago.carthage.software/1.27.1/en/playground/#019e32b2-a082-7482-7ad2-8ecc0f29e120 - https://phpstan.org/r/5044aa92-c599-45a3-ad24-c735db44a125 - https://psalm.dev/r/cf5d2dadd9 If the tool authors haven't independently converged on the correct answer, "agree on a conformance spec" isn't the lightweight coordination it sounds like. The disagreement isn't on edge cases; it's on what the *correct* answer is, and the people running the tools are the ones in the best position to know. ref: - https://github.com/carthage-software/mago/issues/899 (Mago recently added default support; Psalm still lacks this) - https://github.com/carthage-software/mago/issues/1859 (Mago disagreeing with Psalm and PHPStan; solved if we could have `->where::<int>($c, $o, $v)` and not rely on inference) - https://github.com/phpstan/phpstan/issues/12978 (solved with explicit turbofish) - https://github.com/vimeo/psalm/issues/7496 (RFC makes it clear that intersection works with generics if the bound is object or more specific) - https://github.com/vimeo/psalm/issues/5910 (fixed with explicit turbofish) If I went all day trying to find issues solved by this RFC, or inconsistencies between tools, this thread wouldn't end. For the parts the RFC doesn't enforce, such as flow analysis and type narrowing, tools will continue to handle them differently. However, native syntax narrows the surface of disagreement. Tools will start from the same parsed structure and expectations, rather than diverging early on what a docblock like @template T = mixed even means. Internals is not becoming a standards body for unenforced parts; tools will continue to compete on inference and narrowing quality by design. Regarding your suggestion to include all unchecked type information in one place: adding a syntactic tier that PHP parses but ignores would be a new architectural pattern. Beyond that, it creates its own coordination problem. If ~~lowercase-string is parsed by PHP but interpreted by tools, any disagreement on semantics (e.g., whether "" is lowercase, whether the rule is ASCII or multibyte, whether numeric strings count) remains. PHP would host syntax it doesn't understand, and the ecosystem would still lack a governance mechanism for those semantics. making it truly 100% useless. On the visibility of type parameters like TNode: it's true they can look like concrete types at use sites. However, this is consistent with languages like Java, Rust, and TypeScript. Conventions like single uppercase letters or PascalCase (e.g. TKey, TNode) are already well-established in the PHP ecosystem through @template. Native syntax promotes these established signals into the language. Cheers, Seifeddine.

« previous php.internals (#130917) next »