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.