From 5bd431e659075de68c3431b3d95d01f19840056a Mon Sep 17 00:00:00 2001 From: Matheus Martins Date: Mon, 27 Jul 2026 06:48:19 +0000 Subject: [PATCH 1/4] docs(comparison): note the absence of type-argument inference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feature grid compared declaration surface, bounds, variance, and runtime semantics, but never stated a call-site divergence: xphp has no type-argument inference — the ::<> turbofish is mandatory on every generic call, and omitting it is a compile error (needed to pick a specialization under monomorphization). TypeScript, Kotlin, and Rust all infer; Rust is the sharp parallel, borrowing the same ::<> spelling but using it only as the disambiguation fallback over default inference. The bound-erasure RFC has no inference either, but keeps the turbofish optional (omitting runs unvalidated) rather than required. Add a grid row (red cross for xphp and the RFC, both genuinely lacking inference) plus a short note capturing the mandatory-vs-optional-vs- inferred spread. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/comparison.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index 668ad6f0..2037cb9f 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -28,6 +28,7 @@ than erasure can. | Generic functions / methods | ✅ | ✅ | ✅ | ✅ | ✅ | | Generic closures + arrow functions | ✅ | ✅ | ✅ | ✅ | ✅ | | Typed closure signatures (`Closure(int): bool`) | ✅ (erases to `\Closure`; literal conformance checked at compile time) | ✅ (runtime-lenient) | ✅ (function types) | ✅ (`(Int) -> Bool`) | ✅ (`Fn(i32) -> bool`) | +| Type-argument inference (call without `::<>`) | ❌ (explicit turbofish required) | ❌ (turbofish optional; omitting runs unvalidated) | ✅ | ✅ | ✅ (turbofish is the fallback) | | Upper bounds | ✅ | ✅ | ✅ | ✅ | ✅ | | Multiple bounds (intersection) | ✅ | ✅ | ✅ | ✅ | ✅ | | Union bounds + DNF | ✅ | ✅ | ✅ | ❌ (intersection only via `where`) | n/a | @@ -53,6 +54,19 @@ mark features that simply can't exist under bound erasure: there are no specialized classes at runtime, so subtype edges, reified-T operations, and a wildcard sigil all lose their meaning. +**Type-argument inference.** No xphp generic call infers its type +arguments from the values passed — you always write the turbofish: +`identity::($x)`, `new Box::()`. Omitting it is a compile +error (`xphp.missing_type_argument`), because monomorphization needs +the concrete type to pick a specialization. TypeScript, Kotlin, and +Rust all infer instead. Rust is the closest comparison: xphp borrows +its `::<>` turbofish spelling exactly, but where Rust infers by +default and reaches for the turbofish only to disambiguate, xphp +makes it the only spelling. The bound-erasure RFC has no inference +either, yet diverges from xphp in the other direction — there the +turbofish is *optional*: omit it and the call runs unvalidated with +erased-to-`mixed` semantics rather than failing to compile. + ## Where the monomorphic and erasure paths diverge Three features fall out of the monomorphization model that the From 90420c019e6e1e1526924e75c45c90cbb7185d72 Mon Sep 17 00:00:00 2001 From: Matheus Martins Date: Mon, 27 Jul 2026 18:03:26 +0000 Subject: [PATCH 2/4] docs(comparison): flag five features as supported-with-caveats, not fully supported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An honest pass over the feature grid: five rows were marked a plain green check but have real, documented caveats, so they now carry a warning sign with the caveat named in the cell and detailed in a new "Supported with caveats" section (each linking to the full write-up in caveats.md): - Generic closures / arrows: no $this capture, no `static function` closures, and reflection/serializers see the dispatcher rewrite. - Typed closure signatures: parameter/return/property positions only, not a generic argument or bound. - Generic functions / methods: no inference, and a turbofish can't forward a method-level parameter, target another generic template, or use static::/parent::. - Declaration-site variance: violations inside trait-used methods go unchecked; class-level only. - Real subtype edges: some covariant upcasts of erased method-generics are unschedulable, and self-reintroducing derivations may not converge — the celebratory subsection now points at this too. No cell moved between present and absent; the grid just stops overstating these five as caveat-free. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/comparison.md | 59 +++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index 2037cb9f..d3529c49 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -25,20 +25,20 @@ than erasure can. | Feature | xphp | RFC | TS | Kotlin | Rust | |------------------------------------------|-------------------------|------------------|------------------|---------------|-----------------------| | Generic classes / interfaces / traits | ✅ | ✅ | ✅ | ✅ | ✅ | -| Generic functions / methods | ✅ | ✅ | ✅ | ✅ | ✅ | -| Generic closures + arrow functions | ✅ | ✅ | ✅ | ✅ | ✅ | -| Typed closure signatures (`Closure(int): bool`) | ✅ (erases to `\Closure`; literal conformance checked at compile time) | ✅ (runtime-lenient) | ✅ (function types) | ✅ (`(Int) -> Bool`) | ✅ (`Fn(i32) -> bool`) | +| Generic functions / methods | ⚠️ (no inference; can't forward a method-level param, target another generic template, or use `static::`/`parent::`) | ✅ | ✅ | ✅ | ✅ | +| Generic closures + arrow functions | ⚠️ (no `$this` capture or `static function` closures; reflection/serializers see the dispatcher rewrite) | ✅ | ✅ | ✅ | ✅ | +| Typed closure signatures (`Closure(int): bool`) | ⚠️ (param/return/property only — not a generic arg or bound; erases to `\Closure`, literal conformance checked) | ✅ (runtime-lenient) | ✅ (function types) | ✅ (`(Int) -> Bool`) | ✅ (`Fn(i32) -> bool`) | | Type-argument inference (call without `::<>`) | ❌ (explicit turbofish required) | ❌ (turbofish optional; omitting runs unvalidated) | ✅ | ✅ | ✅ (turbofish is the fallback) | | Upper bounds | ✅ | ✅ | ✅ | ✅ | ✅ | | Multiple bounds (intersection) | ✅ | ✅ | ✅ | ✅ | ✅ | | Union bounds + DNF | ✅ | ✅ | ✅ | ❌ (intersection only via `where`) | n/a | | F-bounded recursion (`T : Box`) | ✅ | ✅ | ✅ | ✅ | ✅ | | Default type parameters | ✅ | ✅ | ✅ | ✅ | ✅ | -| Declaration-site variance (`out T` / `in T`) | ✅ | ✅ | ✅ | ✅ | ⚠️ inferred (lifetime-driven; PhantomData for unused type params) | +| Declaration-site variance (`out T` / `in T`) | ⚠️ (class-level only; violations inside trait-`use`d methods go unchecked) | ✅ | ✅ | ✅ | ⚠️ inferred (lifetime-driven; PhantomData for unused type params) | | Inner-template variance composition | ✅ | ✅ | ✅ | ✅ | ✅ | | Reified T at runtime | ✅ (via AOT) | ❌ (erased) | ❌ | ✅ (inline) | ✅ (monomorphic) | | `instanceof OriginalFqn` works | ✅ | ✅ (trivially: only one class exists at runtime) | n/a | n/a | n/a | -| Real subtype edges between specializations | ✅ | ❌ (erased) | n/a | n/a | n/a | +| Real subtype edges between specializations | ⚠️ (common case works; some covariant upcasts are unschedulable or may not converge) | ❌ (erased) | n/a | n/a | n/a | | Generic type aliases | ❌ | ❌ | ✅ | ✅ | ✅ | | Wildcard / `*` (use-site existential) | ⚠️ partial (via marker) | n/a (erased) | ⚠️ via `any` (bivariant escape hatch — loses type discipline) | ✅ (`Box<*>`) | n/a | | Use-site variance | ❌ | ❌ | ❌ | ✅ | n/a | @@ -110,6 +110,12 @@ runtime check. Erasure-based runtimes can't do this because their specializations don't exist as distinct classes. +> ⚠️ Not universal — see [supported with caveats](#supported-with-caveats). +> A covariant upcast to an interface with an *erased* element-consuming +> method can be unschedulable (a loud `xphp.unschedulable_covariant_upcast`, +> never wrong code), and a self-reintroducing derivation can fail to +> converge. + ### `instanceof OriginalFqn` works Every generic template emits a marker interface at the original FQN. @@ -118,6 +124,49 @@ and any other specialization, even though they're physically unrelated classes. You get the "polymorphic over T" mental model without losing instance checks. +## Supported with caveats + +The features marked ⚠️ for xphp in the grid work, but with limits worth +knowing before you lean on them. Each links to the full write-up (with a +reproduction and workaround) in [caveats](../caveats.md). + +- **Generic closures + arrow functions.** A generic closure/arrow can't + capture `$this` ([caveat](../caveats.md#this-capturing-arrows-and-closures-rejected)), + the `static function` closure form isn't supported + ([caveat](../caveats.md#static-closures-not-supported)), and — because + each call site is rewritten to a dispatcher closure — reflection and + closure serializers see the dispatcher's shape rather than your original + body ([caveat](../caveats.md#reflection-on-rewritten-generic-closures)). + Plain (non-`static`, non-`$this`) generic closures and arrows work. +- **Typed closure signatures.** Accepted only in parameter, return, and + property positions. A signature as a generic argument + (`Box`) or a bound is a compile error, and a signature + parameter can't be defaulted or untyped + ([caveat](../caveats.md#closure-signature-types-only-in-parameter-return-and-property-slots)). +- **Generic functions / methods.** The base feature is solid; *composition* + is where the gaps are. There's no type-argument inference — the turbofish + is mandatory (see the grid row). And a turbofish grounded by an enclosing + type parameter can't forward a *method-level* parameter, target a + *different* generic template, or use the `static::`/`parent::` spellings + ([caveat](../caveats.md#generic-turbofish-grounded-by-an-enclosing-type-parameter)). + Receiver-type tracking also gives up across branches that disagree on the + type, and on a local assigned from a free function + ([caveat](../caveats.md#branching-narrowing-precision-loss)). +- **Declaration-site variance.** Variance is enforced on methods declared + directly on the class, but a violation inside a **trait-`use`d** method + slips through unchecked + ([caveat](../caveats.md#variance-validator-and-trait-use)) — audit traits + on variant classes. Variance is class-level only; there's no + method/function-level variance. +- **Real subtype edges.** Emitted for the common case (and a genuine + strength — see above), but not universal: some covariant upcasts to an + interface with an erased element-consuming method are **unschedulable** + and fail loudly (`xphp.unschedulable_covariant_upcast`), a + self-reintroducing list-↔-map derivation can fail to converge, and a + covariant `array`-backed collection trips the optional PHPStan pass at + level 6+ + ([caveat](../caveats.md#covariant-array-backed-collections-trip-the-xphp-check-phpstan-pass)). + ## What's missing today The features marked ❌ in the grid above are conscious deferrals, not From d282ab2d6a5e3751b358f882705e4f93580ef674 Mon Sep 17 00:00:00 2001 From: Matheus Martins Date: Mon, 27 Jul 2026 18:46:20 +0000 Subject: [PATCH 3/4] docs(comparison): correct RFC/Kotlin/TS marks, verified against real compilers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Honesty pass over the non-xphp columns, checked by compiling generic snippets in throwaway containers (TypeScript 7.0.2, Rust 1.97.1, Kotlin 1.4.10) rather than asserting from memory. The RFC column can't be run (no implementation exists) — its claims were checked against the RFC text. Corrections: - RFC "typed closure signatures" was a green check, but the bound-erased generics RFC has no structural closure/callable signature types (it lists them as future work). Now a red cross; PHP has only untyped callable / \Closure. (Verified against the RFC text.) - Kotlin reified T carried a plain check; `class Box` fails with "only type parameters of inline functions can be reified", so it's now a caveat (inline fun only). (Compiler-verified.) - Generic enums: neither Kotlin `enum class` nor a TS `enum` can be generic (compiler-verified); both rows keep their check but note the real mechanism (sealed classes / discriminated unions). Everything else in the TS, Rust, and Kotlin columns was confirmed correct by the same compile checks — declaration-site variance, variadic tuples, union bounds, sum types, type aliases, associated types, use-site variance, reified/monomorphic T, and specialization-needs-nightly all behave as the grid claims. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/comparison.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index d3529c49..f1d0af8d 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -27,7 +27,7 @@ than erasure can. | Generic classes / interfaces / traits | ✅ | ✅ | ✅ | ✅ | ✅ | | Generic functions / methods | ⚠️ (no inference; can't forward a method-level param, target another generic template, or use `static::`/`parent::`) | ✅ | ✅ | ✅ | ✅ | | Generic closures + arrow functions | ⚠️ (no `$this` capture or `static function` closures; reflection/serializers see the dispatcher rewrite) | ✅ | ✅ | ✅ | ✅ | -| Typed closure signatures (`Closure(int): bool`) | ⚠️ (param/return/property only — not a generic arg or bound; erases to `\Closure`, literal conformance checked) | ✅ (runtime-lenient) | ✅ (function types) | ✅ (`(Int) -> Bool`) | ✅ (`Fn(i32) -> bool`) | +| Typed closure signatures (`Closure(int): bool`) | ⚠️ (param/return/property only — not a generic arg or bound; erases to `\Closure`, literal conformance checked) | ❌ (only untyped `callable` / `\Closure`; noted as future work) | ✅ (function types) | ✅ (`(Int) -> Bool`) | ✅ (`Fn(i32) -> bool`) | | Type-argument inference (call without `::<>`) | ❌ (explicit turbofish required) | ❌ (turbofish optional; omitting runs unvalidated) | ✅ | ✅ | ✅ (turbofish is the fallback) | | Upper bounds | ✅ | ✅ | ✅ | ✅ | ✅ | | Multiple bounds (intersection) | ✅ | ✅ | ✅ | ✅ | ✅ | @@ -36,14 +36,14 @@ than erasure can. | Default type parameters | ✅ | ✅ | ✅ | ✅ | ✅ | | Declaration-site variance (`out T` / `in T`) | ⚠️ (class-level only; violations inside trait-`use`d methods go unchecked) | ✅ | ✅ | ✅ | ⚠️ inferred (lifetime-driven; PhantomData for unused type params) | | Inner-template variance composition | ✅ | ✅ | ✅ | ✅ | ✅ | -| Reified T at runtime | ✅ (via AOT) | ❌ (erased) | ❌ | ✅ (inline) | ✅ (monomorphic) | +| Reified T at runtime | ✅ (via AOT) | ❌ (erased) | ❌ | ⚠️ (`inline fun` only — can't reify a class type parameter) | ✅ (monomorphic) | | `instanceof OriginalFqn` works | ✅ | ✅ (trivially: only one class exists at runtime) | n/a | n/a | n/a | | Real subtype edges between specializations | ⚠️ (common case works; some covariant upcasts are unschedulable or may not converge) | ❌ (erased) | n/a | n/a | n/a | | Generic type aliases | ❌ | ❌ | ✅ | ✅ | ✅ | | Wildcard / `*` (use-site existential) | ⚠️ partial (via marker) | n/a (erased) | ⚠️ via `any` (bivariant escape hatch — loses type discipline) | ✅ (`Box<*>`) | n/a | | Use-site variance | ❌ | ❌ | ❌ | ✅ | n/a | | Variadic generics | ❌ | ❌ | ✅ | ❌ | ⚠️ tuples | -| Generic enums / sum types | ❌ | ❌ | ✅ | ✅ | ✅ | +| Generic enums / sum types | ❌ | ❌ | ✅ (via discriminated unions; `enum` can't be generic) | ✅ (via `sealed` classes; `enum class` can't be generic) | ✅ | | Per-arg specialization | ❌ | ❌ (erasure) | ❌ | ❌ | ⚠️ nightly | | Associated types | ❌ | n/a | ❌ | ❌ | ✅ | | `T[]` array sugar | ✅ | ❌ | ✅ | ❌ | ❌ | From 40bbcb751cd4a722c7f07943e97bcfc21bb84f36 Mon Sep 17 00:00:00 2001 From: Matheus Martins Date: Mon, 27 Jul 2026 21:14:09 +0000 Subject: [PATCH 4/4] docs(comparison): link the self-reintroducing-specialization caveat The "Real subtype edges" bullet linked the caveat write-up for the array-backed-collection sub-problem but not for the self-reintroducing list-map derivation, even though that one also has a dedicated section in caveats.md (with a workaround). Add the missing link so a reader who hits the convergence error finds the fix. The unschedulable-upcast sub-problem stays unlinked by design -- it has no caveats section, only the inline error code. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/comparison.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index f1d0af8d..af5b0b11 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -162,7 +162,8 @@ reproduction and workaround) in [caveats](../caveats.md). strength — see above), but not universal: some covariant upcasts to an interface with an erased element-consuming method are **unschedulable** and fail loudly (`xphp.unschedulable_covariant_upcast`), a - self-reintroducing list-↔-map derivation can fail to converge, and a + self-reintroducing list-↔-map derivation can fail to converge + ([caveat](../caveats.md#self-reintroducing-specialization-list--map-derivations)), and a covariant `array`-backed collection trips the optional PHPStan pass at level 6+ ([caveat](../caveats.md#covariant-array-backed-collections-trip-the-xphp-check-phpstan-pass)).