Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .docker/php85.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM php:8.5-cli-alpine

# Dedicated runtime for the `@group php85` tests -- syntax (e.g. the 8.5
# pipe operator `|>`) that the host-version parser can only tokenize on
# PHP 8.5. These run with coverage disabled, so no xdebug/pcov here:
# just composer plus the tools composer + `make test/unit/php85` need.
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

RUN apk add --no-cache \
git \
unzip \
make
20 changes: 16 additions & 4 deletions .github/workflows/ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,27 @@ concurrency:

jobs:
phpunit:
name: PHPUnit
# 8.4 is the supported/default runtime and runs the full suite minus the
# `php85` group; a dedicated 8.5 container runs only that group, which
# covers syntax (e.g. the pipe operator `|>`) that the host-version
# parser can only tokenize on PHP 8.5.
name: PHPUnit (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- php: '8.4'
target: test/unit
- php: '8.5'
target: test/unit/php85
steps:
- uses: actions/checkout@v4

- name: Setup PHP 8.4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
php-version: ${{ matrix.php }}
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2
Expand All @@ -32,7 +44,7 @@ jobs:
uses: ramsey/composer-install@v3

- name: Run PHPUnit
run: make test/unit
run: make ${{ matrix.target }}

phpstan:
name: PHPStan
Expand Down
18 changes: 15 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@
## Test

```bash
make test/unit # PHPUnit
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/mutation # Infection, MSI under a 95 % gate
```

CI gates every PR on both targets. `infection.json5` carries a curated set
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.

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

```bash
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.
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@
# 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`.
test/unit:
php vendor/bin/phpunit
php vendor/bin/phpunit --exclude-group php85

.PHONY: test/unit/php85
# Runs only the PHP 8.5-specific syntax tests (e.g. the pipe operator).
# Requires a PHP 8.5 runtime -- CI uses a dedicated 8.5 container; on 8.4
# these self-skip via #[RequiresPhp].
test/unit/php85:
php vendor/bin/phpunit --group php85

.PHONY: lint/phpstan
# Static analysis at level 7. Memory limit lifted because deep generic
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,18 @@ Write a generic class and use it:
namespace App;

class Collection<T> {
public function __construct(public T ...$items) {}
private array $items = [];

public function add(T $item): void { $this->items[] = $item; }
public function first(): ?T { return $this->items[0] ?? null; }
}

// src/Use.xphp
namespace App;

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

Expand Down
26 changes: 25 additions & 1 deletion bin/xphp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,31 @@

declare(strict_types=1);

require dirname(__DIR__) . '/vendor/autoload.php';
(static function (): void {
$candidates = [
// Set by Composer's generated bin proxy (vendor/bin/xphp).
$GLOBALS['_composer_autoload_path'] ?? null,
// Installed as a dependency: vendor/xphp-lang/xphp/bin -> <project>/vendor/autoload.php
dirname(__DIR__, 3) . '/autoload.php',
// Standalone checkout AND the PHAR (phar://alias.phar/vendor/autoload.php) rely on
// this one -- keep it present and last.
dirname(__DIR__) . '/vendor/autoload.php',
];

foreach ($candidates as $file) {
if (is_string($file) && is_file($file)) {
require $file;

return;
}
}

fwrite(
STDERR,
'xphp: could not locate Composer\'s autoloader. Run `composer install`.' . PHP_EOL,
);
exit(1);
})();

use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ services:
environment:
XDEBUG_MODE: coverage,develop

# PHP 8.5 runtime for the `@group php85` tests. Run them with:
# docker compose run --rm php85 make test/unit/php85
php85:
build:
dockerfile: .docker/php85.Dockerfile
working_dir: /opt/app
volumes:
- ./:/opt/app

xphp:
extends:
service: php
Expand Down
32 changes: 32 additions & 0 deletions docs/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,35 @@ class TempContainer<T> {
}
$x = new TempContainer::<User>();
```

## Newer PHP syntax needs a matching host runtime

### ❌ What doesn't work

Running the transpiler on PHP 8.4 over a source file that uses PHP 8.5
syntax — for example the pipe operator:

```php
$slug = $title |> trim(...) |> strtolower(...);
```

```
Syntax error, unexpected '>'
```

### Why

xphp owns only the generic syntax; everything else is plain PHP. It
parses your source with `nikic/php-parser` configured for the **host**
PHP version (the one running the transpiler). On PHP 8.4 the lexer
can't tokenize 8.5-only syntax like `|>`, so the parse fails before
specialization even begins. There's nothing generic about the line —
it just never reaches the host parser's grammar.

### ✅ Workaround

Run the transpiler on a PHP version that can parse your syntax — e.g.
PHP 8.5 for the pipe operator. Plain (non-generic) code, including
newer-PHP syntax, passes straight through untouched. Note the emitted
PHP still requires a runtime that supports those features to *execute*;
the supported floor for xphp itself is PHP 8.4 (`composer.json`).
7 changes: 4 additions & 3 deletions docs/guides/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ defaults to `dist` and `[cache-dir]` defaults to `.xphp-cache`. The data flows t
turn into AST, the AST populates a Registry and a TypeHierarchy, a
fixed-point loop expands every concrete instantiation into a specialized
class file, and finally the rewritten user code lands in the target
directory while specialized classes land in a cache directory under
`XPHP\Generated\<template>\T_<hash>.php`.
directory while specialized classes land in a cache directory at
`<cache-dir>/Generated/<template>/T_<hash>.php` (autoloaded under the
`XPHP\Generated\` namespace).

> The code's internal labels (`Phase 0`, `Phase 1a`, `Phase 1b.i`,
> `Phase 1b.ii`, `Phase 2`, `Phase 2.5`, `Phase 3`, `Phase 4`,
Expand All @@ -45,7 +46,7 @@ flowchart TD
Spec --> Collector
Loop -- no --> Rewriter["CallSiteRewriter<br/>rewrite Names + emit markers"]
Rewriter --> Emitter["SpecializedClassGenerator<br/>+ FileWriter"]
Emitter --> Files["target/*.php<br/>cache/XPHP/Generated/.../T_&lt;hash&gt;.php"]
Emitter --> Files["target/*.php<br/>cache/Generated/.../T_&lt;hash&gt;.php"]
```

The same lifecycle as a sequence diagram, showing call order between
Expand Down
31 changes: 8 additions & 23 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,18 @@ timeline
: runtime instanceof T
: marker interface per template
Developer experience
: PSR-4 fixtures
: RFC-aligned call-site syntax
: empty turbofish for all-defaults templates
section Next
Editor and tooling
: PhpStorm syntax highlighting
: Language Server Protocol
: Composer plugin for autoload
: Live transpilation via stream wrapper
Compiler ergonomics
: Migration hint for bare call sites
: PHPDoc substitution in generated bodies
: Source maps back to xphp lines
section Discovery
Generic surface
: Generic type aliases
: Variance edges on trait-owned templates
: Branching narrowing precision
section Discovery
Module surface
: internal visibility modifier
: composer-package boundary
Expand Down Expand Up @@ -182,7 +176,6 @@ upcoming one.

### Developer experience

- PSR-4 fixtures.
- RFC-aligned call-site syntax (`Name::<...>` turbofish).
- Empty turbofish (`Name::<>`) for all-defaults templates.

Expand All @@ -192,27 +185,12 @@ upcoming one.

### Editor and tooling

- PhpStorm syntax highlighting.
- Language Server Protocol implementation (diagnostics, hover types,
goto-definition).
- Composer plugin for autoload registration.
- Live transpilation via stream wrapper (no build step in dev).

### Compiler ergonomics

- Compile-time migration hint for bare `Name<...>(...)` call sites
that point users at the `::<...>` turbofish.
- PHPDoc substitution in generated bodies so generated `.php` reads
naturally.
- Source maps (stack traces back to `.xphp` lines).

### Generic surface

- Generic type aliases (e.g. `type Pair<A, B> = ...`).
- Variance edges on trait-owned templates.
- Branching narrowing precision: today conservatively de-specializes
when arms disagree; will track unions with runtime dispatch instead.

---

## Discovery
Expand All @@ -223,6 +201,13 @@ implementation knobs are still being settled. Treat each Discovery
entry as a starting point for community discussion, not a guarantee
to ship.

### Generic surface

- Generic type aliases (e.g. `type Pair<A, B> = ...`).
- Variance edges on trait-owned templates.
- Branching narrowing precision: today conservatively de-specializes
when arms disagree; will track unions with runtime dispatch instead.

### Module surface

- **`internal` visibility modifier**: replace PHPDoc `@internal` hints
Expand Down
Loading
Loading