diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2096a35..bf8fe2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,60 +2,45 @@ ## Monorepo layout -The repository hosts the xphp language plus the tooling that grows around it. -Every shippable artifact lives in its own sub-project with its own build -system, lockfile, tests, and CI workflow. The PHP core compiler lives under -`core/`; satellites (LSP, PhpStorm plugin, etc.) live under `tools//`. +The repository hosts the `xphp` language plus the tooling that grows around it. + +Every shippable artifact lives in its own sub-project with its own build system, +lockfile, tests, and CI workflow. The xphp compiler lives under `core/`; +satellites (LSP, PhpStorm plugin, etc.) live under `tools//`. ``` xphp-lang/ -+-- core/ # the PHP compiler package -| +-- src/, test/, bin/, composer.json -| `-- Makefile, infection.json5, phpunit.xml.dist -+-- docs/ # language-level documentation -+-- playground/ # demo workspace that depends on the core ++-- core/ # the xphp compiler ++-- docs/ # language-level documentation ++-- playground/ # demo workspace that depends on the core +-- tools/ -| +-- lsp/ # Language Server (PHP, phpactor/language-server) -| +-- phpstorm-plugin/ # JetBrains plugin (Kotlin + Gradle) -| `-- vscode-extension/ # VS Code client (TypeScript) +| +-- lsp/ # Language Server (PHP, phpactor/language-server) +| +-- phpstorm-plugin/ # JetBrains plugin (Kotlin + Gradle) +| `-- vscode-extension/ # VS Code client (TypeScript) +-- .github/workflows/ -| +-- ci-core.yml # phpunit + infection for core/ -| `-- ci-.yml # one file per package under tools/ -+-- docker-compose.yml # shared dev stack -`-- README.md, CONTRIBUTING.md # repo-level docs +| +-- ci-core.yml # phpunit + infection for core/ +` -- ci-.yml # one file per package under tools/ ``` -### What goes where - -| Concern | Lives at | -|------------------------------------------|---------------| -| Compiler source + tests | `core/src/`, `core/test/` | -| Language documentation (generics, roadmap, comparison) | `docs/` (root) | -| Demo / acceptance harness | `playground/` | -| Anything that *uses* the compiler externally | `tools//` | -| Per-tool documentation | `tools//README.md` | -| Shared dev tooling (`.docker/`, `.github/`, compose files) | root | - The split is a single principle in disguise: **the core compiler is the product; everything else is a way to consume it**. A package that depends on the core's published behavior (LSP analyzing `.xphp` source, an editor plugin spawning the LSP, a CI lint tool calling `bin/xphp`) is a tool. The core -itself never depends on tools. +itself never depends on external `xphp` tools. ### Adding a new package -The current shape was settled when we added `tools/lsp/` and validated when -we added `tools/phpstorm-plugin/` (Kotlin + Gradle, an entirely different -toolchain than the LSP's PHP + Composer -- both fit the same per-package -shape, which is the proof the convention generalises). To add a fourth -sibling: +The current shape was settled when `tools/lsp/` was a dded and validated when +through `tools/phpstorm-plugin/`. + +To add a tool: 1. **Create the directory** under `tools//`. Pick a name that names the thing concretely (`lsp`, `phpstorm-plugin`) rather than abstractly (`server`, `editor-integration`). 2. **Self-contained build**: each package owns its build system. The LSP has - `tools/lsp/composer.json`; the JetBrains plugin has + `tools/lsp/composer.json`; the PhpStorm plugin has `tools/phpstorm-plugin/build.gradle.kts` + `gradle.properties` (every version pin lives there so a single edit propagates to since-build, IDE target, Kotlin runtime, JVM toolchain). The root never collects @@ -108,8 +93,8 @@ We deliberately **do not** path-filter workflows at the `on:` level. GitHub's branch protection requires named status checks to actually run — a path-filtered workflow that doesn't trigger on an unrelated change reports "expected, never received" and blocks the merge. Running every workflow -every time costs a small amount of CI minutes (the jobs are parallel and each -finishes in ~30s); the alternative is a coordination tax with sharp edges. +every time costs a small amount of CI minutes (the jobs are parallel); the +alternative is a coordination tax with sharp edges. If CI minutes ever become a real concern, the fix is to add job-level `if:` guards using `paths-filter` action results, NOT to add `paths:` at the @@ -120,65 +105,87 @@ workflow level. Leave required status checks intact. ### Unit tests ```bash -make -C core test/unit +make test/unit ``` -Most tests are pure unit tests against `XPHP\Transpiler\Monomorphize\*`. The handful of -**integration tests** compile a fixture under `core/test/fixture/compile//` end-to-end and -either assert on the emitted text or autoload the result and call into it at runtime. Those +Most tests are pure unit tests against `XPHP\Transpiler\Monomorphize\*`. The +handful of **integration tests** compile a fixture under +`core/test/fixture/compile//` end-to-end and either assert on the emitted +text or autoload the result and call into it at runtime. Those runtime tests have one isolation gotcha worth knowing before you add a new one. #### Cross-fixture class-table collisions -`Registry::generatedFqn` (`core/src/Transpiler/Monomorphize/Registry.php`) names every specialized -class as: +`Registry::generatedFqn` (`core/src/Transpiler/Monomorphize/Registry.php`) names +every specialized class as: ``` XPHP\Generated\ \ T_ └── namespace, mirrors template ─┘ └── hash of args ONLY ──┘ ``` -The hash covers the **argument list**, not the template's own FQN — the template's FQN is -already encoded in the namespace path. That's deliberate and right for a single compile: it -collapses identical instantiations into one class file. - -It bites in tests because **multiple fixtures redeclare the same template path**. Five -fixtures define an `App\Containers\Box` (with subtly different bodies — some have a -constructor, the `generic_interface` one implements `Container`, etc.) and all of them -instantiate it with `App\Models\Plastic`. Same template path + same arg means same generated -FQN: `XPHP\Generated\App\Containers\Box\T_de1e0eaa…`. The on-disk files live in different -per-test work directories, but **PHP's class table is process-wide** — once any integration -test `new`s up that FQN, PHP caches *that* body for the rest of the phpunit run. A -later test loading from a different fixture's work dir gets the stale class. - -The failure mode is shape-dependent and the test ordering is randomized, so this surfaces as -an intermittent flake. Two known-good patterns to avoid it: - -1. **Reflection-only assertions** when you're verifying the structural contract (an interface - chain, an `implements` link, the type of a property) rather than runtime behavior. +The hash covers the **argument list**, not the template's own FQN — the +template's FQN is already encoded in the namespace path. That's deliberate and +right for a single compile: it collapses identical instantiations into one class +file. + +It bites in tests because **multiple fixtures redeclare the same template path +**. +Five fixtures define an `App\Containers\Box` (with subtly different bodies — +some +have a constructor, the `generic_interface` one implements `Container`, etc.) +and +all of them instantiate it with `App\Models\Plastic`. Same template path + same +arg means +same generated FQN: `XPHP\Generated\App\Containers\Box\T_de1e0eaa…`. The on-disk +files live in +different per-test work directories, but **PHP's class table is process-wide** — +once any +integration test `new`s up that FQN, PHP caches *that* body for the rest of the +phpunit run. +A later test loading from a different fixture's work dir gets the stale class. + +The failure mode is shape-dependent and the test ordering is randomized, so this +surfaces as an intermittent flake. Two known-good patterns to avoid it: + +1. **Reflection-only assertions** when you're verifying the structural + contract (an interface chain, an `implements` link, the type of a property) + rather than runtime behavior. `ReflectionClass::implementsInterface($marker)`, `::getParentClass()`, - `(new ReflectionProperty($fqn, 'item'))->getType()->getName()` all read the class metadata - without depending on which body PHP's class table happens to hold. **Example:** + `(new ReflectionProperty($fqn, 'item'))->getType()->getName()` all read the + class metadata without depending on which body PHP's class table happens to + hold. + **Example** `test/Transpiler/Monomorphize/CompilerIntegrationTest.php::testInstanceofAgainstOriginalTemplateMatchesAllSpecializations`. -2. **Fixture-unique concrete types** when you legitimately need `new $fqn(...)` at runtime. +2. **Fixture-unique concrete types** when you legitimately need `new $fqn(...)` + at runtime. Add a model class that's only declared inside your fixture (e.g. - `test/fixture/compile/generic_interface/source/Models/Polymer.xphp`) and instantiate - against `Box`. Other fixtures don't redefine `Box`, so the hash is + `test/fixture/compile/generic_interface/source/Models/Polymer.xphp`) and + instantiate + against `Box`. Other fixtures don't redefine `Box`, so the + hash is unique and PHP's class table can't cache a competing body. **Example:** `test/Transpiler/Monomorphize/GenericInterfaceIntegrationTest.php::testSpecializedClassIsInstanceOfOriginalInterfaceMarker`. -Rule of thumb: if your test does `new $generatedFqn(...)` and the concrete type is `Plastic`, -`Metal`, `User`, `Food`, or any other name that already lives in another fixture, switch to -one of the two patterns above before pushing — random-order CI will catch it eventually, and -the diagnostic (`ArgumentCountError`, `instanceof returns false`) won't point at the root +Rule of thumb: if your test does `new $generatedFqn(...)` and the concrete type +is `Plastic`, +`Metal`, `User`, `Food`, or any other name that already lives in another +fixture, switch to +one of the two patterns above before pushing — random-order CI will catch it +eventually, and +the diagnostic (`ArgumentCountError`, `instanceof returns false`) won't point at +the root cause. ### Mutation tests -Mutation testing is the headline quality signal -- the test suite isn't just covering lines, it's surviving deliberate +Mutation testing is the headline quality signal -- the test suite isn't just +covering lines, it's surviving deliberate code perturbations. Run via [Infection](https://infection.github.io/): -The CI workflow runs Infection on every PR and every push to `main`, failing the build if MSI drops below **95%**. -`infection.json5` carries a curated set of per-mutator `ignore` rules for equivalent / cosmetic cases so the report only +The CI workflow runs Infection on every PR and every push to `main`, failing the +build if MSI drops below **95%**. +`infection.json5` carries a curated set of per-mutator `ignore` rules for +equivalent / cosmetic cases so the report only surfaces genuine test gaps when they appear. \ No newline at end of file diff --git a/README.md b/README.md index 054f943..0aa482a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,79 @@ # xphp -`xphp` is a superset of `php` that compiles directly into zero-overhead native, opcache-friendly `php` and gives -developers **runtime safety without runtime penalty**. +## What it is -`xphp` contributes to the community in a pragmatic way. With that in mind, it works around the current Zend Engine -limitations by moving the heavy lifting to an Ahead-of-Time (`AOT`) compilation step. - ---- +`xphp` is a superset of `php` that gives developers real generics, powered by +[monomorphization](https://en.wikipedia.org/wiki/Monomorphization) at compile +time. ## How it works -### 1. The `xphp` source +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. + +## Ecosystem and community first + +Ideally a project should first show how it works, but ecosystem and community +are too much important to be mentioned only at the bottom of the main document. +This project **CAN NOT** be successful without their support. + +The single biggest asset of any programming language is the community and +ecosystem around it, much more than its syntax and features. We believe that +meeting a community where it is, respecting their culture, history and work +compounds far better than asking them to leave all of that behind. + +As `xphp` is simply a superset of `php`, existing `php` code can be easily +converted into `xphp` and `xphp` code can seamlessly consume `php` -- little to +zero effort either way. + +The design choice to compile to vanilla `php` is a deliberate commitment to +contribute to the `php` community and its ecosystem, **not** to compete against +them. + +## Getting started + +### 1. Install the xphp package + +```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`. + +```json5 +{ + "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. + "" + ], + } + } +} +``` + +After that, update your autoload file via `composer dump-autoload`. + +### 3. Write `xphp` code -You define a generic class and instantiate it exactly as you would expect: +You can define a generic class and instantiate it exactly as you would expect: ```php -// src/Collection.xphp +// /Collection.xphp namespace App; class Collection { @@ -31,17 +89,19 @@ class Collection { } } -// src/main.xphp +// /main.xphp +namespace App; + $users = new Collection( new User('Alice'), new User('Bob') ); ``` -### 2. The compile command +### 4. Compile ```bash -`xphp` compile +vendor/bin/xphp compile ``` | Argument | Purpose | @@ -52,10 +112,7 @@ $users = new Collection( p.s. you can `gitignore` files in `` and `` as they can be generated in your CI/CD pipeline. -### 3. The compiled native php - -The compiler monomorphizes the generic `Collection` class into a concrete `Collection_User` class. No impact on -the runtime, it's native `php` code. +#### Sample output ```php // /Generated/Collection_User.php @@ -91,134 +148,67 @@ $users = new Collection_User( ); ``` -### 4. Autoload the generated classes +### 5. Deploy -Add a PSR-4 entry to `composer.json` so the standard autoloader finds the specialized classes without manual `require`, -then update your autoload file via `composer dump-autoload`. +The compiler monomorphizes the generic classes and converts downstream code +into native `php` code. -```json5 -{ - "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. - "" - ], - } - } -} -``` +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 ``` -your-project/ +/ ├── # 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/ ``` ---- +## Principles -## Ecosystem and community matter much more than syntax and features - -The single biggest asset of any programming language is the community and ecosystem around it, much more than its -syntax and features. We believe that meeting a community where it is, respecting their culture, history and work -compounds far better than asking them to leave all of that behind. - -As `xphp` is simply a superset of `php`, existing `php` code can be easily converted into `xphp` and `xphp` code can -seamlessly consume `php` -- little to zero effort either way. - -The design choice to compile to vanilla `php` is a deliberate commitment to contribute to the `php` community and its -ecosystem, **not** to compete against them. - ---- - -## Turning static illusions into runtime reality - -For years, we've relied on a shared agreement to keep our `php` codebases safe: we use docblocks to tell our IDEs and -static analyzers what our data should look like. It's a wonderful system that has pushed the language to new heights. - -However, there is a fundamental limit to this approach. The `php` engine doesn't read our static analysis rules. At -runtime, those type guarantees disappear, leaving our applications vulnerable exactly when it matters most. - -`xphp` doesn't ask you to change how you think about types, but it does change how they are enforced. It relies on the -actual `php` engine. - -Through a process called monomorphization, `xphp` reads your generic code and safely compiles it into specialized, -native `php` classes. If you write `Collection` in `xphp`, the compiler automatically generates a physical -`Collection_User` class. Crucially, the native type hints are baked right in, meaning your code is protected by the -engine itself, not just a comment. - ---- - -## Designing for the runtime, building for developers - -Whenever we make an architectural decision, it must be supported by the following non-negotiable principles: +Whenever we make an architectural decision, it must be supported by the +following non-negotiable principles: ### 1. Zero Runtime Penalty -Abstractions should not cost performance. By relying on monomorphization rather than runtime reflection hacks, the -output is plain `php` classes (`Box_Int`, `Map_String_User`). `opcache` loves this, and execution speed remains -identical to hand-written, hyper-optimized `php`. +Abstractions should not cost performance. By relying on monomorphization rather +than runtime reflection hacks, the output is plain `php` classes. `opcache` +likes that, and execution speed remains identical to handwritten, optimized +`php` code. ### 2. Maximum Runtime Safety -`xphp` bakes the types directly into the generated `php` code. If a boundary is crossed or a third-party plain `php` -library misuses your code, it triggers a native `php` error. The runtime never lies. +`xphp` bakes the types directly into the generated `php` code. If a boundary is +crossed or a third-party plain `php` library misuses your code, it triggers a +native `php` error. The runtime never lies. ### 3. Progressive Enhancement -It must play nicely with legacy codebases. A team should be able to write one `xphp` class in a legacy `php` -application, compile it, and use it seamlessly. No custom runtimes, no `HHVM` style ecosystem splits. +It must play nicely with normal `php` codebases. A team should be able to write +a single `xphp` file in a `php` application, compile it, and use it seamlessly. -### 4. Developer Experience First +No custom runtimes, no `HHVM` style ecosystem splits. -The tooling must feel as fast and native as every modern web tool. IDEs should be able to read `xphp` files, while the -`php` runtime happily consumes the compiled `php` files. +### 4. Developer Experience ---- +The tooling must be fast and native as every modern ecosystem. IDEs should +be able to read `xphp` files, while the `php` runtime happily consumes the +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/) (reification / variance / OpCache -cost / backwards compatibility). 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 aliases, literal types, mapped and conditional types to name a few. - -`xphp` doesn't wait for `php` internals to ship these features. It delivers them today, on top of the runtime the -community and ecosystem already trust. - -- Full generics reference: [docs/generics.md](docs/generics.md) -- Side-by-side comparison against TypeScript, Kotlin, and Rust (what's there, what's missing, what's uniquely possible - with monomorphization): [docs/generics-comparison.md](docs/generics-comparison.md) - ---- - -## Editor tooling +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 compiler itself lives at [core/](core/) (Composer package -`xphp-lang/xphp`); a Language Server Protocol implementation under [tools/lsp/](tools/lsp/) delivers the full editor surface for `.xphp` -files: live diagnostics, hover (xphp generics + PHP semantic, with parameter and return-type substitution), -go-to-definition, find references, rename, document and workspace symbols, and rich completion (member access, -static access, static property, type-arg positions with bound-aware filtering, scope-aware variables, visibility -filtering across same-class and subclass contexts). The same server powers two editor integrations: +The object model that's served the ecosystem for two decades doesn't bend easily. -- **PhpStorm**: plugin at [tools/phpstorm-plugin/](tools/phpstorm-plugin/) targeting PhpStorm 2026.1+ (uses the - IntelliJ Platform LSP API, free for all editions since 2025.2). This is the primary editor target. -- **VS Code**: extension client at [tools/vscode-extension/](tools/vscode-extension/) for local dev - iteration. Marketplace publication is deferred indefinitely. +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 aliases, literal types, mapped and conditional types to name a few. -Both bind to the same TextMate grammar and the same LSP semantics, so editing experience is consistent across editors. +## See also ---- +- [Type-system comparison](core/docs/type-system/comparison.md) +- [Full generics reference](core/docs/type-system/generics/index.md) diff --git a/core/CONTRIBUTING.md b/core/CONTRIBUTING.md new file mode 100644 index 0000000..0ca9e17 --- /dev/null +++ b/core/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing + +## Test + +```bash +make test/unit # PHPUnit +make test/mutation # Infection, MSI under a 95 % gate +``` + +CI gates every PR on both 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. \ No newline at end of file diff --git a/core/README.md b/core/README.md index 4dd61bb..7221caf 100644 --- a/core/README.md +++ b/core/README.md @@ -1,135 +1,55 @@ -# xphp core (compiler) +# xphp core -Ahead-of-time compiler that turns `.xphp` source -- PHP plus generic -templates (`class Box`, `function identity(T $x): T`, `T[]` -sugar, `T: Bound` type-parameter bounds) -- into vanilla `.php` that -runs on any stock PHP 8.4 runtime, with no runtime support library. +This is the package downstream `xphp` tools consume. e.g. LSP, PhpStorm plugin +and VSCode extension. -This is the package downstream tools consume. The Language Server -(`tools/lsp/`), the demo workspace (`playground/`), and indirectly -the PhpStorm plugin and VS Code extension all build on top of this -package's `XphpSourceParser` + `Registry` + `TypeHierarchy` + -`Specializer`. +It contains an ahead-of-time compiler that converts `xphp` source into normal +`php` code compatible with `PSR-4`. It's installed as a `--dev` package, +**no need** for additional runtime packages. ## Status -| Feature | Status | -|---|---| -| Single-parameter generics (`class Box`) | shipped | -| Multi-parameter generics (`Pair`, any arity) | shipped | -| Nested generics at arbitrary depth (`Box>`) | shipped | -| Type-hint positions everywhere (param / return / property / `new`) | shipped | -| Transitive fixed-point specialization (16-iteration depth cap) | shipped | -| Generic interfaces (`interface Container`) | shipped | -| Generic traits as templates (dropped after specialization) | shipped | -| Method-scoped generics (`function NAME(...)` inside a class; static calls) | shipped | -| Free generic functions (`function NAME(...)` at namespace scope) | shipped | -| Type-parameter bounds (`class Box`) | shipped | -| `instanceof` against the original template (marker interface) | shipped | -| Collision-safe generated FQCN (`XPHP\Generated\