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

From: Date: Tue, 09 Jun 2026 14:08:57 +0000
Subject: Re: [RFC] [Discussion] Bound-Erased Generic Types
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to internals+get-131186@lists.php.net to get a copy of this message
Hi Ilija, > From my testing on your branch, the type inference is very limited. > [...] > PHP static analyzers are necessarily very capable at static type > inference, so this is at least a significant discrepancy between > your implementation and a type erasure approach. A reified implementation should require turbofish at every call site rather than try to do engine-level inference. The two paths land in different places here. For an erased model (this RFC), inference belongs in the static-analysis layer. The runtime doesn't need to know what T is. It's been erased to its bound by the time the call happens, so `new Box(42) produces a Box object with value typed as mixed` at runtime. PHPStan, Psalm, Phan, or Mago can independently decide whether to infer Box<int>, Box<int|string>, Box<scalar>, or leave T open at the static layer, and their disagreement doesn't affect runtime semantics. That's why this RFC leaves turbofish optional. Forcing it would be work the engine has no use for. The only callers that need turbofish under erasure are SA tools when they can't figure out user intent, same as TypeScript saying "needs explicit type argument" when nothing in the call site constrains T. For a reified model, inference becomes an engine problem because the runtime actually carries the type argument forward. Now new Box(42) has to decide on a concrete T at construction. The clean fix is to require turbofish at every call site that takes a generic-parametric type, and reject the call as a compile or runtime error otherwise. No engine-side inference, ever. The user states intent explicitly. The engine doesn't guess. That sidesteps the question of what new Box(42) means: under reification it becomes invalid PHP, and the user writes `new Box::<int>(42)`. > In other words, whether a(42) is safe to call for signature > function a<T>(T $value) depends on its implementation, > i.e. > whether it makes use of T. This is introduces a variation of the > function coloring problem, where now all callers must specify > generic types recursively. Function coloring is a different problem. Coloring (in the async/await sense) means a property of one function forces the same property on every function that calls it. If you call an async function, you have to be async yourself, and so on transitively up the call graph. Mandatory turbofish doesn't propagate that way. Consider: ```php function id<T>(T $v): T { return $v; } function str_id(string $v): string { return id::<string>($v); } ``` str_id is not generic. It supplies the type argument at the call site and that's the end of it. The "color" doesn't travel upward. str_id's own signature stays exactly as the author wrote it, with no generic parameters of its own required. Turbofish is a property of the call site. The calling function's own signature is unaffected. The case where you'd add a generic parameter to str_id is when str_id legitimately needs to be parametric, forwarding T through. That's a design choice the author makes. The language doesn't impose it. The issue you found in Rob's branch is real: half-done inference makes call-site safety depend on the callee's implementation. That's a problem with partial inference, not with the call-site syntax. The fix is the same: require turbofish at every call site, no engine-side inference. Once the engine stops guessing, call-site safety stops depending on what the body of the callee does. TypeScript-style inference for T belongs in the SA layer, where it can be tool-specific and revisable. The engine should not be in that business. Cheers, Seifeddine

« previous php.internals (#131186) next »