diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 82992b7..867453d 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -30,11 +30,9 @@ jobs: - name: Install dependencies uses: ramsey/composer-install@v3 - with: - working-directory: core - name: Run PHPUnit - run: make -C core test/unit + run: make test/unit infection: name: Mutation testing @@ -53,13 +51,11 @@ jobs: - name: Install dependencies uses: ramsey/composer-install@v3 - with: - working-directory: core - name: Run Infection # --min-covered-msi=95 fails CI if the mutation score drops below # the gate. Threads=max parallelises mutant runs. - run: make -C core test/mutation + run: make test/mutation - name: Upload Infection report if: always() @@ -67,7 +63,7 @@ jobs: with: name: infection-report path: | - core/var/infection.log - core/var/infection.html + var/infection.log + var/infection.html if-no-files-found: ignore retention-days: 14 diff --git a/.github/workflows/ci-lsp.yml b/.github/workflows/ci-lsp.yml deleted file mode 100644 index 55169cb..0000000 --- a/.github/workflows/ci-lsp.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: LSP CI - -# PHPUnit runs on every PR + push to main as a hard gate. -# Infection (mutation testing) is opt-in via workflow_dispatch -- its -# resource envelope outgrew GitHub-hosted `ubuntu-latest` (~7 GB RAM) -# once the suite passed ~150 tests, and the initial coverage run now -# OOMs as SIGTERM 143 mid-stream. Re-enable as a PR gate once the -# job is reshaped to filter / batch the test set (see follow-up). -on: - pull_request: - push: - branches: [main] - workflow_dispatch: - -# Per-workflow concurrency. See ci-core.yml for the rationale. -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - phpunit-lsp: - name: PHPUnit (LSP) - 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 - - # The LSP package has its own composer.json under tools/lsp/ with a - # path-repo pointing back at the repo root. Cache its deps separately - # from the core install so a core-only change doesn't bust the LSP cache. - - name: Install LSP dependencies - uses: ramsey/composer-install@v3 - with: - working-directory: tools/lsp - - - name: Run PHPUnit (LSP) - run: make -C tools/lsp test - - infection-lsp: - name: Mutation testing (LSP) - # Gate: only run when explicitly requested via the Actions tab. The - # 424-test suite plus per-mutation reruns overruns the GitHub-hosted - # runner's RAM, killing the initial coverage subprocess with SIGTERM - # 143. PRs and merges to main get fast unambiguous signal from the - # PHPUnit job above; Infection scoring is opt-in until the job is - # reshaped to filter / batch the test set. - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - needs: phpunit-lsp - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP 8.4 (with PCOV for coverage) - # PCOV is preferred over Xdebug for Infection's initial run: an - # order of magnitude less memory under the same coverage scope, - # which keeps the runner well clear of the OOM-killer. The - # Makefile target sets pcov.directory + XDEBUG_MODE=off so the - # driver choice is deterministic regardless of what else is loaded. - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: dom, json, mbstring, tokenizer - coverage: pcov - tools: composer:v2 - - - name: Install LSP dependencies - uses: ramsey/composer-install@v3 - with: - working-directory: tools/lsp - - - name: Run Infection (LSP) - # `make -C tools/lsp test/mutation` lazily downloads infection.phar - # 0.33.1 into tools/lsp/var/ (PHAR sidesteps the composer dep conflict - # with phpactor/language-server documented in tools/lsp/README.md). - # Runs with --min-covered-msi=93; current baseline is 94%. - run: make -C tools/lsp test/mutation - - - name: Upload Infection report (LSP) - if: always() - uses: actions/upload-artifact@v4 - with: - name: infection-lsp-report - path: | - tools/lsp/var/infection.log - tools/lsp/var/infection.html - if-no-files-found: ignore - retention-days: 14 diff --git a/.github/workflows/ci-phpstorm-plugin.yml b/.github/workflows/ci-phpstorm-plugin.yml deleted file mode 100644 index 7645b56..0000000 --- a/.github/workflows/ci-phpstorm-plugin.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: PhpStorm Plugin CI - -on: - pull_request: - push: - branches: [main] - -# Per-workflow concurrency. See ci-core.yml for the rationale. -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - gradle-build: - name: Gradle build (PhpStorm plugin) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # PHP + Composer are needed to build the bundled xphp-lsp.phar that - # processResources copies into the plugin jar at build time. Skipping - # this step would still produce a buildable plugin, but it would warn - # and ship without an embedded LSP -- defeating the zero-config - # install path we shipped in chunk 5. - - 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 LSP dependencies - uses: ramsey/composer-install@v3 - with: - working-directory: tools/lsp - - - name: Build xphp-lsp.phar - # Same target ci-lsp.yml's runtime relies on; the plugin's - # processResources picks this up from tools/lsp/var/. - run: make -C tools/lsp build/phar - - - name: Setup JDK 21 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - # Cache key is built from the wrapper version + build script - # hash, so this stays warm across PRs that don't touch the plugin - # build config. - gradle-home-cache-cleanup: true - - - name: Build plugin (compile + test) - # `make build` from this directory is `./gradlew build`, which runs - # compileKotlin, test, and packages the plugin jar via the - # IntelliJ Platform Gradle Plugin's `composedJar` task. - run: make -C tools/phpstorm-plugin build - - - name: Upload plugin jar - uses: actions/upload-artifact@v4 - with: - name: xphp-phpstorm-plugin-jar - path: tools/phpstorm-plugin/build/libs/xphp-phpstorm-plugin-*.jar - if-no-files-found: error - retention-days: 14 - - - name: Upload test reports on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: phpstorm-plugin-test-reports - path: | - tools/phpstorm-plugin/build/reports/tests/ - tools/phpstorm-plugin/build/test-results/ - if-no-files-found: ignore - retention-days: 14 - - verify-plugin: - name: Plugin Verifier (PhpStorm plugin) - runs-on: ubuntu-latest - needs: gradle-build - steps: - - uses: actions/checkout@v4 - - # The verifier itself doesn't read the PHAR, but Gradle's build graph - # treats `processResources` as a prerequisite of `verifyPlugin` -- - # and `processResources` declares xphp-lsp.phar as a required input - # via `inputs.file(...)` in build.gradle.kts. Without the PHAR on - # disk, the build aborts before the verifier even runs: - # - # property 'xphpLspPhar' specifies file '.../xphp-lsp.phar' - # which doesn't exist. - # - # So we rebuild the same PHAR the gradle-build job built. Both - # jobs sit on fresh runners; sharing via an artifact would also - # work but the PHAR rebuild is cheap (~2s) compared to the verifier - # downloading the IDE matrix that follows. - - 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 LSP dependencies - uses: ramsey/composer-install@v3 - with: - working-directory: tools/lsp - - - name: Build xphp-lsp.phar - run: make -C tools/lsp build/phar - - - name: Setup JDK 21 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Run IntelliJ Plugin Verifier - # `verifyPlugin` checks binary compatibility across the IDE matrix - # declared in build.gradle.kts `pluginVerification.ides {}` -- the - # `recommended()` set for since-build 261. A failure here means a - # platform API we depend on changed shape between IDE builds. - run: make -C tools/phpstorm-plugin verify - - - name: Upload Plugin Verifier reports on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: phpstorm-plugin-verifier-reports - path: tools/phpstorm-plugin/build/reports/pluginVerifier/ - if-no-files-found: ignore - retention-days: 14 diff --git a/.github/workflows/release-phpstorm-plugin.yml b/.github/workflows/release-phpstorm-plugin.yml deleted file mode 100644 index 5e31d43..0000000 --- a/.github/workflows/release-phpstorm-plugin.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: PhpStorm Plugin Release - -# Two trigger modes, both supported: -# - automatic: push a tag like `v0.2.0` -> CI builds and uploads the zip -# - manual: run from the Actions tab with a tag input (the tag must -# already exist; the workflow checks out THAT ref so the build is -# reproducible against the committed code, not whatever HEAD is on -# the default branch at trigger time). -on: - workflow_dispatch: - inputs: - tag: - description: 'Existing tag to build (e.g. v0.1.0). Must already be pushed.' - required: true - type: string - -permissions: - # Required to create the Release and upload assets to it. Read-only - # checkout would otherwise fail when softprops/action-gh-release tries - # to POST to the releases API. - contents: write - -# Don't cancel a release mid-upload -- a half-uploaded asset is worse -# than a duplicate run. Concurrency group keys on the tag, so two tags -# released back-to-back don't collide. -concurrency: - group: release-phpstorm-plugin-${{ github.event.inputs.tag || github.ref_name }} - cancel-in-progress: false - -jobs: - release: - name: Build + publish PhpStorm plugin - runs-on: ubuntu-latest - steps: - - name: Resolve tag + version - id: tag - # On a push:tags trigger, GITHUB_REF is `refs/tags/v0.1.0`. - # On workflow_dispatch, the user-supplied input takes over. - # `version` is the tag minus the leading `v` so it threads - # straight into Gradle's pluginVersion (which has no v prefix). - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - tag="${{ github.event.inputs.tag }}" - else - tag="${GITHUB_REF#refs/tags/}" - fi - if [[ ! "$tag" =~ ^v[0-9] ]]; then - echo "::error::tag '$tag' must start with 'v' followed by a digit (e.g. v0.1.0)" - exit 1 - fi - version="${tag#v}" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Checkout at tag - uses: actions/checkout@v4 - with: - ref: ${{ steps.tag.outputs.tag }} - fetch-depth: 1 - - - 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 LSP dependencies - uses: ramsey/composer-install@v3 - with: - working-directory: tools/lsp - - - name: Build bundled xphp-lsp.phar - # processResources copies var/xphp-lsp.phar into the plugin jar. - # Without this step the plugin ships without an embedded LSP and - # the user-facing zero-config install path breaks. - run: make -C tools/lsp build/phar - - - name: Setup JDK 21 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build plugin zip with tag-derived version - # -PpluginVersion overrides the gradle.properties default, so: - # - the produced zip is xphp-phpstorm-plugin-.zip - # - plugin.xml's matches the git tag - # - the JetBrains Marketplace update channel (if ever published) - # reads the tag-driven version, not a stale 0.1.0 - run: | - ./tools/phpstorm-plugin/gradlew \ - -p tools/phpstorm-plugin \ - buildPlugin \ - -PpluginVersion=${{ steps.tag.outputs.version }} - - - name: Locate built zip - id: zip - # `ls` produces a single match because Gradle emits exactly one - # zip per buildPlugin run. Fail loudly if the path doesn't - # match what we expected -- catches a Gradle config drift early. - run: | - path=$(ls tools/phpstorm-plugin/build/distributions/xphp-phpstorm-plugin-${{ steps.tag.outputs.version }}.zip) - if [[ ! -f "$path" ]]; then - echo "::error::zip not found at expected path" - ls -la tools/phpstorm-plugin/build/distributions/ || true - exit 1 - fi - echo "path=$path" >> "$GITHUB_OUTPUT" - echo "name=$(basename "$path")" >> "$GITHUB_OUTPUT" - - - name: Create release and upload zip - # softprops/action-gh-release creates the GitHub Release if it - # doesn't exist yet (push:tags case) and updates it idempotently - # on workflow_dispatch re-runs. `files:` uploads the zip as a - # release asset -- the resulting download URL is: - # https://github.com///releases/download// - # i.e. the tag appears in the URL path AND inside the asset - # filename (`...-.zip`), so the version is impossible - # to miss from either direction. - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.tag.outputs.tag }} - name: ${{ steps.tag.outputs.tag }} - draft: false - prerelease: false - files: ${{ steps.zip.outputs.path }} - generate_release_notes: true - fail_on_unmatched_files: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf8fe2a..0ca9e17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,191 +1,12 @@ # Contributing -## 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 xphp compiler lives under `core/`; -satellites (LSP, PhpStorm plugin, etc.) live under `tools//`. - -``` -xphp-lang/ -+-- 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) -+-- .github/workflows/ -| +-- ci-core.yml # phpunit + infection for core/ -` -- ci-.yml # one file per package under tools/ -``` - -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 external `xphp` tools. - -### Adding a new package - -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 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 - per-package dependencies. - -3. **Cross-package dependency on the core**: PHP packages do this via a - path-repo back to `core/`, the way `tools/lsp/composer.json` declares - `"xphp-lang/xphp": "@dev"` with `repositories: [{type: path, url: - "../../core/"}]`. Other languages need their own analog. The PhpStorm plugin - takes a different route: it doesn't compile against xphp at all -- it - spawns the LSP as a subprocess and bundles the LSP's pre-built PHAR - (via `processResources` copying `../lsp/var/xphp-lsp.phar`) into the - plugin jar at build time. That's the cleanest cross-package dependency - when the consumer doesn't actually need symbols from the dependency. - -4. **Per-package Makefile**: each package ships its own `Makefile` at - `tools//Makefile` with short, package-relative target names: - - ```make - test: # phpunit / gradle test / cargo test / ... - test/mutation: # if the package has a mutation surface - ``` - - Invoke from the repo root with `make -C tools/ ` or - directly from inside the package with `make `. The root - `Makefile` does NOT have pass-through targets — each Makefile is the - single source of truth for its package's recipes, no delegation - layer to drift. - -5. **Per-package CI workflow** at `.github/workflows/ci-.yml`. Mirror - the existing `ci-lsp.yml` or `ci-phpstorm-plugin.yml` shape: one - workflow per package, each with its own concurrency group, each - running unconditionally (no path filters — see the next section). - `ci-phpstorm-plugin.yml` doubles as the worked example for a package - whose CI needs **both** ecosystems (PHP to build the bundled PHAR, - then JDK + Gradle to build the plugin around it). - -6. **Add the package to the roadmap** - ([`docs/roadmap.md`](/docs/roadmap.md) Shipped → Tooling once it ships) - and consider a one-line entry in the [README](/README.md) pointing at - it. - -### CI: one workflow file per package, no path filters - -`.github/workflows/ci-core.yml`, `ci-lsp.yml`, and `ci-phpstorm-plugin.yml` -are parallel, independent workflows. Each runs on every PR and every push -to `main`. A new package gets a fourth file in the same shape. - -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); 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 -workflow level. Leave required status checks intact. - -## Testing - -### Unit tests +## Test ```bash -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 -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: - +make test/unit # PHPUnit +make test/mutation # Infection, MSI under a 95 % gate ``` -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. - `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** - `test/Transpiler/Monomorphize/CompilerIntegrationTest.php::testInstanceofAgainstOriginalTemplateMatchesAllSpecializations`. - -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 - 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 -cause. - -### Mutation tests - -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 -surfaces genuine test gaps when they appear. \ No newline at end of file +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/Makefile b/Makefile similarity index 100% rename from core/Makefile rename to Makefile diff --git a/README.md b/README.md index 0aa482a..be26ebe 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ## 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. +`xphp` is a superset of `php` that gives developers real generics, +powered by [monomorphization](https://en.wikipedia.org/wiki/Monomorphization) at compile time. + +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. ## How it works @@ -17,22 +19,57 @@ 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. +ecosystem around it, much more than its syntax and features. -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. +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. 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. +**contribute** to the `php` community and its ecosystem. + +## 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. `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. + +### 3. Progressive Enhancement + +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. + +No custom runtimes, no `HHVM` style ecosystem splits. + +### 4. Developer Experience + +The tooling must be fast and native as in 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/). + +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. ## Getting started @@ -166,48 +203,6 @@ Meaning every place where generics are declared or used is converted into normal └── composer.json # PSR-4: XPHP\Generated\ => /Generated/ ``` -## 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. `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. - -### 3. Progressive Enhancement - -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. - -No custom runtimes, no `HHVM` style ecosystem splits. - -### 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/). - -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. - ## See also - [Type-system comparison](core/docs/type-system/comparison.md) diff --git a/core/bin/xphp b/bin/xphp similarity index 100% rename from core/bin/xphp rename to bin/xphp diff --git a/core/composer.json b/composer.json similarity index 57% rename from core/composer.json rename to composer.json index 2b06343..7dedfc6 100644 --- a/core/composer.json +++ b/composer.json @@ -2,21 +2,26 @@ "name": "xphp-lang/xphp", "description": "Parse xphp files and convert them into php files", "type": "library", + "keywords": [ + "xphp", + "php", + "generics", + "transpiler", + "monomorphization", + "type-system" + ], + "homepage": "https://github.com/xphp-lang/xphp", "license": "MIT", - "autoload": { - "psr-4": { - "XPHP\\": [ - "src/", - "test/" - ] - } - }, "authors": [ { "name": "Matheus Martins", "email": "math3usmartins@github.com" } ], + "support": { + "issues": "https://github.com/xphp-lang/xphp/issues", + "source": "https://github.com/xphp-lang/xphp" + }, "require": { "php": "^8.4.0", "nikic/php-parser": "^5.7", @@ -26,10 +31,20 @@ "phpunit/phpunit": "^13.0", "infection/infection": "^0.33" }, + "autoload": { + "psr-4": { + "XPHP\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "XPHP\\": "test/" + } + }, + "bin": [ + "bin/xphp" + ], "config": { - "platform": { - "php": "8.4.21" - }, "allow-plugins": { "infection/extension-installer": true } diff --git a/core/composer.lock b/composer.lock similarity index 99% rename from core/composer.lock rename to composer.lock index 0b199ba..8ff9d4a 100644 --- a/core/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": "066da994082a1fe974ba5ae899c3c976", + "content-hash": "5ff71bb52c079c1ea97156c3c08a9532", "packages": [ { "name": "nikic/php-parser", @@ -4342,8 +4342,5 @@ "php": "^8.4.0" }, "platform-dev": {}, - "platform-overrides": { - "php": "8.4.21" - }, "plugin-api-version": "2.9.0" } diff --git a/core/CONTRIBUTING.md b/core/CONTRIBUTING.md deleted file mode 100644 index 0ca9e17..0000000 --- a/core/CONTRIBUTING.md +++ /dev/null @@ -1,12 +0,0 @@ -# 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 deleted file mode 100644 index 7221caf..0000000 --- a/core/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# xphp core - -This is the package downstream `xphp` tools consume. e.g. LSP, PhpStorm plugin -and VSCode extension. - -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 - -The following features are already supported: - -- Single-parameter generics (`class Box`) -- Multi-parameter generics (`Pair`, any arity) -- Nested generics at arbitrary depth (`Box>`) -- Type-hint positions everywhere (param / return / property / `new`) -- Transitive fixed-point specialization (16-iteration depth cap) -- Generic interfaces (`interface Container`) -- Generic traits as templates (dropped after specialization) -- Method-scoped generics (`function NAME(...)` inside a class; static calls) -- Free generic functions (`function NAME(...)` at namespace scope) -- Type-parameter bounds (`class Box`) -- `instanceof` against the original template (marker interface) -- Collision-safe generated FQCN (`XPHP\Generated\