Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
5d04c86
feat(diagnostics): add Diagnostic value-object model for the check gate
math3usmartins Jun 17, 2026
b9e1173
feat(check): collect generic bound violations via an optional diagnos…
math3usmartins Jun 17, 2026
598a3fb
feat(check): add a validate-only pass that collects generic errors
math3usmartins Jun 17, 2026
6f86e03
refactor(check): move variance-position validation to a collectable p…
math3usmartins Jun 17, 2026
90450e8
refactor(check): make inner-variance composition collectable
math3usmartins Jun 17, 2026
c4e6815
refactor(check): run variance-position before defaults; tidy docblocks
math3usmartins Jun 17, 2026
7e85687
feat(check): add Text, JSON, and GitHub diagnostic renderers
math3usmartins Jun 17, 2026
b077a4c
test(check): pin GitHub renderer percent/carriage-return escaping
math3usmartins Jun 17, 2026
3034f64
feat(check): add the `xphp check` command with per-file parse resilience
math3usmartins Jun 17, 2026
21f855b
refactor(check): read source files outside the parse try/catch
math3usmartins Jun 17, 2026
d552742
docs: document the `xphp check` diagnostics gate
math3usmartins Jun 17, 2026
c34b0a1
docs: clarify that `xphp check` is not yet a substitute for `xphp com…
math3usmartins Jun 18, 2026
078f9d9
feat(check): collect method/function/closure-level generic errors
math3usmartins Jun 18, 2026
1892b73
test(check): cover the static-closure collect path; document new codes
math3usmartins Jun 18, 2026
d008c05
build(phpstan): raise analysis to level 9 and fix the surfaced findings
math3usmartins Jun 18, 2026
984c2fd
ci: self-test the `xphp check` gate end-to-end (+ PHAR smoke)
math3usmartins Jun 18, 2026
dfc79c0
feat(check): foundations for running PHPStan over compiled output
math3usmartins Jun 18, 2026
8e27841
feat(check): run PHPStan over representative specializations and pars…
math3usmartins Jun 18, 2026
cd721d4
feat(check): map PHPStan findings to .xphp and wire the pass into `ch…
math3usmartins Jun 18, 2026
b85392a
test(check): group the PHPStan-pass tests and split them out of test/…
math3usmartins Jun 18, 2026
8a8e24e
docs(check): document the PHPStan pass; add CI job + changelog
math3usmartins Jun 18, 2026
ed4b1c7
feat(check): machinery to detect undeclared type parameters (no-op)
math3usmartins Jun 18, 2026
1bba5b5
feat(check): flag undeclared type parameters in generic members
math3usmartins Jun 18, 2026
1a9fc6d
feat(check): extend undeclared-type detection to method-level generics
math3usmartins Jun 18, 2026
dbb4dda
feat(check): flag undeclared names in generic bounds and defaults
math3usmartins Jun 18, 2026
82a5f78
feat(check): report too many type arguments instead of silently trunc…
math3usmartins Jun 18, 2026
cc41010
fix(check): don't flag a scalar bound as an undeclared type
math3usmartins Jun 18, 2026
5bae46d
docs(changelog): record the undeclared-type and too-many-args checks
math3usmartins Jun 18, 2026
68f1911
docs: add Architecture Decision Records
math3usmartins Jun 18, 2026
263dd2d
docs(adr): correct ADR-0005 — unknown bounds are rejected, not deferred
math3usmartins Jun 18, 2026
0c6b640
docs(adr): correct three records to match the code
math3usmartins Jun 18, 2026
c5b7aba
docs: correct public docs against the code (check/PHPStan shipped, pi…
math3usmartins Jun 18, 2026
e9a3ed7
docs: correct the static-closure rejection rationale in caveats
math3usmartins Jun 18, 2026
7f8be38
feat(monomorphize): resolve generic instance methods through inheritance
math3usmartins Jun 18, 2026
747592c
feat(monomorphize): error on unresolved generic-method turbofish calls
math3usmartins Jun 18, 2026
094b6ef
feat(monomorphize): resolve static + nullsafe generic methods through…
math3usmartins Jun 18, 2026
f9dfc7a
docs: document generic-method resolution through inheritance
math3usmartins Jun 18, 2026
a9cafea
docs(roadmap): record deferred generic-completeness gaps
math3usmartins Jun 19, 2026
7513764
feat(monomorphize): allow a type-parameter-typed constructor on a var…
math3usmartins Jun 19, 2026
e43ae2b
docs(variance): make method-level variance a documented permanent bou…
math3usmartins Jun 19, 2026
0b74eb6
feat(check): recognize a `Hashable` value-equality bound
math3usmartins Jun 19, 2026
f09e2c3
docs: document covariant typed-construction + Hashable bound; changelog
math3usmartins Jun 19, 2026
d231faf
docs(adr): record variance-erased constructors and the class-level-va…
math3usmartins Jun 19, 2026
95833b6
chore: scrub internal ticket references from tracked comments
math3usmartins Jun 21, 2026
907f106
feat(monomorphize)!: emit real types for variant constructor params (…
math3usmartins Jun 21, 2026
117ca30
docs: variant constructor params are real-typed, not erased
math3usmartins Jun 21, 2026
6a6c211
test(monomorphize): move constructor-variance tests to fixtures
math3usmartins Jun 21, 2026
87e8db6
test,docs: rename ctor -> constructor and Contra -> ContraVariance
math3usmartins Jun 21, 2026
963aa42
refactor: expand ad-hoc abbreviations project-wide
math3usmartins Jun 21, 2026
c1c8b3e
docs: correct VariancePositionValidator docblock for real-typed const…
math3usmartins Jun 21, 2026
9d310db
feat(monomorphize): treat by-reference parameters as an invariant pos…
math3usmartins Jun 21, 2026
7c7a133
feat(monomorphize)!: reject `final` on a variant class instead of str…
math3usmartins Jun 21, 2026
631d466
docs: fix variance covariant example + roadmap position summary
math3usmartins Jun 21, 2026
876db57
docs: add caveat for covariant getters tripping the PHPStan pass
math3usmartins Jun 21, 2026
c8919ef
feat(variance): allow variance markers on private properties
math3usmartins Jun 21, 2026
a506e50
docs(variance): document private-property variance + PHPStan-clean test
math3usmartins Jun 21, 2026
7d1ee6b
docs(variance): correct the array-backed PHPStan caveat to match real…
math3usmartins Jun 21, 2026
d4150eb
docs(roadmap): list the shipped `Hashable` value-equality bound
math3usmartins Jun 21, 2026
0c989ca
feat(bounds): namespace the value-equality bound as `XPHP\Hashable`
math3usmartins Jun 21, 2026
d257a49
refactor(bounds): drop the special-cased value-equality bound
math3usmartins Jun 21, 2026
ad05245
docs(adr): consolidate the value-equality ADRs and fix forward-refere…
math3usmartins Jun 21, 2026
aad2b7e
feat(check): warn when a variant template's element type isn't in the…
math3usmartins Jun 21, 2026
1186f80
feat(compile): make the emit path root-aware for multi-root builds
math3usmartins Jun 22, 2026
ad62c0f
feat(config): add xphp.json manifest parser
math3usmartins Jun 22, 2026
dc2bb21
feat(config): resolve xphp.json manifests into a multi-root source set
math3usmartins Jun 22, 2026
db324f7
feat(cli): resolve compile/check sources from an xphp.json manifest
math3usmartins Jun 22, 2026
9ac12e8
docs: document the xphp.json manifest + record ADR-0017
math3usmartins Jun 22, 2026
5ee437d
feat(config): support `**` globstar for recursive include discovery
math3usmartins Jun 22, 2026
f326d87
docs: recommend the xphp.json manifest as the default project setup
math3usmartins Jun 22, 2026
a7eb060
feat(monomorphize): add Registry::substituteBound to ground bound typ…
math3usmartins Jun 22, 2026
3bf3894
feat(monomorphize): thread receiver type args through the supertype c…
math3usmartins Jun 22, 2026
bed2336
feat(monomorphize): ground a method-generic bound on an enclosing typ…
math3usmartins Jun 22, 2026
d4c7f71
test(monomorphize): pin the enclosing-param bound grounding limitations
math3usmartins Jun 23, 2026
56352a5
docs(adr): use the Fruit/Banana/Rock example vocabulary consistently
math3usmartins Jun 23, 2026
93ece6a
docs: use \Stringable instead of an invented "Named" marker in the co…
math3usmartins Jun 23, 2026
950eb1e
fix(monomorphize): check a class bound that references a sibling type…
math3usmartins Jun 23, 2026
d20cc7a
feat(monomorphize): ground a branch-merged receiver when every arm ag…
math3usmartins Jun 23, 2026
56069a7
feat(monomorphize): ground a receiver whose type comes from a method …
math3usmartins Jun 23, 2026
f0f59a7
feat(monomorphize): fail a method-generic bound that can't be proven
math3usmartins Jun 24, 2026
a822bce
feat(monomorphize): fail a turbofish call on an undeterminable receiver
math3usmartins Jun 24, 2026
c04b03e
docs: record ground-or-fail for enclosing-parameter method bounds
math3usmartins Jun 24, 2026
f11a15d
docs: catalog the two ground-or-fail diagnostics with sample code
math3usmartins Jun 24, 2026
5853249
docs: correct the forwarding "workaround" for an enclosing-bound self…
math3usmartins Jun 24, 2026
b378367
feat(monomorphize): fail a self-call that forwards a type parameter
math3usmartins Jun 24, 2026
371e31e
feat(monomorphize): detect when a method-generic bound can be erased …
math3usmartins Jun 24, 2026
4d41bc6
feat(monomorphize): lower a direct-input enclosing-bounded method by …
math3usmartins Jun 24, 2026
9252086
docs: forwarding an enclosing-bounded parameter now compiles and runs
math3usmartins Jun 24, 2026
d93d64b
docs: document the unspecializable-self-call diagnostic and erasure l…
math3usmartins Jun 24, 2026
85c9a25
test(monomorphize): runtime gates for multi-class-param and param-wid…
math3usmartins Jun 24, 2026
9dea3fc
test(monomorphize): runtime gate for a two-bounded-param erasable method
math3usmartins Jun 24, 2026
26525a8
refactor(monomorphize): gate erasable-method retention without a cont…
math3usmartins Jun 24, 2026
886f8f4
docs: bring the ADR and roadmap current with the erasure lowering
math3usmartins Jun 24, 2026
87df3ae
fix(monomorphize): keep a specialization's source parent when emittin…
math3usmartins Jun 24, 2026
e765009
refactor(monomorphize): extract the variance-subtype decision into a …
math3usmartins Jun 24, 2026
a609565
feat(monomorphize): schedule the implementer for a covariant upcast t…
math3usmartins Jun 24, 2026
5344daf
docs(monomorphize): document the covariant-interface element-method u…
math3usmartins Jun 24, 2026
4457656
docs(changelog): assemble the 0.3.0 section and backfill 0.2.1
math3usmartins Jun 24, 2026
df3bad9
fix(monomorphize): report a turbofish-less method-generic call instea…
math3usmartins Jun 25, 2026
24b5041
fix(monomorphize): report a turbofish-less generic free-function call
math3usmartins Jun 25, 2026
ad655c6
fix(monomorphize): report a turbofish-less generic closure call
math3usmartins Jun 25, 2026
b2b30f7
docs(monomorphize): document that a turbofish-less generic call is an…
math3usmartins Jun 25, 2026
9cce945
fix(monomorphize): close three edge cases in turbofish-less call dete…
math3usmartins Jun 25, 2026
37a743b
docs(changelog): record the turbofish-less generic-call diagnostic
math3usmartins Jun 25, 2026
946afb8
fix(monomorphize): include closure-template maps in the scope-snapsho…
math3usmartins Jun 25, 2026
555af5d
feat(monomorphize): emit an erased upcast member directly when inheri…
math3usmartins Jun 25, 2026
43c3601
fix(monomorphize): reject direct emission when the enclosing paramete…
math3usmartins Jun 25, 2026
29a5000
docs: document direct emission of a covariant-upcast member; ADR for …
math3usmartins Jun 25, 2026
54b3086
docs(adr): show the alias and insteadof cases that make full trait mo…
math3usmartins Jun 26, 2026
775bbbf
fix(monomorphize): emit the covariant edge for a cross-template gener…
math3usmartins Jun 26, 2026
b02bd5d
docs: document variance composing through a nested generic type-argument
math3usmartins Jun 26, 2026
e67b524
fix(monomorphize): compose variance through type-constructor args via…
math3usmartins Jun 26, 2026
b414970
fix(monomorphize): don't double-report a visible promoted constructor…
math3usmartins Jun 26, 2026
2311046
docs: document a covariant class consuming a contravariant generic
math3usmartins Jun 26, 2026
60decad
feat(monomorphize): localize the non-convergent-specialization diagno…
math3usmartins Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
47 changes: 47 additions & 0 deletions .github/workflows/ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,53 @@ jobs:
- name: Run PHPStan
run: make lint/phpstan

phpstan-pass:
# The `xphp check` PHPStan-integration tests (@group phpstan) shell out to a
# real phpstan binary, so they're excluded from `make test/unit` and run here.
# `composer install` provides vendor/bin/phpstan (a require-dev dependency).
name: PHPStan pass (xphp check)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2

- name: Install dependencies
uses: ramsey/composer-install@v3

- name: Run the PHPStan-pass tests
run: make test/phpstan-pass

check:
# End-to-end self-test of the `xphp check` gate: runs the real bin/xphp
# binary against the check fixtures and asserts the 0/1/2 exit contract.
# The in-process CheckCommandTest can't observe the shipped binary's
# process exit code or its rendered stdout, so this covers that gap.
name: xphp check (self-test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2

- name: Install dependencies
uses: ramsey/composer-install@v3

- name: Run xphp check self-test
run: make test/check

infection:
name: Mutation testing
runs-on: ubuntu-latest
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ jobs:
# release fails BEFORE the asset is uploaded.
run: php dist/xphp.phar list

- name: Smoke-test `check` on the PHAR
# Exercise the released artifact as a real gate: same 0/1/2
# exit-contract assertions as CI, but against dist/xphp.phar
# instead of bin/xphp. Fails the release before upload if the
# packaged binary can't validate sources.
run: make test/check XPHP_BIN="php dist/xphp.phar"

- name: Compute SHA256 sum
# `sha256sum` outputs `<hex> <filename>`; running it from
# the dist/ directory keeps the filename relative so users
Expand Down
189 changes: 186 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,187 @@ All notable changes to `xphp` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - Unreleased

_In progress on this branch — content still accumulating; date set at tag time._

### Added

- **Multi-root builds via an `xphp.json` manifest.** A project declares its source
roots, output directory, and hash length in an `xphp.json` at the project root;
`xphp compile` and `xphp check` auto-detect it (or take an explicit `--config`).
Roots are merged into a single source set — a template in one root can reference a
type in another — and include patterns accept a `**` globstar for recursive
discovery. A consuming build compiles only the `.xphp` sources whose own
`xphp.json` opts in (so `"vendor/**"` picks up the dependencies that ship a
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
contains<U : E>(U $value): bool }` — has its bound **grounded** against the receiver's
concrete type argument: `Box<Fruit>::contains<Banana>` is accepted when `Banana <: Fruit`,
and a genuine violation (`Box<Fruit>::contains<Rock>`) is rejected with the bound shown as
the real type, not `E`. The receiver's argument is threaded up the `extends`/`implements`
chain, so a method declared on a generic interface/base and inherited by a concrete
collection is grounded too. The receiver's type is determined from a typed parameter or
`$this->prop`, a `new`-constructed local, a value whose type comes from a method return /
chained call / `self`/`static` factory, and a branch whose arms agree on the same
parameterised type. A bound that references a sibling parameter is grounded the same way,
at the class level (`class Pair<T, U : T>`) and the method level (`<U, V : U>`). This is the
sound, element-typed alternative to a `mixed` parameter on a covariant `<+E>` collection
(`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
local — rather than an unchecked call. Nothing knowable is skipped, and no check is deferred
to runtime. **Lowering:** a method whose bounded parameter is used only as a direct input
(`U $value`) is emitted as one `E`-typed member per class instantiation
(`contains_<Fruit>(Fruit)`) rather than one per call-site type — so a self-call that *forwards*
the parameter, `probe<U : E>(U $v) { return $this->contains::<U>($v); }`, compiles and runs (the
idiomatic way to call an element-consuming method from inside the class). A `$this`-rooted
forward to a *non-erasable* method (parameter used nested, in the return, or structurally), and
a direct concrete `$this->contains::<Banana>()`, remain compile errors
(`xphp.unspecializable_self_call` / `xphp.bound_unprovable`) — never a runtime fault. **Covariant
interfaces:** the method may be declared on a covariant interface (`Collection<+E>`) and called
through an upcast (`ListColl<Book>` used as `Collection<Product>`) — the implementer specialization
is scheduled and inherited down the covariant chain automatically. When inheritance can't carry it
there — the implementing class has another `extends` parent, implements only a *parent* of the
interface, or reorders the `implements` clause — the member is instead emitted **directly** onto the
upcast source, with its bounded parameter widened to the supertype argument and its body read at the
source's own element type (sound because the source's element is a subtype of the supertype). The
upcast remains a compile error (`xphp.unschedulable_covariant_upcast`) — never emitted load- or
runtime-fataling code — only where no emittable class body exists (a truly abstract or trait-only
method), where the method's return type names the element parameter (the widened argument would
escape through a narrower return), or where its parameters are bounded by different enclosing
parameters (no single member can be derived). See [type bounds](docs/syntax/type-bounds.md) and
[ADR-0018](docs/adr/0018-grounding-method-generic-bounds-on-enclosing-type-parameters.md).
- **Generic methods resolved through inheritance.** A generic method declared on a
base or abstract class is now callable by turbofish on a *subclass* receiver —
instance (`$child->m::<int>()`), static (`Child::m::<int>()`), and nullsafe
(`$child?->m::<int>()`). It is specialized once on its declaring class and
inherited, so the call dispatches to the real member. An **unresolved** turbofish —
a generic method that exists on neither the receiver nor any ancestor — is now a
compile error (`xphp.unresolved_generic_call`) instead of an emitted call to a
stripped method that fatals at runtime.
- **`xphp check`** — a validate-without-emitting CI gate. It runs every generic
validation `xphp compile` does (bounds, variance, defaults, missing/duplicate
generics, unsupported closures), but collects **all** problems in one run —
each as a structured diagnostic with a `file:line` — instead of aborting on the
first. Exit codes: `0` clean, `1` ≥1 error, `2` operational failure.
`--format=text|json|github` (the `github` format emits PR annotations).
- **PHPStan over the compiled output.** When the generic checks pass, `xphp check`
compiles to a throwaway directory, runs **your** PHPStan over the concrete
(monomorphized) output, and maps each finding back to the originating `.xphp`
template declaration — naming the concrete instantiation that surfaced it. One
config (your `phpstan.neon` drives level/rules), one gate, one exit code.
`phpstan/phpstan` stays optional and is never bundled in the PHAR; a missing
binary or a failed run is a non-failing Warning. Opt out with `--no-phpstan`;
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
`T ...$items`. The parameter keeps its **real** element type on every
specialisation (`Book ...$items`, not `mixed`), so construction is
**runtime-type-checked** while `ImmutableList<Book>` still extends
`ImmutableList<Product>` — PHP exempts `__construct` from LSP, so the
specialisations' constructors may differ across the edge. A public/protected
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
on a **private** property — declared or promoted, mutable or readonly — so the
natural covariant shape
`class Producer<+T> { public function __construct(private T $item) {} ... }`
compiles, keeps its **real** substituted slot type (nothing erased), and stays
runtime-type-checked. PHP does not type-check private property types across an
`extends` chain (a private slot is per-declaring-scope and never inherited) and
a private member is invisible to the variance surface, so it carries any variance
soundly. Public/protected properties (including public/protected promoted params,
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`
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
container relates by that container's element type (`Tuple<ImmutableList<Book>, Tag>`
is usable where a `Tuple<Collection<Product>, Tag>` is required, because
`ImmutableList<Book> ⊑ Collection<Product>`). The argument relationship is proven by
threading the subtype's element up its `implements`/`extends` chain to the supertype's
template and comparing under the inner template's variance; the edge is emitted only
when positively provable, so the covariance now holds at runtime (`instanceof`, type
hints) and not only at `check`. Previously such an upcast passed `check` but fatal'd at
load. See [variance](docs/syntax/variance.md).
- **A contravariant generic may be consumed by a covariant class.** A method parameter typed
by a contravariant generic of the class's covariant parameter — `class Box<+E> { pick(
Comparator<E> $c): ?E }` where `Comparator<-T>` — is now accepted: `E` sits in a
contravariant slot inside a contravariant parameter position, which composes to a covariant
position a `+E` may occupy (sound under upcast — a `Comparator<Product>` compares the `Book`
elements of a `Box<Book>` viewed as `Box<Product>`). Variance validation now routes every
type-constructor-nested type-parameter through the composing check (which already knew the
inner slot's variance) instead of judging it by the bare outer position, so this sound,
`mixed`-free `sortedWith`/`minWith`/`pick` shape compiles instead of being wrongly rejected.
A bare `E` in a parameter position is still rejected — only the composed position is
covariant. See [variance](docs/syntax/variance.md).

### Changed

- **BREAKING — a `final` variant class is now rejected.** A `final class Box<+T>`
previously compiled, with the generated specialization silently dropping `final`
so the `extends` subtype edge between specializations could land — which made
`ReflectionClass::isFinal()` disagree with the written source. A covariant /
contravariant class marked `final` is now a compile error (a `final` class can't
anchor the `extends` edge); omit `final` on a variant template. Non-variant
generic classes are unaffected. See [variance](docs/syntax/variance.md).

### Fixed

- **Undeclared type parameters are now rejected** instead of silently compiling to
a reference to a non-existent class. A bare, single-segment, non-imported type
name used in a generic member, bound, or default that is neither a declared type
parameter nor a known type — e.g. `interface Foo<Z> { add(T $x); }` or
`class Box<T: Nonexistent>` — fails `xphp compile` and is reported by `xphp check`
as `xphp.undeclared_type`. Covers class/interface/trait members and method,
function, closure, and arrow generics. Imported (`use`) and fully-qualified names
are unaffected.
- **Too many type arguments are now rejected** instead of silently truncated:
`Box::<int, string>` for a one-parameter `Box` reports `xphp.too_many_type_arguments`.
- **A turbofish call on an undeterminable receiver is now rejected** instead of
silently emitting a runtime fatal. A generic method call like `$x->m::<int>()` is
specialized at compile time and the generic method is stripped from its class, so
when the receiver's type can't be determined — an untyped `foreach` variable, a
local whose type is ambiguous after a branch — the call previously compiled to
`$x->m(...)`, a call to a method that no longer exists (an "undefined method" fatal
at runtime). It now fails `xphp compile` and is reported by `xphp check` as
`xphp.undetermined_receiver`, with the fix: give the receiver a statically-known
type. Ground or fail — the compiler never emits a call it knows will fatal.
- **A class bound that references a sibling type parameter is now checked**
correctly. A bound such as `class Pair<T, U : T>` grounds `T` against the
supplied argument instead of treating `T` as a phantom class — which previously
rejected valid code with a misleading "does not extend/implement T".
- **A scalar bound is no longer flagged as an undeclared type.** A bound naming a
scalar (`int`, `string`, …) is no longer reported as `xphp.undeclared_type`.
- **A turbofish-less call to a generic method, function, or closure is now rejected**
instead of silently skipped. A method generic takes no inference, so a forgotten
turbofish (`$x->pick('a')` instead of `$x->pick::<string>('a')`) previously emitted a
call to the stripped mangled member and fataled at runtime — clean `check` and
`compile`. It now fails `xphp compile` and is collected by `xphp check` as
`xphp.missing_type_argument`, across instance, static, free-function, and closure
calls. A generic whose type parameters are all defaulted still resolves; a
first-class callable (`pick(...)`) and a non-generic call are unaffected.

## [0.2.1] - 2026-06-17

### Fixed

- The Composer autoloader is located when xphp is installed as a project
dependency, not only when run from its own checkout.
- Bare imported (`use`) names are fully-qualified in specialized classes, so a
generated specialization in a mirrored namespace resolves them correctly.

## [0.2.0]

The first feature release on top of the core monomorphization pipeline.
Expand Down Expand Up @@ -77,8 +258,9 @@ for a hands-on look at every feature below.

These are documented in full in the [caveats](docs/caveats.md):

- Variance markers (`+T` / `-T`) are class-level only — not yet on
methods, free functions, closures, or arrows.
- Variance markers (`+T` / `-T`) are class-level only **by design** — not
supported on methods, free functions, closures, or arrows (a function or
closure specialization has no stable class identity for a subtype edge).
- Generic closures and arrows that capture `$this`, and `static`
closures, are rejected at the call site.
- Reflection and closure serializers see the dispatcher shape, not the
Expand All @@ -101,6 +283,7 @@ These are documented in full in the [caveats](docs/caveats.md):
- Build-time hash-collision detection and a configurable
`XPHP_HASH_LENGTH` (16–64).

[Unreleased]: https://github.com/xphp-lang/xphp/compare/v0.2.0...HEAD
[0.3.0]: https://github.com/xphp-lang/xphp/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/xphp-lang/xphp/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/xphp-lang/xphp/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/xphp-lang/xphp/releases/tag/v0.1.0
23 changes: 19 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
## Test

```bash
make test/unit # PHPUnit on the default PHP 8.4 runtime
make test/unit/php85 # only tests tagged `@group php85`; needs a PHP 8.5 runtime
make test/mutation # Infection, MSI under a 95 % gate
make test/unit # PHPUnit on the default PHP 8.4 runtime
make test/unit/php85 # only tests tagged `@group php85`; needs a PHP 8.5 runtime
make test/phpstan-pass # only tests tagged `@group phpstan` (the `xphp check` PHPStan pass)
make test/mutation # Infection, MSI under a 95 % gate
```

The supported runtime is PHP `^8.4`. Tests that exercise newer-PHP syntax
(e.g. the 8.5 pipe operator) are tagged `@group php85`; `make test/unit`
excludes them and they self-skip via `#[RequiresPhp]` off an 8.5 host. CI
runs them in a dedicated PHP 8.5 job.

The `xphp check` PHPStan-pass tests are tagged `@group phpstan` — they shell out
to a real phpstan subprocess (slow), so `make test/unit` excludes them and they
self-skip when `vendor/bin/phpstan` is absent (the same self-skip idea as
`@group php85`). CI runs them in a dedicated job; locally use `make
test/phpstan-pass`. (Not to be confused with `make lint/phpstan`, which runs
PHPStan over `src/`.)

No PHP 8.5 locally? Run them in the bundled 8.5 container:

```bash
Expand All @@ -21,4 +29,11 @@ docker compose run --rm php85 make test/unit/php85

CI gates every PR on these targets. `infection.json5` carries a curated set
of per-mutator `ignore` rules for genuinely-equivalent / defensive
mutations so the report only surfaces real test gaps.
mutations so the report only surfaces real test gaps.

## Architecture decisions

Architecturally significant decisions — the ones that are expensive to reverse —
are recorded as [Architecture Decision Records](docs/adr/README.md). When you make
such a decision, add a new ADR under `docs/adr/` (copy
[`docs/adr/0000-adr-template.md`](docs/adr/0000-adr-template.md)).
Loading
Loading