Hi Tim,

> Static analyzers can only prove the presence of errors, but not the absence 
> of them. [...] PHPStan only checks usage of `mixed` starting at level 9. 
> Guess what level is being used by Symfony and Laravel respectively?

The level-9 framing is true today but not after this RFC ships.
Generic type information currently lives in optional levels because it
lives in optional syntax (docblocks). Once code lives in native
syntax, SA tools must treat violations of it as hard errors at low
levels. The move from optional-level checks to baseline checks is
exactly what native syntax enables.

Laravel, and Symfony use PHPStan/Psalm at lower levels today (higher
in Psalm's case) because their codebases contain code that can't all
be verified to the strictest standard without significant cleanup.
Once generics are in PHP itself, the cleanup pressure shifts: a misuse
becomes a language-level violation that any SA tool will flag at any
level, not a level-9-only concern.

What changes after this RFC is the *category* of generic violations:
they become language-level errors rather than optional-level checks.
SA tools will report them at the user's current level, not push the
user to a higher level. A Laravel codebase running PHPStan at level 5
today would achieve full generic-arity, bound, and parametric LSP
enforcement at level 5 after migrating to native generics, without
changing the level. That's the point: native syntax means the language
did the work, and tools surface violations at whatever strictness the
user already has configured.

> This is typically done with `assert($foo instanceof SomeClass);`, something 
> that PHP will double-check for you at runtime. My understanding based on the 
> discussion is that the RFC specifically excludes support for `instanceof 
> SomeClass<SomeType>` [...]

This is independent of the RFC. `unserialize()` returning a value of
the right type has always required user-side assertion in PHP, and
will continue to. The RFC doesn't ship `instanceof Box<int>` because
the type argument isn't carried at runtime under bound erasure, same
constraint Java, Kotlin, Scala, and Hack live with, and the same
workaround applies: assert against the bound class (`$foo instanceof
Box`), then use a docblock or static analysis for the type-argument
narrowing.

See: https://kotlinlang.org/docs/generics.html#generics-type-checks-and-casts

This is one of the cases the RFC explicitly defers to a future
reified-generics RFC. Saying "this RFC doesn't solve
unserialize-narrowing" is correct but it's the same argument as saying
"this RFC doesn't solve every type-system gap PHP has ever had." While
true, that isn't an argument against shipping the parts it does solve.

> [...] integer ranges or similar.

PHP's type system has grown one feature per RFC for a decade: scalar
types, union types, intersection types, DNF, true/false/null types.
None of those shipped the entire wishlist either. `class-string<T>`,
integer ranges, non-empty-string, negated types, and literal types can
each be their own future RFC.

> But at the same time this PHP script is accepted by PHPStan despite throwing 
> "Uncaught TypeError: foo(): Argument #1 ($bar) must be of type string, int 
> given" at runtime [...]

That's a bug worth reporting to PHPStan. Mago catches it correctly
https://mago.carthage.software/1.27.1/en/playground/#019e28c0-28e7-a525-cb82-710da883858c,
disagreement between SA tools on specific cases is a real ecosystem
issue (this RFC's "Why people use generics" mentions this to an
extent), and tools improve over time. A false positive, or a false
negative in one tool doesn't mean that "SA-checked code is a different
language." (You will find a ton of false positives in Mago and Psalm,
too.)

> So depending on the configuration the existing - third party - static 
> analysis tools are accepting programs written in a custom programming 
> language [...] being neither a subset, nor a superset of PHP.

This framing proves more than you'd want it to. If SA-checked PHP is
"a different language" because tools occasionally disagree with the
runtime, then by the same logic Symfony, Doctrine, Laravel, PHPUnit,
PSL, and most of the major PHP ecosystem are "not PHP". Every one of
those projects relies on SA to an extent to verify code the engine
doesn't check, including the generic type relationships that motivate
this RFC. That's a substantial portion of production PHP. Drawing the
boundary that way means one of two things:

1. The language should grow to verify everything those tools verify
(which is a much more aggressive runtime-enforcement position than
even Rowan or Larry are taking)
2. The "different language" framing is too strict.

The actual situation is that PHP has a runtime-checked layer (what the
engine validates) and an SA-checked layer (what tools verify). The two
layers complement each other. Native generics formalize a part of the
SA-checked layer that the engine can partially absorb. The
declaration-side, link-time, and turbofish-arity validation mentioned
in the RFC's enforcement section is actual engine work, not just
syntax. What's left in the SA-checked layer is the parametric-flow
analysis, which the language lacks the capability to perform.

Cheers,
Seifeddine.

Reply via email to