From 708358e09bb21277e726658ea3e0ac57108af6af Mon Sep 17 00:00:00 2001 From: Matheus Martins Date: Mon, 15 Jun 2026 01:19:47 +0200 Subject: [PATCH] Upgrade to xphp v0.2.0 turbofish instantiation syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xphp >= 0.2 requires the `::<…>` operator at generic instantiation sites; bare `new Foo()` is now a parse error. Rewrite synthesized specialization roots via a `toTurbofish()` helper, update the demo call sites, bump the composer requirement, and refresh the docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- composer.json | 4 ++-- demo/README.md | 2 +- demo/src/Demo/Catalog.xphp | 8 ++++---- src/Compiler/XphpCompiler.php | 25 ++++++++++++++++++++++--- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f9c5ada..10aad4c 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ when any `.xphp` source changes. No separate `xphp:compile` step is needed. generated `XPHP\Generated\…` namespace. Write `\App\Finder\SourceInterface`, not a `use`d `SourceInterface`. - **You don't need a call site.** A generic that's only injected is never written as - `new Foo()` anywhere, yet monomorphization is usage-driven. The bundle bridges + `new Foo::()` anywhere, yet monomorphization is usage-driven. The bundle bridges this by synthesizing a throwaway instantiation for each declared binding at compile time (transparent; cleaned up afterwards). *(Stopgap: a future xphp release may accept specialization roots directly.)* diff --git a/composer.json b/composer.json index 51081de..d178b52 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,11 @@ }, "require": { "php": "^8.4.0", - "xphp-lang/xphp": "^0.1", "symfony/config": "^8.0", "symfony/console": "^8.0", "symfony/dependency-injection": "^8.0", - "symfony/http-kernel": "^8.0" + "symfony/http-kernel": "^8.0", + "xphp-lang/xphp": "^v0.2.0" }, "require-dev": { "symfony/framework-bundle": "^8.0", diff --git a/demo/README.md b/demo/README.md index 68698c5..c8e3c53 100644 --- a/demo/README.md +++ b/demo/README.md @@ -14,7 +14,7 @@ src/ ├── Command/DemoCommand.php # handwritten PHP — calls into the compiled Catalog ├── Models/Plastic.php # handwritten PHP — left untouched by the build ├── Containers/Box.xphp # generic template class Box -├── Demo/Catalog.xphp # uses new Box() (so it must be .xphp) +├── Demo/Catalog.xphp # uses new Box::() (so it must be .xphp) └── Kernel.php ``` diff --git a/demo/src/Demo/Catalog.xphp b/demo/src/Demo/Catalog.xphp index 136f8e0..dbea70d 100644 --- a/demo/src/Demo/Catalog.xphp +++ b/demo/src/Demo/Catalog.xphp @@ -8,7 +8,7 @@ use App\Containers\Box; use App\Models\Plastic; /** - * Exercises the generic. Because it instantiates `new Box()`, this file + * Exercises the generic. Because it instantiates `new Box::()`, this file * must be .xphp — the compiler rewrites those call sites to the concrete * XPHP\Generated\App\Containers\Box\T_ classes. The public API * (`colors()`, `boxClass()`) is plain PHP, so handwritten code can call it @@ -21,10 +21,10 @@ final class Catalog */ public function colors(): array { - $red = new Box(); + $red = new Box::(); $red->set(new Plastic('red')); - $blue = new Box(); + $blue = new Box::(); $blue->set(new Plastic('blue')); return [$red->get()->color, $blue->get()->color]; @@ -36,7 +36,7 @@ final class Catalog */ public function boxClass(): string { - $box = new Box(); + $box = new Box::(); return $box::class; } diff --git a/src/Compiler/XphpCompiler.php b/src/Compiler/XphpCompiler.php index b133fc6..11ab316 100644 --- a/src/Compiler/XphpCompiler.php +++ b/src/Compiler/XphpCompiler.php @@ -108,10 +108,10 @@ private function runCompiler(): string * Monomorphization is usage-driven: the transpiler only emits a `T_` * specialization where source actually instantiates `Foo`. A generic that is * only declared as a DI service (and injected via an interface) is never written - * as `new Foo()` anywhere, so its specialization would never be generated. + * as `new Foo::()` anywhere, so its specialization would never be generated. * * As a stopgap we synthesize a throwaway `.xphp` file containing one - * `new Template();` per declared root, dropped into the source set so the + * `new Template::();` per declared root, dropped into the source set so the * transpiler treats it as a usage. The rewritten copy that lands in the target dir * is dead code (it is never autoloaded — the file declares no class) and is removed * by {@see removeRootsArtifacts()} along with the source file. @@ -127,12 +127,31 @@ private function writeRootsFile(): void $lines = ['roots as $root) { - $lines[] = sprintf('new %s();', $root); + $lines[] = sprintf('new %s();', self::toTurbofish($root)); } file_put_contents($dir . \DIRECTORY_SEPARATOR . 'roots.xphp', implode("\n", $lines) . "\n"); } + /** + * Turn a canonical generic-instantiation expression (`Foo\Bar`) into the + * call-site turbofish form xphp expects (`Foo\Bar::`). xphp >= 0.2 requires + * the `::<…>` operator at instantiation sites — bare `new Foo()` is now a + * parse error (`Foo < Baz` is ambiguous with comparison). Only the outermost + * `<` is the call site; nested type arguments keep their plain `<…>` form. A root + * with no type arguments becomes an empty turbofish (`Foo::<>`) so an + * all-defaulted template still triggers specialization. + */ + private static function toTurbofish(string $canonical): string + { + $pos = strpos($canonical, '<'); + if ($pos === false) { + return $canonical . '::<>'; + } + + return substr($canonical, 0, $pos) . '::' . substr($canonical, $pos); + } + private function removeRootsArtifacts(): void { if ($this->roots === []) {