Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 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
13050fd
docs: propose erased seams to break self-reintroducing specialization
math3usmartins Jun 29, 2026
8b6b936
test: characterize the self-reintroducing grouping boundary
math3usmartins Jun 29, 2026
61b86b0
docs: cite prior art for the erased seam and fix a stray fence
math3usmartins Jun 29, 2026
999312e
docs: accept diagnose-and-restructure for self-reintroducing speciali…
math3usmartins Jun 29, 2026
d974a51
feat(monomorphize): name every cycle class and its source file when s…
math3usmartins Jun 29, 2026
128fb64
test(monomorphize): assert the whole divergence message instead of su…
math3usmartins Jun 29, 2026
dfe9c4f
test(monomorphize): pin the exact incompatible value-list specs in th…
math3usmartins Jun 29, 2026
1a6c1fa
test(monomorphize): document why the load fatal is asserted by parts
math3usmartins Jun 29, 2026
75f3d3a
docs: document the workaround for self-reintroducing specialization
math3usmartins Jun 29, 2026
7ae3ed4
test+docs: adapt the self-reintroducing-tower characterization to eag…
math3usmartins Jun 29, 2026
e87c87d
fix(monomorphize): supply erased members across a covariant nested-ge…
math3usmartins Jun 30, 2026
91e7612
fix(monomorphize): recognize scalar type names in a generic bound leaf
math3usmartins Jun 30, 2026
3b6f8d1
fix(monomorphize): resolve scalar-aliased class names as classes in e…
math3usmartins Jun 30, 2026
b05ea8c
feat(cli): run the validation gate by default on compile
math3usmartins Jun 30, 2026
ba8e706
Merge pull request #21 from xphp-lang/feat/safe-default-compile-gate
math3usmartins Jul 5, 2026
3c9a03e
feat(variance)!: adopt Kotlin-style `out`/`in` variance markers
math3usmartins Jul 6, 2026
bf628d0
fix(variance): render diagnostics with `out`/`in` markers
math3usmartins Jul 6, 2026
29e352e
docs(variance): document `out`/`in` variance markers
math3usmartins Jul 6, 2026
4a371db
Merge pull request #22 from xphp-lang/feat/kotlin-variance-markers
math3usmartins Jul 6, 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
234 changes: 231 additions & 3 deletions CHANGELOG.md

Large diffs are not rendered by default.

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)).
25 changes: 21 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
# targets here.

.PHONY: test/unit
# Default runtime is PHP 8.4 (composer requires ^8.4). Tests exercising
# newer-PHP syntax are tagged `@group php85` and excluded here; they run
# on an 8.5 runtime via `make test/unit/php85`.
# Default runtime is PHP 8.4 (composer requires ^8.4). Two groups are excluded
# here: `php85` (newer-PHP syntax; runs on 8.5 via `make test/unit/php85`) and
# `phpstan` (the `xphp check` PHPStan pass, which shells out to a real phpstan
# subprocess and is slow; runs via `make test/phpstan-pass`).
test/unit:
php vendor/bin/phpunit --exclude-group php85
php vendor/bin/phpunit --exclude-group php85 --exclude-group phpstan

.PHONY: test/phpstan-pass
# The `xphp check` PHPStan-integration tests (tagged `@group phpstan`). They
# shell out to the consumer's phpstan binary and self-skip when vendor/bin/phpstan
# is absent. NOTE: distinct from `lint/phpstan`, which runs PHPStan over src/.
test/phpstan-pass:
php vendor/bin/phpunit --group phpstan

.PHONY: test/unit/php85
# Runs only the PHP 8.5-specific syntax tests (e.g. the pipe operator).
Expand All @@ -35,6 +43,15 @@ lint/phpstan:
test/mutation:
php -d memory_limit=-1 vendor/bin/infection --show-mutations=max --threads=max --min-covered-msi=95

.PHONY: test/check
# End-to-end self-test of the `check` gate: runs the real bin/xphp binary
# against the check fixtures and asserts the 0/1/2 exit contract plus that the
# text/json/github renderers all emit. Reused by release.yml against the built
# PHAR (override XPHP_BIN="php dist/xphp.phar"). Complements the in-process
# CheckCommandTest, which can't observe the shipped binary's process exit code.
test/check:
sh test/smoke/check.sh

# Humbug Box is the standard tool for compiling a Composer-managed
# PHP project into a single self-contained PHAR. Pinned to a known-
# good release (Box 4.6.6 supports PHP 8.4) so a new Box version
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,47 @@ compile. `dist/` holds your rewritten code; `.xphp-cache/Generated/`
holds the specialized classes. Both can be gitignored and rebuilt
in CI.

For a real project — and **required** once you consume another package's
generics — drop an `xphp.json` manifest at the project root instead of
repeating the paths on every invocation:

```json
{
"sources": ["src"],
"include": ["vendor/**"],
"target": "dist",
"cache": ".xphp-cache"
}
```

Then `compile`/`check` take no positional source — they resolve the
manifest (auto-detected in the working directory, or via `--config`):

```bash
vendor/bin/xphp compile # compiles this package + every included one
vendor/bin/xphp check
```

`include` globs auto-discover installed xphp packages (`vendor/**` finds
every one, at any depth, with no edit when you add another), so the
downstream build compiles the whole union into its own output. See
[Getting started](docs/getting-started.md#the-recommended-project-setup-an-xphpjson-manifest)
for the distribution model. The single-directory form above keeps working
unchanged.

To validate generics without emitting anything — a CI gate that reports
every bound/variance/etc. problem with a `file:line`:

```bash
vendor/bin/xphp check src # exit 1 if any error; --format=text|json|github
```

`check` runs all of xphp's generic validation (the specialization-loop guards
aside) and then, when those pass, runs **your** PHPStan over the compiled output
and maps the findings back to the `.xphp` template — one config, one gate (pass
`--no-phpstan` to skip it). You still run `compile` to emit the PHP. See
[Errors and diagnostics](docs/errors.md#xphp-check--validate-without-emitting).

## See also

- [Getting started](docs/getting-started.md) -- full walkthrough including PSR-4 details, runtime semantics, and what the generated PHP looks like
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"require": {
"php": "^8.4.0",
"nikic/php-parser": "^5.7",
"symfony/console": "^8.0"
"symfony/console": "^8.0",
"symfony/process": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^13.0",
Expand Down
132 changes: 66 additions & 66 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading