diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3b547..dca8bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 { public function contains(U $value): bool }` — has its bound **grounded** against the receiver's concrete type argument: `Box::contains` is accepted when `Banana <: Fruit`, and a genuine violation (`Box::contains`) is rejected with the bound shown as @@ -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`) and the method level (``). 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 `` 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 @@ -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::()`, 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`) and called through an upcast (`ListColl` used as `Collection`) — 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`), 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 @@ -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` 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` still extends @@ -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 { 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 @@ -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` holding a covariant container relates by that container's element type (`Tuple, Tag>` is usable where a `Tuple, Tag>` is required, because `ImmutableList ⊑ Collection`). The argument relationship is proven by @@ -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 $c): ?E }` where `Comparator<-T>` — is now accepted: `E` sits in a + by a contravariant generic of the class's covariant parameter — `class Box { pick( + Comparator $c): ?E }` where `Comparator` — 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` compares the `Book` + position a `out E` may occupy (sound under upcast — a `Comparator` compares the `Book` elements of a `Box` viewed as `Box`). 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, @@ -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`, `class Consumer`. + 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 @@ -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` 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 / diff --git a/docs/adr/0013-typed-constructor-parameters-on-variant-classes.md b/docs/adr/0013-typed-constructor-parameters-on-variant-classes.md index 5129a82..d8b9dae 100644 --- a/docs/adr/0013-typed-constructor-parameters-on-variant-classes.md +++ b/docs/adr/0013-typed-constructor-parameters-on-variant-classes.md @@ -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` actually extends `Producer` when `Banana` extends `Fruit` and `T` is covariant. A covariant immutable collection — Kotlin's `List`, 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 { 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. @@ -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 @@ -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 @@ -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` and assert each specialization's constructor keeps its real element type, that the chain autoloads with **no** fatal, that an `ImmutableList` **throws** on a non-`Banana` element, and that the contravariant constructor chain autoloads and constructs equally cleanly. diff --git a/docs/adr/0014-variance-markers-are-class-level-only.md b/docs/adr/0014-variance-markers-are-class-level-only.md index f1fce93..a267d74 100644 --- a/docs/adr/0014-variance-markers-are-class-level-only.md +++ b/docs/adr/0014-variance-markers-are-class-level-only.md @@ -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` extends `Producer`, and PHP's native type system carries the subtype relationship. That mechanism needs a stable, nominal class identity at each end of the edge. @@ -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(...)`, `$f = fn(...) => ...`): support it somehow, ignore it, or reject it. ## Decision Drivers @@ -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. diff --git a/docs/adr/0015-variance-markers-on-private-properties.md b/docs/adr/0015-variance-markers-on-private-properties.md index e3c7d3c..44aa6b1 100644 --- a/docs/adr/0015-variance-markers-on-private-properties.md +++ b/docs/adr/0015-variance-markers-on-private-properties.md @@ -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` extends `Producer` 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 @@ -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 { public function __construct(private T $item) {} public function get(): T { return $this->item; } } diff --git a/docs/adr/0018-grounding-method-generic-bounds-on-enclosing-type-parameters.md b/docs/adr/0018-grounding-method-generic-bounds-on-enclosing-type-parameters.md index 0045495..29639fd 100644 --- a/docs/adr/0018-grounding-method-generic-bounds-on-enclosing-type-parameters.md +++ b/docs/adr/0018-grounding-method-generic-bounds-on-enclosing-type-parameters.md @@ -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` 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 $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. @@ -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` grounds against an `ArrayList` receiver). The covered receiver shapes: - a parameter or `$this->prop` of declared generic type (`Box $b`); - a `new Box::()` local (and a closure-`use` capture of one); @@ -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::contains` 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` is upcast to a supertype + on a covariant *interface* (`Collection`) and a concrete `ListColl` is upcast to a supertype specialization (`Collection`), that specialization declares a *distinct* abstract erased member (`contains_`, separate from `contains_` — distinct names keep the covariant edge from narrowing a parameter). The concrete implementation is carried down the covariant chain from the @@ -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>`) and the documented rule that bounds + of a sibling class parameter's bound (`class Pair`) is now flagged, consistently with the + already-rejected inner-argument case (`Sortable>`) and the documented rule that bounds are an invariant position. The supported method-level shape (`contains`, 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 extends Base<+E>`) accept, a -multi-argument (`Pair::containsValue`) accept that grounds the right parameter, a reject +end-to-end: a direct and an **inherited** (`ArrayList extends Base`) accept, a +multi-argument (`Pair::containsValue`) 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`), and a forward to a *non-erasable* method fails with +chain, a multi-class-param `Map`), and a forward to a *non-erasable* method fails with `xphp.unspecializable_self_call`. A sibling-parameter bound (`class Pair`) is unit-tested accept/reject with the grounded sibling shown, and a method-own sibling bound (``) is grounded against the turbofish arguments. The receiver-argument threading is unit-tested for chains, diff --git a/docs/adr/0019-trait-members-are-not-modeled-in-the-type-hierarchy.md b/docs/adr/0019-trait-members-are-not-modeled-in-the-type-hierarchy.md index cca2b19..76b0276 100644 --- a/docs/adr/0019-trait-members-are-not-modeled-in-the-type-hierarchy.md +++ b/docs/adr/0019-trait-members-are-not-modeled-in-the-type-hierarchy.md @@ -51,9 +51,9 @@ the class hierarchy already uses. That is a self-contained feature, not a tweak *original* name and emitted under the *alias*: ```php - trait SearchOps<+E> { public function locate(U $value): int { /* scan $this->items */ } } - interface OrderedCollection<+E> extends Collection { public function indexOf(U $value): int; } - class ListColl<+E> extends LinkedNode implements OrderedCollection { + trait SearchOps { public function locate(U $value): int { /* scan $this->items */ } } + interface OrderedCollection extends Collection { public function indexOf(U $value): int; } + class ListColl extends LinkedNode implements OrderedCollection { use SearchOps { locate as indexOf; } // the alias is what satisfies indexOf } ``` @@ -67,9 +67,9 @@ the class hierarchy already uses. That is a self-contained feature, not a tweak algorithm (a silent correctness bug) or a duplicate: ```php - trait LinearSearch<+E> { public function contains(U $v): bool { /* O(n) scan */ } } - trait HashSearch<+E> { public function contains(U $v): bool { /* hash lookup */ } } - class FastColl<+E> extends RingBuffer implements Collection { + trait LinearSearch { public function contains(U $v): bool { /* O(n) scan */ } } + trait HashSearch { public function contains(U $v): bool { /* hash lookup */ } } + class FastColl extends RingBuffer implements Collection { use LinearSearch, HashSearch { HashSearch::contains insteadof LinearSearch; } } ``` diff --git a/docs/adr/0020-diagnose-and-restructure-self-reintroducing-specialization.md b/docs/adr/0020-diagnose-and-restructure-self-reintroducing-specialization.md index 29baedc..54df222 100644 --- a/docs/adr/0020-diagnose-and-restructure-self-reintroducing-specialization.md +++ b/docs/adr/0020-diagnose-and-restructure-self-reintroducing-specialization.md @@ -11,10 +11,10 @@ own type family in a strictly larger form**. The canonical shape is a covariant collection with a grouping derivation: ``` -class ImmutableList<+E> { +class ImmutableList { function groupBy(callable $keyOf): ImmutableMap> { … } } -class ImmutableMap implements Map { +class ImmutableMap implements Map { function values(): OrderedCollection { // re-exposes V — here, a list return new ImmutableList::(...); } diff --git a/docs/caveats.md b/docs/caveats.md index b0ef27b..68cfd17 100644 --- a/docs/caveats.md +++ b/docs/caveats.md @@ -128,16 +128,16 @@ id::(42); // works ### ❌ What doesn't work ```php -function process<+T>(T $x): T { /* ... */ } // free function +function process(T $x): T { /* ... */ } // free function class Box { - public function map<+U>(callable $f): Box { /* ... */ } // method + public function map(callable $f): Box { /* ... */ } // method } -$producer = function<+T>(): T { /* ... */ }; // closure -$arrow = fn<+T>(T $x): T => $x; // arrow +$producer = function(): T { /* ... */ }; // closure +$arrow = fn(T $x): T => $x; // arrow ``` ``` -Variance markers `+T` / `-T` are not supported on methods, functions, +Variance markers `out T` / `in T` are not supported on methods, functions, closures, or arrow functions — variance is a class-level-only feature by design: a function or closure specialization has no stable class identity to anchor a subtype `extends` edge to. Move the generic to a class-level @@ -160,7 +160,7 @@ parameters stay invariant. Put the template on a named class and use its method: ```php -class Producer<+T> { +class Producer { public function __invoke(): T { /* ... */ } } ``` @@ -343,7 +343,7 @@ trait HasItem { public function set(T $item): void { /* ... */ } } -class Container<+T> { // covariant +class Container { // covariant use HasItem; // The `set(T $item)` from the trait places T in a contravariant // position. The validator should reject -- but it doesn't, because @@ -376,12 +376,12 @@ can see it. > container no longer hits this — store the element in a `private T` property (PHP > doesn't type-check private slots across the `extends` edge), and the emitted > `get(): T` over a real-typed `private T` field is PHPStan-clean at every level. -> See the [`Producer<+T>`](syntax/variance.md#example) example. +> See the [`Producer`](syntax/variance.md#example) example. ### ❌ What gets flagged ```php -class ImmutableList<+T> { +class ImmutableList { private array $items; // many elements → `array` backing, not `T` public function __construct(T ...$items) { $this->items = $items; } public function get(int $i): T { return $this->items[$i]; } @@ -640,12 +640,12 @@ A derivation whose result type re-wraps the receiver's own type family in a *growing* form does not compile once that result is instantiated: ```php -class ImmutableList<+E> { +class ImmutableList { // Seeds a map of sub-lists: the result type reintroduces the receiver's own // family (ImmutableList) one level deeper. public function groupBy(callable $keyOf): ImmutableMap> { /* ... */ } } -class ImmutableMap { +class ImmutableMap { public function values(): OrderedCollection { /* ... */ } // re-exposes the value as a list } diff --git a/docs/errors.md b/docs/errors.md index 3a1f51a..1bcb174 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -39,7 +39,7 @@ The `json` and `github` formats tag each diagnostic with a stable code: | `xphp.default_bound_violation` | a parameter's default doesn't satisfy its own bound | | `xphp.missing_type_argument` | a required type argument was omitted and has no default — including a **turbofish-less call** to a generic method, function, or closure (`$x->pick('a')` instead of `$x->pick::('a')`): a method generic takes no inference, so the type argument must be supplied explicitly | | `xphp.too_many_type_arguments` | more type arguments were supplied than the template declares (e.g. `Box::` for a one-parameter `Box`) | -| `xphp.variance_position` | a `+T`/`-T` parameter appears in a position its variance forbids | +| `xphp.variance_position` | an `out T` / `in T` parameter appears in a position its variance forbids | | `xphp.inner_variance` | variance is violated through another generic's slot (composition) | | `xphp.undefined_template` | a generic was instantiated but never declared | | `xphp.undeclared_type` | a generic member, bound, or default names a type that is neither a declared type parameter nor a known type — e.g. `interface Foo { add(T $x); }` or `class Box` where the name is a stray/typo'd parameter (would otherwise compile to a reference to a non-existent class). Imported (`use`) and fully-qualified names are never flagged. A real class referenced bare must be imported or fully-qualified — including a class in the *same namespace* that lives in a plain `.php` file (which `xphp check` doesn't scan), even though PHP itself wouldn't require the `use` | @@ -110,7 +110,7 @@ In CI (GitHub Actions), one step gates the build and annotates the diff: |----------------------------|------| | `captures \`$this\`` | [Caveats — `$this`-capturing arrows and closures](caveats.md#this-capturing-arrows-and-closures-rejected) | | `static closures cannot yet be specialized` | [Caveats — `static` closures not supported](caveats.md#static-closures-not-supported) | -| `Variance markers \`+T\` / \`-T\` are not yet supported` | [Caveats — variance markers are class-level only](caveats.md#variance-markers-are-class-level-only) | +| `Variance markers \`out T\` / \`in T\` are not supported` | [Caveats — variance markers are class-level only](caveats.md#variance-markers-are-class-level-only) | | `Variance violation in template` | [Variance](syntax/variance.md) | | `Generic bound violated` | [Type bounds](syntax/type-bounds.md) | | `Default for generic parameter \`...\` violates the parameter's bound` | [Type bounds](syntax/type-bounds.md) + [Defaults](syntax/defaults.md) | @@ -152,9 +152,11 @@ generic function at file scope. ### Variance markers on non-class templates ``` -Variance markers `+T` / `-T` are not yet supported on methods, -functions, closures, or arrow functions; move the generic to a -class-level type parameter. +Variance markers `out T` / `in T` are not supported on methods, +functions, closures, or arrow functions — variance is a +class-level-only feature by design: a function or closure +specialization has no stable class identity to anchor a subtype +`extends` edge to. Move the generic to a class-level type parameter. ``` ### Variance composition violation @@ -178,7 +180,7 @@ allowed for variance. **public or protected**; a *private* property (declared or promoted) is exempt, because PHP doesn't type-check private slots across the `extends` chain. A **by-reference parameter** (`T &$x`) is invariant (it is read and written back), -so neither `+T` nor `-T` is allowed there. +so neither `out T` nor `in T` is allowed there. ### Variant class declared `final` @@ -187,7 +189,7 @@ A variant class cannot be declared `final`: its specializations participate in `extends` subtype edges that a `final` class cannot anchor. Remove `final`. ``` -A `+T` / `-T` class is specialized into a chain of `extends`-linked classes; a +A `out T` / `in T` class is specialized into a chain of `extends`-linked classes; a `final` class can't be a parent in that chain. Drop `final` from the declaration. ### Bound violations @@ -217,7 +219,7 @@ build fails — ground or fail, never an unchecked call. See [type bounds — ground or fail](syntax/type-bounds.md#ground-or-fail). ```php -class Box<+E> { +class Box { public function contains(U $value): bool { /* ... */ } } @@ -238,7 +240,7 @@ A `$this`-rooted self-call gets a variant of the message (the receiver is bound names a class parameter fails the same way (no instance to ground `E`): ```php -class Box<+E> { +class Box { public function contains(U $value): bool { /* ... */ } public function probe(): bool { return $this->contains::(new Banana()); // E is abstract here @@ -270,7 +272,7 @@ to it. When the target is **not** erasable (the parameter appears nested, in the return, or structurally), the forward can't be specialized: ```php -class Box<+E> { +class Box { public function nested(Box $items): bool { /* ... */ } // not erasable (U nested) public function relay(Box $items): bool { return $this->nested::($items); // forwards to a non-erasable target diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index 6eb1ec6..c05dd7c 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -32,7 +32,7 @@ than erasure can. | Union bounds + DNF | ✅ | ✅ | ✅ | ❌ (intersection only via `where`) | n/a | | F-bounded recursion (`T : Box`) | ✅ | ✅ | ✅ | ✅ | ✅ | | Default type parameters | ✅ | ✅ | ✅ | ✅ | ✅ | -| Declaration-site variance (`+T` / `-T`) | ✅ | ✅ | ✅ | ✅ | ⚠️ inferred (lifetime-driven; PhantomData for unused type params) | +| Declaration-site variance (`out T` / `in T`) | ✅ | ✅ | ✅ | ✅ | ⚠️ 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 | @@ -81,7 +81,7 @@ runtime — just the concrete class name substituted in. ### Real subtype edges between specializations ```php -class Producer<+T> { +class Producer { public function get(): T { /* ... */ } } ``` diff --git a/docs/guides/how-it-works.md b/docs/guides/how-it-works.md index f0b99ca..1e92857 100644 --- a/docs/guides/how-it-works.md +++ b/docs/guides/how-it-works.md @@ -286,7 +286,7 @@ walks each pair of specializations of the same variant template and, where the type arguments are related the right way, adds the `extends` / `implements` link between them: `Producer` actually `extends Producer` when `Banana extends Fruit` and -`T` is covariant (`+T`), dually for contravariant (`-T`). The edges +`T` is covariant (`out T`), dually for contravariant (`in T`). The edges are appended to the cloned specialization's `implements` / `extends` list and survive the next stage untouched -- the rewriter only rewrites *template* `Class_` / `Interface_` nodes, not specialized diff --git a/docs/roadmap.md b/docs/roadmap.md index 23becd2..9a1201f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -168,14 +168,14 @@ upcoming one. ### Variance -- `+T` and `-T` markers on type parameters. +- `out T` and `in T` markers on type parameters. - Position rules enforced at compile time (covariant in return, contravariant in param, any variance in a plain non-promoted constructor parameter or a *private* property — declared or promoted; both forbidden in public/protected properties — including public/protected promoted constructor params — by-reference params, bounds, and defaults). -- Real-typed construction: a `+T`/`-T` constructor parameter keeps its +- Real-typed construction: a `out T`/`in T` constructor parameter keeps its concrete type (nothing erased), so construction is runtime-type-checked. - A `final` variant class is rejected (a `final` class can't anchor the `extends` edge). diff --git a/docs/syntax/classes-and-interfaces.md b/docs/syntax/classes-and-interfaces.md index 87ebb55..3417c01 100644 --- a/docs/syntax/classes-and-interfaces.md +++ b/docs/syntax/classes-and-interfaces.md @@ -73,7 +73,7 @@ implementation detail. - **Traits don't get markers** — `instanceof SomeTrait` doesn't work in PHP, so generic traits are dropped from the emit after specialization. -- **Variance edges** — when `+T` or `-T` is declared, specializations +- **Variance edges** — when `out T` or `in T` is declared, specializations get real `extends` chains. See [variance](variance.md). ## See also diff --git a/docs/syntax/closures-and-arrows.md b/docs/syntax/closures-and-arrows.md index e175553..c0ee6d0 100644 --- a/docs/syntax/closures-and-arrows.md +++ b/docs/syntax/closures-and-arrows.md @@ -91,7 +91,7 @@ ref-ness is preserved end-to-end. generic function at file scope instead. See [caveats](../caveats.md#static-closures-not-supported). -- > ⚠️ **Variance markers not allowed** — `+T` / `-T` are rejected on +- > ⚠️ **Variance markers not allowed** — `out T` / `in T` are rejected on anonymous templates. They have no stable identity for an `extends` chain. See [caveats](../caveats.md#variance-markers-are-class-level-only). diff --git a/docs/syntax/index.md b/docs/syntax/index.md index ae22a5f..9eec30c 100644 --- a/docs/syntax/index.md +++ b/docs/syntax/index.md @@ -16,7 +16,7 @@ first. | [Methods and functions](methods-and-functions.md) | Generic methods (static + instance), generic free functions, bare top-level | | [Closures and arrows](closures-and-arrows.md) | `function(...)`, `fn(...) => ...`, captures incl. by-ref | | [Type bounds](type-bounds.md) | `T : Stringable`, `T : A & B`, `T : (A & B) \| C`, F-bounded `T : Box` | -| [Variance](variance.md) | `+T`, `-T`, position rules, subtype edges between specializations | +| [Variance](variance.md) | `out T`, `in T`, position rules, subtype edges between specializations | | [Defaults](defaults.md) | `T = int`, forward refs `Pair`, empty turbofish `$f::<>()` | | [Pseudo-types](pseudo-types.md) | `self` / `static` / `parent` and the `new self::(...)` form | | [Turbofish](turbofish.md) | All four call-site shapes plus variable and empty turbofish | @@ -59,8 +59,8 @@ class Sortable> {} // F-bounded class Pair {} // Variance -abstract class Producer<+T> { abstract public function get(): T; } // covariant -abstract class Consumer<-T> { abstract public function set(T $x): void; } // contravariant +abstract class Producer { abstract public function get(): T; } // covariant +abstract class Consumer { abstract public function set(T $x): void; } // contravariant // Default type params class Cache {} diff --git a/docs/syntax/type-bounds.md b/docs/syntax/type-bounds.md index ce0659d..708dcfb 100644 --- a/docs/syntax/type-bounds.md +++ b/docs/syntax/type-bounds.md @@ -86,21 +86,21 @@ once it sees `public int $value`. source set. The same "not in the source set" condition on a *variance* type argument is a non-failing warning rather than an error — see [variance](variance.md#unprovable-variance-edges). -- Bounds are an **invariant position** for variance markers — `+T` - or `-T` are rejected inside a bound expression (whether as a bare - leaf, `class Pair<+T, U : T>`, or nested, `Sortable<+T : Box>`). +- Bounds are an **invariant position** for variance markers — `out T` + or `in T` are rejected inside a bound expression (whether as a bare + leaf, `class Pair`, or nested, `Sortable>`). See [variance](variance.md). ## Bounding a method type parameter by the enclosing class parameter A method-level type parameter may be bounded by one of the **enclosing class's** type parameters. This is the sound way to give a covariant -`<+E>` collection an element-consuming method without dropping to +`` collection an element-consuming method without dropping to `mixed`: the argument is constrained to a subtype of the element type, -while the covariant `+E` never enters a parameter position. +while the covariant `out E` never enters a parameter position. ```php -class Box<+E> { +class Box { // U is a method type parameter (invariant), bounded by the class's E. public function contains(U $value): bool { /* ... */ } } @@ -152,7 +152,7 @@ is instantiated, so for now it fails (the *forwarding* form below is the way to make it compile and run): ```php -class Box<+E> { +class Box { public function contains(U $value): bool { /* ... */ } public function probe(): bool { @@ -169,7 +169,7 @@ Move such a call to a context where the receiver has a concrete element type generic and forward the parameter:** ```php -class Box<+E> { +class Box { public function contains(U $value): bool { /* ... */ } // ✅ Forwarding a method parameter compiles and runs: both methods take U @@ -198,14 +198,14 @@ The method may be declared on a covariant **interface** and called through a covariant **upcast** — the shape a collections library uses: ```php -interface Collection<+E> { +interface Collection { public function contains(U $value): bool; } -abstract class AbstractColl<+E> implements Collection { +abstract class AbstractColl implements Collection { public function __construct(private E ...$items) {} public function contains(U $value): bool { /* ... */ } } -class ListColl<+E> extends AbstractColl {} +class ListColl extends AbstractColl {} function anyProduct(Collection $c): bool { return $c->contains::(new Product()); @@ -219,7 +219,7 @@ Each interface specialization declares its own erased member `contains_` — distinct, so the covariant edge never narrows a parameter). When the element-consuming body sits on a **parent-less covariant base** that passes its type parameters straight to the interface — the -`AbstractColl<+E> implements Collection` shape above — the implementation is +`AbstractColl implements Collection` shape above — the implementation is **inherited** through the covariant chain. When inheritance can't carry it — the implementing class has another `extends` diff --git a/docs/syntax/variance.md b/docs/syntax/variance.md index 4a16695..3505513 100644 --- a/docs/syntax/variance.md +++ b/docs/syntax/variance.md @@ -1,7 +1,7 @@ # Variance Variance markers tell the compiler how subtyping flows through a -generic parameter. `+T` declares the parameter covariant; `-T` +generic parameter. `out T` declares the parameter covariant; `in T` declares it contravariant; unmarked is invariant. With markers in place, specializations get real `extends` chains and PHP's native LSP carries the subtype relationship. @@ -18,13 +18,13 @@ namespace App; // A *private* `T` property is variance-exempt — PHP doesn't type-check private // slots across the `extends` chain — so the backing field keeps its real type // (a public/protected `T` property would be rejected; see the rules below). -class Producer<+T> { +class Producer { public function __construct(private T $item) {} public function get(): T { return $this->item; } } // Contravariant: T appears in parameter positions only -class Consumer<-T> { +class Consumer { public function accept(T $x): void { /* ... */ } } @@ -37,7 +37,7 @@ class Box { class Fruit {} class Banana extends Fruit {} -// With +T, this is now a valid downcast at the type-system level: +// With out T, this is now a valid downcast at the type-system level: function eat(Producer $p): Fruit { return $p->get(); } @@ -68,20 +68,20 @@ PHP's native LSP handles the relationship from there — passing a `Producer` where a `Producer` is required Just Works, including reflection and `instanceof`. -For contravariant `-T`, the edge flips: `Consumer extends Consumer`. +For contravariant `in T`, the edge flips: `Consumer extends Consumer`. ### Nested type-arguments (a generic as a type-argument) A covariant slot can hold another generic as its argument, and the edge composes through it — including when the inner argument is a generic of a -**different but related template**. A covariant `Tuple<+A, +B>` holding a +**different but related template**. A covariant `Tuple` holding a covariant container relates by *its* element relationship: ```php -interface Collection<+E> { /* … */ } -class ImmutableList<+E> implements Collection { /* … */ } -interface Tuple<+A, +B> { /* … */ } -class Couple<+A, +B> implements Tuple { /* … */ } +interface Collection { /* … */ } +class ImmutableList implements Collection { /* … */ } +interface Tuple { /* … */ } +class Couple implements Tuple { /* … */ } // Book <: Product, ImmutableList implements Collection, all covariant ⇒ // Tuple, Tag> ⊑ Tuple, Tag> @@ -120,7 +120,7 @@ source set the compiler builds its hierarchy from, so the edge can be proven and Position rules, enforced at compile time over the collected definitions (`Registry::validateVariancePositions`): -| Position | `+T` allowed? | `-T` allowed? | +| Position | `out T` allowed? | `in T` allowed? | |---------------------------------------|---------------|---------------| | Method return type | ✅ | ❌ | | Method parameter | ❌ | ✅ | @@ -152,11 +152,11 @@ strictly invariant.) A **by-reference parameter** (`function f(T &$x)`) is likewise invariant: the caller's variable is both read and written back through the reference, so it acts -as input *and* output — neither `+T` nor `-T` is sound. This holds in method, +as input *and* output — neither `out T` nor `in T` is sound. This holds in method, constructor, and nested closure/arrow signatures. A **plain (non-promoted) constructor parameter** is the exception: it may -carry `+T` / `-T` at any variance, and xphp emits it with its **real** +carry `out T` / `in T` at any variance, and xphp emits it with its **real** substituted type. A constructor parameter isn't part of the externally-visible variance surface (a constructor is never reached through an upcast reference — the same reason Kotlin exempts constructor parameters from variance checks), and @@ -166,7 +166,7 @@ covariant immutable collection take *type-checked* construction input (see below). A *promoted* constructor parameter is a property, so it follows the property rules above: a public/protected one stays strictly invariant, while a **private** one is exempt and keeps its real type — which is exactly what makes -the covariant single-value `Producer<+T>` shape at the top of this page work. +the covariant single-value `Producer` shape at the top of this page work. ### Covariant immutable collections (typed construction) @@ -174,7 +174,7 @@ A covariant container can take its element type in its constructor — the backbone of a read-only `List`-style collection: ```php -class ImmutableList<+T> { +class ImmutableList { private array $items; public function __construct(T ...$items) { $this->items = $items; } public function get(int $i): T { return $this->items[$i]; } @@ -194,7 +194,7 @@ The constructor parameter keeps its real element type on every specialisation ImmutableList` without a PHP fatal — PHP doesn't signature-check `__construct` across the chain. (A variant class **cannot be declared `final`**: its specializations are linked by `extends` edges, which a `final` class can't -anchor, so `final` on a `+T`/`-T` class is rejected at compile time.) +anchor, so `final` on a `out T`/`in T` class is rejected at compile time.) > ✅ **Construction is runtime-type-checked.** Because the constructor parameter > keeps its real type, PHP enforces it at construction: building an @@ -203,7 +203,7 @@ anchor, so `final` on a `+T`/`-T` class is rejected at compile time.) > one property shape that can't carry a real `T` is a **non-private** (public or > protected) stored property — PHP enforces invariant property types across the > edge for visible members. A **private** stored property *can* hold a real `T` -> (see the single-value `Producer<+T>` at the top), so a covariant single-value +> (see the single-value `Producer` at the top), so a covariant single-value > container needs no `mixed` backing at all. A *multi-element* collection like > `ImmutableList` is different: many elements live in one `private array $items` > field, which xphp emits without a value-type annotation — so it compiles and @@ -219,14 +219,14 @@ signatures, the bounds compose: ```php // Container's X is invariant, so Container reads as invariant // regardless of T's outer variance. -class P<+T> { +class P { public function f(): Container {} // REJECTED } ``` `T` appears in a covariant outer position (return), but the inner `Container` has `X` as invariant — the composed position is -invariant, so the outer `+T` is rejected. The validator walks every +invariant, so the outer `out T` is rejected. The validator walks every generic class's method signatures, bounds, and defaults to apply this composition. @@ -235,16 +235,16 @@ A consuming method that takes a **contravariant** generic is sound on a covariant class: ```php -interface Comparator<-T> { public function compare(T $a, T $b): int; } +interface Comparator { public function compare(T $a, T $b): int; } -class Box<+E> { +class Box { public function pick(Comparator $c): ?E { /* … */ } // ALLOWED } ``` -`E` is in a contravariant slot (`Comparator<-T>`) inside a contravariant +`E` is in a contravariant slot (`Comparator`) inside a contravariant parameter position — contra ∘ contra = **covariant**, which a covariant -`+E` may occupy. (Under an upcast, a `Box` viewed as `Box` +`out E` may occupy. (Under an upcast, a `Box` viewed as `Box` takes a `Comparator`, which by contravariance compares the `Book` elements — sound.) This is the element-consuming counterpart to the covariant immutable constructor: a `mixed`-free, fluent diff --git a/src/Transpiler/Monomorphize/Compiler.php b/src/Transpiler/Monomorphize/Compiler.php index 14ce724..8f33b64 100644 --- a/src/Transpiler/Monomorphize/Compiler.php +++ b/src/Transpiler/Monomorphize/Compiler.php @@ -115,7 +115,7 @@ public function compile( $registry->validateDefaultsAgainstBounds(); // Inner-template variance composition: every template's variance // markers are known by now, so cases the parse-time validator - // couldn't catch (e.g. `class P<+T> { f(): Container }` where + // couldn't catch (e.g. `class P { f(): Container }` where // Container's slot is invariant) fail here BEFORE instantiations // amplify the error. $registry->validateInnerVariance(); diff --git a/src/Transpiler/Monomorphize/EnclosingBoundErasure.php b/src/Transpiler/Monomorphize/EnclosingBoundErasure.php index a854446..7a3335c 100644 --- a/src/Transpiler/Monomorphize/EnclosingBoundErasure.php +++ b/src/Transpiler/Monomorphize/EnclosingBoundErasure.php @@ -14,7 +14,7 @@ /** * Decides whether a generic method whose type parameter is bounded by an enclosing class type - * parameter (`class Box<+E> { contains(U $value): bool }`) can be lowered by **erasing `U` + * parameter (`class Box { contains(U $value): bool }`) can be lowered by **erasing `U` * to its bound `E`** — i.e. specialized once per class instantiation (`contains_(Fruit)`) * instead of once per call-site turbofish (`contains_(Banana)`). * @@ -86,7 +86,7 @@ public static function isErasable(ClassMethod $method, array $methodParams, arra * parameter widens to the supertype lets a supertype value escape through a subtype return (a runtime * `TypeError`). Such a shape can't be emitted directly and must fail loudly. * - * Only the return type is inspected: an enclosing parameter is covariant (`+E`), so variance checking + * Only the return type is inspected: an enclosing parameter is covariant (`out E`), so variance checking * forbids it from appearing in any method *parameter* position before this point — the return type is * the only signature position it can legally occupy. A bounded method parameter is typed by the * *method* generic (`U`), not by `E`, so it never matches here either. diff --git a/src/Transpiler/Monomorphize/InnerVarianceValidator.php b/src/Transpiler/Monomorphize/InnerVarianceValidator.php index 69e430e..ce90081 100644 --- a/src/Transpiler/Monomorphize/InnerVarianceValidator.php +++ b/src/Transpiler/Monomorphize/InnerVarianceValidator.php @@ -23,7 +23,7 @@ * Inner-template variance composition check for one generic definition. * * When a variance-marked type-param appears inside another generic's type-arg - * (e.g. `class P<+T> { f(): Container }` where `Container`'s slot is + * (e.g. `class P { f(): Container }` where `Container`'s slot is * invariant), the effective variance at that position is the composition of the * outer position and the inner slot's variance. This catches cases the * position validator can't, because the effective variance only resolves once @@ -341,7 +341,7 @@ private function assertLeaf( } // $declared is Covariant or Contravariant at this point — the Invariant // case passes every allowed-list and early-returns above. - $sigil = $declared === Variance::Covariant ? '+' : '-'; + $sigil = $declared === Variance::Covariant ? 'out ' : 'in '; $where = $innerLabel !== null ? sprintf(' (via slot %d of %s)', $innerSlot, $innerLabel) : ''; diff --git a/src/Transpiler/Monomorphize/Registry.php b/src/Transpiler/Monomorphize/Registry.php index cc0ec2f..b375807 100644 --- a/src/Transpiler/Monomorphize/Registry.php +++ b/src/Transpiler/Monomorphize/Registry.php @@ -609,7 +609,7 @@ private static function varianceEdgeUnprovableMessage( Variance $variance, string $typeDisplay, ): string { - $marker = $variance === Variance::Covariant ? '+' : '-'; + $marker = $variance === Variance::Covariant ? 'out ' : 'in '; return sprintf( "Variance edge cannot be proven while instantiating %s.\n" . " type parameter %s%s is %s, but %s is not in the source set the hierarchy was built from (and is not a recognized PHP built-in),\n" diff --git a/src/Transpiler/Monomorphize/SpecializationCloser.php b/src/Transpiler/Monomorphize/SpecializationCloser.php index 582b58b..27853eb 100644 --- a/src/Transpiler/Monomorphize/SpecializationCloser.php +++ b/src/Transpiler/Monomorphize/SpecializationCloser.php @@ -13,7 +13,7 @@ * Closes the specialization set under the covariant-upcast implementation requirement. * * A method whose type parameter is bounded by an enclosing class parameter - * (`interface Collection<+E> { contains(E2 $value): bool }`) is lowered by erasing `E2` to + * (`interface Collection { contains(E2 $value): bool }`) is lowered by erasing `E2` to * its bound `E` — one member per class instantiation, mangled on that class's own `E`. So * `Collection` declares the abstract `contains_(Book)` and `Collection` declares * a DISTINCT abstract `contains_(Product)` (distinct names are required: a single shared @@ -291,7 +291,7 @@ private function emitDirectly( * single-inheritance chain — a body for each erased member its covariant-upcast obligations expose. * * The fixpoint's schedule-and-inherit path supplies the ONE member the class chain threads. Under a - * covariant DIAMOND (a multi-parameter or nested covariant element type, e.g. `Tuple<+A,+B>` over + * covariant DIAMOND (a multi-parameter or nested covariant element type, e.g. `Tuple` over * `Book <: Product`) the same concrete is, by per-argument covariance, an instance of SEVERAL supertype * specializations at once, and PHP single inheritance can carry only one of them — leaving the sibling * obligations' distinctly-mangled abstract members unimplemented (a class-load fatal). This pass walks diff --git a/src/Transpiler/Monomorphize/TypeHierarchy.php b/src/Transpiler/Monomorphize/TypeHierarchy.php index 01154a7..ad250bc 100644 --- a/src/Transpiler/Monomorphize/TypeHierarchy.php +++ b/src/Transpiler/Monomorphize/TypeHierarchy.php @@ -192,7 +192,7 @@ public function ancestorChain(string $fqn): array * * Given a receiver of static type `$subFqn<$subArgs>` and a `$superFqn` reachable through its * `extends`/`implements` clauses, returns the type arguments that `$superFqn`'s OWN parameters - * are bound to as witnessed from that receiver. For `class ArrayList<+E> implements Collection`, + * are bound to as witnessed from that receiver. For `class ArrayList implements Collection`, * `resolveInheritedArgs('App\ArrayList', [Product], 'App\Collection')` yields `[Product]`. At each * hop the current class's parameters are substituted with the current arguments into the supertype * clause's arguments (so nested clauses like `implements Foo>` ground throughout). diff --git a/src/Transpiler/Monomorphize/TypeParam.php b/src/Transpiler/Monomorphize/TypeParam.php index 29d0d62..c3bd976 100644 --- a/src/Transpiler/Monomorphize/TypeParam.php +++ b/src/Transpiler/Monomorphize/TypeParam.php @@ -23,8 +23,8 @@ * type-param references in the default. * * `variance` controls whether subtype edges are emitted between specializations. - * `Invariant` (no prefix) is the default. `Covariant` (`+T`) lifts `T1 <: T2` - * to `Box <: Box`. `Contravariant` (`-T`) flips the direction. + * `Invariant` (no marker) is the default. `Covariant` (`out T`) lifts `T1 <: T2` + * to `Box <: Box`. `Contravariant` (`in T`) flips the direction. * * Both expressions are built by `XphpSourceParser::resolveAndAttach` after * resolving each leaf class name against the file's namespace + use map. diff --git a/src/Transpiler/Monomorphize/Variance.php b/src/Transpiler/Monomorphize/Variance.php index 4ede913..fe3b9c7 100644 --- a/src/Transpiler/Monomorphize/Variance.php +++ b/src/Transpiler/Monomorphize/Variance.php @@ -7,19 +7,19 @@ /** * Variance marker on a type parameter. * - * - `Invariant` (the default, no prefix): T can appear in both input and + * - `Invariant` (the default, no marker): T can appear in both input and * output positions; specializations are unrelated unless their args are * identical. - * - `Covariant` (`+T`): T can appear in method return positions (and in a + * - `Covariant` (`out T`): T can appear in method return positions (and in a * plain constructor parameter — see below). `T1 <: T2` lifts to * `Box <: Box`. - * - `Contravariant` (`-T`): T can appear in method parameter positions (and in + * - `Contravariant` (`in T`): T can appear in method parameter positions (and in * a plain constructor parameter). `T1 <: T2` lifts to `Box <: Box` * (flipped). * * A **public/protected** property position (mutable AND readonly, including a * public/protected *promoted* constructor parameter), bounds, and defaults are - * strict-invariant for both `+T` and `-T`: PHP enforces invariant property types + * strict-invariant for both `out T` and `in T`: PHP enforces invariant property types * across `extends` chains regardless of `readonly`, so a covariance allowance * there would PHP-fatal at autoload when the variance edge emits. A **private** * property (declared or promoted, mutable or readonly), by contrast, may carry diff --git a/src/Transpiler/Monomorphize/VarianceEdgeEmitter.php b/src/Transpiler/Monomorphize/VarianceEdgeEmitter.php index d813209..ddb24cb 100644 --- a/src/Transpiler/Monomorphize/VarianceEdgeEmitter.php +++ b/src/Transpiler/Monomorphize/VarianceEdgeEmitter.php @@ -13,7 +13,7 @@ * Emits subtype edges between specializations of the same generic template * based on each type-param's variance. * - * For a template `Producer<+T>` with `Banana <: Fruit` at the PHP class level, + * For a template `Producer` with `Banana <: Fruit` at the PHP class level, * this adds `extends Producer_Fruit_` to the cloned `Producer_Banana` * Class_ node. * @@ -176,7 +176,7 @@ private static function addImplementsEdges(ClassLike $ast, array $directSupers): if ($ast instanceof Class_) { // PHP allows a class exactly ONE parent. A specialized class that already carries a source - // `extends` (e.g. `class ListColl<+E> extends AbstractColl` → `ListColl_Book extends + // `extends` (e.g. `class ListColl extends AbstractColl` → `ListColl_Book extends // AbstractColl_Book`) must keep it: that parent carries the inherited member bodies and the // source-declared `is-a` relationships. A same-template covariant super // (`ListColl <: ListColl`) cannot ALSO be a direct parent under single diff --git a/src/Transpiler/Monomorphize/VariancePositionValidator.php b/src/Transpiler/Monomorphize/VariancePositionValidator.php index 3e9b993..d368030 100644 --- a/src/Transpiler/Monomorphize/VariancePositionValidator.php +++ b/src/Transpiler/Monomorphize/VariancePositionValidator.php @@ -43,7 +43,7 @@ * * Why public/protected properties are strict-invariant: PHP enforces invariant * property types across the `extends` chain regardless of `readonly`. A covariant - * +T in a subtype property declaration would PHP-fatal at autoload when the + * `out T` in a subtype property declaration would PHP-fatal at autoload when the * variance edge `Producer_Banana extends Producer_Fruit` lands. The semantic * argument ("readonly = output-only") doesn't override PHP's static-type rule. * @@ -55,7 +55,7 @@ * The Specializer emits the real substituted type there, and each specialization * re-emits its own field + accessor, so no inherited method ever reads a * divergent-typed private slot. This is what lets the covariant getter pattern - * `class Producer<+T> { public function __construct(private T $item) {} … }` be + * `class Producer { public function __construct(private T $item) {} … }` be * both real-typed and sound. * * Why a non-promoted constructor parameter may carry any variance: a @@ -67,8 +67,8 @@ * *promoted* constructor parameter is a property, so it falls under the property * rules above: strict-invariant when public/protected, any variance when private. * - * F-bounded variance (`class Sortable<+T : Comparable>`) is rejected - * because `+T` appears inside its own bound (an invariant position). + * F-bounded variance (`class Sortable>`) is rejected + * because `out T` appears inside its own bound (an invariant position). * * Errors include the param name, variance marker, and the position class * so the user sees what's wrong without reading the implementation. @@ -229,8 +229,8 @@ private function checkProperty(Property $property): void return; } // Public/protected: PHP enforces invariant property types across `extends` - // chains regardless of `readonly`. Even +T on a readonly property would - // PHP-fatal at autoload when the variance edge lands. + // chains regardless of `readonly`. Even `out T` on a readonly property + // would PHP-fatal at autoload when the variance edge lands. $position = $property->isReadonly() ? 'readonly property' : 'mutable property'; $this->checkPhpType($type, [Variance::Invariant], $position); } @@ -267,9 +267,9 @@ private function checkMethod(ClassMethod $method): void } // A by-reference parameter is read AND written back through the // caller's variable, so it's an invariant position regardless of - // method vs constructor — neither +T nor -T is sound there. Checked - // before the variant-constructor any-variance branch so `-T &$x` in - // a constructor is rejected too. + // method vs constructor — neither `out T` nor `in T` is sound there. + // Checked before the variant-constructor any-variance branch so + // `in T &$x` in a constructor is rejected too. if ($param->byRef) { $this->checkPhpType($param->type, [Variance::Invariant], 'by-reference parameter'); continue; @@ -387,7 +387,7 @@ private function checkPhpType(Node $type, array $allowed, string $position): voi // A type-param NESTED inside a type constructor (`Box`, `Comparator`) is NOT judged // here: its effective variance is the composition of this position with the referenced // type's slot variance, which only InnerVarianceValidator resolves. Descending with this - // (uncomposed) position would wrongly reject a sound `Comparator` on a covariant `+E` + // (uncomposed) position would wrongly reject a sound `Comparator` on a covariant `out E` // (contra ∘ contra = covariant) and wrongly pass an unsound one. This validator owns only // DIRECT occurrences; the composing pass owns the nested ones. return; @@ -419,8 +419,8 @@ private static function violationMessage( ?string $hostParam, ): string { $marker = match ($variance) { - Variance::Covariant => '+', - Variance::Contravariant => '-', + Variance::Covariant => 'out ', + Variance::Contravariant => 'in ', Variance::Invariant => '', }; $context = $hostParam !== null diff --git a/src/Transpiler/Monomorphize/XphpSourceParser.php b/src/Transpiler/Monomorphize/XphpSourceParser.php index 70190b0..5e24f34 100644 --- a/src/Transpiler/Monomorphize/XphpSourceParser.php +++ b/src/Transpiler/Monomorphize/XphpSourceParser.php @@ -612,27 +612,46 @@ private static function parseTypeParamList( $sawDefault = false; $i = self::skipWs($tokens, $openIdx + 1); while ($i < $n) { - // Variance prefix `+` (covariant) or `-` (contravariant). Both are - // single-char tokens at this position. Class-level only -- methods, - // functions, closures, and arrow functions reject them because - // their specializations aren't keyed by stable identities that - // PHP would resolve via `extends` chains. - $variance = Variance::Invariant; + // The old Scala/Hack markers `+T` / `-T` were replaced by the + // Kotlin-style `out T` / `in T`. Reject the glyphs with a migration + // hint rather than letting them fall through to a bare `null` (which + // would surface downstream as an opaque PHP syntax error). if ($i < $n && ($tokens[$i]->text === '+' || $tokens[$i]->text === '-')) { - if (!$allowVariance) { - throw new RuntimeException( - 'Variance markers `+T` / `-T` are not supported on methods, ' - . 'functions, closures, or arrow functions — variance is a ' - . 'class-level-only feature by design: a function or closure ' - . 'specialization has no stable class identity to anchor a ' - . 'subtype `extends` edge to. Move the generic to a class-level ' - . 'type parameter.', - ); + throw new RuntimeException( + 'The `+T` / `-T` variance syntax was replaced by `out T` / `in T`. ' + . 'Write `out` for covariance and `in` for contravariance, e.g. ' + . '`class Box` or `class Consumer`.', + ); + } + + // Kotlin-style variance markers `out` (covariant) / `in` + // (contravariant). They are contextual keywords: plain `T_STRING` + // tokens that act as a marker only when immediately followed (past + // whitespace) by the parameter name — a required space separates + // marker and name (`out T`, never `outT`, which is one token and an + // ordinary name). Class-level only: methods, functions, closures, + // and arrow functions reject variance because their specializations + // aren't keyed by stable identities that PHP would resolve via + // `extends` chains. + $variance = Variance::Invariant; + if ($i < $n && in_array($tokens[$i]->text, ['out', 'in'], true)) { + $afterMarker = self::skipWs($tokens, $i + 1); + if ($afterMarker < $n && self::isNameToken($tokens[$afterMarker])) { + if (!$allowVariance) { + throw new RuntimeException( + 'Variance markers `out T` / `in T` are not supported on methods, ' + . 'functions, closures, or arrow functions — variance is a ' + . 'class-level-only feature by design: a function or closure ' + . 'specialization has no stable class identity to anchor a ' + . 'subtype `extends` edge to. Move the generic to a class-level ' + . 'type parameter.', + ); + } + $variance = $tokens[$i]->text === 'out' + ? Variance::Covariant + : Variance::Contravariant; + $i = $afterMarker; } - $variance = $tokens[$i]->text === '+' - ? Variance::Covariant - : Variance::Contravariant; - $i++; } if (!self::isNameToken($tokens[$i])) { @@ -641,6 +660,20 @@ private static function parseTypeParamList( $paramName = ltrim($tokens[$i]->text, '\\'); $i++; + // Reserve `out` / `in` as variance markers: they can never name a + // type parameter. This one check at the name slot catches every + // shape uniformly — `class Box` (no following name, so `out` + // is read here as the name), `class Box` (marker consumed, + // second `out` read as the name), `class Pair`, + // `class Box` — all reject with a single diagnostic. + if (in_array($paramName, ['out', 'in'], true)) { + throw new RuntimeException( + '`out` and `in` are variance markers and cannot name a type parameter. ' + . 'Use `out T` for covariance or `in T` for contravariance, and pick a ' + . 'different name for the parameter itself.', + ); + } + $bound = null; $afterName = self::skipWs($tokens, $i); if ($afterName < $n && $tokens[$afterName]->text === ':') { diff --git a/test/Transpiler/Monomorphize/EnclosingBoundErasureTest.php b/test/Transpiler/Monomorphize/EnclosingBoundErasureTest.php index 752662a..f847a12 100644 --- a/test/Transpiler/Monomorphize/EnclosingBoundErasureTest.php +++ b/test/Transpiler/Monomorphize/EnclosingBoundErasureTest.php @@ -21,7 +21,7 @@ final class EnclosingBoundErasureTest extends TestCase { + class Box { public function contains(U $value): bool { return true; } public function probe(U $value): bool { return $this->contains::($value); } public function pair(U $a, V $b): bool { return true; } diff --git a/test/Transpiler/Monomorphize/EnclosingParamBoundIntegrationTest.php b/test/Transpiler/Monomorphize/EnclosingParamBoundIntegrationTest.php index 7ec9c1f..10cb210 100644 --- a/test/Transpiler/Monomorphize/EnclosingParamBoundIntegrationTest.php +++ b/test/Transpiler/Monomorphize/EnclosingParamBoundIntegrationTest.php @@ -57,7 +57,7 @@ public function testDirectEnclosingParamBoundAcceptsSubtype(): void { + class Box { public function contains(U $value): bool { return true; } } PHP, @@ -85,7 +85,7 @@ public function testInheritedEnclosingParamBoundAcceptsSubtype(): void { + abstract class Base { public function contains(U $value): bool { return true; } } PHP, @@ -93,7 +93,7 @@ public function contains(U $value): bool { return true; } extends Base {} + class ArrayList extends Base {} PHP, 'Use.xphp' => <<<'PHP' extends Base {} public function testMultiArgEnclosingParamBoundGroundsTheRightParameter(): void { - // `Pair::containsValue` — grounding must pick V (index 1), not K. If it used + // `Pair::containsValue` — grounding must pick V (index 1), not K. If it used // K (Food), `Banana <: Food` would also pass, so make K a type Banana is NOT a subtype of. $this->compile([ 'Models.xphp' => self::MODELS, @@ -118,7 +118,7 @@ public function testMultiArgEnclosingParamBoundGroundsTheRightParameter(): void { + class Pair { public function containsValue(U $value): bool { return true; } } PHP, @@ -145,7 +145,7 @@ public function testEnclosingParamBoundRejectsNonSubtypeWithGroundedMessage(): v { + class Box { public function contains(U $value): bool { return true; } } PHP, @@ -174,7 +174,7 @@ public function testUnboundedMethodGenericIsUnaffected(): void { + class Box { public function pick(U $value): U { return $value; } } PHP, @@ -204,7 +204,7 @@ public function testThisSelfCallWithConcreteTurbofishIsUnprovableAndHardFails(): { + class Box { public function contains(U $value): bool { return true; } public function probe(): bool { return $this->contains::(new Banana()); } } @@ -422,7 +422,7 @@ public function testSelfReturningChainCarriesReceiverArgsAndRejects(): void { + class Box { public function copy(): static { return $this; } public function contains(U $value): bool { return true; } } @@ -482,7 +482,7 @@ public function testDeepSelfReturningChainResolvesWithoutBlowup(): void { + class Box { public function copy(): static { return $this; } public function contains(U $value): bool { return true; } } @@ -575,7 +575,7 @@ public function testStaticMethodClassParamBoundIsUnprovableAndHardFails(): void { + class Box { public static function pick(U $value): bool { return true; } } PHP, @@ -600,7 +600,7 @@ public function testThisSelfCallUnprovableIsCollectedInCheckMode(): void { + class Box { public function contains(U $value): bool { return true; } public function probe(): bool { return $this->contains::(new Banana()); } } @@ -690,7 +690,7 @@ function pick(array $boxes): void { { + class Box { public function contains(U $value): bool { return true; } public function probe(U $value): bool { return $this->contains::($value); } } @@ -754,7 +754,7 @@ public function testTwoEnclosingBoundedParamsEraseAndRunAtRuntime(): void #[RunInSeparateProcess] public function testMultiClassParamErasureMangleKeysOnTheBoundsReferentAtRuntime(): void { - // `containsValue` on `Map` mangles on V (Fruit), not K (string). Call-site and + // `containsValue` on `Map` mangles on V (Fruit), not K (string). Call-site and // Specializer must agree on that key, or the call resolves to nothing. Executed. $fixture = CompiledFixture::compile( __DIR__ . '/../../fixture/compile/enclosing_bound_erasure_map_multiparam/source', @@ -806,7 +806,7 @@ public function testInheritedErasableMemberResolvesAtRuntime(): void #[RunInSeparateProcess] public function testErasureIsVarianceSafeOnTheCovariantChainAtRuntime(): void { - // The variance gate: Box<+E> builds a covariant extends-chain; the distinct E-mangled + // The variance gate: Box builds a covariant extends-chain; the distinct E-mangled // contains_ members coexist with no LSP fatal, and a Box dispatches the inherited // contains_. Proven by executing the real compiled output. $fixture = CompiledFixture::compile( @@ -844,7 +844,7 @@ public function testCovariantInterfaceUpcastResolvesTheErasedMemberAtRuntime(): #[RunInSeparateProcess] public function testSubInterfaceMethodDirectEmittedUnderUpcastRunsAtRuntime(): void { - // The headline case: `indexOf` on the sub-interface `OrderedCollection<+E>`, body on + // The headline case: `indexOf` on the sub-interface `OrderedCollection`, body on // `ListColl extends AbstractColl` (a class with a parent). Upcast `ListColl` → // `OrderedCollection` can't inherit `indexOf_` through a covariant edge, so it's // emitted directly onto `ListColl` (reading its inherited Book-typed $items). `contains` @@ -883,7 +883,7 @@ public function testDirectEmittedBodyResolvesTheClassParamToTheUpcastSourceAtRun #[RunInSeparateProcess] public function testMultiParamCovariantInterfaceUpcastResolvesAtRuntime(): void { - // Multi-param: `HashMap` (K invariant, +V covariant) upcast to `MMap`. + // Multi-param: `HashMap` (K invariant, out V covariant) upcast to `MMap`. // The closer must schedule `AbstractMap` — keep K=Id, raise V to Product — and the // erased `containsValue` (mangled on V) must resolve through the covariant chain. Executed. $fixture = CompiledFixture::compile( @@ -901,7 +901,7 @@ public function testMultiParamCovariantInterfaceUpcastResolvesAtRuntime(): void #[RunInSeparateProcess] public function testVarianceEdgeDoesNotOverwriteASourceParentAtRuntime(): void { - // A covariant class with a SOURCE parent (`ListColl<+E> extends Base`) instantiated at two + // A covariant class with a SOURCE parent (`ListColl extends Base`) instantiated at two // args (Fruit, Banana). The variance edge emitter must keep each specialization's source // `extends Base` rather than overwrite it with the same-template covariant super // (`ListColl extends ListColl`) — overwriting would sever the inherited @@ -924,7 +924,7 @@ public function testVarianceEdgeDoesNotOverwriteASourceParentAtRuntime(): void { + interface Collection { public function contains(E2 $value): bool; } PHP; @@ -932,7 +932,7 @@ public function contains(E2 $value): bool; implements Collection { + abstract class AbstractColl implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -950,7 +950,7 @@ public function testCovariantUpcastSchedulesTheConcreteSupertypeSpecialization() 'Book.xphp' => self::BOOK, 'Collection.xphp' => self::COLLECTION_IFACE, 'AbstractColl.xphp' => self::ABSTRACT_COLL, - 'ListColl.xphp' => " extends AbstractColl {}\n", + 'ListColl.xphp' => " extends AbstractColl {}\n", 'Use.xphp' => <<<'PHP' implements Collection { + class ListColl implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1054,7 +1054,7 @@ public function testInterfaceWithANonGenericMethodStillFindsTheErasableOne(): vo { + interface Collection { public function size(): int; public function contains(E2 $value): bool; } @@ -1063,7 +1063,7 @@ public function contains(E2 $value): bool; implements Collection { + abstract class AbstractColl implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1071,7 +1071,7 @@ public function size(): int { return \count($this->items); } public function contains(E2 $value): bool { return \in_array($value, $this->items, true); } } PHP, - 'ListColl.xphp' => " extends AbstractColl {}\n", + 'ListColl.xphp' => " extends AbstractColl {}\n", 'Use.xphp' => <<<'PHP' self::BOOK, 'Collection.xphp' => self::COLLECTION_IFACE, 'AbstractColl.xphp' => self::ABSTRACT_COLL, - 'ListColl.xphp' => " extends AbstractColl {}\n", + 'ListColl.xphp' => " extends AbstractColl {}\n", 'Use.xphp' => <<<'PHP' extends Collection { public function firstKind(U $value): bool; } + interface OrderedCollection extends Collection { public function firstKind(U $value): bool; } PHP, 'AbstractColl.xphp' => self::ABSTRACT_COLL, 'ListColl.xphp' => <<<'PHP' extends AbstractColl implements OrderedCollection { + class ListColl extends AbstractColl implements OrderedCollection { public function firstKind(U $value): bool { return $value instanceof E; } } PHP, @@ -1178,14 +1178,14 @@ public function testMultipleDistinctBoundsHardFailsDirectEmission(): void { public function pick(U $a, V $b): bool; } + interface BiColl { public function pick(U $a, V $b): bool; } PHP, - 'Mid.xphp' => " {}\n", + 'Mid.xphp' => " {}\n", 'ListColl.xphp' => <<<'PHP' extends Mid implements BiColl { + class ListColl extends Mid implements BiColl { public function pick(U $a, V $b): bool { return true; } } PHP, @@ -1223,14 +1223,14 @@ public function testEnclosingParamInSignatureHardFailsDirectEmission(): void extends Collection { public function firstOr(U $fallback): E; } + interface OrderedCollection extends Collection { public function firstOr(U $fallback): E; } PHP, 'AbstractColl.xphp' => self::ABSTRACT_COLL, 'ListColl.xphp' => <<<'PHP' extends AbstractColl implements OrderedCollection { + class ListColl extends AbstractColl implements OrderedCollection { public function firstOr(U $fallback): E { return $this->items[0] ?? $fallback; } } PHP, @@ -1248,7 +1248,7 @@ function first(OrderedCollection $c): Product { return $c->firstOr::`) instantiated at + // A covariant element type with two covariant slots (`Tuple`) instantiated at // all four Book/Product combos forms a DIAMOND, so `Lst>` is an instance of // `Collection` at several `Tuple` supertypes at once. Single inheritance carries the erased // `contains` for one diamond path only; the post-edge gap-fill supplies the incomparable siblings @@ -1272,7 +1272,7 @@ public function testMultiPathDiamondClosureSuppliesEverySiblingAcrossInterfacesA // The order-robustness gate. The minimal diamond reaches one concrete spec through one interface; // the real defect surfaced only when a spec is reached as an upcast implementer through SEVERAL // paths in a larger closure. Here two interfaces (`Collection::contains`, `Lookup::indexOf`) and two - // concretes (`Lst`, `Bag`) all converge on the same `Tuple<+A,+B>` element diamond, so each concrete + // concretes (`Lst`, `Bag`) all converge on the same `Tuple` element diamond, so each concrete // spec carries eight erased obligations discovered along multiple routes. Single inheritance threads // one path per interface; the post-edge gap-fill — running after the chain is final — must supply // every remaining sibling on both concretes regardless of discovery order, or a spec loads @@ -1311,7 +1311,7 @@ public function testReturnEnclosingParamOnParentlessBaseSurvivesPlainUpcast(): v public function testReturnEnclosingParamUnderDiamondHardFails(): void { - // The same return-E method (`firstOr(S): E`), but the element type is a covariant `Tuple<+A,+B>` + // The same return-E method (`firstOr(S): E`), but the element type is a covariant `Tuple` // instantiated at all four Book/Product combos — a DIAMOND. The primary `firstOr` is inherited, but // the incomparable sibling obligation cannot be carried by single inheritance AND cannot be // direct-emitted (return-E). The gap-fill must FAIL LOUDLY at compile time, never emit a spec that @@ -1330,19 +1330,19 @@ public function testReturnEnclosingParamUnderDiamondHardFails(): void { public function contains(S $element): bool; } + interface Collection { public function contains(S $element): bool; } PHP, 'OrderedCollection.xphp' => <<<'PHP' extends Collection { public function firstOr(S $fallback): E; } + interface OrderedCollection extends Collection { public function firstOr(S $fallback): E; } PHP, 'AbstractColl.xphp' => <<<'PHP' implements OrderedCollection { + abstract class AbstractColl implements OrderedCollection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1354,19 +1354,19 @@ public function firstOr(S $fallback): E { return $this->items[0] ?? $fall extends AbstractColl implements OrderedCollection {} + class ListColl extends AbstractColl implements OrderedCollection {} PHP, 'Tuple.xphp' => <<<'PHP' { public function first(): A; public function second(): B; } + interface Tuple { public function first(): A; public function second(): B; } PHP, 'Couple.xphp' => <<<'PHP' implements Tuple { + class Couple implements Tuple { public function __construct(private A $a, private B $b) {} public function first(): A { return $this->a; } public function second(): B { return $this->b; } @@ -1399,26 +1399,26 @@ public function testNestedGenericDiamondEmitsTheSiblingMemberDirectly(): void { public function contains(S $element): bool; } + interface Collection { public function contains(S $element): bool; } PHP, 'AbstractColl.xphp' => <<<'PHP' implements Collection { + abstract class AbstractColl implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } public function contains(S $element): bool { return \in_array($element, $this->items, true); } } PHP, - 'Lst.xphp' => " extends AbstractColl implements Collection {}\n", - 'Tuple.xphp' => " { public function first(): A; public function second(): B; }\n", + 'Lst.xphp' => " extends AbstractColl implements Collection {}\n", + 'Tuple.xphp' => " { public function first(): A; public function second(): B; }\n", 'Couple.xphp' => <<<'PHP' implements Tuple { + class Couple implements Tuple { public function __construct(private A $a, private B $b) {} public function first(): A { return $this->a; } public function second(): B { return $this->b; } @@ -1459,13 +1459,13 @@ public function testInheritedReturnEnclosingMemberIsNotReEmittedOntoTheUpcastSou extends Collection { public function firstOr(S $fallback): E; } + interface OrderedCollection extends Collection { public function firstOr(S $fallback): E; } PHP, 'AbstractColl.xphp' => <<<'PHP' implements OrderedCollection { + abstract class AbstractColl implements OrderedCollection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1473,7 +1473,7 @@ public function contains(E2 $value): bool { return \in_array($value, $th public function firstOr(S $fallback): E { return $this->items[0] ?? $fallback; } } PHP, - 'ListColl.xphp' => " extends AbstractColl implements OrderedCollection {}\n", + 'ListColl.xphp' => " extends AbstractColl implements OrderedCollection {}\n", 'Use.xphp' => <<<'PHP' $c): Product { return $c->firstOr::expectException(RuntimeException::class); - $this->expectExceptionMessageMatches('/`\+E` appears in method parameter position/'); + $this->expectExceptionMessageMatches('/`out E` appears in method parameter position/'); $this->compileResult([ 'Product.xphp' => self::PRODUCT, @@ -1508,14 +1508,14 @@ public function testEnclosingParamInParameterPositionIsRejectedByVarianceFirst() extends Collection { public function pairContains(U $value, E $other): bool; } + interface OrderedCollection extends Collection { public function pairContains(U $value, E $other): bool; } PHP, 'AbstractColl.xphp' => self::ABSTRACT_COLL, 'ListColl.xphp' => <<<'PHP' extends AbstractColl implements OrderedCollection { + class ListColl extends AbstractColl implements OrderedCollection { public function pairContains(U $value, E $other): bool { return \in_array($value, $this->items, true) && \in_array($other, $this->items, true); } @@ -1545,14 +1545,14 @@ public function testTwoSameBoundParamsDirectEmitUnderUpcast(): void extends Collection { public function eitherIn(U $a, V $b): bool; } + interface OrderedCollection extends Collection { public function eitherIn(U $a, V $b): bool; } PHP, 'AbstractColl.xphp' => self::ABSTRACT_COLL, 'ListColl.xphp' => <<<'PHP' extends AbstractColl implements OrderedCollection { + class ListColl extends AbstractColl implements OrderedCollection { public function eitherIn(U $a, V $b): bool { return \in_array($a, $this->items, true) || \in_array($b, $this->items, true); } @@ -1584,13 +1584,13 @@ public function testBodyOnAParentInterfaceBaseEmitsDirectlyForTheSubInterface(): extends Collection { public function indexOf(U $value): int; } + interface OrderedCollection extends Collection { public function indexOf(U $value): int; } PHP, 'AbstractColl.xphp' => <<<'PHP' implements Collection { + abstract class AbstractColl implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1598,7 +1598,7 @@ public function contains(U $value): bool { return \in_array($value, $this public function indexOf(U $value): int { return \count($this->items); } } PHP, - 'ListColl.xphp' => " extends AbstractColl implements OrderedCollection {}\n", + 'ListColl.xphp' => " extends AbstractColl implements OrderedCollection {}\n", 'Use.xphp' => <<<'PHP' self::PRODUCT, 'Book.xphp' => self::BOOK, 'Collection.xphp' => self::COLLECTION_IFACE, - 'Mid.xphp' => " {}\n", + 'Mid.xphp' => " {}\n", 'ListColl.xphp' => <<<'PHP' extends Mid implements Collection { + class ListColl extends Mid implements Collection { /** @var list */ protected array $items; public function __construct(E ...$items) { $this->items = $items; } @@ -1654,7 +1654,7 @@ function probe(Collection $c): bool { return $c->contains::(ne public function testReorderedImplementsClauseEmitsTheMemberDirectly(): void { // The declaring class implements the interface with its parameters REORDERED - // (`class Holder<+A, B> implements Pair`), so the implementing spec can't be derived by + // (`class Holder implements Pair`), so the implementing spec can't be derived by // inversion for scheduling. Direct emission doesn't need to invert — it emits onto the // upcast-source class with the supertype's bound value — so this now compiles (no scheduled spec). $result = $this->compileResult([ @@ -1665,7 +1665,7 @@ public function testReorderedImplementsClauseEmitsTheMemberDirectly(): void { + interface Pair { public function contains(U $value): bool; } PHP, @@ -1673,7 +1673,7 @@ public function contains(U $value): bool; implements Pair { + class Holder implements Pair { /** @var list */ protected array $items; public function __construct(A ...$items) { $this->items = $items; } @@ -1864,7 +1864,7 @@ public function testNullsafeForwardedSelfCallIsAlsoRewritten(): void { + class Box { public function contains(U $value): bool { return true; } public function probe(U $value): bool { return $this?->contains::($value); } } @@ -1936,7 +1936,7 @@ public function testForwardedSelfCallWithArityErrorDoesNotDoubleReport(): void { + class Box { public function contains(U $value): bool { return true; } public function probe(U $value): bool { return $this->contains::($value); } } @@ -1986,7 +1986,7 @@ private static function box(): string { + class Box { public function contains(U $value): bool { return true; } } PHP; diff --git a/test/Transpiler/Monomorphize/EnclosingParamBoundLimitationTest.php b/test/Transpiler/Monomorphize/EnclosingParamBoundLimitationTest.php index bdc7fd3..94b91d5 100644 --- a/test/Transpiler/Monomorphize/EnclosingParamBoundLimitationTest.php +++ b/test/Transpiler/Monomorphize/EnclosingParamBoundLimitationTest.php @@ -130,7 +130,7 @@ class Rock {} { + class Box { public function contains(U $value): bool { return true; } } PHP; @@ -147,7 +147,7 @@ class Banana extends Fruit {} { + class Box { public function register(U $value): void {} } PHP; diff --git a/test/Transpiler/Monomorphize/InnerVarianceIntegrationTest.php b/test/Transpiler/Monomorphize/InnerVarianceIntegrationTest.php index a6e586c..cc4d2a3 100644 --- a/test/Transpiler/Monomorphize/InnerVarianceIntegrationTest.php +++ b/test/Transpiler/Monomorphize/InnerVarianceIntegrationTest.php @@ -46,10 +46,10 @@ protected function tearDown(): void public function testCovariantOuterInInvariantInnerSlotFailsCompilation(): void { // The canonical case the parse-time validator CANNOT catch: at parse - // time we don't yet know Container's slot is invariant, so `+T` in a + // time we don't yet know Container's slot is invariant, so `out T` in a // covariant (return) position passes the surface check. The inner- // variance pass composes compose(Covariant, Invariant) = Invariant and - // rejects `+T`. If the `validateInnerVariance()` call is removed from + // rejects `out T`. If the `validateInnerVariance()` call is removed from // Compiler::compile, the violation slips through and compilation // succeeds -- so this test fails, killing the MethodCallRemoval mutant. $sourceDir = $this->workDir . '/src'; @@ -70,10 +70,10 @@ public function __construct(public X $item) {} file_put_contents($pFile, <<<'PHP' + // composed position is invariant-only and out T is illegal there. + class P { private mixed $store = null; @@ -89,7 +89,7 @@ public function f(): Container $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Variance violation in template P'); - $this->expectExceptionMessage('+T'); + $this->expectExceptionMessage('out T'); $this->expectExceptionMessage('invariant-only position'); $this->expectExceptionMessage('Container'); $compiler->compile($sources, $sourceDir, $this->targetDir, $this->cacheDir); diff --git a/test/Transpiler/Monomorphize/RegistryInnerVarianceTest.php b/test/Transpiler/Monomorphize/RegistryInnerVarianceTest.php index 18c2f74..013304e 100644 --- a/test/Transpiler/Monomorphize/RegistryInnerVarianceTest.php +++ b/test/Transpiler/Monomorphize/RegistryInnerVarianceTest.php @@ -34,7 +34,7 @@ final class RegistryInnerVarianceTest extends TestCase { public function testDirectAndNestedViolationsAreEachReportedExactlyOnce(): void { - // P (direct +T-in-param) is owned by the position pass; Q (a nested composition violation) is + // P (direct out T-in-param) is owned by the position pass; Q (a nested composition violation) is // owned by the composing inner pass. With disjoint responsibilities (no skip handoff), the two // passes report exactly one diagnostic each — P's `variance_position` and Q's `inner_variance`, // with no double-report of P by the inner pass. @@ -67,7 +67,7 @@ public function testDirectAndNestedViolationsAreEachReportedExactlyOnce(): void public function testDirectViolationsAreNotDoubleReportedByTheComposingPass(): void { - // Two direct +T-in-param violations: both owned by the position pass. The composing inner pass + // Two direct out T-in-param violations: both owned by the position pass. The composing inner pass // reports only NESTED occurrences, so it adds nothing here — exactly two diagnostics total, both // `variance_position`, with no double-report. $collector = new DiagnosticCollector(); @@ -156,9 +156,9 @@ public function testCollectModeGathersInnerVarianceDiagnosticInsteadOfThrowing() public function testCovariantOuterInInvariantInnerSlotIsRejected(): void { // class Container {} // X is Invariant - // class P<+T> { function f(): Container } + // class P { function f(): Container } // Outer pos = Covariant (return); inner slot = Invariant. - // effective = Invariant; +T not in {Invariant} -> reject. + // effective = Invariant; out T not in {Invariant} -> reject. $registry = $this->registryWith([ $this->makeDefinition('App\\Container', 'Container', [new TypeParam('X')], new Class_(new Identifier('Container'))), $this->makeDefinition( @@ -176,7 +176,7 @@ public function testCovariantOuterInInvariantInnerSlotIsRejected(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Variance violation in template P'); - $this->expectExceptionMessage('+T'); + $this->expectExceptionMessage('out T'); $this->expectExceptionMessage('invariant-only position'); $this->expectExceptionMessage('Container'); $registry->validateInnerVariance(); @@ -185,9 +185,9 @@ public function testCovariantOuterInInvariantInnerSlotIsRejected(): void public function testContravariantOuterInInvariantInnerSlotIsRejected(): void { // class Container {} - // class P<-T> { function f(Container $x): void } + // class P { function f(Container $x): void } // Outer pos = Contravariant (param); inner slot = Invariant. - // effective = Invariant; -T not in {Invariant} -> reject. + // effective = Invariant; in T not in {Invariant} -> reject. $registry = $this->registryWith([ $this->makeDefinition('App\\Container', 'Container', [new TypeParam('X')], new Class_(new Identifier('Container'))), $this->makeDefinition( @@ -207,17 +207,17 @@ public function testContravariantOuterInInvariantInnerSlotIsRejected(): void ]); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('-T'); + $this->expectExceptionMessage('in T'); $this->expectExceptionMessage('invariant-only position'); $registry->validateInnerVariance(); } public function testCovariantOuterInContravariantInnerSlotIsRejected(): void { - // class Sink<-X> {} - // class P<+T> { function f(): Sink } + // class Sink {} + // class P { function f(): Sink } // Outer pos = Covariant; inner slot = Contravariant. - // effective = flip(Covariant) = Contravariant; +T not in {Invariant, Contravariant} -> reject. + // effective = flip(Covariant) = Contravariant; out T not in {Invariant, Contravariant} -> reject. $registry = $this->registryWith([ $this->makeDefinition( 'App\\Sink', @@ -239,17 +239,17 @@ public function testCovariantOuterInContravariantInnerSlotIsRejected(): void ]); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('+T'); + $this->expectExceptionMessage('out T'); $this->expectExceptionMessage('contravariant-only position'); $registry->validateInnerVariance(); } public function testCovariantOuterInCovariantInnerSlotIsAccepted(): void { - // class Producer<+X> {} - // class P<+T> { function f(): Producer } + // class Producer {} + // class P { function f(): Producer } // Outer pos = Covariant; inner slot = Covariant. - // effective = Covariant; +T in {Invariant, Covariant} -> accept. + // effective = Covariant; out T in {Invariant, Covariant} -> accept. $registry = $this->registryWith([ $this->makeDefinition( 'App\\Producer', @@ -276,10 +276,10 @@ public function testCovariantOuterInCovariantInnerSlotIsAccepted(): void public function testContravariantPathThroughDoubleFlipIsAccepted(): void { - // class Sink<-X> {} - // class P<+T> { function f(Sink $x): void } + // class Sink {} + // class P { function f(Sink $x): void } // Outer pos = Contravariant (param); inner slot = Contravariant. - // effective = flip(Contravariant) = Covariant; +T in {Invariant, Covariant} -> accept. + // effective = flip(Contravariant) = Covariant; out T in {Invariant, Covariant} -> accept. $registry = $this->registryWith([ $this->makeDefinition( 'App\\Sink', @@ -309,7 +309,7 @@ public function testContravariantPathThroughDoubleFlipIsAccepted(): void public function testInvariantOuterIsAcceptedInAnyInnerSlot(): void { - // class Producer<+X> {} + // class Producer {} // class P { function f(): Producer } // T is Invariant // effective for T: compose(Covariant, Covariant) = Covariant; // Invariant in {Invariant, Covariant} -> accept. @@ -339,7 +339,7 @@ public function testInvariantOuterIsAcceptedInAnyInnerSlot(): void public function testNonGenericInnerTypeIsNoOp(): void { - // class P<+T> { function f(): SomeOpaqueClass } + // class P { function f(): SomeOpaqueClass } // No xphp:genericArgs attribute; no leaf is T; walker no-ops. $registry = $this->registryWith([ $this->makeDefinition( @@ -361,8 +361,8 @@ public function testNonGenericInnerTypeIsNoOp(): void public function testScalarInnerArgIsNoOp(): void { - // class Producer<+X> {} - // class P<+T> { function f(): Producer } // int is scalar, not T + // class Producer {} + // class P { function f(): Producer } // int is scalar, not T $registry = $this->registryWith([ $this->makeDefinition( 'App\\Producer', @@ -390,10 +390,10 @@ public function testScalarInnerArgIsNoOp(): void public function testTwoDeepNestingComposesAllTheWay(): void { // class Container {} // Invariant - // class Outer<+Y> {} // Covariant - // class P<+T> { function f(): Outer> } + // class Outer {} // Covariant + // class P { function f(): Outer> } // Effective at T's leaf: compose(Covariant, Covariant) = Covariant; then compose(Covariant, Invariant) = Invariant. - // +T not in {Invariant} -> reject. + // out T not in {Invariant} -> reject. $registry = $this->registryWith([ $this->makeDefinition('App\\Container', 'Container', [new TypeParam('X')], new Class_(new Identifier('Container'))), $this->makeDefinition( @@ -425,8 +425,8 @@ public function testTwoDeepNestingComposesAllTheWay(): void public function testTwoDeepNestingAllCovariantIsAccepted(): void { - // Replace Container with Container<+X> -- compose(Covariant, Covariant) = Covariant twice. - // +T in {Invariant, Covariant} -> accept. + // Replace Container with Container -- compose(Covariant, Covariant) = Covariant twice. + // out T in {Invariant, Covariant} -> accept. $registry = $this->registryWith([ $this->makeDefinition( 'App\\Container', @@ -461,7 +461,7 @@ public function testTwoDeepNestingAllCovariantIsAccepted(): void public function testUnknownInnerTemplateFallsBackToInvariant(): void { - // class P<+T> { function f(): VendorThing } // VendorThing not registered + // class P { function f(): VendorThing } // VendorThing not registered // Conservative-unknown: inner slot treated as Invariant -> reject. $registry = $this->registryWith([ $this->makeDefinition( @@ -505,10 +505,10 @@ public function testIsNoOpForVarianceFreeTemplates(): void public function testConstructorPromotedPropertyWithVariantInnerSlotIsRejected(): void { - // class Container<-X> {} - // class P<+T> { function __construct(public Container $c) } + // class Container {} + // class P { function __construct(public Container $c) } // Constructor param outer-pos is Invariant; inner slot Contravariant. - // compose(Invariant, Contravariant) = Invariant; +T not in {Invariant} -> reject. + // compose(Invariant, Contravariant) = Invariant; out T not in {Invariant} -> reject. // Pins the constructor-promoted-property -> Invariant outer-pos branch. $registry = $this->registryWith([ $this->makeDefinition( @@ -543,10 +543,10 @@ public function testConstructorPromotedPropertyWithVariantInnerSlotIsRejected(): public function testUnionTypeArmHitsInnerVarianceCheck(): void { - // class Producer<-X> {} - // class P<+T> { function f(): Producer|null } + // class Producer {} + // class P { function f(): Producer|null } // Outer pos = Covariant; arm Producer's slot = Contravariant. - // compose(Covariant, Contravariant) = Contravariant; +T not in {Invariant, Contravariant} -> reject. + // compose(Covariant, Contravariant) = Contravariant; out T not in {Invariant, Contravariant} -> reject. $producerOrNull = new UnionType([ $this->genericName('App\\Producer', [new TypeRef('T', isTypeParam: true)]), new Identifier('null'), @@ -573,10 +573,10 @@ public function testUnionTypeArmHitsInnerVarianceCheck(): void public function testFBoundWithContravariantInnerSlotIsRejected(): void { - // class Sink<-X> {} - // class P<+T : Sink> {} + // class Sink {} + // class P> {} // Bound is an Invariant outer-position (PHP class-compat); inner slot - // Contravariant; compose(Invariant, Contravariant) = Invariant; +T not in {Invariant} -> + // Contravariant; compose(Invariant, Contravariant) = Invariant; out T not in {Invariant} -> // reject. Pins the type-param bound walk. $registry = $this->registryWith([ $this->makeDefinition( @@ -600,15 +600,15 @@ public function testFBoundWithContravariantInnerSlotIsRejected(): void ]); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('+T'); + $this->expectExceptionMessage('out T'); $this->expectExceptionMessage('invariant-only position'); $registry->validateInnerVariance(); } public function testFBoundUnionAndIntersectionAreWalked(): void { - // class Sink<-X> {} - // class P<+T : Sink & Sink> {} // BoundIntersection of two leaves + // class Sink {} + // class P & Sink> {} // BoundIntersection of two leaves // Both arms hit the same violation. Pins BoundUnion/BoundIntersection // walk. $boundLeaf = new BoundLeaf(new TypeRef('App\\Sink', [new TypeRef('T', isTypeParam: true)])); @@ -640,10 +640,10 @@ public function testFBoundUnionAndIntersectionAreWalked(): void public function testDefaultExpressionWalkRejectsVariantInnerSlot(): void { - // class Container<-X> {} - // class P<+T, U = Container> {} + // class Container {} + // class P> {} // Default is an Invariant outer-position; inner Container slot is Contravariant; - // compose(Invariant, Contravariant) = Invariant; +T not in {Invariant} -> reject. + // compose(Invariant, Contravariant) = Invariant; out T not in {Invariant} -> reject. // Pins the type-param default walk. $registry = $this->registryWith([ $this->makeDefinition( @@ -667,7 +667,7 @@ public function testDefaultExpressionWalkRejectsVariantInnerSlot(): void ]); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('+T'); + $this->expectExceptionMessage('out T'); $this->expectExceptionMessage('invariant-only position'); $registry->validateInnerVariance(); } @@ -678,7 +678,7 @@ public function testLeadingBackslashInTemplateFqnIsStripped(): void // The ltrim must strip it; otherwise the registry lookup misses the // template and we'd fall to conservative-unknown (Invariant). // Here Container's X is Covariant -- if we strip correctly, the - // composition stays Covariant and accepts +T. If ltrim is removed, + // composition stays Covariant and accepts out T. If ltrim is removed, // lookup misses, conservative-Invariant kicks in, rejects. $genericNode = new Name(['App', 'Container']); $genericNode->setAttribute(XphpSourceParser::ATTR_GENERIC_ARGS, [new TypeRef('T', isTypeParam: true)]); @@ -705,7 +705,7 @@ public function testLeadingBackslashInTemplateFqnIsStripped(): void public function testNullableTypeWrapsInnerGenericForVarianceCheck(): void { - // class P<+T> { function f(): ?Container } where Container's X is Invariant. + // class P { function f(): ?Container } where Container's X is Invariant. // The NullableType must recurse; the inner Container still triggers // the invariant rejection. Pins the NullableType walker branch. $registry = $this->registryWith([ @@ -734,7 +734,7 @@ public function testTypeRefArgsRecurseThroughKnownInnerTemplate(): void // outer pos via bound = Invariant // Container's X = Covariant -> compose(Invariant, Covariant) = Invariant // Producer's X = Invariant -> compose(Invariant, Invariant) = Invariant - // +T not in {Invariant} -> reject. + // out T not in {Invariant} -> reject. // Pins the TypeRef recursion path AND the inner-def lookup via TypeRef name. $registry = $this->registryWith([ $this->makeDefinition( @@ -799,7 +799,7 @@ public function testErrorMessageUsesInnerTemplateShortNameNotFqn(): void public function testLeadingBackslashInTypeRefNameIsStripped(): void { - // Exercises the walkTypeRef ltrim. Setup: `class P<+T> { f(): Outer> }`. + // Exercises the walkTypeRef ltrim. Setup: `class P { f(): Outer> }`. // The OUTER Outer lookup uses ATTR_TEMPLATE_FQN (walkPhpType branch); // the INNER `\App\Inner` lookup goes through walkTypeRef which uses // ltrim on `$ref->name`. If ltrim is dropped, the Inner lookup misses, @@ -874,10 +874,10 @@ public function testTypeRefErrorMessageUsesInnerTemplateShortName(): void public function testInvariantDeclaredInInvariantPositionIsAccepted(): void { // class Container {} // X is Invariant - // class P { function f(): Container } // U is Invariant + // class P { function f(): Container } // U is Invariant // U is Invariant declared; outer pos Covariant; inner slot Invariant; // compose(Covariant, Invariant) = Invariant; Invariant in {Invariant} -> accept. - // The other type-param +T gives buildVarianceMap a non-empty map so + // The other type-param out T gives buildVarianceMap a non-empty map so // the walk actually runs. Pins the // `Variance::Invariant => [Variance::Invariant]` allowed-list. $registry = $this->registryWith([ @@ -904,8 +904,8 @@ public function testInvariantDeclaredInInvariantPositionIsAccepted(): void public function testInvariantDeclaredInCovariantPositionIsAccepted(): void { - // class Producer<+X> {} - // class P { function f(): Producer } + // class Producer {} + // class P { function f(): Producer } // U is Invariant declared; outer pos Covariant; inner slot Covariant; // compose(Covariant, Covariant) = Covariant; Invariant in {Invariant, Covariant} -> accept. // Pins the second item of `Variance::Covariant => [Invariant, Covariant]` allowed-list. @@ -938,8 +938,8 @@ public function testInvariantDeclaredInCovariantPositionIsAccepted(): void public function testInvariantDeclaredInContravariantPositionIsAccepted(): void { - // class Producer<+X> {} - // class P { function f(Producer $x): void } + // class Producer {} + // class P { function f(Producer $x): void } // U is Invariant declared; outer pos Contravariant (param); inner Covariant; // compose(Contravariant, Covariant) = Contravariant; Invariant in {Invariant, Contravariant} -> accept. // Pins the first item of `Variance::Contravariant => [Invariant, Contravariant]` allowed-list. @@ -976,7 +976,7 @@ public function testInvariantDeclaredInContravariantPositionIsAccepted(): void public function testStaticMethodReturnTypeIsWalked(): void { // class Container {} // Invariant - // class P<+T> { public static function f(): Container } + // class P { public static function f(): Container } // Static methods walk the same as instance methods. $method = new ClassMethod( new Identifier('f'), @@ -1003,7 +1003,7 @@ public function testStaticMethodReturnTypeIsWalked(): void public function testDirectPropertyCovariantOuterIsOwnedByThePositionPass(): void { - // class P<+T> { public T $item; } — a DIRECT covariant type-param in an invariant + // class P { public T $item; } — a DIRECT covariant type-param in an invariant // (visible-property) position. This is a direct occurrence, owned by the position pass; the // composing inner pass reports only type-constructor-NESTED occurrences, so it stays SILENT here // (no double-report). The position pass is the one that rejects it. diff --git a/test/Transpiler/Monomorphize/RegistryVarianceEdgeDiagnosticTest.php b/test/Transpiler/Monomorphize/RegistryVarianceEdgeDiagnosticTest.php index fbce2d6..fd26f0c 100644 --- a/test/Transpiler/Monomorphize/RegistryVarianceEdgeDiagnosticTest.php +++ b/test/Transpiler/Monomorphize/RegistryVarianceEdgeDiagnosticTest.php @@ -48,7 +48,7 @@ public function testWarningMessageHasExactText(): void $expected = <<<'TXT' Variance edge cannot be proven while instantiating App\Producer. - type parameter +T is covariant, but App\Book is not in the source set the hierarchy was built from (and is not a recognized PHP built-in), + type parameter out T is covariant, but App\Book is not in the source set the hierarchy was built from (and is not a recognized PHP built-in), so the compiler cannot prove its subtype edges — this specialization is not linked to related ones and the covariant relationship silently does not apply at runtime. Add App\Book to the source set the hierarchy is built from to enable the edge. @@ -57,13 +57,13 @@ public function testWarningMessageHasExactText(): void self::assertSame($expected, $collector->all()[0]->message); } - public function testContravariantOverUnprovableLeafIsWarnedWithMinusMarker(): void + public function testContravariantOverUnprovableLeafIsWarnedWithInMarker(): void { $collector = new DiagnosticCollector(); $this->registry($collector)->recordInstantiation('App\\Consumer', [new TypeRef('App\\Book')]); self::assertCount(1, $collector->all()); - self::assertStringContainsString('type parameter -T is contravariant', $collector->all()[0]->message); + self::assertStringContainsString('type parameter in T is contravariant', $collector->all()[0]->message); } public function testProvableDeclaredLeafIsSilent(): void @@ -116,7 +116,7 @@ public function testGenericArgIsSilentLeafOnly(): void public function testTwoVariantPositionsEachUnprovableWarnTwice(): void { - // `Pair<+A, +B>` over two unprovable leaves → one warning per covariant position. + // `Pair` over two unprovable leaves → one warning per covariant position. $collector = new DiagnosticCollector(); $this->registry($collector)->recordInstantiation( 'App\\Pair', @@ -143,7 +143,7 @@ public function testLeadingBackslashTemplateNameResolvesAndIsNormalisedInMessage public function testEarlierInvariantPositionDoesNotShortCircuitLaterVariant(): void { - // `Mixed`: the invariant A is skipped, but the walk must continue to the + // `Mixed`: the invariant A is skipped, but the walk must continue to the // covariant B and still warn (pins `continue`, not `break`, on the invariant skip). $collector = new DiagnosticCollector(); $this->registry($collector)->recordInstantiation( @@ -157,7 +157,7 @@ public function testEarlierInvariantPositionDoesNotShortCircuitLaterVariant(): v public function testEarlierScalarPositionDoesNotShortCircuitLaterVariant(): void { - // `Pair<+A, +B>` with a scalar A: A is skipped, B still warns (pins `continue` on + // `Pair` with a scalar A: A is skipped, B still warns (pins `continue` on // the scalar/type-param/generic skip). $collector = new DiagnosticCollector(); $this->registry($collector)->recordInstantiation( @@ -171,7 +171,7 @@ public function testEarlierScalarPositionDoesNotShortCircuitLaterVariant(): void public function testEarlierDeclaredPositionDoesNotShortCircuitLaterVariant(): void { - // `Pair<+A, +B>` with a declared A: A is skipped (provable), B still warns (pins + // `Pair` with a declared A: A is skipped (provable), B still warns (pins // `continue` on the isDeclared skip). $collector = new DiagnosticCollector(); $this->registry($collector)->recordInstantiation( diff --git a/test/Transpiler/Monomorphize/SpecializationTowerBoundaryTest.php b/test/Transpiler/Monomorphize/SpecializationTowerBoundaryTest.php index 9683e1e..69b9001 100644 --- a/test/Transpiler/Monomorphize/SpecializationTowerBoundaryTest.php +++ b/test/Transpiler/Monomorphize/SpecializationTowerBoundaryTest.php @@ -18,8 +18,8 @@ * failure into a hang, an OOM, or a silently-wrong build would be caught. A `dyn`-style erased seam that * would make the natural source compile is deferred (see ADR-0020). * - * The fixture is a minimal two-template cycle (`Lst<+E>::toMap(): Mp>` + - * `Mp::values(): Lst`) — small enough to reach the depth cap quickly under any memory config. + * The fixture is a minimal two-template cycle (`Lst::toMap(): Mp>` + + * `Mp::values(): Lst`) — small enough to reach the depth cap quickly under any memory config. */ final class SpecializationTowerBoundaryTest extends TestCase { diff --git a/test/Transpiler/Monomorphize/TypeHierarchyInheritedArgsTest.php b/test/Transpiler/Monomorphize/TypeHierarchyInheritedArgsTest.php index 00354b4..86fbefc 100644 --- a/test/Transpiler/Monomorphize/TypeHierarchyInheritedArgsTest.php +++ b/test/Transpiler/Monomorphize/TypeHierarchyInheritedArgsTest.php @@ -171,7 +171,7 @@ public function testDiamondWithConflictingArgsIsAmbiguousAndReturnsNull(): void public function testMultiArgClauseUsingOnlyOneParamThreadsTheRightArg(): void { // ImmutableMap implements Collection — only the index-1 param flows up (the - // `Map::containsValue` shape). The dropped K must not leak into the grounding. + // `Map::containsValue` shape). The dropped K must not leak into the grounding. $h = self::hierarchy( ['App\\ImmutableMap' => [self::ref('App\\Collection', [self::tp('V')])], 'App\\Collection' => []], ['App\\ImmutableMap' => ['K', 'V'], 'App\\Collection' => ['E']], diff --git a/test/Transpiler/Monomorphize/VarianceEdgeIntegrationTest.php b/test/Transpiler/Monomorphize/VarianceEdgeIntegrationTest.php index e887b14..2bc8a12 100644 --- a/test/Transpiler/Monomorphize/VarianceEdgeIntegrationTest.php +++ b/test/Transpiler/Monomorphize/VarianceEdgeIntegrationTest.php @@ -39,7 +39,7 @@ protected function tearDown(): void #[RunInSeparateProcess] public function testCovariantImmutableCollectionTakesTypedConstructorInput(): void { - // A covariant immutable collection `ImmutableList<+T>` with a `T`-typed + // A covariant immutable collection `ImmutableList` with a `T`-typed // constructor. The constructor param keeps its REAL element type on each // specialization (`Fruit ...` / `Banana ...`) — PHP exempts `__construct` // from LSP, so `ImmutableList` extends `ImmutableList` with @@ -93,7 +93,7 @@ public function testCovariantImmutableCollectionTakesTypedConstructorInput(): vo #[RunInSeparateProcess] public function testCovariantPrivatePropertyStoresRealTypeAndIsRuntimeChecked(): void { - // A covariant container `Box<+T>` that stores its element in a PRIVATE + // A covariant container `Box` that stores its element in a PRIVATE // promoted property of type `T`. Each specialization keeps the REAL slot // type (`private Banana $item` / `private Fruit $item`), the variance edge // `Box extends Box` autoloads with NO fatal (PHP doesn't @@ -144,7 +144,7 @@ public function testCovariantPrivatePropertyStoresRealTypeAndIsRuntimeChecked(): #[RunInSeparateProcess] public function testCrossTemplateGenericArgUpcastEmitsEdgeAndRunsAtRuntime(): void { - // A covariant `Couple<+A, +B> implements Tuple` holding a covariant container + // A covariant `Couple implements Tuple` holding a covariant container // `ImmutableList` as its first type-argument is upcast to `Tuple, // Tag>`. That requires the covariant edge `Tuple, Tag> ⊑ // Tuple, Tag>`, whose per-argument check must recognize `ImmutableList @@ -190,8 +190,8 @@ public function testCrossTemplateGenericArgUpcastEmitsEdgeAndRunsAtRuntime(): vo #[RunInSeparateProcess] public function testComparatorParamOnCovariantClassCompilesAndRunsUnderUpcast(): void { - // A covariant `Box<+E>` with a `pick(Comparator $c): ?E` consuming method — the sound shape - // where `E` sits in a contravariant slot (Comparator<-T>) inside a contravariant parameter + // A covariant `Box` with a `pick(Comparator $c): ?E` consuming method — the sound shape + // where `E` sits in a contravariant slot (Comparator) inside a contravariant parameter // (contra ∘ contra = covariant). Previously rejected `xphp.variance_position` even though sound; // the composing variance pass now accepts it. A `Box` is upcast to `Box` and // `pick` is called with a `ById` (a Comparator, hence by contravariance a @@ -220,7 +220,7 @@ public function testBoundedCovariantConstructorKeepsConcreteType(): void public function testMixedVarianceConstructorKeepsConcreteTypes(): void { - // `Pair<+A, B>`: the covariant `A` constructor param keeps its concrete type, the + // `Pair`: the covariant `A` constructor param keeps its concrete type, the // invariant `B` param keeps its concrete substituted type, and a plain // scalar param (`int $tag`) is left untouched (it isn't a type-param). $generated = $this->compileFixtureAndReadGenerated('compile/generic_mixed_variance_constructor/source'); @@ -230,7 +230,7 @@ public function testMixedVarianceConstructorKeepsConcreteTypes(): void public function testContravariantConstructorParamKeepsConcreteType(): void { - // Symmetry with the covariant case: a `-T` constructor param keeps its real type + // Symmetry with the covariant case: a `in T` constructor param keeps its real type // too, and the contravariant edge (Consumer extends Consumer) // stays valid because constructors are LSP-exempt. $generated = $this->compileFixtureAndReadGenerated('compile/generic_contravariant_constructor/source'); @@ -280,7 +280,7 @@ public function testInvariantClassConstructorIsNotErasedAndKeepsFinal(): void public function testCovariantSubtypeEdgeIsEmittedAsExtendsForClassSpecializations(): void { - // Fixture: `variance_covariant_happy/`. Producer<+T>, Banana <: Fruit. + // Fixture: `variance_covariant_happy/`. Producer, Banana <: Fruit. // Two specializations; Producer_Banana extends Producer_Fruit because // +T is covariant. $sourceDir = realpath(__DIR__ . '/../../fixture/compile/variance_covariant_happy/source') @@ -318,7 +318,7 @@ public function testCovariantSubtypeEdgeIsEmittedAsExtendsForClassSpecialization public function testContravariantSubtypeEdgeFlipsDirection(): void { - // Fixture: `variance_contravariant_happy/`. Consumer<-T>, Dog <: Animal. + // Fixture: `variance_contravariant_happy/`. Consumer, Dog <: Animal. // With contravariance, the edge flips: Consumer_Animal extends // Consumer_Dog (not the other way around). $sourceDir = realpath(__DIR__ . '/../../fixture/compile/variance_contravariant_happy/source') @@ -439,7 +439,7 @@ public function testNoEdgeBetweenUnrelatedSpecializations(): void file_put_contents($sourceDir . '/Containers/Producer.xphp', <<<'PHP' + class Producer { public function get(): T { throw new \LogicException; } } @@ -493,7 +493,7 @@ class Apple extends Fruit {} public function testScalarArgsSkipVarianceEdgeEmission(): void { - // `Producer<+T>` instantiated with int and string -- no PHP-level + // `Producer` instantiated with int and string -- no PHP-level // subtype relationship between scalars, so no edge is emitted in // either direction. $sourceDir = $this->workDir . '/src-scalar'; @@ -501,7 +501,7 @@ public function testScalarArgsSkipVarianceEdgeEmission(): void file_put_contents($sourceDir . '/Producer.xphp', <<<'PHP' + class Producer { public function get(): T { throw new \LogicException; } } @@ -540,7 +540,7 @@ public function get(): T { throw new \LogicException; } public function testTransitiveEdgesCollapseToDirectParent(): void { - // Banana <: Apple <: Fruit. Three specializations of Producer<+T>. + // Banana <: Apple <: Fruit. Three specializations of Producer. // The variance edges form a chain: Producer_Banana extends Producer_Apple, // Producer_Apple extends Producer_Fruit. Producer_Banana does NOT need a // direct edge to Producer_Fruit (PHP resolves it transitively). @@ -549,7 +549,7 @@ public function testTransitiveEdgesCollapseToDirectParent(): void file_put_contents($sourceDir . '/P.xphp', <<<'PHP' { public function get(): T { throw new \LogicException; } } + class P { public function get(): T { throw new \LogicException; } } PHP); file_put_contents($sourceDir . '/Fruit.xphp', <<<'PHP' ` composes covariance + intersection bound // + default. Verifies the integration: parse succeeds, bound is // checked, default pads, variance edges emit (single specialization @@ -638,7 +638,7 @@ public function testInterfaceSpecializationsGetMultiExtendsButFilterTransitives( file_put_contents($sourceDir . '/IProducer.xphp', <<<'PHP' { public function get(): T; } + interface IProducer { public function get(): T; } PHP); file_put_contents($sourceDir . '/Fruit.xphp', <<<'PHP' [ - "\n{\n public function set(T \$x): void {}\n}\n", - ['+T', 'method parameter'], + "\n{\n public function set(T \$x): void {}\n}\n", + ['out T', 'method parameter'], ]; yield 'contravariant in method return' => [ - "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", - ['-T', 'method return'], + "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", + ['in T', 'method return'], ]; yield 'covariant in mutable property' => [ - "\n{\n public T \$item;\n}\n", + "\n{\n public T \$item;\n}\n", ['mutable property'], ]; yield 'covariant in readonly property' => [ - "\n{\n public readonly T \$item;\n public function get(): T { return \$this->item; }\n}\n", + "\n{\n public readonly T \$item;\n public function get(): T { return \$this->item; }\n}\n", ['readonly property'], ]; - // NOTE: a NESTED type-param in a bound (`+T : Box`) is owned by the composing + // NOTE: a NESTED type-param in a bound (`out T : Box`) is owned by the composing // inner-variance pass, not this direct-position pass — see // testNestedTypeParamIsRejectedByTheComposingPass. // A covariant param as the BARE leaf of a sibling class param's bound is a bound position // too, flagged consistently with the inner-arg `Box` case above. (Distinct from the // supported method-level `contains` shape, where U is a *method* type parameter.) yield 'covariant in sibling bare bound' => [ - "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", + "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", ['bound'], ]; - // NOTE: `+T` in a *non-promoted* constructor parameter of a variant class is + // NOTE: `out T` in a *non-promoted* constructor parameter of a variant class is // ALLOWED — a constructor parameter is variance-exempt (constructors aren't // called through upcast references, and PHP exempts `__construct` from LSP), so // the real type is emitted there. See VarianceEdgeIntegrationTest's covariant // immutable-collection test. A *promoted* constructor param is a PROPERTY, which stays // strictly invariant (a `T`-typed property would PHP-fatal across the chain): yield 'covariant in promoted constructor property' => [ - "\n{\n public function __construct(public T \$item) {}\n}\n", + "\n{\n public function __construct(public T \$item) {}\n}\n", ['constructor parameter'], ]; // A *protected* property/promoted property is also a visible property (PHP @@ -65,11 +65,11 @@ public static function rejectedSources(): iterable // only PRIVATE is exempt. These pin the PRIVATE-bit detection against a // mutant that swaps the visibility bit for PROTECTED. yield 'covariant in protected promoted constructor property' => [ - "\n{\n public function __construct(protected T \$item) {}\n}\n", + "\n{\n public function __construct(protected T \$item) {}\n}\n", ['constructor parameter'], ]; yield 'covariant in protected mutable property' => [ - "\n{\n protected T \$item;\n}\n", + "\n{\n protected T \$item;\n}\n", ['mutable property'], ]; // Asymmetric visibility (PHP 8.4): a `public private(set)` property is @@ -78,45 +78,45 @@ public static function rejectedSources(): iterable // stays strictly invariant. Only a truly *private* slot is exempt. These // pin the PRIVATE-bit detection against a mutant that swaps it for PRIVATE_SET. yield 'covariant in public private(set) promoted constructor property' => [ - "\n{\n public function __construct(public private(set) T \$item) {}\n}\n", + "\n{\n public function __construct(public private(set) T \$item) {}\n}\n", ['constructor parameter'], ]; yield 'covariant in public private(set) declared property' => [ - "\n{\n public private(set) T \$item;\n}\n", + "\n{\n public private(set) T \$item;\n}\n", ['mutable property'], ]; yield 'covariant in nested closure parameter' => [ - "\n{\n public function emit(): array\n {\n \$f = function (T \$x) {};\n return [];\n }\n}\n", + "\n{\n public function emit(): array\n {\n \$f = function (T \$x) {};\n return [];\n }\n}\n", ['nested closure/arrow parameter'], ]; yield 'contravariant in nested arrow return' => [ - "\n{\n public function pipe(): array\n {\n \$f = fn (): T => null;\n return [];\n }\n}\n", + "\n{\n public function pipe(): array\n {\n \$f = fn (): T => null;\n return [];\n }\n}\n", ['nested closure/arrow return'], ]; // NOTE: a NESTED type-param in a method parameter/return (`Box`) is owned by the // composing inner-variance pass — see testNestedTypeParamIsRejectedByTheComposingPass. yield 'interface method signature' => [ - "\n{\n public function feed(T \$x): void;\n}\n", - ['+T'], + "\n{\n public function feed(T \$x): void;\n}\n", + ['out T'], ]; // A by-reference parameter is read AND written back, so it is an // invariant position — neither +T nor -T is allowed there. yield 'contravariant in by-reference parameter' => [ - "\n{\n public function swap(T &\$x): void {}\n}\n", - ['-T', 'by-reference parameter'], + "\n{\n public function swap(T &\$x): void {}\n}\n", + ['in T', 'by-reference parameter'], ]; yield 'covariant in by-reference parameter' => [ - "\n{\n public function swap(T &\$x): void {}\n}\n", - ['+T', 'by-reference parameter'], + "\n{\n public function swap(T &\$x): void {}\n}\n", + ['out T', 'by-reference parameter'], ]; yield 'contravariant in nested closure by-reference parameter' => [ - "\n{\n public function pipe(): array\n {\n \$f = function (T &\$x) {};\n return [];\n }\n}\n", + "\n{\n public function pipe(): array\n {\n \$f = function (T &\$x) {};\n return [];\n }\n}\n", ['by-reference parameter'], ]; // A variant class can't be `final`: its specializations are linked by // real `extends` edges, which a `final` class can't anchor. yield 'final variant class' => [ - "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", + "\n{\n public function get(): T { throw new \\LogicException; }\n}\n", ['cannot be declared `final`'], ]; } @@ -132,28 +132,28 @@ public static function rejectedSources(): iterable public static function allowedSources(): iterable { yield 'covariant in private promoted constructor property' => [ - "\n{\n public function __construct(private T \$item) {}\n public function get(): T { return \$this->item; }\n}\n", + "\n{\n public function __construct(private T \$item) {}\n public function get(): T { return \$this->item; }\n}\n", ]; yield 'covariant in private declared property' => [ - "\n{\n private T \$item;\n public function get(): T { return \$this->item; }\n}\n", + "\n{\n private T \$item;\n public function get(): T { return \$this->item; }\n}\n", ]; yield 'covariant in private readonly declared property' => [ - "\n{\n private readonly T \$item;\n public function get(): T { return \$this->item; }\n}\n", + "\n{\n private readonly T \$item;\n public function get(): T { return \$this->item; }\n}\n", ]; yield 'covariant in private readonly promoted property' => [ - "\n{\n public function __construct(private readonly T \$item) {}\n public function get(): T { return \$this->item; }\n}\n", + "\n{\n public function __construct(private readonly T \$item) {}\n public function get(): T { return \$this->item; }\n}\n", ]; yield 'contravariant in private promoted constructor property' => [ - "\n{\n public function __construct(private T \$item) {}\n public function accept(T \$x): void {}\n}\n", + "\n{\n public function __construct(private T \$item) {}\n public function accept(T \$x): void {}\n}\n", ]; // Inner-generic private members are exempt too — the inner-variance walk // skips them, so `private Container` doesn't trip composition even though // a *visible* `Container` property would (Container's slot is invariant). yield 'covariant in private inner-generic declared property' => [ - "\n{\n private Box \$item;\n public function get(): T { throw new \\LogicException; }\n}\n", + "\n{\n private Box \$item;\n public function get(): T { throw new \\LogicException; }\n}\n", ]; yield 'covariant in private inner-generic promoted property' => [ - "\n{\n public function __construct(private Box \$item) {}\n public function get(): T { throw new \\LogicException; }\n}\n", + "\n{\n public function __construct(private Box \$item) {}\n public function get(): T { throw new \\LogicException; }\n}\n", ]; } @@ -179,7 +179,7 @@ public function testVariancePositionIsRejectedInCompileMode(string $source, arra * A type-param NESTED inside a type constructor (`Box` in a parameter, return, or bound) is * judged by the COMPOSING inner-variance pass, not the direct-position pass: its effective variance * is the composition of the outer position with the inner slot's variance (here Box's invariant - * slot ⇒ invariant ⇒ `+T`/`-T` rejected). The direct-position pass deliberately does NOT descend + * slot ⇒ invariant ⇒ `out T`/`in T` rejected). The direct-position pass deliberately does NOT descend * into type-constructor args, so these are reported by `validateInnerVariance` with the composing * "via slot N of …" message — never double-reported by both passes. * @@ -208,37 +208,37 @@ public function testNestedTypeParamIsRejectedByTheComposingPass(string $source, public static function nestedComposingRejections(): iterable { yield 'covariant nested in method parameter' => [ - "\n{\n public function set(Box \$x): void {}\n}\n", - ['+T', 'invariant-only position', 'via slot 0 of'], + "\n{\n public function set(Box \$x): void {}\n}\n", + ['out T', 'invariant-only position', 'via slot 0 of'], ]; yield 'contravariant nested in method return' => [ - "\n{\n public function fetch(): Box { throw new \\LogicException; }\n}\n", - ['-T', 'invariant-only position', 'via slot 0 of'], + "\n{\n public function fetch(): Box { throw new \\LogicException; }\n}\n", + ['in T', 'invariant-only position', 'via slot 0 of'], ]; yield 'covariant nested in bound' => [ - ">\n{\n public function get(): T { throw new \\LogicException; }\n}\n", - ['+T', 'invariant-only position', 'via slot 0 of'], + ">\n{\n public function get(): T { throw new \\LogicException; }\n}\n", + ['out T', 'invariant-only position', 'via slot 0 of'], ]; // A NESTED type-param in a VISIBLE (non-promoted) declared property — `public Box $item` on - // `+T`. The property's outer position is invariant; Box's invariant slot composes to invariant, - // so `+T` is rejected by the composing pass via the declared-property walk (a private property + // `out T`. The property's outer position is invariant; Box's invariant slot composes to invariant, + // so `out T` is rejected by the composing pass via the declared-property walk (a private property // would be exempt — only visible ones are walked). yield 'covariant nested in visible declared property' => [ - "\n{\n public Box \$item;\n}\n", - ['+T', 'invariant-only position', 'via slot 0 of'], + "\n{\n public Box \$item;\n}\n", + ['out T', 'invariant-only position', 'via slot 0 of'], ]; - // Composition through a NON-invariant inner slot. `Producer<+X>` as a method PARAMETER: - // compose(contravariant param, covariant slot) = contravariant → a covariant `+E` is rejected. + // Composition through a NON-invariant inner slot. `Producer` as a method PARAMETER: + // compose(contravariant param, covariant slot) = contravariant → a covariant `out E` is rejected. yield 'covariant Producer param composes to contravariant' => [ - " { public function get(): X; }\nclass Box<+E>\n{\n public function take(Producer \$p): void {}\n}\n", - ['+E', 'contravariant-only position', 'via slot 0 of'], + " { public function get(): X; }\nclass Box\n{\n public function take(Producer \$p): void {}\n}\n", + ['out E', 'contravariant-only position', 'via slot 0 of'], ]; - // The mirror that confirms the composing pass owns the unsound direction too: `Sink<-E>` with a - // `Comparator<-T>` parameter — compose(contravariant param, contravariant slot) = covariant → a - // contravariant `-E` is rejected. (The sound covariant case is the accept test below.) + // The mirror that confirms the composing pass owns the unsound direction too: `Sink` with a + // `Comparator` parameter — compose(contravariant param, contravariant slot) = covariant → a + // contravariant `in E` is rejected. (The sound covariant case is the accept test below.) yield 'contravariant Sink with Comparator composes to covariant' => [ - " { public function compare(T \$a, T \$b): int; }\nclass Sink<-E>\n{\n public function pick(Comparator \$c): void {}\n}\n", - ['-E', 'covariant-only position', 'via slot 0 of'], + " { public function compare(T \$a, T \$b): int; }\nclass Sink\n{\n public function pick(Comparator \$c): void {}\n}\n", + ['in E', 'covariant-only position', 'via slot 0 of'], ]; } @@ -263,15 +263,15 @@ public function testSoundNestedCompositionIsAccepted(string $source): void */ public static function soundNestedCompositions(): iterable { - // The headline sound case: a `Comparator<-T>` parameter on a covariant `+E` — compose(contra - // param, contra slot) = covariant, which `+E` may occupy. Sound; must be accepted. + // The headline sound case: a `Comparator` parameter on a covariant `out E` — compose(contra + // param, contra slot) = covariant, which `out E` may occupy. Sound; must be accepted. yield 'Comparator param on covariant class' => [ - " { public function compare(T \$a, T \$b): int; }\nclass Box<+E>\n{\n public function pick(Comparator \$c): void {}\n}\n", + " { public function compare(T \$a, T \$b): int; }\nclass Box\n{\n public function pick(Comparator \$c): void {}\n}\n", ]; - // A `Producer<+X>` RETURN on a covariant `+E` — compose(covariant return, covariant slot) = + // A `Producer` RETURN on a covariant `out E` — compose(covariant return, covariant slot) = // covariant. Sound; must be accepted. yield 'Producer return on covariant class' => [ - " { public function get(): X; }\nclass Box<+E>\n{\n public function make(): Producer { throw new \\LogicException; }\n}\n", + " { public function get(): X; }\nclass Box\n{\n public function make(): Producer { throw new \\LogicException; }\n}\n", ]; } @@ -299,7 +299,7 @@ public function testPrivatePromotedDoesNotShortCircuitLaterConstructorParam(): v // slot) must still be reached and rejected. The position phase passes both // params (a bare/inner-generic ctor param is position-allowed in a variant // class), so inner-variance is the phase that must catch the trailing one. - $source = "\n{\n public function __construct(private T \$first, Box \$second) {}\n}\n"; + $source = "\n{\n public function __construct(private T \$first, Box \$second) {}\n}\n"; $registry = $this->registryFor($source); $registry->validateVariancePositions(); // must NOT throw — both params position-allowed @@ -316,7 +316,7 @@ public function testVisiblePromotedConstructorPropertyIsReportedExactlyOnce(): v // also report it — it cedes the direct leaf of a PROMOTED constructor param (only a NON-promoted // constructor param, which the position pass exempts, stays owned by the composing pass). In // check-mode (both passes run) this must yield EXACTLY ONE diagnostic, not two. - $source = "\n{\n public function __construct(public T \$item) {}\n}\n"; + $source = "\n{\n public function __construct(public T \$item) {}\n}\n"; $collector = new DiagnosticCollector(); $registry = $this->registryFor($source, $collector); @@ -333,7 +333,7 @@ public function testNonPromotedNonBareConstructorParamIsOwnedByTheComposingPassO // position pass, so the composing pass keeps ownership of its direct leaf — exactly one // `inner_variance` diagnostic, no double-report. (The bare-`T` immutable shape stays exempt by // both; a promoted `public T $item` is owned by the position pass — see the test above.) - $source = "\n{\n public function __construct(?T \$x) {}\n}\n"; + $source = "\n{\n public function __construct(?T \$x) {}\n}\n"; $collector = new DiagnosticCollector(); $registry = $this->registryFor($source, $collector); @@ -347,7 +347,7 @@ public function testNonPromotedNonBareConstructorParamIsOwnedByTheComposingPassO public function testViolationIsCollectedWithMemberLineInCheckMode(): void { // Line 5 holds `public function set(T $x)`. - $source = "\n{\n public function set(T \$x): void {}\n}\n"; + $source = "\n{\n public function set(T \$x): void {}\n}\n"; $collector = new DiagnosticCollector(); $registry = $this->registryFor($source, $collector); @@ -366,8 +366,8 @@ public function testAllViolationsAcrossDefinitionsCollectedInOneRun(): void { // Two distinct templates, each with a variance violation — both reported. $source = "\n{\n public function set(T \$x): void {}\n}\n" - . "class Consumer<-T>\n{\n public function get(): T { throw new \\LogicException; }\n}\n"; + . "class Producer\n{\n public function set(T \$x): void {}\n}\n" + . "class Consumer\n{\n public function get(): T { throw new \\LogicException; }\n}\n"; $collector = new DiagnosticCollector(); $registry = $this->registryFor($source, $collector); @@ -395,7 +395,7 @@ public function testByReferenceParamDoesNotShortCircuitLaterParams(): void // A by-ref param violation must not stop the walk: a *later* violating // param in the same signature is still reported (pins `continue`, not // `break`, after the by-ref check). - $source = "\n{\n public function f(T &\$a, T \$b): void {}\n}\n"; + $source = "\n{\n public function f(T &\$a, T \$b): void {}\n}\n"; $collector = new DiagnosticCollector(); $registry = $this->registryFor($source, $collector); diff --git a/test/Transpiler/Monomorphize/VarianceSubtypingTest.php b/test/Transpiler/Monomorphize/VarianceSubtypingTest.php index af65f34..fa79fac 100644 --- a/test/Transpiler/Monomorphize/VarianceSubtypingTest.php +++ b/test/Transpiler/Monomorphize/VarianceSubtypingTest.php @@ -27,7 +27,7 @@ final class VarianceSubtypingTest extends TestCase private const BANANA = 'App\\Banana'; private const BOX = 'App\\Box'; - /** Banana <: Fruit; Box<+T> defined so the nested-generic recursion has a template to read. */ + /** Banana <: Fruit; Box defined so the nested-generic recursion has a template to read. */ private function subtyping(): VarianceSubtyping { return new VarianceSubtyping($this->hierarchy()); @@ -195,7 +195,7 @@ public function testDifferentInnerTemplatesAreNotSubtype(): void // ---- Cross-template generic type-argument subtyping ---- // - // `ImmutableList<+E> implements Collection<+E>`, `Book <: Product`. The hierarchy carries the + // `ImmutableList implements Collection`, `Book <: Product`. The hierarchy carries the // PARAMETERISED supertype edge so `resolveInheritedArgs` can thread `ImmutableList` up to // `Collection`. `Mid implements Collection` is the MALFORMED case — a 2-arg // parameterised super against a 1-param target — exercising the load-bearing count() arity guard. @@ -235,7 +235,7 @@ private function crossRegistry(): Registry { $hierarchy = $this->crossHierarchy(); $registry = new Registry(Registry::DEFAULT_HASH_HEX_LENGTH, $hierarchy); - // Only the PARENT template's definition is read (for its slot variance); Collection<+E>. + // Only the PARENT template's definition is read (for its slot variance); Collection. $registry->recordDefinition( self::COLLECTION, 'Collection', diff --git a/test/Transpiler/Monomorphize/XphpSourceParserTest.php b/test/Transpiler/Monomorphize/XphpSourceParserTest.php index ec1a38e..3b06c90 100644 --- a/test/Transpiler/Monomorphize/XphpSourceParserTest.php +++ b/test/Transpiler/Monomorphize/XphpSourceParserTest.php @@ -2141,7 +2141,7 @@ public function testCovariantTypeParamIsParsedAndStored(): void $source = <<<'PHP' +class Producer { public function get(): T { throw new \LogicException; } } @@ -2159,7 +2159,7 @@ public function testContravariantTypeParamIsParsedAndStored(): void $source = <<<'PHP' +class Consumer { public function set(T $x): void {} } @@ -2190,7 +2190,7 @@ public function testMixedVarianceTypeParamsAreParsed(): void $source = <<<'PHP' +interface Iter { public function key(): K; public function current(): V; @@ -2211,12 +2211,12 @@ public function testMethodLevelVarianceIsRejected(): void namespace App; class C { - public function id<+T>(T $x): T { return $x; } + public function id(T $x): T { return $x; } } PHP; $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Variance markers `+T` / `-T` are not supported on methods, functions, closures, or arrow functions'); + $this->expectExceptionMessage('Variance markers `out T` / `in T` are not supported on methods, functions, closures, or arrow functions'); $parser->parse($source); } @@ -2225,7 +2225,7 @@ public function testFreeFunctionVarianceIsRejected(): void $source = <<<'PHP' (T $x): T { return $x; } +function id(T $x): T { return $x; } PHP; $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); $this->expectException(\RuntimeException::class); @@ -2234,12 +2234,140 @@ function id<-T>(T $x): T { return $x; } $parser->parse($source); } + public function testOutMarkerParsesAsCovariant(): void + { + $class = self::parseSingleClass(<<<'PHP' + {} +PHP); + $params = $class->getAttribute(XphpSourceParser::ATTR_GENERIC_PARAMS); + self::assertSame('T', $params[0]->name); + self::assertSame(Variance::Covariant, $params[0]->variance); + } + + public function testInMarkerParsesAsContravariant(): void + { + $class = self::parseSingleClass(<<<'PHP' + {} +PHP); + $params = $class->getAttribute(XphpSourceParser::ATTR_GENERIC_PARAMS); + self::assertSame('T', $params[0]->name); + self::assertSame(Variance::Contravariant, $params[0]->variance); + } + + public function testMultiParamVarianceMarkersParseIndependently(): void + { + // Both entries carry a marker; the second is comma-preceded — guards the + // migration blind spot where only the leading marker gets recognized. + $class = self::parseSingleClass(<<<'PHP' + {} +PHP); + $params = $class->getAttribute(XphpSourceParser::ATTR_GENERIC_PARAMS); + self::assertSame(['A', 'B'], array_map(static fn (TypeParam $p): string => $p->name, $params)); + self::assertSame(Variance::Covariant, $params[0]->variance); + self::assertSame(Variance::Contravariant, $params[1]->variance); + } + + public function testBoundedCovariantMarkerParses(): void + { + $class = self::parseSingleClass(<<<'PHP' + {} +PHP); + $params = $class->getAttribute(XphpSourceParser::ATTR_GENERIC_PARAMS); + self::assertSame('K', $params[0]->name); + self::assertSame(Variance::Covariant, $params[0]->variance); + self::assertNotNull($params[0]->bound); + } + + public function testOldPlusMarkerIsRejectedWithMigrationHint(): void + { + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The `+T` / `-T` variance syntax was replaced by `out T` / `in T`'); + $parser->parse(" {}\n"); + } + + public function testOldMinusMarkerIsRejectedWithMigrationHint(): void + { + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('was replaced by `out T` / `in T`'); + $parser->parse(" {}\n"); + } + + public function testOutReservedAsTypeParamNameIsRejected(): void + { + // No name follows, so `out` lands in the name slot and the reserve check fires. + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('`out` and `in` are variance markers and cannot name a type parameter'); + $parser->parse(" {}\n"); + } + + public function testOutFollowedByOutIsRejectedAtNameSlot(): void + { + // Marker `out` consumed, then `out` again as the name — the name-slot + // reserve closes the hole a marker-only lookahead would have accepted. + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('cannot name a type parameter'); + $parser->parse(" {}\n"); + } + + public function testReservedInAmongMultipleParamsIsRejected(): void + { + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('cannot name a type parameter'); + $parser->parse(" {}\n"); + } + + public function testReservedInWithBoundIsRejected(): void + { + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('cannot name a type parameter'); + $parser->parse(" {}\n"); + } + + public function testNamesStartingWithOutOrInAreValidInvariantParams(): void + { + // Only the exact lowercase tokens `out`/`in` are reserved; names that + // merely start with them stay ordinary invariant parameters. + $class = self::parseSingleClass(<<<'PHP' + {} +PHP); + $params = $class->getAttribute(XphpSourceParser::ATTR_GENERIC_PARAMS); + self::assertSame(['outer', 'Input', 'outT'], array_map(static fn (TypeParam $p): string => $p->name, $params)); + foreach ($params as $p) { + self::assertSame(Variance::Invariant, $p->variance); + } + } + + private static function parseSingleClass(string $source): Class_ + { + $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); + $class = self::findFirstClass($parser->parse($source)); + self::assertNotNull($class); + + return $class; + } + public function testContravariantInInputPositionIsAccepted(): void { $source = <<<'PHP' +class Consumer { public function set(T $x): void {} } @@ -2533,7 +2661,7 @@ public function testGenericClosureVarianceIsRejected(): void $source = <<<'PHP' (T $x): T { return $x; }; +$f = function(T $x): T { return $x; }; PHP; $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); $this->expectException(\RuntimeException::class); @@ -2547,7 +2675,7 @@ public function testArrowFunctionVarianceIsRejected(): void $source = <<<'PHP' (T $x): T => $x; +$f = fn(T $x): T => $x; PHP; $parser = new XphpSourceParser((new ParserFactory())->createForHostVersion()); $this->expectException(\RuntimeException::class); diff --git a/test/fixture/check/inner_variance/source/P.xphp b/test/fixture/check/inner_variance/source/P.xphp index 74b4a61..d7764fd 100644 --- a/test/fixture/check/inner_variance/source/P.xphp +++ b/test/fixture/check/inner_variance/source/P.xphp @@ -7,7 +7,7 @@ namespace App\Check\InnerVariance; // Composition violation the position check MISSES: `+T` sits in a covariant // return position, but through Container's INVARIANT slot the effective position // is invariant — so `+T` is not allowed. Only the inner-variance pass catches it. -class P<+T> +class P { public function f(): Container { diff --git a/test/fixture/check/method_and_class_errors/source/Producer.xphp b/test/fixture/check/method_and_class_errors/source/Producer.xphp index 8276ea2..d9f1cfd 100644 --- a/test/fixture/check/method_and_class_errors/source/Producer.xphp +++ b/test/fixture/check/method_and_class_errors/source/Producer.xphp @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Check\MethodAndClass; // Class-level: covariant +T in an input position. -class Producer<+T> +class Producer { public function set(T $value): void { diff --git a/test/fixture/check/parse_error/source/ClosureVariance.xphp b/test/fixture/check/parse_error/source/ClosureVariance.xphp index fbba7fc..9b2efed 100644 --- a/test/fixture/check/parse_error/source/ClosureVariance.xphp +++ b/test/fixture/check/parse_error/source/ClosureVariance.xphp @@ -6,6 +6,6 @@ namespace App\Check\ParseError; // xphp-specific parse-time rejection (not a PHP syntax error): variance markers // are not allowed on closures. Throws a RuntimeException during parse, with no line. -$f = function <+T>(T $x): T { +$f = function (T $x): T { return $x; }; diff --git a/test/fixture/check/variance_constructor_mixed_params/source/P.xphp b/test/fixture/check/variance_constructor_mixed_params/source/P.xphp index 4813f25..880bf93 100644 --- a/test/fixture/check/variance_constructor_mixed_params/source/P.xphp +++ b/test/fixture/check/variance_constructor_mixed_params/source/P.xphp @@ -6,7 +6,7 @@ namespace App\ConstructorMixed; // Rejected: the allowed bare leading `T` must not stop the walk from reaching the // bad trailing `?T` (a non-bare shape) — the inner-variance check still rejects it. -class P<+T> +class P { public function __construct(T $a, ?T $b) { diff --git a/test/fixture/check/variance_constructor_nested_generic/source/P.xphp b/test/fixture/check/variance_constructor_nested_generic/source/P.xphp index 805408b..7eca5ef 100644 --- a/test/fixture/check/variance_constructor_nested_generic/source/P.xphp +++ b/test/fixture/check/variance_constructor_nested_generic/source/P.xphp @@ -6,7 +6,7 @@ namespace App\ConstructorNested; // Rejected: `T` reaches the constructor through another generic's invariant slot // (`Box`), which is not a bare type-param — the inner-variance check rejects it. -class P<+T> +class P { public function __construct(Box $b) { diff --git a/test/fixture/check/variance_constructor_nullable/source/P.xphp b/test/fixture/check/variance_constructor_nullable/source/P.xphp index ac6f505..e8e2e56 100644 --- a/test/fixture/check/variance_constructor_nullable/source/P.xphp +++ b/test/fixture/check/variance_constructor_nullable/source/P.xphp @@ -6,7 +6,7 @@ namespace App\ConstructorNullable; // Rejected: `?T` is not a bare type-param — a non-bare shape isn't supported in // a constructor parameter, so the inner-variance check rejects it. -class P<+T> +class P { public function __construct(?T $x) { diff --git a/test/fixture/check/variance_edge_provable/source/Producer.xphp b/test/fixture/check/variance_edge_provable/source/Producer.xphp index 8c3c55e..5ca76dd 100644 --- a/test/fixture/check/variance_edge_provable/source/Producer.xphp +++ b/test/fixture/check/variance_edge_provable/source/Producer.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Check\VarianceEdgeProvable; -class Producer<+T> +class Producer { public function get(): T { diff --git a/test/fixture/check/variance_edge_unprovable/source/Producer.xphp b/test/fixture/check/variance_edge_unprovable/source/Producer.xphp index ebca57e..7014b84 100644 --- a/test/fixture/check/variance_edge_unprovable/source/Producer.xphp +++ b/test/fixture/check/variance_edge_unprovable/source/Producer.xphp @@ -6,7 +6,7 @@ namespace App\Check\VarianceEdgeUnprovable; // Covariant `+T` (T in a return position) — a variant template whose specializations // get real `extends` edges between provable subtype pairs. -class Producer<+T> +class Producer { public function get(): T { diff --git a/test/fixture/check/variance_violation/source/Producer.xphp b/test/fixture/check/variance_violation/source/Producer.xphp index fa98c77..d438743 100644 --- a/test/fixture/check/variance_violation/source/Producer.xphp +++ b/test/fixture/check/variance_violation/source/Producer.xphp @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Check\VarianceViolation; // Covariant `+T` appears in an input (method parameter) position — not allowed. -class Producer<+T> +class Producer { public function set(T $value): void { diff --git a/test/fixture/compile/comparator_param_covariant_upcast/source/Box.xphp b/test/fixture/compile/comparator_param_covariant_upcast/source/Box.xphp index 15da7fe..592c063 100644 --- a/test/fixture/compile/comparator_param_covariant_upcast/source/Box.xphp +++ b/test/fixture/compile/comparator_param_covariant_upcast/source/Box.xphp @@ -5,9 +5,9 @@ declare(strict_types=1); namespace App; // A covariant container with a `Comparator` CONSUMING method — the sound shape where `E` sits in a -// contravariant slot (Comparator<-T>) inside a contravariant parameter position, so +// contravariant slot (Comparator) inside a contravariant parameter position, so // contra ∘ contra = covariant, which a covariant `+E` may occupy. -class Box<+E> +class Box { /** @var list */ private array $items; diff --git a/test/fixture/compile/comparator_param_covariant_upcast/source/ById.xphp b/test/fixture/compile/comparator_param_covariant_upcast/source/ById.xphp index 01ff20b..dea18b9 100644 --- a/test/fixture/compile/comparator_param_covariant_upcast/source/ById.xphp +++ b/test/fixture/compile/comparator_param_covariant_upcast/source/ById.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -// A concrete Comparator. By contravariance (Comparator<-T>), a Comparator is also a +// A concrete Comparator. By contravariance (Comparator), a Comparator is also a // Comparator, so it can compare the Book elements of a Box upcast to Box. class ById implements Comparator { diff --git a/test/fixture/compile/comparator_param_covariant_upcast/source/Comparator.xphp b/test/fixture/compile/comparator_param_covariant_upcast/source/Comparator.xphp index 46c34bb..d8a9a1b 100644 --- a/test/fixture/compile/comparator_param_covariant_upcast/source/Comparator.xphp +++ b/test/fixture/compile/comparator_param_covariant_upcast/source/Comparator.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Comparator<-T> +interface Comparator { public function compare(T $a, T $b): int; } diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/AbstractColl.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/AbstractColl.xphp index 29522b5..f642871 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/AbstractColl.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/AbstractColl.xphp @@ -1,7 +1,7 @@ implements Collection, Lookup +abstract class AbstractColl implements Collection, Lookup { /** @var list */ protected array $items; diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Bag.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Bag.xphp index 2881c05..d184fec 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Bag.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Bag.xphp @@ -1,4 +1,4 @@ extends AbstractColl implements Collection, Lookup {} +class Bag extends AbstractColl implements Collection, Lookup {} diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Collection.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Collection.xphp index 1959ffc..05a44ae 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Collection.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Collection.xphp @@ -1,7 +1,7 @@ +interface Collection { public function contains(S $element): bool; } diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Couple.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Couple.xphp index 50f635c..3aa2790 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Couple.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Couple.xphp @@ -1,7 +1,7 @@ implements Tuple +class Couple implements Tuple { public function __construct(private A $a, private B $b) {} public function first(): A { return $this->a; } diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lookup.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lookup.xphp index dbb7b46..0146582 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lookup.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lookup.xphp @@ -1,7 +1,7 @@ +interface Lookup { public function indexOf(S $element): int; } diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lst.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lst.xphp index f38bd0e..0230069 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lst.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Lst.xphp @@ -1,4 +1,4 @@ extends AbstractColl implements Collection, Lookup {} +class Lst extends AbstractColl implements Collection, Lookup {} diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Tuple.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Tuple.xphp index e18e6e7..5fad2b4 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Tuple.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Tuple.xphp @@ -1,7 +1,7 @@ +interface Tuple { public function first(): A; public function second(): B; diff --git a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Use.xphp b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Use.xphp index aa6271f..48c963e 100644 --- a/test/fixture/compile/covariant_upcast_multipath_diamond/source/Use.xphp +++ b/test/fixture/compile/covariant_upcast_multipath_diamond/source/Use.xphp @@ -2,7 +2,7 @@ declare(strict_types=1); namespace App; // Lst and Bag are TWO concrete implementers, each reached as a covariant-upcast implementer through TWO -// interface ancestry chains (Collection's `contains`, Lookup's `indexOf`). The element type Tuple<+A,+B> +// interface ancestry chains (Collection's `contains`, Lookup's `indexOf`). The element type Tuple // over Book<:Product is a DIAMOND, so each concrete spec is obligated at four incomparable Tuple supertypes // per interface — eight erased members converging on one spec through several discovery paths. Single // inheritance threads one path per interface; the post-edge gap-fill must supply every remaining sibling on diff --git a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/AbstractColl.xphp b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/AbstractColl.xphp index d84a435..4836f04 100644 --- a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/AbstractColl.xphp +++ b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/AbstractColl.xphp @@ -2,7 +2,7 @@ declare(strict_types=1); namespace App; use function in_array; -abstract class AbstractColl<+E> implements Collection +abstract class AbstractColl implements Collection { /** @var list */ protected array $items; diff --git a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Collection.xphp b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Collection.xphp index 1959ffc..05a44ae 100644 --- a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Collection.xphp +++ b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Collection.xphp @@ -1,7 +1,7 @@ +interface Collection { public function contains(S $element): bool; } diff --git a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Couple.xphp b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Couple.xphp index 50f635c..3aa2790 100644 --- a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Couple.xphp +++ b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Couple.xphp @@ -1,7 +1,7 @@ implements Tuple +class Couple implements Tuple { public function __construct(private A $a, private B $b) {} public function first(): A { return $this->a; } diff --git a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Lst.xphp b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Lst.xphp index 010628b..f1d3379 100644 --- a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Lst.xphp +++ b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Lst.xphp @@ -1,4 +1,4 @@ extends AbstractColl implements Collection {} +class Lst extends AbstractColl implements Collection {} diff --git a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Tuple.xphp b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Tuple.xphp index e18e6e7..5fad2b4 100644 --- a/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Tuple.xphp +++ b/test/fixture/compile/covariant_upcast_nested_generic_diamond/source/Tuple.xphp @@ -1,7 +1,7 @@ +interface Tuple { public function first(): A; public function second(): B; diff --git a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/AbstractColl.xphp b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/AbstractColl.xphp index 5d30671..d07eae8 100644 --- a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/AbstractColl.xphp +++ b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/AbstractColl.xphp @@ -5,7 +5,7 @@ use function in_array; // Parent-less covariant base that holds BOTH erased bodies. firstOr returns E (return-position // enclosing parameter): it can be supplied ONLY by inheritance (grounded entirely at the supertype), // never by direct emission. The post-edge gap-fill must leave it to inheritance here (no diamond). -abstract class AbstractColl<+E> implements OrderedCollection +abstract class AbstractColl implements OrderedCollection { /** @var list */ protected array $items; diff --git a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/Collection.xphp b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/Collection.xphp index 1959ffc..05a44ae 100644 --- a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/Collection.xphp +++ b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/Collection.xphp @@ -1,7 +1,7 @@ +interface Collection { public function contains(S $element): bool; } diff --git a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/ListColl.xphp b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/ListColl.xphp index 87f48f4..06f0d4f 100644 --- a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/ListColl.xphp +++ b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/ListColl.xphp @@ -1,4 +1,4 @@ extends AbstractColl implements OrderedCollection {} +class ListColl extends AbstractColl implements OrderedCollection {} diff --git a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/OrderedCollection.xphp b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/OrderedCollection.xphp index 4bd3ff8..0c6ed14 100644 --- a/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/OrderedCollection.xphp +++ b/test/fixture/compile/covariant_upcast_return_enclosing_inherited/source/OrderedCollection.xphp @@ -1,7 +1,7 @@ extends Collection +interface OrderedCollection extends Collection { public function firstOr(S $fallback): E; } diff --git a/test/fixture/compile/cross_template_generic_arg_upcast/source/Collection.xphp b/test/fixture/compile/cross_template_generic_arg_upcast/source/Collection.xphp index 79d6270..58d6b6a 100644 --- a/test/fixture/compile/cross_template_generic_arg_upcast/source/Collection.xphp +++ b/test/fixture/compile/cross_template_generic_arg_upcast/source/Collection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Collection<+E> +interface Collection { public function first(): ?E; } diff --git a/test/fixture/compile/cross_template_generic_arg_upcast/source/Couple.xphp b/test/fixture/compile/cross_template_generic_arg_upcast/source/Couple.xphp index ee55a0f..191abca 100644 --- a/test/fixture/compile/cross_template_generic_arg_upcast/source/Couple.xphp +++ b/test/fixture/compile/cross_template_generic_arg_upcast/source/Couple.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Couple<+A, +B> implements Tuple +class Couple implements Tuple { public function __construct(private A $a, private B $b) {} diff --git a/test/fixture/compile/cross_template_generic_arg_upcast/source/ImmutableList.xphp b/test/fixture/compile/cross_template_generic_arg_upcast/source/ImmutableList.xphp index 8cb0712..698f089 100644 --- a/test/fixture/compile/cross_template_generic_arg_upcast/source/ImmutableList.xphp +++ b/test/fixture/compile/cross_template_generic_arg_upcast/source/ImmutableList.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class ImmutableList<+E> implements Collection +class ImmutableList implements Collection { /** @var list */ private array $items; diff --git a/test/fixture/compile/cross_template_generic_arg_upcast/source/Tuple.xphp b/test/fixture/compile/cross_template_generic_arg_upcast/source/Tuple.xphp index 527bfce..6aecda7 100644 --- a/test/fixture/compile/cross_template_generic_arg_upcast/source/Tuple.xphp +++ b/test/fixture/compile/cross_template_generic_arg_upcast/source/Tuple.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Tuple<+A, +B> +interface Tuple { public function left(): A; diff --git a/test/fixture/compile/enclosing_bound_erasure_covariant_chain/source/Box.xphp b/test/fixture/compile/enclosing_bound_erasure_covariant_chain/source/Box.xphp index 66a6281..d848dbd 100644 --- a/test/fixture/compile/enclosing_bound_erasure_covariant_chain/source/Box.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_covariant_chain/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { public function contains(U $value): bool { diff --git a/test/fixture/compile/enclosing_bound_erasure_covariant_chain/verify/runtime.php b/test/fixture/compile/enclosing_bound_erasure_covariant_chain/verify/runtime.php index b1246d7..bec2d84 100644 --- a/test/fixture/compile/enclosing_bound_erasure_covariant_chain/verify/runtime.php +++ b/test/fixture/compile/enclosing_bound_erasure_covariant_chain/verify/runtime.php @@ -5,7 +5,7 @@ /** * Runtime verify for `enclosing_bound_erasure_covariant_chain`. * - * Box<+E> specializes into a covariant `extends` chain (Box extends Box extends + * Box specializes into a covariant `extends` chain (Box extends Box extends * Box). Each specialization carries its own E-mangled `contains_` and inherits its * ancestors'; the distinct names mean no parameter-narrowing LSP fatal across the chain. A * Box used where a Box is expected dispatches the inherited `contains_`. diff --git a/test/fixture/compile/enclosing_bound_erasure_forwarding/source/Box.xphp b/test/fixture/compile/enclosing_bound_erasure_forwarding/source/Box.xphp index c8298a7..489cd34 100644 --- a/test/fixture/compile/enclosing_bound_erasure_forwarding/source/Box.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_forwarding/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { public function contains(U $value): bool { diff --git a/test/fixture/compile/enclosing_bound_erasure_inherited/source/ArrayList.xphp b/test/fixture/compile/enclosing_bound_erasure_inherited/source/ArrayList.xphp index 600d38c..63a8db0 100644 --- a/test/fixture/compile/enclosing_bound_erasure_inherited/source/ArrayList.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_inherited/source/ArrayList.xphp @@ -4,6 +4,6 @@ declare(strict_types=1); namespace App; -class ArrayList<+E> extends Base +class ArrayList extends Base { } diff --git a/test/fixture/compile/enclosing_bound_erasure_inherited/source/Base.xphp b/test/fixture/compile/enclosing_bound_erasure_inherited/source/Base.xphp index e702b61..6752f7a 100644 --- a/test/fixture/compile/enclosing_bound_erasure_inherited/source/Base.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_inherited/source/Base.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class Base<+E> +abstract class Base { public function contains(U $value): bool { diff --git a/test/fixture/compile/enclosing_bound_erasure_map_multiparam/source/Map.xphp b/test/fixture/compile/enclosing_bound_erasure_map_multiparam/source/Map.xphp index b155611..eb40877 100644 --- a/test/fixture/compile/enclosing_bound_erasure_map_multiparam/source/Map.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_map_multiparam/source/Map.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Map +class Map { public function describe(K $key): string { diff --git a/test/fixture/compile/enclosing_bound_erasure_map_multiparam/verify/runtime.php b/test/fixture/compile/enclosing_bound_erasure_map_multiparam/verify/runtime.php index 6ea0515..33ce0c1 100644 --- a/test/fixture/compile/enclosing_bound_erasure_map_multiparam/verify/runtime.php +++ b/test/fixture/compile/enclosing_bound_erasure_map_multiparam/verify/runtime.php @@ -5,7 +5,7 @@ /** * Runtime verify for `enclosing_bound_erasure_map_multiparam`. * - * `containsValue` on `Map` is bounded by the SECOND class parameter V. The erased member + * `containsValue` on `Map` is bounded by the SECOND class parameter V. The erased member * must mangle on V's concrete value (Fruit), not K (string) — the call site (keyed on the receiver's * V) and the Specializer must agree on that key. That the call resolves and runs proves the * multi-class-param mangle keys on the bound's referent. diff --git a/test/fixture/compile/enclosing_bound_erasure_param_widening/source/Box.xphp b/test/fixture/compile/enclosing_bound_erasure_param_widening/source/Box.xphp index 66a6281..d848dbd 100644 --- a/test/fixture/compile/enclosing_bound_erasure_param_widening/source/Box.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_param_widening/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { public function contains(U $value): bool { diff --git a/test/fixture/compile/enclosing_bound_erasure_two_params/source/Box.xphp b/test/fixture/compile/enclosing_bound_erasure_two_params/source/Box.xphp index 085d62c..a040abe 100644 --- a/test/fixture/compile/enclosing_bound_erasure_two_params/source/Box.xphp +++ b/test/fixture/compile/enclosing_bound_erasure_two_params/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { // Two enclosing-bounded method params: both erase to E; the member mangles on [E, E]. public function bothAreFruit(U $a, V $b): bool diff --git a/test/fixture/compile/enclosing_bound_interface_upcast/source/AbstractColl.xphp b/test/fixture/compile/enclosing_bound_interface_upcast/source/AbstractColl.xphp index d1b9ee2..e930d6c 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast/source/AbstractColl.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast/source/AbstractColl.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class AbstractColl<+E> implements Collection +abstract class AbstractColl implements Collection { /** @var list */ protected array $items; diff --git a/test/fixture/compile/enclosing_bound_interface_upcast/source/Collection.xphp b/test/fixture/compile/enclosing_bound_interface_upcast/source/Collection.xphp index 324e3c1..7fc274f 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast/source/Collection.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast/source/Collection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Collection<+E> +interface Collection { public function contains(E2 $value): bool; } diff --git a/test/fixture/compile/enclosing_bound_interface_upcast/source/ListColl.xphp b/test/fixture/compile/enclosing_bound_interface_upcast/source/ListColl.xphp index a011bae..21c1eb8 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast/source/ListColl.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast/source/ListColl.xphp @@ -4,6 +4,6 @@ declare(strict_types=1); namespace App; -class ListColl<+E> extends AbstractColl +class ListColl extends AbstractColl { } diff --git a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/AbstractMap.xphp b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/AbstractMap.xphp index 3de3b59..1856e8c 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/AbstractMap.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/AbstractMap.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class AbstractMap implements MMap +abstract class AbstractMap implements MMap { /** @var list */ protected array $values; diff --git a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/HashMap.xphp b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/HashMap.xphp index 90897ba..c247cd0 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/HashMap.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/HashMap.xphp @@ -4,6 +4,6 @@ declare(strict_types=1); namespace App; -class HashMap extends AbstractMap +class HashMap extends AbstractMap { } diff --git a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/MMap.xphp b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/MMap.xphp index 388c3ba..79f8603 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast_map/source/MMap.xphp +++ b/test/fixture/compile/enclosing_bound_interface_upcast_map/source/MMap.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface MMap +interface MMap { public function containsValue(U $value): bool; } diff --git a/test/fixture/compile/enclosing_bound_interface_upcast_map/verify/runtime.php b/test/fixture/compile/enclosing_bound_interface_upcast_map/verify/runtime.php index 8746940..5cbde10 100644 --- a/test/fixture/compile/enclosing_bound_interface_upcast_map/verify/runtime.php +++ b/test/fixture/compile/enclosing_bound_interface_upcast_map/verify/runtime.php @@ -5,7 +5,7 @@ /** * Runtime verify for `enclosing_bound_interface_upcast_map`. * - * A two-parameter `HashMap` (K invariant, +V covariant) is upcast to `MMap`. + * A two-parameter `HashMap` (K invariant, out V covariant) is upcast to `MMap`. * The erased `containsValue` mangles on V, so the closer must schedule `AbstractMap` * — varying only the covariant V to the supertype arg while keeping the invariant K = Id — with NO * explicit `HashMap` anywhere. Executing the output proves the threading is correct. diff --git a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/AbstractColl.xphp b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/AbstractColl.xphp index 74d8b56..c4236e8 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/AbstractColl.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/AbstractColl.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class AbstractColl<+E> implements Collection +abstract class AbstractColl implements Collection { /** @var list */ protected array $items; diff --git a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/Collection.xphp b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/Collection.xphp index e935eb9..047f33c 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/Collection.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/Collection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Collection<+E> +interface Collection { public function contains(U $value): bool; } diff --git a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/ListColl.xphp b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/ListColl.xphp index 2c49b48..f6c3787 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/ListColl.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/ListColl.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class ListColl<+E> extends AbstractColl implements OrderedCollection +class ListColl extends AbstractColl implements OrderedCollection { public function indexOf(U $value): int { diff --git a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/OrderedCollection.xphp b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/OrderedCollection.xphp index 0f66e86..705289c 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/OrderedCollection.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_direct_emit/source/OrderedCollection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface OrderedCollection<+E> extends Collection +interface OrderedCollection extends Collection { public function indexOf(U $value): int; } diff --git a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/AbstractColl.xphp b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/AbstractColl.xphp index 74d8b56..c4236e8 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/AbstractColl.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/AbstractColl.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class AbstractColl<+E> implements Collection +abstract class AbstractColl implements Collection { /** @var list */ protected array $items; diff --git a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/Collection.xphp b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/Collection.xphp index e935eb9..047f33c 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/Collection.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/Collection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface Collection<+E> +interface Collection { public function contains(U $value): bool; } diff --git a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/ListColl.xphp b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/ListColl.xphp index c122553..b140430 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/ListColl.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/ListColl.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class ListColl<+E> extends AbstractColl implements OrderedCollection +class ListColl extends AbstractColl implements OrderedCollection { // Body references the CLASS parameter E structurally (`instanceof E`). Under the upcast this member // is emitted directly onto ListColl; E must resolve to the upcast-source's concrete (Book), diff --git a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/OrderedCollection.xphp b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/OrderedCollection.xphp index 7d13d98..2de3783 100644 --- a/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/OrderedCollection.xphp +++ b/test/fixture/compile/enclosing_bound_subinterface_structural_class_param/source/OrderedCollection.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -interface OrderedCollection<+E> extends Collection +interface OrderedCollection extends Collection { public function firstKind(U $value): bool; } diff --git a/test/fixture/compile/enclosing_param_bound_compound_drop/source/Box.xphp b/test/fixture/compile/enclosing_param_bound_compound_drop/source/Box.xphp index 019da97..15acbc1 100644 --- a/test/fixture/compile/enclosing_param_bound_compound_drop/source/Box.xphp +++ b/test/fixture/compile/enclosing_param_bound_compound_drop/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { // U must be BOTH `\Stringable` (have a `__toString`) AND a subtype of the element type E. The // `\Stringable` half is checkable with no knowledge of E; only the `E` half needs the receiver's diff --git a/test/fixture/compile/enclosing_param_bound_lenient_drop/source/Box.xphp b/test/fixture/compile/enclosing_param_bound_lenient_drop/source/Box.xphp index d7fdb33..c1b36b3 100644 --- a/test/fixture/compile/enclosing_param_bound_lenient_drop/source/Box.xphp +++ b/test/fixture/compile/enclosing_param_bound_lenient_drop/source/Box.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Box<+E> +class Box { // U must be a subtype of the element type E (the sound, element-typed shape on a // covariant collection). The bound is checked only when E can be grounded. diff --git a/test/fixture/compile/generic_contravariant_constructor/source/Consumer.xphp b/test/fixture/compile/generic_contravariant_constructor/source/Consumer.xphp index 52c3f85..6a388e9 100644 --- a/test/fixture/compile/generic_contravariant_constructor/source/Consumer.xphp +++ b/test/fixture/compile/generic_contravariant_constructor/source/Consumer.xphp @@ -7,7 +7,7 @@ namespace App\ContravariantConstructor; // Contravariant constructor param: keeps its real type too. The edge flips — // `Consumer` extends `Consumer` — and the chain still loads // because PHP exempts `__construct` from LSP. -class Consumer<-T> +class Consumer { private array $items; diff --git a/test/fixture/compile/generic_covariant_bounded_constructor/source/Box.xphp b/test/fixture/compile/generic_covariant_bounded_constructor/source/Box.xphp index ecb8ed0..0ef6eb2 100644 --- a/test/fixture/compile/generic_covariant_bounded_constructor/source/Box.xphp +++ b/test/fixture/compile/generic_covariant_bounded_constructor/source/Box.xphp @@ -6,7 +6,7 @@ namespace App\BoundConstructor; // Bounded covariant constructor param: keeps its REAL substituted type (the // concrete arg), not the bound and not `mixed`. -class Box<+T : \Stringable> +class Box { private array $items; diff --git a/test/fixture/compile/generic_covariant_immutable_constructor/source/ImmutableList.xphp b/test/fixture/compile/generic_covariant_immutable_constructor/source/ImmutableList.xphp index 7669989..41b6770 100644 --- a/test/fixture/compile/generic_covariant_immutable_constructor/source/ImmutableList.xphp +++ b/test/fixture/compile/generic_covariant_immutable_constructor/source/ImmutableList.xphp @@ -10,7 +10,7 @@ namespace App\CovariantConstructor; // `extends` chain (ImmutableList extends ImmutableList) is // valid and construction is runtime-type-checked. Not `final` — a variant class // can't be (its specializations are linked by `extends` edges). -class ImmutableList<+T> +class ImmutableList { private array $items; diff --git a/test/fixture/compile/generic_covariant_immutable_constructor/verify/runtime.php b/test/fixture/compile/generic_covariant_immutable_constructor/verify/runtime.php index 9388b0c..8bac4c9 100644 --- a/test/fixture/compile/generic_covariant_immutable_constructor/verify/runtime.php +++ b/test/fixture/compile/generic_covariant_immutable_constructor/verify/runtime.php @@ -4,7 +4,7 @@ /** * Runtime verify for `generic_covariant_immutable_constructor`: - * a covariant immutable collection `ImmutableList<+T>` with a `T`-typed + * a covariant immutable collection `ImmutableList` with a `T`-typed * constructor. `ImmutableList` is usable where `ImmutableList` * is expected (covariant `extends` edge), the constructor keeps its REAL element * type on each specialization (no erasure), and that real type is enforced by diff --git a/test/fixture/compile/generic_covariant_private_property/source/Box.xphp b/test/fixture/compile/generic_covariant_private_property/source/Box.xphp index 8d5b505..dc599dc 100644 --- a/test/fixture/compile/generic_covariant_private_property/source/Box.xphp +++ b/test/fixture/compile/generic_covariant_private_property/source/Box.xphp @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\CovariantPrivateProperty; // Covariant container holding its element in a PRIVATE promoted property of -// type `T`. This is the natural `class Producer<+T>` shape — and it is sound: +// type `T`. This is the natural `class Producer` shape — and it is sound: // PHP does NOT type-check private property types across an `extends` chain (a // private slot is per-declaring-scope, never inherited), so the variance edge // `Box extends Box` lands with NO autoload fatal even though the @@ -14,7 +14,7 @@ namespace App\CovariantPrivateProperty; // exempts `__construct` from LSP), so construction is runtime-type-checked. // Not `final` — a variant class can't be (its specializations are linked by // `extends` edges). -class Box<+T> +class Box { public function __construct(private T $item) { diff --git a/test/fixture/compile/generic_covariant_private_property/verify/runtime.php b/test/fixture/compile/generic_covariant_private_property/verify/runtime.php index 21f0903..d47479a 100644 --- a/test/fixture/compile/generic_covariant_private_property/verify/runtime.php +++ b/test/fixture/compile/generic_covariant_private_property/verify/runtime.php @@ -4,7 +4,7 @@ /** * Runtime verify for `generic_covariant_private_property`: - * a covariant container `Box<+T>` that stores its element in a PRIVATE promoted + * a covariant container `Box` that stores its element in a PRIVATE promoted * property of type `T`. The variance edge `Box extends Box` * autoloads with no PHP fatal even though each specialization declares a * divergent-typed private slot (PHP doesn't type-check private property types diff --git a/test/fixture/compile/generic_mixed_variance_constructor/source/Pair.xphp b/test/fixture/compile/generic_mixed_variance_constructor/source/Pair.xphp index 564ed93..96daab8 100644 --- a/test/fixture/compile/generic_mixed_variance_constructor/source/Pair.xphp +++ b/test/fixture/compile/generic_mixed_variance_constructor/source/Pair.xphp @@ -4,9 +4,9 @@ declare(strict_types=1); namespace App\MixedConstructor; -// `Pair<+A, B>`: the covariant `A` and the invariant `B` constructor params both +// `Pair`: the covariant `A` and the invariant `B` constructor params both // keep their concrete substituted types; the scalar `int $tag` is untouched. -class Pair<+A, B> +class Pair { private array $slots; diff --git a/test/fixture/compile/generic_two_covariant_constructor/source/Two.xphp b/test/fixture/compile/generic_two_covariant_constructor/source/Two.xphp index b2d48ae..5ec9685 100644 --- a/test/fixture/compile/generic_two_covariant_constructor/source/Two.xphp +++ b/test/fixture/compile/generic_two_covariant_constructor/source/Two.xphp @@ -6,7 +6,7 @@ namespace App\TwoConstructor; // Two covariant params: BOTH `T`-typed constructor params keep their real // substituted types (nothing is erased for any variant constructor param). -class Two<+A, +B> +class Two { private array $slots; diff --git a/test/fixture/compile/self_reintroducing_tower/source/Lst.xphp b/test/fixture/compile/self_reintroducing_tower/source/Lst.xphp index c1b3525..bffa87d 100644 --- a/test/fixture/compile/self_reintroducing_tower/source/Lst.xphp +++ b/test/fixture/compile/self_reintroducing_tower/source/Lst.xphp @@ -8,7 +8,7 @@ namespace App; // receiver's own family (Lst) one level deeper. Paired with Mp::values() (which re-exposes the value as // a Lst), instantiating Lst re-seeds Lst -> Mp -> Lst -> ... without bound under eager structural // discovery — a tiny shape that reaches the depth cap fast (compile-only; never converges). -class Lst<+E> +class Lst { public function toMap(): Mp> { diff --git a/test/fixture/compile/self_reintroducing_tower/source/Mp.xphp b/test/fixture/compile/self_reintroducing_tower/source/Mp.xphp index 06a516c..c857dde 100644 --- a/test/fixture/compile/self_reintroducing_tower/source/Mp.xphp +++ b/test/fixture/compile/self_reintroducing_tower/source/Mp.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -class Mp +class Mp { public function values(): Lst { diff --git a/test/fixture/compile/variance_contravariant_happy/source/Containers/Consumer.xphp b/test/fixture/compile/variance_contravariant_happy/source/Containers/Consumer.xphp index 5d19ebc..7537964 100644 --- a/test/fixture/compile/variance_contravariant_happy/source/Containers/Consumer.xphp +++ b/test/fixture/compile/variance_contravariant_happy/source/Containers/Consumer.xphp @@ -8,7 +8,7 @@ namespace App\VarianceContravariantHappy\Containers; // With Dog <: Animal, the emitter adds the FLIPPED edge: // Consumer_Animal extends Consumer_Dog (so anywhere a Consumer is // expected, a Consumer works -- it accepts a wider input). -class Consumer<-T> +class Consumer { public function consume(T $value): void { diff --git a/test/fixture/compile/variance_covariant_happy/source/Containers/Producer.xphp b/test/fixture/compile/variance_covariant_happy/source/Containers/Producer.xphp index 668cf25..7a6b27e 100644 --- a/test/fixture/compile/variance_covariant_happy/source/Containers/Producer.xphp +++ b/test/fixture/compile/variance_covariant_happy/source/Containers/Producer.xphp @@ -13,7 +13,7 @@ namespace App\VarianceCovariantHappy\Containers; // With Banana <: Fruit, the emitter adds // `Producer_Banana extends Producer_Fruit` (Class_ specializations get a // single `extends`, since PHP allows only one class inheritance). -class Producer<+T> +class Producer { private mixed $item = null; diff --git a/test/fixture/compile/variance_edge_preserves_source_parent/source/Base.xphp b/test/fixture/compile/variance_edge_preserves_source_parent/source/Base.xphp index e702b61..6752f7a 100644 --- a/test/fixture/compile/variance_edge_preserves_source_parent/source/Base.xphp +++ b/test/fixture/compile/variance_edge_preserves_source_parent/source/Base.xphp @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App; -abstract class Base<+E> +abstract class Base { public function contains(U $value): bool { diff --git a/test/fixture/compile/variance_edge_preserves_source_parent/source/ListColl.xphp b/test/fixture/compile/variance_edge_preserves_source_parent/source/ListColl.xphp index a8cfa6b..cb0e8cb 100644 --- a/test/fixture/compile/variance_edge_preserves_source_parent/source/ListColl.xphp +++ b/test/fixture/compile/variance_edge_preserves_source_parent/source/ListColl.xphp @@ -4,6 +4,6 @@ declare(strict_types=1); namespace App; -class ListColl<+E> extends Base +class ListColl extends Base { } diff --git a/test/fixture/compile/variance_edge_preserves_source_parent/source/Use.xphp b/test/fixture/compile/variance_edge_preserves_source_parent/source/Use.xphp index 00aa10b..7fe62f3 100644 --- a/test/fixture/compile/variance_edge_preserves_source_parent/source/Use.xphp +++ b/test/fixture/compile/variance_edge_preserves_source_parent/source/Use.xphp @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App; // Two covariant specializations of a class that has a SOURCE parent: -// `ListColl<+E> extends Base`. Because `ListColl <: ListColl` (covariant E, +// `ListColl extends Base`. Because `ListColl <: ListColl` (covariant E, // Banana extends Fruit), the variance edge emitter would, if it overwrote the source `extends`, // replace `ListColl extends Base` with `extends ListColl` — severing the // inherited `contains_` member (its body lives on Base). The direct call below diff --git a/test/fixture/compile/variance_edge_preserves_source_parent/verify/runtime.php b/test/fixture/compile/variance_edge_preserves_source_parent/verify/runtime.php index 2a36e9f..a0c6517 100644 --- a/test/fixture/compile/variance_edge_preserves_source_parent/verify/runtime.php +++ b/test/fixture/compile/variance_edge_preserves_source_parent/verify/runtime.php @@ -5,7 +5,7 @@ /** * Runtime verify for `variance_edge_preserves_source_parent`. * - * `ListColl<+E> extends Base` is instantiated at two covariant args (Fruit, Banana). The variance + * `ListColl extends Base` is instantiated at two covariant args (Fruit, Banana). The variance * edge emitter must NOT overwrite each specialization's source `extends Base` with the same-template * covariant super (`ListColl extends ListColl`): single inheritance allows one parent, * and the source parent carries the inherited `contains_` member. Overwriting would drop diff --git a/test/fixture/compile/variance_with_defaults_and_bounds/source/Containers/Cache.xphp b/test/fixture/compile/variance_with_defaults_and_bounds/source/Containers/Cache.xphp index 1413007..5c0fda6 100644 --- a/test/fixture/compile/variance_with_defaults_and_bounds/source/Containers/Cache.xphp +++ b/test/fixture/compile/variance_with_defaults_and_bounds/source/Containers/Cache.xphp @@ -12,7 +12,7 @@ namespace App\VarianceWithDefaultsAndBounds\Containers; // Properties are bound-typed (Stringable) because PHP enforces invariant // property types, so storing +K directly would PHP-fatal at autoload when // the variance edge lands. -class Cache<+K : \Stringable & \Countable, V = mixed> +class Cache { private ?\Stringable $key = null; public V $value;