diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5c8d7bb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Pin line endings on checked-in snapshot artifacts so byte-equality +# assertions hold across Linux/macOS/Windows contributors regardless +# of git autocrlf settings. +test/snapshot/**/*.expected.php text eol=lf +test/fixture/compile/**/*.xphp text eol=lf diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 867453d..94e6fde 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -34,6 +34,26 @@ jobs: - name: Run PHPUnit run: make test/unit + phpstan: + name: PHPStan + 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 PHPStan + run: make lint/phpstan + infection: name: Mutation testing runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..63820e0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +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.2.0] + +The first feature release on top of the core monomorphization pipeline. +Generics now cover the full declaration surface — classes, interfaces, +traits, methods, free functions, closures, and arrow functions — each +with bounds, defaults, and variance, behind forward-compatible turbofish +syntax at every call site. See the [syntax tour](docs/syntax/index.md) +for a hands-on look at every feature below. + +### Added + +- **Type-parameter bounds**, checked at compile time, with error + messages that point at the source-level instantiation rather than the + generated hash: + - Single upper bound — `class Box`. + - Intersection multi-bound — `T: \Stringable & \Countable`. + - Disjunctive normal form (DNF) — `T: (\Stringable & \Countable) | \Iterator`. + - F-bounded recursion — `class Sortable>`. + - Built-in interface whitelist (`Stringable`, `Countable`, `Iterator`, + `Traversable`, `ArrayAccess`, `JsonSerializable`, `Throwable`, …) so a + user class that `implements \Stringable` satisfies a bound without the + interface needing a source declaration. +- **Default type parameters** at every level — class, method, free + function, closure, and arrow — with forward references + (`Pair`), declaration-time bound checks on fully-concrete + defaults, and empty-turbofish `::<>` (and bare `new Cache;`) for + all-defaults templates. +- **Variance** — `+T` (covariant) and `-T` (contravariant) markers on + type parameters: + - Position rules enforced at parse time (covariant in return, + contravariant in parameter; both forbidden in properties, + constructors, bounds, and defaults). + - Real subtype edges between specializations — `Producer` + actually `extends Producer` when `Banana extends Fruit`. + - Inner-template variance composition across nested generic args, + validated at compile time after all templates are known. +- **Function-level generics**: + - Generic methods on static and instance receivers. + - Generic free functions at namespace scope and bare top-level. + - Nullsafe instance turbofish — `$obj?->m::()`. + - Receiver-type analysis for `$this`, typed parameters, typed + properties, and local `$x = new Foo()` assignments; conservative + de-specialization when branch arms disagree on a class. +- **Generic closures and arrow functions** — `function(...) { … }` and + `fn(...) => …`, specialized through a closure dispatcher: + - Explicit `use (...)` clauses, including by-reference `use (&$x)`. + - Implicit arrow captures lifted into the dispatch shape. + - Default type parameters on closures and arrows. +- **Pseudo-types** — `self` / `static` / `parent` in type-hint + positions and at constructor sites (`new self::()`, + `new static::()`, `new parent::()`). +- **`T[]` array sugar** — documentation-friendly shorthand that lowers to + plain `array` at compile time. (`array` remains rejected, per the + RFC.) +- **Documentation** — a full `docs/` tree: a per-feature + [syntax tour](docs/syntax/index.md), [getting started](docs/getting-started.md), + [how it works](docs/guides/how-it-works.md), + [runtime semantics](docs/guides/runtime-semantics.md), + [type-system comparison](docs/guides/comparison.md), + [caveats](docs/caveats.md), [errors](docs/errors.md), and the + [roadmap](docs/roadmap.md). + +### Changed + +- Call-site syntax aligned with the PHP RFC: turbofish `Name::<...>` at + call sites. Parenless `new Name<...>` is now rejected with a message + pointing at the `::<...>` form. + +### Known limitations + +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. +- 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 + original closure body. +- The variance validator does not walk into trait-imported method + signatures. + +## [0.1.0] + +### Added + +- Initial release: the core monomorphization pipeline that compiles + `.xphp` generic **classes** into vanilla PHP — one specialized class + per concrete instantiation, with native type hints and no runtime + dispatch overhead. +- Arbitrarily nested generics with a fixed-point specialization loop + (depth cap plus cycle detection). +- Marker-interface trick so `$x instanceof App\Box` holds across every + `Box<...>` specialization. +- 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.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 diff --git a/Makefile b/Makefile index 6f222c6..e323a9a 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,20 @@ test/unit: php vendor/bin/phpunit +.PHONY: lint/phpstan +# Static analysis at level 7. Memory limit lifted because deep generic +# array shapes (BoundDict's recursive operand chains, marker shape +# stacks) push the default 256M ceiling. +lint/phpstan: + php vendor/bin/phpstan analyse --memory-limit=2G --no-progress + .PHONY: test/mutation # Gate at 95% (current is 100%): keeps a small headroom so a single # new mutation can land in a follow-up commit and still pass while # the test that kills it is being written. Raise to 100% once the # repo is stable enough that no new test gaps are expected. test/mutation: - php vendor/bin/infection --show-mutations=max --threads=max --min-covered-msi=95 + php -d memory_limit=-1 vendor/bin/infection --show-mutations=max --threads=max --min-covered-msi=95 # Humbug Box is the standard tool for compiling a Composer-managed # PHP project into a single self-contained PHAR. Pinned to a known- diff --git a/README.md b/README.md index 2be9534..f318ffe 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,34 @@ ## What it is `xphp` is a superset of `php` that gives developers real generics, -powered by [monomorphization](https://en.wikipedia.org/wiki/Monomorphization) at compile time. +powered by monomorphization at compile time -- one specialized class +per concrete instantiation, no runtime dispatch overhead. In a more inspirational mood, it is a fast lane for the `php` language, a bridge between what developers need today and what `php` will support in the future. +> **Heads up**: `xphp` is heavily inspired by +> [PHP RFC: bound-erased generic types](https://wiki.php.net/rfc/bound_erased_generic_types), +> and the RFC drives the surface syntax -- turbofish `Name::<...>` at call +> sites, bare `<...>` at declarations and type-hint positions, `:` for bounds. +> The **intent** is that any `.xphp` source you write today stays valid against +> a future PHP runtime. +> +> Runtime semantics may diverge. `xphp` monomorphizes each generic +> instantiation into a distinct, fully-typed class -- the concrete type is +> baked in and visible to reflection. The RFC erases bounds at runtime +> instead. Both are honest design choices for different goals, and the gap +> may widen as the RFC evolves. `xphp` will track the syntax wherever +> practical and call out any divergence explicitly in the docs. + ## How it works Generics specialize into concrete classes with native typehints the engine enforces, so the safety is real and the abstraction compiles away to nothing. -The compiler turns `xphp` into regular `php`. In the end it's good ~~old~~ -modern `php`, but developers and AI agents have richer abstractions to design -better solutions. +The compiler turns `xphp` into regular `php`. The runtime sees ordinary +classes; richer abstractions live entirely in the source you write and +at build time. ## Ecosystem and community first @@ -62,147 +77,73 @@ compiled `php` files. ## Generics: the start, not the finish line -Adding native generics to `php` -- a [long-awaited php feature](https://wiki.php.net/rfc/generics) -- -is genuinely [hard work](https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/). +Adding native generics to `php` -- +a [long-awaited php feature](https://wiki.php.net/rfc/generics) -- +is +genuinely [hard work](https://thephp.foundation/blog/2024/08/19/state-of-generics-and-collections/). -The object model that's served the ecosystem for two decades doesn't bend easily. +The object model that's served the ecosystem for two decades doesn't bend +easily. Supporting generics proves that the compile-to-vanilla model handles non-trivial -type-system additions. The remaining features are on the [roadmap](docs/roadmap.md): +type-system additions. The remaining features are on +the [roadmap](docs/roadmap.md): type aliases, literal types, mapped and conditional types to name a few. -## Getting started - -### 1. Install the xphp package +## Quick start ```bash composer require --dev xphp-lang/xphp ``` -### 2. Enhance your PSR-4 autoload config - -Add a PSR-4 entry to `composer.json` so the standard autoloader finds the -specialized classes without manual `require`. +Add the autoload mapping to your `composer.json` and run +`composer dump-autoload`: -```json5 +```json { "autoload": { "psr-4": { - // generics will be converted into specialized classes, - // they need to have their own namespace. - "XPHP\\Generated\\": "/Generated/", - "App\\": [ - // path to your normal/regular php code - "", - // some `xphp` files just need to be rewritten into native php to use specialized classes, - // but their namespace will remain the same. - "" - ], + "XPHP\\Generated\\": ".xphp-cache/Generated/", + "App\\": ["src", "dist"] } } } ``` -After that, update your autoload file via `composer dump-autoload`. - -### 3. Write `xphp` code - -You can define a generic class and instantiate it exactly as you would expect: +Write a generic class and use it: ```php -// /Collection.xphp +// src/Collection.xphp namespace App; class Collection { - private T[] $items; - - // The generic 'T' is used directly in the constructor signature - public function __construct(T ...$items) { - $this->items = $items; - } - - public function first(): ?T { - return $this->items[0] ?? null; - } + public function __construct(public T ...$items) {} + public function first(): ?T { return $this->items[0] ?? null; } } -// /main.xphp +// src/Use.xphp namespace App; -$users = new Collection( - new User('Alice'), - new User('Bob') -); +$users = new Collection::(new User('Alice'), new User('Bob')); +echo $users->first()->name; ``` -### 4. Compile +Compile: ```bash -vendor/bin/xphp compile +vendor/bin/xphp compile src dist .xphp-cache ``` -| Argument | Required | Default | Purpose | -|------------|----------|----------------|--------------------------------------------------------------------------------------| -| `` | yes | -- | Directory of `.xphp` files (PSR-4 layout) | -| `` | no | `dist` | Where rewritten `.php` files land -- your user code with generic call sites replaced | -| `` | no | `.xphp-cache` | Where specialized classes live | - -p.s. you can `gitignore` files in `` and `` as they can be generated in your CI/CD pipeline. - -#### Sample output - -The sample below uses a readable name (`Collection_User`) for clarity. The compiler actually emits hashed FQNs of the form `\XPHP\Generated\App\Collection\T_` -- see the [generics reference](docs/type-system/generics/index.md) for the real scheme. - -```php -// /Generated/Collection_User.php -namespace XPHP\Generated; - -use App\User; - -class Collection_User { - private array $items; - - // The generic 'T' is replaced natively with the concrete 'User' type - public function __construct(User ...$items) { - $this->items = $items; - } - - public function first(): ?User { - return $this->items[0] ?? null; - } -} - -// /main.php -namespace App; - -use XPHP\Generated\Collection_User; - -// The generic instantiation is mapped directly to the generated class -$users = new Collection_User( - new User('Alice'), - new User('Bob') -); -``` - -### 5. Deploy - -The compiler monomorphizes the generic classes and converts downstream code -into native `php` code. - -Meaning every place where generics are declared or used is converted into normal -`php` code. No impact on the runtime. You still deploy `php` code. - -### Project structure - -``` -/ -├── # php/xphp source files (PSR-4: namespace mirrors directory structure) -├── # rewritten .php (gitignored, generated) -├── # specialized classes (gitignored, generated) -└── composer.json # PSR-4: XPHP\Generated\ => /Generated/ -``` +That's the whole loop: install, set up autoload, write `.xphp`, +compile. `dist/` holds your rewritten code; `.xphp-cache/Generated/` +holds the specialized classes. Both can be gitignored and rebuilt +in CI. ## See also -- [Type-system comparison](docs/type-system/comparison.md) -- [Full generics reference](docs/type-system/generics/index.md) +- [Getting started](docs/getting-started.md) -- full walkthrough including PSR-4 details, runtime semantics, and what the generated PHP looks like +- [Syntax tour](docs/syntax/index.md) +- [Caveats](docs/caveats.md) +- [Type-system comparison](docs/guides/comparison.md) +- [Roadmap](docs/roadmap.md) +- [Changelog](CHANGELOG.md) diff --git a/composer.json b/composer.json index 7dedfc6..255bce2 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ }, "require-dev": { "phpunit/phpunit": "^13.0", - "infection/infection": "^0.33" + "infection/infection": "^0.33", + "phpstan/phpstan": "^2.2" }, "autoload": { "psr-4": { @@ -38,7 +39,8 @@ }, "autoload-dev": { "psr-4": { - "XPHP\\": "test/" + "XPHP\\": "test/", + "XPHP\\TestSupport\\": "test/TestSupport/" } }, "bin": [ diff --git a/composer.lock b/composer.lock index 8ff9d4a..f57c263 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5ff71bb52c079c1ea97156c3c08a9532", + "content-hash": "cb46e25ce55ce78ec96df2693e53d69a", "packages": [ { "name": "nikic/php-parser", @@ -1858,6 +1858,70 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.2.2", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-06-05T09:00:01+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "14.1.9", diff --git a/docs/caveats.md b/docs/caveats.md new file mode 100644 index 0000000..ce37261 --- /dev/null +++ b/docs/caveats.md @@ -0,0 +1,495 @@ +# Caveats + +Every shipped feature in xphp has trade-offs. This page collects the +ones you'll hit in real code, in the order they're likely to bite, +each with the underlying reason and the workaround. + +Pages in the [syntax tour](syntax/) link back to specific sections +here using anchor links — search this page for the same heading text. + +## `$this`-capturing arrows and closures rejected + +### ❌ What doesn't work + +```php +class Holder { + public int $v = 5; + public function go(): int { + $f = fn(T $x): int => $x + $this->v; + return $f::(2); + } +} +``` + +``` +Generic arrow `$f::<...>(...)` captures `$this`, which is not yet +supported. Rewrite as a method on the enclosing class, or extract +the value of $this->property into a local variable before the arrow. +``` + +The same error fires for generic closures whose body references +`$this`. + +### Why + +The closure variable rewrite produces a dispatcher closure that +forwards to top-level specialized functions. Top-level functions +can't see the enclosing class's `$this`, and PHP doesn't allow +`use ($this)` on a closure either. Carrying `$this` would need +either a method-rewriting pass (today's level) or rewriting `$this->v` +to a lifted `mixed $__xphp_this` param everywhere — a deeper +refactor that's queued up for later. + +### ✅ Workaround + +Either rewrite as a method on the class: + +```php +class Holder { + public int $v = 5; + public function go(): int { + return $this->add::(2); + } + public function add(T $x): int { + return $x + $this->v; + } +} +``` + +Or extract the value to a local before the closure: + +```php +class Holder { + public int $v = 5; + public function go(): int { + $base = $this->v; + $f = fn(T $x): int => $x + $base; + return $f::(2); + } +} +``` + +--- + +## `static` closures not supported + +### ❌ What doesn't work + +```php +$f = static function(T $x): T { return $x; }; +$f::(42); +``` + +``` +Generic static closures cannot yet be specialized at call sites. +Rewrite the call site for `$f::<...>(...)` to use a named generic +function at file scope. +``` + +The same form is rejected at parse time when combined with defaults: + +```php +$f = static function(T $x): T { return $x; }; +``` + +``` +Generic parameter `T` has a default value, which is not yet supported +on static closures. Drop the `static` modifier or assign the closure +to a named function. +``` + +### Why + +The dispatcher closure that xphp emits to route specialized calls +needs a `$this`-binding target for one of the planned future +extensions. `static` closures explicitly block `$this` binding, +which removes that hook. + +### ✅ Workaround + +Drop the `static` modifier, or lift the body to a named function: + +```php +$f = function(T $x): T { return $x; }; // works +// or +function id(T $x): T { return $x; } +id::(42); // works +``` + +--- + +## Variance markers are class-level only + +### ❌ What doesn't work + +```php +function process<+T>(T $x): T { /* ... */ } // free function +class Box { + public function map<+U>(callable $f): Box { /* ... */ } // method +} +$producer = function<+T>(): T { /* ... */ }; // closure +$arrow = fn<+T>(T $x): T => $x; // arrow +``` + +``` +Variance markers `+T` / `-T` are not yet supported on methods, +functions, closures, or arrow functions; move the generic to a +class-level type parameter. +``` + +### Why + +Variance turns into real `extends` chains between specialized classes +(see [variance](syntax/variance.md)). Methods, functions, closures, +and arrows don't have a stable identity to anchor an `extends` chain +to — their specializations are functions, not classes, so there's +nothing for the subtype edge to attach to. + +### ✅ Workaround + +Put the template on a named class and use its method: + +```php +class Producer<+T> { + public function __invoke(): T { /* ... */ } +} +``` + +--- + +## Reflection on rewritten generic closures + +### ❌ What doesn't work + +```php +$triple = function(A $a, B $b, C $c): array { return [$a, $b, $c]; }; +$triple::(1, 'two', true); + +// After compile: +$ref = new ReflectionFunction($triple); +$ref->getNumberOfParameters(); // returns 2 -- the dispatcher's + // (string $tag, mixed ...$args) shape, + // NOT the original three. +$ref->getParameters()[0]->getName(); // returns '__xphp_tag' +``` + +Closure serializers (`opis/closure`, +`laravel/serializable-closure`) serialize the dispatcher closure +rather than the original body. + +### Why + +The variable holding the generic closure is rewritten to a +**dispatcher** closure with a fixed `(string $__xphp_tag, mixed ...$__xphp_args)` +signature. The real body lives in one or more top-level functions +keyed by the tag. Reflection only sees the dispatcher. + +Pre-this-machinery, generic closures couldn't be specialized at all +(they were rejected outright), so reflection-based serializers +couldn't see anything. The 2-arg dispatcher shape is an improvement +over a hard reject, just not a transparent one. + +### ✅ Workaround + +If your code path serializes closures via reflection, use a named +generic function instead: + +```php +function pair(K $k, V $v): array { return [$k, $v]; } + +// Now reflection sees the real `pair_T_` specialization: +$ref = new ReflectionFunction('App\\pair_T_'); +$ref->getNumberOfParameters(); // 2 (the real K, V) +``` + +If you need the variable form for some other reason (currying, etc.), +serialize the captured state separately and reconstruct the closure +on the receiving side. + +--- + +## Reified-T is an xphp-specific divergence + +### ❌ What doesn't port + +```php +function decode(string $json): T { + $data = json_decode($json, true); + return new T(...$data); // works in xphp +} + +if ($x instanceof T) { /* ... */ } // works in xphp +$class = T::class; // works in xphp +``` + +The same code under a future RFC-aligned (erasure-based) PHP runtime +would NOT work — `T` would be erased at runtime, so `new T(...)`, +`instanceof T`, and `T::class` all become meaningless. + +### Why + +xphp uses monomorphization: every `Box` is a real class with +`int` baked into every signature. Inside the specialized body, `T` +literally becomes `int`, so reified-T operations Just Work. + +The RFC's erasure model doesn't keep T at runtime, so any code +relying on T being a runtime value would break on the future native +PHP runtime. + +### ✅ Workaround + +If forward-portability to a future RFC-aligned runtime matters more +than reified-T ergonomics, avoid `instanceof T`, `T::class`, and +`new T(...)`. Pass class names as explicit arguments instead: + +```php +function decode(string $json, string $class): mixed { + $data = json_decode($json, true); + return new $class(...$data); +} +$user = decode($payload, User::class); +``` + +If forward-portability isn't a concern (you're not planning to +target a future erased-T runtime), use reified-T freely — it's one +of the things monomorphization makes naturally easy. + +The README's "Heads up" banner mentions this divergence too. + +--- + +## Branching narrowing precision loss + +### ❌ What's less precise than ideal + +```php +$x = new Foo(); +if ($cond) { + $x = new Bar(); +} +$x->m::($arg); // de-specializes -- not a Foo or Bar method call +``` + +The post-branch call drops to a non-specialized path because the +analysis can't prove a single class for `$x`. + +> This is a **precision** issue, not a soundness one. xphp will NOT +> pick the wrong class — it just gives up on the specialization. + +### Why + +Receiver-type analysis is conservative: when `$x` is reassigned +inside a branch and the arms don't agree on a class, post-branch +calls fall back to a non-specialized path. Otherwise the compiler +could pick a class that doesn't match what the variable actually +holds at runtime. + +The same-arms-agree shape IS supported: + +```php +$x = new Foo(); +if ($cond) { + $x = new Foo(); // same class as the other arm +} +$x->m::($arg); // specializes against Foo +``` + +### ✅ Workaround + +Either separate the call sites by branch: + +```php +if ($cond) { + $x = new Bar(); + $x->m::($arg); // specializes against Bar +} else { + $x = new Foo(); + $x->m::($arg); // specializes against Foo +} +``` + +Or use a typed local before the call: + +```php +/** @var Foo|Bar $x */ +$x = $cond ? new Bar() : new Foo(); +// Manually call the right specialized name on each branch. +``` + +A future version may add union-type tracking with runtime dispatch, +but it's queued behind higher-priority work. + +--- + +## Variance validator and trait `use` + +### ❌ What doesn't get checked + +```php +trait HasItem { + public function set(T $item): void { /* ... */ } +} + +class Container<+T> { // 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 + // it doesn't walk into trait-imported methods. +} +``` + +The variance validator emits no error today on trait-imported method +signatures. If the trait's method places T in a forbidden position, +the violation slips through compilation. + +### Why + +The validator walks `ClassMethod` nodes declared directly on the +ClassLike. Trait method bodies are stitched into the class by Zend +at compile time; the AST we walk pre-stitching doesn't see them. + +### ✅ Workaround + +Manually audit traits used by variant generic classes. If you control +the trait, copy the body into the class directly so the validator +can see it. + +--- + +## `T[]` is xphp-only + +### ❌ What doesn't work + +```php +class Map { + private array $items; // rejected +} +``` + +The +[PHP RFC for generics](https://wiki.php.net/rfc/bound_erased_generic_types) +explicitly bans the `array` syntax, and xphp follows. + +### Why + +The RFC took a clear position against expanding `array` into a +generic. xphp matches that boundary so source written today is +forward-compatible. + +### ✅ Workaround + +Use the `T[]` sugar (xphp-only but innocuous) or write `array` +directly: + +```php +class Map { + private array $items; // works, no compile-time element check +} +``` + +If you need a typed key/value container, build it from a generic +class: + +```php +class Map { + private array $items = []; + public function set(K $k, V $v): void { $this->items[$k] = $v; } + public function get(K $k): V { return $this->items[$k]; } +} +``` + +--- + +## Duplicate generic template declaration + +### ❌ What doesn't work + +Two `.xphp` files in the same source set both declare a generic +function (or class) with the same FQN: + +``` +Generic function template "App\identity" already declared (in +src/Util.xphp); duplicate declaration in src/Other.xphp. +``` + +(The same error fires for classes — the message uses +`Generic template` instead of `Generic function template`.) + +### Why + +Specialization keys every template by FQN. Two definitions with the +same name would collide on the generated specializations and silently +overwrite each other; xphp surfaces both source paths instead so you +can see what's clashing. + +### ✅ Workaround + +Pick one canonical location. If you genuinely need the same name in +two namespaces, put them in different namespaces — the FQN differs +and they won't collide. + +--- + +## Invalid default expression shape + +### ❌ What doesn't work + +```php +class Bad {} // union not allowed +class Bad {} // nullable not allowed +class Bad {} // intersection not allowed +``` + +``` +Generic parameter `T` has an invalid default; only a single concrete +or generic type is allowed after `=` (no nullable or union shapes). +``` + +### Why + +Defaults are substituted at instantiation time. Supporting union / +intersection / nullable defaults would require a runtime decision on +which arm to materialize, which isn't compatible with +monomorphization's "one specialization per concrete arg tuple" model. + +### ✅ Workaround + +Pick a single default and let users override it explicitly with +turbofish: + +```php +class Container {} +// users can still write: new Container:: or new Container:: +``` + +--- + +## Anonymous classes can't be generic + +### ❌ What doesn't work + +```php +$x = new class { + public T $item; +}; +``` + +Both the RFC and xphp forbid this — `new class { ... }` is a +parse error. + +### Why + +Anonymous classes have no name to carry through specialization. The +template-to-specialization machinery is keyed on the FQN; anonymous +classes don't have one. + +### ✅ Workaround + +Lift to a named class: + +```php +class TempContainer { + public T $item; +} +$x = new TempContainer::(); +``` diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..1e14d3c --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,147 @@ +# Errors and diagnostics + +Every compile-time rejection from xphp lists the error message +verbatim below, paired with the docs section that explains the +constraint. Search this page for the text your compile output shows. + +## Quick index + +| If the message contains... | Read | +|----------------------------|------| +| `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 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) | +| `has no default but follows a parameter with a default` | [Defaults — required-after-default rule](syntax/defaults.md) | +| `has an invalid default; only a single concrete or generic type is allowed` | [Caveats — invalid default expression shape](caveats.md#invalid-default-expression-shape) | +| `cannot use itself as a bound` | [Type bounds — F-bounded](syntax/type-bounds.md) | +| `already declared` ... `duplicate declaration` | [Caveats — duplicate generic template declaration](caveats.md#duplicate-generic-template-declaration) | +| `was instantiated but never defined` | The template was used but no source file declared it — typo or missing import | +| `was instantiated with N type argument(s) but parameter ... has no default` | [Defaults](syntax/defaults.md) — supply all required args or add defaults | +| `Nested generic specialization exceeded depth` | A generic refers to itself transitively too deeply (compiler aborts at depth 16) — usually a recursive instantiation cycle. Refactor to break the cycle. | +| `Parser returned null AST` | The source file isn't valid PHP after the generic strip pass. Run `php -l .xphp` mentally on the cleaned source — most often a syntax error in the user code that's unrelated to generics. | + +## Full error texts (verbatim) + +For grep-from-output workflows, here are the message strings exactly +as the compiler emits them. + +### `$this`-capturing arrows / closures + +``` +Generic `$::<...>(...)` captures `$this`, which +is not yet supported. Rewrite as a method on the enclosing class, or +extract the value of $this->property into a local variable before +the . +``` + +### `static` closures + +``` +Generic static closures cannot yet be specialized at call sites. +Rewrite the call site for `$::<...>(...)` to use a named +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 composition violation + +``` +Variance violation in template