Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
322bb8c
feat(parser): align call-site syntax with PHP RFC bound-erased generics
math3usmartins Jun 4, 2026
f8d3ab7
fix(parser): reject parenless `new Name<…>` + flag unimplemented docs
math3usmartins Jun 4, 2026
b779fab
docs: link every RFC mention to wiki.php.net/rfc/bound_erased_generic…
math3usmartins Jun 4, 2026
364a589
docs(readme): add inspiration / divergence banner
math3usmartins Jun 4, 2026
c9f8a78
feat(parser): reject top-level self-reference bounds at declaration time
math3usmartins Jun 4, 2026
beb4955
feat(parser): recognize `self<T>` / `static<T>` / `parent<T>` in type…
math3usmartins Jun 4, 2026
ab8918d
test(parser): lock anonymous-class generic decl rejection
math3usmartins Jun 4, 2026
ec42bcd
feat(compiler): specialize bare top-level free generic functions
math3usmartins Jun 4, 2026
fe426a1
fix(parser): wire `self<T>` / `static<T>` / `parent<T>` through compile
math3usmartins Jun 5, 2026
0b5ad45
feat(compiler): specialize instance-method generic calls (item 11)
math3usmartins Jun 4, 2026
2baca04
feat(parser): multi-bound + BoundExpr + F-bounded recursion
math3usmartins Jun 5, 2026
b83a76c
refactor(parser): hoist namespace + use-map tracking into a shared he…
math3usmartins Jun 5, 2026
46ecad7
feat(parser): default type arguments
math3usmartins Jun 5, 2026
a4969af
feat(parser): variance annotations +T / -T
math3usmartins Jun 6, 2026
75d0549
refactor(parser): widen marker shape with bytePosition + kind
math3usmartins Jun 6, 2026
819fe0c
feat(parser): lift defaults to methods + free functions
math3usmartins Jun 6, 2026
ee947db
feat(parser): variance validator recurses into method bodies
math3usmartins Jun 6, 2026
42f286d
feat(parser): recognize generic closures, arrows, and variable turbofish
math3usmartins Jun 6, 2026
b623d82
feat(parser): specialize generic closures via top-level hoist
math3usmartins Jun 6, 2026
41448b1
feat(specializer): merge branching receiver types when all arms agree
math3usmartins Jun 6, 2026
f8915cd
feat(parser): strip pseudo-type turbofish without recording a marker
math3usmartins Jun 6, 2026
6fca224
feat(specializer): compose inner template variance after defs collect
math3usmartins Jun 6, 2026
343d8a2
feat(specializer): closure dispatcher with sentinel-arg-prefix routing
math3usmartins Jun 6, 2026
35d6421
feat(specializer): specialize generic arrows via dispatcher captures
math3usmartins Jun 6, 2026
3cf8ec7
feat(specializer): specialize closures with use() clauses incl. by-ref
math3usmartins Jun 6, 2026
3b5fa59
feat(parser): allow defaults on closures and arrows
math3usmartins Jun 6, 2026
4a4b401
test(fixture): durable artifacts for the four dispatcher consumers
math3usmartins Jun 6, 2026
04b2239
build(make): remove infection memory limit so dev runs don't OOM
math3usmartins Jun 6, 2026
2157a48
docs(public): refresh docs/ tree with syntax tour, caveats, errors
math3usmartins Jun 6, 2026
9124d59
test: introduce SnapshotHash + CompiledFixture, co-locate fixture
math3usmartins Jun 7, 2026
1df613d
build(phpstan): adopt at level 5, triage src/ findings to zero
math3usmartins Jun 10, 2026
99f6a17
build(phpstan): raise to level 6, type iterable values
math3usmartins Jun 10, 2026
8201769
build(phpstan): raise to level 7, triage 55 findings to zero
math3usmartins Jun 10, 2026
a70de6d
ci(phpstan): gate Core CI on level-7 static analysis
math3usmartins Jun 10, 2026
06673ca
test(infection): kill validateInnerVariance escape, justify 6 equival…
math3usmartins Jun 10, 2026
24e7dbb
docs(changelog): add CHANGELOG with v0.2.0 release notes
math3usmartins Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .github/workflows/ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<T: \Stringable>`.
- Intersection multi-bound — `T: \Stringable & \Countable`.
- Disjunctive normal form (DNF) — `T: (\Stringable & \Countable) | \Iterator`.
- F-bounded recursion — `class Sortable<T: Comparable<T>>`.
- 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<A, B = A>`), 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<Banana>`
actually `extends Producer<Fruit>` 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::<T>()`.
- 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<T>(...) { … }` and
`fn<T>(...) => …`, 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<T>` / `static<T>` / `parent<T>` in type-hint
positions and at constructor sites (`new self::<T>()`,
`new static::<T>()`, `new parent::<T>()`).
- **`T[]` array sugar** — documentation-friendly shorthand that lowers to
plain `array` at compile time. (`array<K, V>` 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
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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-
Expand Down
163 changes: 52 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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\\": "<cache>/Generated/",
"App\\": [
// path to your normal/regular php code
"<source>",
// some `xphp` files just need to be rewritten into native php to use specialized classes,
// but their namespace will remain the same.
"<target>"
],
"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
// <source>/Collection.xphp
// src/Collection.xphp
namespace App;

class Collection<T> {
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; }
}

// <source>/main.xphp
// src/Use.xphp
namespace App;

$users = new Collection<User>(
new User('Alice'),
new User('Bob')
);
$users = new Collection::<User>(new User('Alice'), new User('Bob'));
echo $users->first()->name;
```

### 4. Compile
Compile:

```bash
vendor/bin/xphp compile <source> <target> <cache>
vendor/bin/xphp compile src dist .xphp-cache
```

| Argument | Required | Default | Purpose |
|------------|----------|----------------|--------------------------------------------------------------------------------------|
| `<source>` | yes | -- | Directory of `.xphp` files (PSR-4 layout) |
| `<target>` | no | `dist` | Where rewritten `.php` files land -- your user code with generic call sites replaced |
| `<cache>` | no | `.xphp-cache` | Where specialized classes live |

p.s. you can `gitignore` files in `<target>` and `<cache>` 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_<hash>` -- see the [generics reference](docs/type-system/generics/index.md) for the real scheme.

```php
// <cache>/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;
}
}

// <target>/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

```
<root>/
├── <source> # php/xphp source files (PSR-4: namespace mirrors directory structure)
├── <target> # rewritten .php (gitignored, generated)
├── <cache> # specialized classes (gitignored, generated)
└── composer.json # PSR-4: XPHP\Generated\ => <cache>/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)
Loading
Loading