Skip to content
Merged
Show file tree
Hide file tree
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
35 changes: 22 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ _In progress on this branch — content still accumulating; date set at tag time
manifest, and skips those that don't). See
[getting started](docs/getting-started.md).
- **Element-typed methods on covariant collections.** A method-level type parameter
bounded by an enclosing class type parameter — `class Box<+E> { public function
bounded by an enclosing class type parameter — `class Box<out E> { public function
contains<U : E>(U $value): bool }` — has its bound **grounded** against the receiver's
concrete type argument: `Box<Fruit>::contains<Banana>` is accepted when `Banana <: Fruit`,
and a genuine violation (`Box<Fruit>::contains<Rock>`) is rejected with the bound shown as
Expand All @@ -32,7 +32,7 @@ _In progress on this branch — content still accumulating; date set at tag time
chained call / `self`/`static` factory, and a branch whose arms agree on the same
parameterised type. A bound that references a sibling parameter is grounded the same way,
at the class level (`class Pair<T, U : T>`) and the method level (`<U, V : U>`). This is the
sound, element-typed alternative to a `mixed` parameter on a covariant `<+E>` collection
sound, element-typed alternative to a `mixed` parameter on a covariant `<out E>` collection
(`U` is invariant — not method-level variance). **Ground or fail:** where the receiver's
type argument genuinely can't be determined, the bound can't be proven, so it is a compile
error (`xphp.bound_unprovable`) with an actionable remedy — bind the receiver to a typed
Expand All @@ -45,14 +45,14 @@ _In progress on this branch — content still accumulating; date set at tag time
forward to a *non-erasable* method (parameter used nested, in the return, or structurally), and
a direct concrete `$this->contains::<Banana>()`, remain compile errors
(`xphp.unspecializable_self_call` / `xphp.bound_unprovable`) — never a runtime fault. **Covariant
interfaces:** the method may be declared on a covariant interface (`Collection<+E>`) and called
interfaces:** the method may be declared on a covariant interface (`Collection<out E>`) and called
through an upcast (`ListColl<Book>` used as `Collection<Product>`) — the implementer specialization
is scheduled and inherited down the covariant chain automatically. When inheritance can't carry it
there — the implementing class has another `extends` parent, implements only a *parent* of the
interface, or reorders the `implements` clause — the member is instead emitted **directly** onto the
upcast source, with its bounded parameter widened to the supertype argument and its body read at the
source's own element type (sound because the source's element is a subtype of the supertype). When the
element type is itself a covariant generic (e.g. a `Tuple<+A, +B>`), per-argument covariance makes the
element type is itself a covariant generic (e.g. a `Tuple<out A, out B>`), per-argument covariance makes the
source an instance of the interface at *several* supertype arguments at once — a diamond that single
inheritance can carry only one path of; the remaining obligations are supplied directly once the
inheritance chain is final, so a covariant container of covariant containers upcasts soundly. The
Expand Down Expand Up @@ -86,7 +86,7 @@ _In progress on this branch — content still accumulating; date set at tag time
override discovery with `--phpstan-bin` / `--phpstan-config`.
- **Type-parameter-typed constructors on variant classes.** A covariant /
contravariant class may take its type parameter in a (non-promoted) constructor
parameter — e.g. a covariant immutable `ImmutableList<+T>` built from
parameter — e.g. a covariant immutable `ImmutableList<out T>` built from
`T ...$items`. The parameter keeps its **real** element type on every
specialisation (`Book ...$items`, not `mixed`), so construction is
**runtime-type-checked** while `ImmutableList<Book>` still extends
Expand All @@ -95,10 +95,10 @@ _In progress on this branch — content still accumulating; date set at tag time
promoted constructor param remains a visible property (strictly invariant — PHP
enforces property types across the edge for visible members), but a *private*
one is exempt (see below). See [variance](docs/syntax/variance.md).
- **Variance markers on private properties.** A `+T` / `-T` marker is now allowed
- **Variance markers on private properties.** A `out T` / `in T` marker is now allowed
on a **private** property — declared or promoted, mutable or readonly — so the
natural covariant shape
`class Producer<+T> { public function __construct(private T $item) {} ... }`
`class Producer<out T> { public function __construct(private T $item) {} ... }`
compiles, keeps its **real** substituted slot type (nothing erased), and stays
runtime-type-checked. PHP does not type-check private property types across an
`extends` chain (a private slot is per-declaring-scope and never inherited) and
Expand All @@ -107,14 +107,14 @@ _In progress on this branch — content still accumulating; date set at tag time
and an externally-readable `public private(set)` property) stay strictly
invariant. A covariant single-value getter over a `private T` field is also
PHPStan-clean. See [variance](docs/syntax/variance.md).
- **By-reference parameters are an invariant variance position.** A `+T` / `-T`
- **By-reference parameters are an invariant variance position.** A `out T` / `in T`
type parameter used in a by-reference parameter (`&$x`) is now rejected: a
by-reference slot is both read and written through the caller's binding, so it is
invariant — the same rule already applied to a mutable property. See
[variance](docs/syntax/variance.md).
- **Variance composes through a nested generic type-argument.** A covariant slot whose
argument is itself a generic of a *different but related* template now emits its
`extends`/`implements` edge — so a covariant `Tuple<+A, +B>` holding a covariant
`extends`/`implements` edge — so a covariant `Tuple<out A, out B>` holding a covariant
container relates by that container's element type (`Tuple<ImmutableList<Book>, Tag>`
is usable where a `Tuple<Collection<Product>, Tag>` is required, because
`ImmutableList<Book> ⊑ Collection<Product>`). The argument relationship is proven by
Expand All @@ -124,10 +124,10 @@ _In progress on this branch — content still accumulating; date set at tag time
hints) and not only at `check`. Previously such an upcast passed `check` but fatal'd at
load. See [variance](docs/syntax/variance.md).
- **A contravariant generic may be consumed by a covariant class.** A method parameter typed
by a contravariant generic of the class's covariant parameter — `class Box<+E> { pick(
Comparator<E> $c): ?E }` where `Comparator<-T>` — is now accepted: `E` sits in a
by a contravariant generic of the class's covariant parameter — `class Box<out E> { pick(
Comparator<E> $c): ?E }` where `Comparator<in T>` — is now accepted: `E` sits in a
contravariant slot inside a contravariant parameter position, which composes to a covariant
position a `+E` may occupy (sound under upcast — a `Comparator<Product>` compares the `Book`
position a `out E` may occupy (sound under upcast — a `Comparator<Product>` compares the `Book`
elements of a `Box<Book>` viewed as `Box<Product>`). Variance validation now routes every
type-constructor-nested type-parameter through the composing check (which already knew the
inner slot's variance) instead of judging it by the bare outer position, so this sound,
Expand All @@ -137,6 +137,15 @@ _In progress on this branch — content still accumulating; date set at tag time

### Changed

- **BREAKING — variance markers are now `out T` / `in T`.** Declaration-site variance
is written with the Kotlin-style keywords `out` (covariant) and `in` (contravariant)
instead of the previous `+T` / `-T`: `class Producer<out T>`, `class Consumer<in T>`.
The keywords are contextual — a leading `out`/`in` is a marker only when immediately
followed by the parameter name (`out T`, never `outT`), so ordinary parameters whose
names merely start with those letters are unaffected; the words are reserved and
cannot themselves name a parameter. Old `+T` / `-T` source now fails with a migration
error pointing at the new spelling. Variance semantics are unchanged — only the
surface syntax moves. See [Variance](docs/syntax/variance.md).
- **`xphp compile` runs the validation gate by default.** Compile now runs the same
gate as `xphp check` (the generic validators plus PHPStan over the compiled output)
*before* emitting, and fails the build — emitting nothing — when the gate reports an
Expand All @@ -148,7 +157,7 @@ _In progress on this branch — content still accumulating; date set at tag time
behavior). `--no-phpstan` runs only the generic validators; a missing PHPStan degrades
to a non-failing warning.
See [ADR-0021](docs/adr/0021-compile-runs-the-check-gate-by-default.md).
- **BREAKING — a `final` variant class is now rejected.** A `final class Box<+T>`
- **BREAKING — a `final` variant class is now rejected.** A `final class Box<out T>`
previously compiled, with the generated specialization silently dropping `final`
so the `extends` subtype edge between specializations could land — which made
`ReflectionClass::isFinal()` disagree with the written source. A covariant /
Expand Down
10 changes: 5 additions & 5 deletions docs/adr/0013-typed-constructor-parameters-on-variant-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

## Context and Problem Statement

Declaration-site variance (`+T` / `-T`) lowers to real `extends` edges between
Declaration-site variance (`out T` / `in T`) lowers to real `extends` edges between
specializations: `Producer<Banana>` actually extends `Producer<Fruit>` when `Banana`
extends `Fruit` and `T` is covariant. A covariant immutable collection — Kotlin's
`List<out T>`, the backbone of an immutability-first collections library — wants to take
its element type as **construction input**: `class ImmutableList<+T> { public function
its element type as **construction input**: `class ImmutableList<out T> { public function
__construct(T ...$items) }`. The question is whether a variant class can accept its type
parameter in a constructor across the variance `extends` edge, and with what type.

Expand Down Expand Up @@ -41,7 +41,7 @@ error. (Verified empirically, both directions.) So no erasure is needed.

## Decision Outcome

Chosen: **permit a non-promoted constructor parameter to carry `+T` / `-T`, emitted with
Chosen: **permit a non-promoted constructor parameter to carry `out T` / `in T`, emitted with
its real substituted type.** A constructor parameter is *variance-position-exempt* — a
constructor is never reached through an upcast reference, so it isn't part of the
externally-visible variance surface (the same reason Kotlin allows `out T` in a
Expand All @@ -54,7 +54,7 @@ non-`Banana` throws a `TypeError`.
A corollary about `final`: a `final` class can't be a parent in an `extends` edge, so a
variant class cannot be `final`. Rather than silently strip `final` from the generated
specializations (which would make `ReflectionClass::isFinal()` lie about them), xphp
**rejects** `final` on a `+T`/`-T` class at compile time.
**rejects** `final` on a `out T`/`in T` class at compile time.

The relaxation is narrow. **Visible (public/protected) properties stay strictly
invariant** — mutable, `readonly`, and public/protected *promoted* constructor parameters
Expand Down Expand Up @@ -87,7 +87,7 @@ class at any variance (a public/protected promoted one stays invariant; a privat
one is exempt); `InnerVarianceValidator` skips a bare variance-marked constructor parameter
(`isExemptVariantConstructorParam`) and still rejects the non-bare shapes. [`Specializer::specialize`](../../src/Transpiler/Monomorphize/Specializer.php)
substitutes the real type into the constructor parameter — no erasure step. Tests compile a
covariant `ImmutableList<+T>` and assert each specialization's constructor keeps its real
covariant `ImmutableList<out T>` and assert each specialization's constructor keeps its real
element type, that the chain autoloads with **no** fatal, that an `ImmutableList<Banana>`
**throws** on a non-`Banana` element, and that the contravariant constructor chain
autoloads and constructs equally cleanly.
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/0014-variance-markers-are-class-level-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Context and Problem Statement

Declaration-site variance (`+T` / `-T`) is realized as real `extends` edges between
Declaration-site variance (`out T` / `in T`) is realized as real `extends` edges between
*specialized classes*: `Producer<Banana>` extends `Producer<Fruit>`, and PHP's native type
system carries the subtype relationship. That mechanism needs a stable, nominal class
identity at each end of the edge.
Expand All @@ -13,7 +13,7 @@ Method-, function-, closure-, and arrow-scoped generics don't have one. Their
specializations are *functions* — mangled methods appended to a class, or top-level
functions, keyed by a call-site hash — not classes that can sit in an `extends` chain. So
the question is what to do when a type parameter on one of those carries a variance marker
(`function map<+U>(...)`, `$f = fn<-T>(...) => ...`): support it somehow, ignore it, or
(`function map<out U>(...)`, `$f = fn<in T>(...) => ...`): support it somehow, ignore it, or
reject it.

## Decision Drivers
Expand All @@ -29,7 +29,7 @@ reject it.
- **Implement method-level variance** — synthesize some stable identity for function
specializations so a subtype relationship can be expressed. Large, and there is no natural
PHP construct for "one function is a subtype of another."
- **Accept the markers and ignore them** — parse `+U` / `-U` on a function-scoped generic
- **Accept the markers and ignore them** — parse `out U` / `in U` on a function-scoped generic
but emit nothing. Silently unsound: the declared variance would have no effect.
- **Reject them at parse time as a permanent boundary** — and document the rationale.

Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0015-variance-markers-on-private-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Context and Problem Statement

Declaration-site variance (`+T` / `-T`) lowers to real `extends` edges between
Declaration-site variance (`out T` / `in T`) lowers to real `extends` edges between
specializations: `Producer<Banana>` extends `Producer<Fruit>` when `Banana` extends `Fruit`
and `T` is covariant ([ADR-0001](0001-monomorphization-over-type-erasure.md)). A variant
class therefore can't carry its type parameter in a position PHP would reject across that
Expand All @@ -15,7 +15,7 @@ mutable, `readonly`, or promoted — was rejected at compile time, justified by
invariant property types across an `extends` chain." So the natural covariant shape

```php
class Producer<+T> {
class Producer<out T> {
public function __construct(private T $item) {}
public function get(): T { return $this->item; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

## Context and Problem Statement

A covariant collection `class Box<+E>` cannot take `E` in a parameter position, so an
A covariant collection `class Box<out E>` cannot take `E` in a parameter position, so an
element-consuming method (`contains`, `indexOf`, an immutable `withAdded`) classically falls back to
`mixed`. The *sound* spelling is a **method-level** type parameter bounded by the class parameter —
`public function contains<U : E>(U $value): bool` — so the argument is constrained to a subtype of
the element type while the covariant `+E` never enters a parameter position (the same shape Hack
the element type while the covariant `out E` never enters a parameter position (the same shape Hack
uses for the element-search methods on its covariant `ConstVector`). `U` is invariant, so this is **not** method-level
variance ([ADR-0014](0014-variance-markers-are-class-level-only.md)) — it only needs the enclosing
`E` to be resolved.
Expand Down Expand Up @@ -44,7 +44,7 @@ the emitted code carries nothing).

- **Determination floor — maximise what's knowable first.** The receiver's type arguments are
recovered from flow typing and threaded up the parameterized `extends`/`implements` chain to the
method's **declaring** class (so a method inherited from `Collection<+E>` grounds against an
method's **declaring** class (so a method inherited from `Collection<out E>` grounds against an
`ArrayList<Fruit>` receiver). The covered receiver shapes:
- a parameter or `$this->prop` of declared generic type (`Box<Fruit> $b`);
- a `new Box::<Fruit>()` local (and a closure-`use` capture of one);
Expand Down Expand Up @@ -92,7 +92,7 @@ the emitted code carries nothing).
element-consuming method from inside the class. The bound is still checked at the call site before
erasure, so `Box<Fruit>::contains<Rock>` is still rejected.
- **A covariant upcast to an interface schedules its implementer.** When the erasable method is declared
on a covariant *interface* (`Collection<+E>`) and a concrete `ListColl<Book>` is upcast to a supertype
on a covariant *interface* (`Collection<out E>`) and a concrete `ListColl<Book>` is upcast to a supertype
specialization (`Collection<Product>`), that specialization declares a *distinct* abstract erased member
(`contains_<Product>`, separate from `contains_<Book>` — distinct names keep the covariant edge from
narrowing a parameter). The concrete implementation is carried down the covariant chain from the
Expand All @@ -113,23 +113,23 @@ the emitted code carries nothing).
checking are unaffected.
- Variance — making a bare-type-parameter bound leaf a first-class type-parameter reference also
lets the variance phase see it: a covariant/contravariant class parameter used as the **bare** leaf
of a sibling class parameter's bound (`class Pair<+T, U : T>`) is now flagged, consistently with the
already-rejected inner-argument case (`Sortable<+T : Box<T>>`) and the documented rule that bounds
of a sibling class parameter's bound (`class Pair<out T, U : T>`) is now flagged, consistently with the
already-rejected inner-argument case (`Sortable<out T : Box<T>>`) and the documented rule that bounds
are an invariant position. The supported method-level shape (`contains<U : E>`, where `U` is a
*method* parameter) is unaffected.

### Confirmation

The grounding, the inheritance threading, the determination floor, and the hard-fail are covered
end-to-end: a direct and an **inherited** (`ArrayList<Fruit> extends Base<+E>`) accept, a
multi-argument (`Pair<K, +V>::containsValue<U : V>`) accept that grounds the right parameter, a reject
end-to-end: a direct and an **inherited** (`ArrayList<Fruit> extends Base<out E>`) accept, a
multi-argument (`Pair<K, out V>::containsValue<U : V>`) accept that grounds the right parameter, a reject
whose message shows the grounded bound, the determined-receiver cases (parameter / property /
closure-`use` / method-return / chain / `self`-`static` / branch-arms-agree) accepting or rejecting on
the grounded type, and the unprovable cases (a raw generic parameter, a branch whose arms disagree, a
static class-parameter bound, and a *direct concrete* `$this` self-call) failing with
`xphp.bound_unprovable` — both thrown in `compile` and collected in `check`. The erasure lowering is
exercised by executing the compiled output (a forwarding self-call, an inherited member, a covariant
chain, a multi-class-param `Map<K, +V>`), and a forward to a *non-erasable* method fails with
chain, a multi-class-param `Map<K, out V>`), and a forward to a *non-erasable* method fails with
`xphp.unspecializable_self_call`. A sibling-parameter bound (`class Pair<T, U : T>`) is
unit-tested accept/reject with the grounded sibling shown, and a method-own sibling bound (`<U, V : U>`)
is grounded against the turbofish arguments. The receiver-argument threading is unit-tested for chains,
Expand Down
Loading
Loading