Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions docs/guides/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,25 @@ 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) | ❌ (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) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Union bounds + DNF | ✅ | ✅ | ✅ | ❌ (intersection only via `where`) | n/a |
| F-bounded recursion (`T : Box<T>`) | ✅ | ✅ | ✅ | ✅ | ✅ |
| 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) |
| 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 | | ❌ (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 |
| 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 | ✅ | ❌ | ✅ | ❌ | ❌ |
Expand All @@ -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::<int>($x)`, `new Box::<int>()`. 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
Expand Down Expand Up @@ -96,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.
Expand All @@ -104,6 +124,50 @@ 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<Closure(int): int>`) 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
([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)).
Comment thread
greptile-apps[bot] marked this conversation as resolved.

## What's missing today

The features marked ❌ in the grid above are conscious deferrals, not
Expand Down
Loading