diff --git a/README.md b/README.md index 69ac98da761..6ea3d92c36f 100755 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ -**Snapshot (2026-09-05, from [`docs/status-snapshot.json`](docs/status-snapshot.json)):** self-host spine **8217** / **8217** · capability matrix **4054** rows (VM yes **4021**, JIT **1883**, AOT **1737**) · differential cases **323** · apps corpus **1**/**12** ready · VM driver probe target ~**20**ms · local CI streak **2**d ([#36401](https://github.com/PurHur/php-compiler/issues/36401)) +**Snapshot (2026-09-05, from [`docs/status-snapshot.json`](docs/status-snapshot.json)):** self-host spine **8218** / **8218** · capability matrix **4054** rows (VM yes **4021**, JIT **1883**, AOT **1737**) · differential cases **324** · apps corpus **1**/**12** ready · VM driver probe target ~**20**ms · local CI streak **2**d ([#36401](https://github.com/PurHur/php-compiler/issues/36401)) ## Current implementation status @@ -22,7 +22,7 @@ | **VM (`phpc run`)** | ✅ Production-shaped for dev/CI | Broadest language coverage; reference executor and JIT/AOT fallback | | **AOT (`phpc build`)** | ✅ For curated subset | Standalone binaries for examples **000–009** and small CGI apps; Composer stacks tracked in [#36382](https://github.com/PurHur/php-compiler/issues/36382) / [#36380](https://github.com/PurHur/php-compiler/issues/36380) | | **JIT (`bin/jit.php`)** | 🚧 Partial | LLVM IR for many constructs; **MCJIT execute** still flaky ([#98](https://github.com/PurHur/php-compiler/issues/98)) | -| **Self-host north star** | 🚧 | Spine **8217** / **8217** ✅ · M3 HelloWorld `emit_path=native` via gen-0 argv helper ([#22178](https://github.com/PurHur/php-compiler/issues/22178)) · `BOOTSTRAP_M4_REQUIRE_NATIVE_EMIT=1` refuses sidecar COPY ([#36146](https://github.com/PurHur/php-compiler/issues/36146)) · daily `make north-star5-verify-fast` · `--strict` only before bootstrap merges | +| **Self-host north star** | 🚧 | Spine **8218** / **8218** ✅ · M3 HelloWorld `emit_path=native` via gen-0 argv helper ([#22178](https://github.com/PurHur/php-compiler/issues/22178)) · `BOOTSTRAP_M4_REQUIRE_NATIVE_EMIT=1` refuses sidecar COPY ([#36146](https://github.com/PurHur/php-compiler/issues/36146)) · daily `make north-star5-verify-fast` · `--strict` only before bootstrap merges | ### What you can rely on today @@ -38,7 +38,7 @@ Counts from `php script/bootstrap-spine-count.php` / `docs/status-snapshot.json` | Milestone | Status | What it means | |-----------|--------|----------------| | **M0–M1** | ✅ | `compiler_minimal` + compile-smoke bundles link and run natively | -| **M2** | ✅ **8217** / **8217** | Full Phase A inventory in spine smoke | +| **M2** | ✅ **8218** / **8218** | Full Phase A inventory in spine smoke | | **M3** | ✅ / 🚧 | HelloWorld `emit_path=native` via gen-0 argv helper ([#22178](https://github.com/PurHur/php-compiler/issues/22178)); `BOOTSTRAP_M3_REQUIRE_NATIVE_EMIT=1` refuses sidecar COPY ([#21860](https://github.com/PurHur/php-compiler/issues/21860), [#36146](https://github.com/PurHur/php-compiler/issues/36146)) | | **M4** | 🚧 | `BOOTSTRAP_M4_REQUIRE_NATIVE_EMIT=1` refuses sidecar COPY ([#36146](https://github.com/PurHur/php-compiler/issues/36146)); default `bootstrap-loop-probe` may exit **2** when the ladder is degraded — not a false OK | | **M5** | 🚧 | Daily: `make north-star5-verify-fast` · before bootstrap merges: `make north-star5-verify ARGS=--strict` | @@ -255,7 +255,7 @@ Full matrices (auto-generated): [`docs/capabilities.md`](docs/capabilities.md) ( **Self-host (experimental, not “stable app” scope)** -See [Current implementation status](#current-implementation-status-july-2026) for the full M0–M5 ladder. Summary: M0–M5 bootstrap gates ✅; spine **8215** / **8215**; M3 strict native + inventory argv ✅; M4 full `bootstrap-loop-probe` ✅; M5 **`north-star5-verify-fast`** (daily) ✅ — **`--strict` red at step 4a2** ([#21417](https://github.com/PurHur/php-compiler/issues/21417)) ([#1492](https://github.com/PurHur/php-compiler/issues/1492), [#8559](https://github.com/PurHur/php-compiler/issues/8559)). Recent: native spine bundle probe, fast VM execute smoke ([#2201](https://github.com/PurHur/php-compiler/issues/2201)), `GeneratorYieldSourceMarker` spine unit ([#10356](https://github.com/PurHur/php-compiler/pull/10356)). +See [Current implementation status](#current-implementation-status-july-2026) for the full M0–M5 ladder. Summary: M0–M5 bootstrap gates ✅; spine **8218** / **8218**; M3 strict native + inventory argv ✅; M4 full `bootstrap-loop-probe` ✅; M5 **`north-star5-verify-fast`** (daily) ✅ — **`--strict` red at step 4a2** ([#21417](https://github.com/PurHur/php-compiler/issues/21417)) ([#1492](https://github.com/PurHur/php-compiler/issues/1492), [#8559](https://github.com/PurHur/php-compiler/issues/8559)). Recent: native spine bundle probe, fast VM execute smoke ([#2201](https://github.com/PurHur/php-compiler/issues/2201)), `GeneratorYieldSourceMarker` spine unit ([#10356](https://github.com/PurHur/php-compiler/pull/10356)). **What we do not target in v1.x** diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index bc868f7f170..ea654d6c54e 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,11 +8,11 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 8217 | -| Phase A inventory files (M2 ratio SSOT) | 8217 | +| PHP files on vm.php path | 8218 | +| Phase A inventory files (M2 ratio SSOT) | 8218 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 26019 | +| Source constructs flagged (warnings) | 26021 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -7068,6 +7068,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `lib/JIT/Concern/EmitJitReturn.php` | 0 | 2 | | `lib/JIT/Concern/InitJitMethodCall.php` | 0 | 3 | | `lib/JIT/Concern/InitJitStaticCall.php` | 0 | 15 | +| `lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php` | 0 | 2 | | `lib/JIT/Concern/M3EmitTuSidecarLinktime.php` | 0 | 2 | | `lib/JIT/Concern/PropertyFetchCoalesceAndCompileTimeString.php` | 0 | 6 | | `lib/JIT/Concern/PropertyIncDecCompile.php` | 0 | 19 | @@ -50483,87 +50484,87 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT.php` **Warnings** (review for bootstrap subset): -- new JIT\Call\ExternalMethod (line 363) -- new JIT\Scope (line 392) -- new Operand\Literal (line 642) -- new Operand\Literal (line 658) -- new JIT\Call\Native (line 1136) -- new Variable (line 2142) -- new JIT\Call\Native (line 2168) -- new Variable (line 2258) -- new JIT\Call\Vararg (line 2285) -- new JIT\Call\Native (line 2295) -- new JIT\Call\Native (line 2584) -- new JIT\Variable (line 2824) -- new JIT\Call\Native (line 2841) -- new JIT\Call\Native (line 2886) -- new JIT\Call\Native (line 2956) -- new JIT\Call\Native (line 2998) -- new JIT\Call\Native (line 3043) -- new JIT\Call\Native (line 3098) -- new JIT\Call\Native (line 4285) -- new JIT\Call\Native (line 4316) -- new JIT\Call\Native (line 4363) -- new JIT\Call\Native (line 4437) -- new JIT\Call\Native (line 4491) -- new JIT\Call\Native (line 4578) -- new JIT\Call\Native (line 5071) -- new JIT\Call\Native (line 5102) -- new JIT\Call\Native (line 5138) -- new JIT\Call\Native (line 5173) -- new JIT\Call\Native (line 5208) -- new JIT\Call\Native (line 5243) -- new JIT\Call\Native (line 5355) -- new JIT\Call\Native (line 5597) -- new JIT\Call\Native (line 5635) -- new JIT\Call\Native (line 5669) -- new JIT\Call\Native (line 5790) -- new JIT\Call\Native (line 6440) -- new JIT\Call\Native (line 6512) -- new Operand\Literal (line 7023) -- new Variable (line 7077) -- new Variable (line 7121) -- new Variable (line 7181) -- new Variable (line 7877) -- new Variable (line 7897) -- new Variable (line 7918) -- new Variable (line 7970) -- new JIT\Call\ForeachIndexedClosureCall (line 8452) -- new Variable (line 8541) -- new Variable (line 8571) -- new Variable (line 8590) -- new Variable (line 8620) -- new Variable (line 8657) -- new Variable (line 8677) -- new Variable (line 8690) -- new Variable (line 8733) -- new JIT\Variable (line 9523) -- new JIT\Variable (line 9586) -- new Variable (line 9686) -- new Variable (line 9730) -- new Variable (line 9782) -- new Variable (line 9793) -- new Variable (line 9804) -- new Variable (line 9824) -- new Variable (line 9887) -- new Variable (line 9950) -- new Variable (line 10020) -- new Variable (line 10051) -- new Variable (line 10119) -- new Operand\Literal (line 10128) -- new Variable (line 10143) -- new Variable (line 10477) -- new JIT\Call\VmCoerceVariableToString (line 10626) -- new JIT\Call\NoOpConstruct (line 10829) -- new Operand\Literal (line 11071) -- new VM (line 11271) -- new VM\Variable (line 11280) -- new VM\ClassEntry (line 11385) -- new Func\PHP (line 11404) -- new VM\ClassEntry (line 11450) -- new Variable (line 11467) -- new Variable (line 12672) -- 365 class method(s) +- new JIT\Call\ExternalMethod (line 366) +- new JIT\Scope (line 395) +- new Operand\Literal (line 645) +- new Operand\Literal (line 661) +- new JIT\Call\Native (line 1139) +- new Variable (line 2145) +- new JIT\Call\Native (line 2171) +- new Variable (line 2261) +- new JIT\Call\Vararg (line 2288) +- new JIT\Call\Native (line 2298) +- new JIT\Call\Native (line 2587) +- new JIT\Variable (line 2827) +- new JIT\Call\Native (line 2844) +- new JIT\Call\Native (line 2889) +- new JIT\Call\Native (line 2959) +- new JIT\Call\Native (line 3001) +- new JIT\Call\Native (line 3046) +- new JIT\Call\Native (line 3101) +- new JIT\Call\Native (line 4288) +- new JIT\Call\Native (line 4319) +- new JIT\Call\Native (line 4366) +- new JIT\Call\Native (line 4440) +- new JIT\Call\Native (line 4494) +- new JIT\Call\Native (line 4581) +- new JIT\Call\Native (line 5074) +- new JIT\Call\Native (line 5105) +- new JIT\Call\Native (line 5141) +- new JIT\Call\Native (line 5176) +- new JIT\Call\Native (line 5211) +- new JIT\Call\Native (line 5246) +- new JIT\Call\Native (line 5358) +- new JIT\Call\Native (line 5600) +- new JIT\Call\Native (line 5638) +- new JIT\Call\Native (line 5672) +- new JIT\Call\Native (line 5793) +- new JIT\Call\Native (line 6443) +- new JIT\Call\Native (line 6515) +- new Operand\Literal (line 7026) +- new Variable (line 7080) +- new Variable (line 7124) +- new Variable (line 7184) +- new Variable (line 7880) +- new Variable (line 7900) +- new Variable (line 7921) +- new Variable (line 7973) +- new JIT\Call\ForeachIndexedClosureCall (line 8455) +- new Variable (line 8544) +- new Variable (line 8574) +- new Variable (line 8593) +- new Variable (line 8623) +- new Variable (line 8660) +- new Variable (line 8680) +- new Variable (line 8693) +- new Variable (line 8736) +- new JIT\Variable (line 9526) +- new JIT\Variable (line 9589) +- new Variable (line 9689) +- new Variable (line 9733) +- new Variable (line 9785) +- new Variable (line 9796) +- new Variable (line 9807) +- new Variable (line 9827) +- new Variable (line 9890) +- new Variable (line 9953) +- new Variable (line 10023) +- new Variable (line 10054) +- new Variable (line 10122) +- new Operand\Literal (line 10131) +- new Variable (line 10146) +- new Variable (line 10480) +- new JIT\Call\VmCoerceVariableToString (line 10629) +- new JIT\Call\NoOpConstruct (line 10832) +- new Operand\Literal (line 11074) +- new VM (line 11274) +- new VM\Variable (line 11283) +- new VM\ClassEntry (line 11388) +- new Func\PHP (line 11407) +- new VM\ClassEntry (line 11453) +- new Variable (line 11470) +- new Variable (line 11637) +- 335 class method(s) - 15 closure(s) ### `lib/JIT/Analyzer.php` @@ -58180,6 +58181,12 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - 12 class method(s) - 1 closure(s) +### `lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php` + +**Warnings** (review for bootstrap subset): +- trait LocalReleaseUnsetAndVarFetchDest (line 23) +- 30 class method(s) + ### `lib/JIT/Concern/M3EmitTuSidecarLinktime.php` **Warnings** (review for bootstrap subset): @@ -58584,7 +58591,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/DiscardedPureCallElision.php` **Warnings** (review for bootstrap subset): -- 81 class method(s) +- 83 class method(s) ### `lib/JIT/DnfParamCheck.php` @@ -59719,8 +59726,8 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/NoThrowCallElision.php` **Warnings** (review for bootstrap subset): -- new Operand\Literal (line 2869) -- 92 class method(s) +- new Operand\Literal (line 2959) +- 94 class method(s) ### `lib/JIT/NonObjectPropertyFetchHelper.php` @@ -60074,7 +60081,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/JIT/SpineChunkRuntimeMethodDemote.php` **Warnings** (review for bootstrap subset): -- new OpCode (line 199) +- new OpCode (line 200) - 11 class method(s) ### `lib/JIT/SpineChunkStandardHelperBind.php` diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index d47076e5afc..6b362e40164 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -7155,6 +7155,7 @@ "lib/JIT/Concern/EmitJitReturn.php", "lib/JIT/Concern/InitJitMethodCall.php", "lib/JIT/Concern/InitJitStaticCall.php", + "lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php", "lib/JIT/Concern/M3EmitTuSidecarLinktime.php", "lib/JIT/Concern/PropertyFetchCoalesceAndCompileTimeString.php", "lib/JIT/Concern/PropertyIncDecCompile.php", @@ -8451,9 +8452,9 @@ "test/bootstrap-aot/lib_opcode/main.php" ], "totals": { - "inventory_files": 8217, + "inventory_files": 8218, "excluded": 0, - "eligible": 8217, + "eligible": 8218, "aot_lint_targets": 113, "aot_link_targets": 100, "aot_link_lib_targets": 1 diff --git a/docs/bootstrap-selfhost.md b/docs/bootstrap-selfhost.md index aac956e9cfc..606035a967d 100644 --- a/docs/bootstrap-selfhost.md +++ b/docs/bootstrap-selfhost.md @@ -2,7 +2,7 @@ **Gen-0 without Zend:** `BOOTSTRAP_M5_NO_ZEND=1 make bootstrap-selfhost-link` installs `prelinked/bootstrap-gen0/bin-compile-aot` and links `compiler_minimal` without `php bin/compile.php` ([#3053](https://github.com/PurHur/php-compiler/issues/3053)). **M5 lib spine compile:** `BOOTSTRAP_NO_ZEND_FALLBACK=1 make bootstrap-selfhost-lib-spine-smoke` (default in link script) refuses host `php bin/compile.php` on the spine emit path ([#8716](https://github.com/PurHur/php-compiler/issues/8716)). **Contributor workflow:** [bootstrap-dev-workflow.md](bootstrap-dev-workflow.md) (tiered gen-1+ path, spine checklist, CI). -**Project north star:** The **compiler fully compiles itself** — native AOT from `lib/` (no `vendor/` at cold boot), then compiles PHP and rebuilds the next compiler revision without Zend. **M2 spine:** **8215** / **8215** Phase A inventory (`php script/bootstrap-spine-count.php`; `check-selfhost-spine-coverage-sync.php`). **M5 daily gate:** `make north-star5-verify-fast` (~1–2 min) ✅; **`--strict`** (~1h) pre-merge only. Committed `prelinked/bootstrap-gen0/` sidecars + vendor **7410/7412** cold boot. **Hot loop:** VM driver execute probe ~**20ms**; full spine relink only with `BOOTSTRAP_VM_DRIVER_EXECUTE_PROBE_FULL_LINK=1` ([#2201](https://github.com/PurHur/php-compiler/issues/2201)). **Living tracker:** [#1492](https://github.com/PurHur/php-compiler/issues/1492) (was [#1056](https://github.com/PurHur/php-compiler/issues/1056)) · **re-root doc:** [self-host-target.md](self-host-target.md) · **generation ladder:** [bootstrap-generations.md](bootstrap-generations.md) · **M5 fast path:** [bootstrap-m5-fast-path.md](bootstrap-m5-fast-path.md) · public status: [development-status § North star](https://purhur.github.io/php-compiler/development-status.html#north-star-self-host). Parent tracking: [#78](https://github.com/PurHur/php-compiler/issues/78) (roadmap), [#212](https://github.com/PurHur/php-compiler/issues/212) (closed umbrella). +**Project north star:** The **compiler fully compiles itself** — native AOT from `lib/` (no `vendor/` at cold boot), then compiles PHP and rebuilds the next compiler revision without Zend. **M2 spine:** **8218** / **8218** Phase A inventory (`php script/bootstrap-spine-count.php`; `check-selfhost-spine-coverage-sync.php`). **M5 daily gate:** `make north-star5-verify-fast` (~1–2 min) ✅; **`--strict`** (~1h) pre-merge only. Committed `prelinked/bootstrap-gen0/` sidecars + vendor **7410/7412** cold boot. **Hot loop:** VM driver execute probe ~**20ms**; full spine relink only with `BOOTSTRAP_VM_DRIVER_EXECUTE_PROBE_FULL_LINK=1` ([#2201](https://github.com/PurHur/php-compiler/issues/2201)). **Living tracker:** [#1492](https://github.com/PurHur/php-compiler/issues/1492) (was [#1056](https://github.com/PurHur/php-compiler/issues/1056)) · **re-root doc:** [self-host-target.md](self-host-target.md) · **generation ladder:** [bootstrap-generations.md](bootstrap-generations.md) · **M5 fast path:** [bootstrap-m5-fast-path.md](bootstrap-m5-fast-path.md) · public status: [development-status § North star](https://purhur.github.io/php-compiler/development-status.html#north-star-self-host). Parent tracking: [#78](https://github.com/PurHur/php-compiler/issues/78) (roadmap), [#212](https://github.com/PurHur/php-compiler/issues/212) (closed umbrella). ## Current gates diff --git a/docs/index.md b/docs/index.md index 7acd589b5eb..e19bdb69b35 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,7 @@ Bench history page: [`pages/bench.html`](pages/bench.html). Do not quote a ratio ## Internals -[`architecture-review-2026-07.md`](architecture-review-2026-07.md) · [`self-host-target.md`](self-host-target.md) · [`bootstrap-m5-fast-path.md`](bootstrap-m5-fast-path.md) · ADRs under [`adr/`](adr/). Spine coverage **8217** / **8217**. +[`architecture-review-2026-07.md`](architecture-review-2026-07.md) · [`self-host-target.md`](self-host-target.md) · [`bootstrap-m5-fast-path.md`](bootstrap-m5-fast-path.md) · ADRs under [`adr/`](adr/). Spine coverage **8218** / **8218**. ## Contributing for agents diff --git a/docs/pages/development-status.md b/docs/pages/development-status.md index ce80917487e..83bb881ee95 100644 --- a/docs/pages/development-status.md +++ b/docs/pages/development-status.md @@ -15,7 +15,7 @@ permalink: /development-status.html | **North star** | Compiler compiles itself without Zend ([#1492](https://github.com/PurHur/php-compiler/issues/1492)) | | **v1.1.0 theme** | M5 fast-path stability; enum/property hooks; `preg_match` JIT; `spl_autoload*`; php-in-PHP JIT helpers ([#78](https://github.com/PurHur/php-compiler/issues/78)) | | **Wave 3** | Language **7410/7412** · Stdlib **7410/7412** on master ([#1380](https://github.com/PurHur/php-compiler/issues/1380)) | -| **Spine SSOT** | `php script/bootstrap-spine-count.php` → **8215** / **8215** | +| **Spine SSOT** | `php script/bootstrap-spine-count.php` → **8218** / **8218** | | **Builtin matrix** | **1555** functions ([`docs/capabilities.md`](https://github.com/PurHur/php-compiler/blob/master/docs/capabilities.md)) | | **Try it** | [`docs/GETTING-STARTED.md`](https://github.com/PurHur/php-compiler/blob/master/docs/GETTING-STARTED.md) | @@ -50,7 +50,7 @@ permalink: /development-status.html ### Still open (high signal) - **MCJIT execute** — `bin/jit.php -r` SIGSEGV ([#98](https://github.com/PurHur/php-compiler/issues/98)) -- **Literal spine ratio** — **8215/8215** ✅ (Jul 2026) +- **Literal spine ratio** — **8218/8218** ✅ (Jul 2026) - **Compile-spine stub retirement** — shrink `PHP_COMPILER_SELFHOST_AOT` on M3 allowlist ([#1402](https://github.com/PurHur/php-compiler/issues/1402)) - **007-ThrowsWeb AOT execute** — invalid POST segfault at runtime (link OK; slice `EXAMPLES_AOT_SMOKE_ONLY=007`) - **LLVM 14+ upgrade** — experimental `script/install-llvm14.sh` ([#174](https://github.com/PurHur/php-compiler/issues/174)) @@ -62,7 +62,7 @@ permalink: /development-status.html - **`phpc` CLI** — `run`, `serve`, `build`, `deploy`, `lint`, `test`, `init`, `doctor` - **Examples 000–009** — VM and AOT link/execute for the curated web subset - **Self-host M0** — `compiler_minimal bundle OK` ✅ -- **Self-host M2** — spine **8215/8215** ✅; native link + lint ✅ +- **Self-host M2** — spine **8218/8218** ✅; native link + lint ✅ - **Self-host M3** — HelloWorld strict `emit_path=native` ✅ ([#1493](https://github.com/PurHur/php-compiler/issues/1493)); inventory argv `bin/compile.php` ✅ ([#3024](https://github.com/PurHur/php-compiler/issues/3024) closed); compile-smoke strict native ✅ ([#1937](https://github.com/PurHur/php-compiler/issues/1937)) - **Self-host M4** — `make bootstrap-loop-probe` full ladder ✅; gen-2→gen-3 full-spine recompile ✅ - **Self-host M3–M5** — vendor prelink **7410/7412** ✅; **`make north-star5-verify-fast`** daily ✅; VM probe ~**20ms**. **`--strict` red** ([#21417](https://github.com/PurHur/php-compiler/issues/21417)) and **M3/M4 emit paths are prelinked blob COPIES, not native compiles** ([#21860](https://github.com/PurHur/php-compiler/issues/21860)) — the byte-identical gen-0/gen-2/gen-3 result follows from copying and is not fixpoint evidence @@ -92,7 +92,7 @@ See [`docs/bootstrap-selfhost.md`](https://github.com/PurHur/php-compiler/blob/m |-----------|--------| | **M0** — Small `lib/` bundle runs | ✅ | | **M1** — Compiler-shaped bundle + compile-smoke | ✅ | -| **M2** — Spine toward full inventory | ✅ **8215** / **8215** | +| **M2** — Spine toward full inventory | ✅ **8218** / **8218** | | **M3** — Native compiles PHP (no Zend emit) | ✅ Smoke + inventory argv driver strict native | | **M4** — Bootstrap loop (next revision) | ✅ `bootstrap-loop-probe` full ladder | | **M5** — Full self-host, no `vendor/` cold boot | ✅ Presenter strict + compiled-only empty `build/` cold boot ([#3053](https://github.com/PurHur/php-compiler/issues/3053)) | diff --git a/docs/pages/index.html b/docs/pages/index.html index 217b5b84904..b76f4e919c0 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -391,7 +391,7 @@

Self-host progress

fix/spine-aot-jit-blockers @ a8cffaa0e (15 Jun 2026, LLVM 9). - M2 spine SSOT: php script/bootstrap-spine-count.php**8215** / **8215** Phase A inventory (literal coverage ✅). + M2 spine SSOT: php script/bootstrap-spine-count.php**8218** / **8218** Phase A inventory (literal coverage ✅). M4: make bootstrap-loop-probe full ladder ✅. M5: make north-star5-verify-fast ✅ daily; --strict pre-merge; VM driver probe ~20ms in CI loops. Remaining: MCJIT execute ([#98](https://github.com/PurHur/php-compiler/issues/98)); honest PHP main() in full spine AOT (native bundle-OK probe is bootstrap smoke). @@ -437,7 +437,7 @@

M4

M5

Vendor prelink 7410/7410 ✅; committed .o cold boot ✅; - north-star5-verify-fast ✅ daily; --strict pre-merge; spine **8215** / **8215**; prelinked gen-0 refreshed; + north-star5-verify-fast ✅ daily; --strict pre-merge; spine **8218** / **8218**; prelinked gen-0 refreshed; fast VM execute probe (~20ms); compiled-only cold boot via BOOTSTRAP_M5_NO_ZEND=1 ✅ (#3053, #8559, diff --git a/docs/pages/status.html b/docs/pages/status.html index 202bada2453..a82e14a8e43 100644 --- a/docs/pages/status.html +++ b/docs/pages/status.html @@ -11,13 +11,13 @@

← Overview

Status snapshot

-

Generated by script/status/snapshot.php from docs/status-snapshot.json (#36395) at .

+

Generated by script/status/snapshot.php from docs/status-snapshot.json (#36395) at .

- + - + diff --git a/docs/roadmap-wave3.md b/docs/roadmap-wave3.md index 970f710d48e..bea542a11d1 100644 --- a/docs/roadmap-wave3.md +++ b/docs/roadmap-wave3.md @@ -19,7 +19,7 @@ Regenerate capability truth: `php script/capability-matrix.php`, `php script/cap Related merges outside this wave: `goto` ([#1228](https://github.com/PurHur/php-compiler/issues/1228) / [#1333](https://github.com/PurHur/php-compiler/pull/1333)), anonymous classes ([#1233](https://github.com/PurHur/php-compiler/issues/1233) / [#1386](https://github.com/PurHur/php-compiler/pull/1386)). -**M2 spine:** **8215** / **8215** (`php script/bootstrap-spine-count.php`) — full Phase A inventory in `compiler_lib_spine_smoke`; coverage sync ✅ (`check-selfhost-spine-coverage-sync.php`). Native spine **link** + **lint** ✅ ([#2134](https://github.com/PurHur/php-compiler/issues/2134), [#8559](https://github.com/PurHur/php-compiler/issues/8559)). **M5 daily gate:** `make north-star5-verify-fast` + VM probe ~20ms ([#2201](https://github.com/PurHur/php-compiler/issues/2201)); `--strict` pre-merge only. M4 gen-2→gen-3 recompile ✅. Target doc: [self-host-target.md](self-host-target.md) +**M2 spine:** **8218** / **8218** (`php script/bootstrap-spine-count.php`) — full Phase A inventory in `compiler_lib_spine_smoke`; coverage sync ✅ (`check-selfhost-spine-coverage-sync.php`). Native spine **link** + **lint** ✅ ([#2134](https://github.com/PurHur/php-compiler/issues/2134), [#8559](https://github.com/PurHur/php-compiler/issues/8559)). **M5 daily gate:** `make north-star5-verify-fast` + VM probe ~20ms ([#2201](https://github.com/PurHur/php-compiler/issues/2201)); `--strict` pre-merge only. M4 gen-2→gen-3 recompile ✅. Target doc: [self-host-target.md](self-host-target.md) ## Language (#1354–#1366) diff --git a/docs/self-host-target.md b/docs/self-host-target.md index 784c767ab96..2fc7339ad69 100644 --- a/docs/self-host-target.md +++ b/docs/self-host-target.md @@ -45,7 +45,7 @@ Research stretch (not a release gate): that same binary can compile the next rev |-------|-------|--------| | **Bootstrap driver** | Prelinked gen-0 refreshed via honest inventory argv emit; native `build/bin-compile-aot-inventory` for M4/M5 | Compiled `bin/compile.php` only | | **Bundle size** | **7410/7412** literal Phase A inventory in spine smoke | Full vm.php closure | -| **Inventory coverage** | **8215** / **8215** ✅ | Full closure | +| **Inventory coverage** | **8218** / **8218** ✅ | Full closure | | **HelloWorld** | ✅ `emit_path=native` via gen-0 argv emit helper (`DRIVER -o OUT SOURCE`; [#22178](https://github.com/PurHur/php-compiler/issues/22178)) | Native compile for arbitrary PHP | | **Bootstrap loop (M4)** | `make bootstrap-loop-probe` full ladder ✅ — gen-1→gen-2, gen-2→gen-3 full spine, full-revision argv | Native full revision rebuild | | **Vendor** | **7410/7412** vendor `object_ok`; committed `.o` cold boot without `vendor/` ✅; `make north-star5-verify-fast` daily ✅; `--strict` ❌ **red at step 4a2** ([#21417](https://github.com/PurHur/php-compiler/issues/21417)) | No Zend `vendor/autoload.php` at bootstrap | @@ -79,7 +79,7 @@ Research stretch (not a release gate): that same binary can compile the next rev |-----------|----------------|--------|-----| | **M0** | AOT can link a **small** honest `lib/` subset | ✅ | 100% | | **M1** | Bundle is **compiler-shaped** (lint + compile-smoke) | ✅ | 100% | -| **M2** | Spine grows toward full `bin/vm.php` inventory | ✅ **8215** / **8215** | **100%** | +| **M2** | Spine grows toward full `bin/vm.php` inventory | ✅ **8218** / **8218** | **100%** | | **M3** | Self-host binary **compiles external PHP** (HelloWorld) without Zend emit | ✅ `emit_path=native` via gen-0 argv helper ([#22178](https://github.com/PurHur/php-compiler/issues/22178)) | **~90%** | | **M4** | Self-host binary **rebuilds** the next compiler tree | 🚧 ladder runs but gen-1→gen-2 is a COPY ([#21860](https://github.com/PurHur/php-compiler/issues/21860)) | **~60%** | | **M5 (release)** | Static `phpc` builds user/Composer projects | 🚧 fast gate ✅; corpus app smoke open ([#36380](https://github.com/PurHur/php-compiler/issues/36380)); see [ADR #36393](adr/36393-selfhost-user-payoff.md) | **payoff** | @@ -91,7 +91,7 @@ Research stretch (not a release gate): that same binary can compile the next rev | Indicator | Formula | Jul 2026 | |-----------|---------|----------| -| **M2 spine progress** | `require_once` units in `compiler_lib_spine_smoke` ÷ Phase A inventory file count | **8215** / **8215** (`php script/bootstrap-spine-count.php`) | +| **M2 spine progress** | `require_once` units in `compiler_lib_spine_smoke` ÷ Phase A inventory file count | **8218** / **8218** (`php script/bootstrap-spine-count.php`) | | **Public “Self-host” row** | Same M2 ratio until M3–M5 gates add weight ([`development-status.md`](pages/development-status.md)) | **~97%** | | **M5 vendor prelink** | `object_ok` packages ÷ 3 | **3 / 3** (cfg, types, llvm) | | **Composite (internal)** | Milestone weights in table above (M0–M1 = 100%, M2 = spine %, M3–M5 = gate %) | **~65%** | diff --git a/docs/status-snapshot.json b/docs/status-snapshot.json index 373c931bbb6..d7c89cf163a 100644 --- a/docs/status-snapshot.json +++ b/docs/status-snapshot.json @@ -1,14 +1,14 @@ { "generated_by": "script/status/snapshot.php", "issue": 36395, - "generated_at": "2026-09-05T04:33:06Z", - "spine": 8217, - "inventory": 8217, + "generated_at": "2026-09-05T04:53:30Z", + "spine": 8218, + "inventory": 8218, "builtins_matrix_rows": 4054, "builtins_vm_yes": 4021, "builtins_jit_yes": 1883, "builtins_aot_yes": 1737, - "differential_cases": 323, + "differential_cases": 324, "apps_packages": 12, "apps_ready": 1, "vm_driver_probe_ms_target": 20, diff --git a/lib/JIT.php b/lib/JIT.php index 5ee30449177..677a810e272 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -32,6 +32,7 @@ require_once __DIR__.'/JIT/Concern/CallResultOperandAssign.php'; require_once __DIR__.'/JIT/Concern/ByRefFormalAssignAndCallArgAdapt.php'; require_once __DIR__.'/JIT/Concern/ResolveJitOutgoingCall.php'; +require_once __DIR__.'/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php'; require_once __DIR__.'/JIT/RuntimeInitVmContext.php'; require_once __DIR__.'/JIT/RuntimeInitCompiler.php'; require_once __DIR__.'/JIT/RuntimeInitParsePipeline.php'; @@ -81,6 +82,8 @@ class JIT { use CallResultOperandAssign; use ByRefFormalAssignAndCallArgAdapt; use ResolveJitOutgoingCall; + use LocalReleaseUnsetAndVarFetchDest; + private static int $functionNumber = 0; private static int $blockNumber = 0; /** Nested php-in-PHP helper compiles during an outer JIT::compile() (#10528). */ @@ -11477,1044 +11480,6 @@ private function jitVariableFromVmArray(VM\Variable $vm): Variable return JIT\VmConstantJit::toVariable($this->context, $vm); } - /** Release boxed locals before user function return (Zend end of scope; #4096). */ - private function releaseJitFunctionLocalsAtReturn(Block $block): void - { - if (null === $block->func) { - return; - } - $fnName = $block->func->name; - if ('{main}' === $fnName || str_ends_with($fnName, '::__destruct')) { - return; - } - $byRefParamNames = []; - foreach ($block->paramByRef as $paramIdx => $_) { - if (isset($block->paramNames[$paramIdx]) && '' !== $block->paramNames[$paramIdx]) { - $byRefParamNames[$block->paramNames[$paramIdx]] = true; - } - } - /** @var array $released */ - $released = []; - /** @var array $localNames */ - $localNames = []; - foreach ($this->jitFunctionNamedScopeSlots($block) as [$name, ,]) { - if ('this' !== $name && '' !== $name) { - $localNames[$name] = true; - } - } - foreach ($this->jitFunctionAssignTargets($block) as $destOp) { - $name = JIT\OperandName::resolve($destOp); - if (null !== $name && '' !== $name) { - $localNames[$name] = true; - } - } - foreach ($block->orig->deadOperands ?? [] as $deadOp) { - $name = JIT\OperandName::resolve($deadOp); - if (null !== $name && '' !== $name) { - $localNames[$name] = true; - } - } - foreach (array_keys($localNames) as $name) { - if (isset($released[$name])) { - continue; - } - $resolved = $this->context->resolveRefAliasName($name); - $var = $this->context->namedVariableBindings[$resolved] ?? null; - if (null === $var) { - continue; - } - $this->releaseJitCanonicalNamedLocalAtReturn( - $name, - $var, - $byRefParamNames, - $released - ); - } - } - - /** - * @param array $byRefParamNames - * @param array $released - */ - private function releaseJitCanonicalNamedLocalAtReturn( - string $name, - Variable $var, - array $byRefParamNames, - array &$released - ): void { - if ('this' === $name || isset($released[$name])) { - return; - } - if (isset($byRefParamNames[$name])) { - return; - } - if (Variable::KIND_VARIABLE !== $var->kind) { - return; - } - if ($var->borrowedValueEntry || null !== $var->valueBoxAliasPtr) { - return; - } - if (Variable::TYPE_VALUE === $var->type) { - $this->jitWriteNullForUnset(JIT\JitValueBox::valuePtrFromVariable($this->context, $var)); - $released[$name] = true; - - return; - } - // Native packed arrays (e.g. `string[1]`) still have IS_REFCOUNTED on the - // element type. loadValue+delref would bitcast the array aggregate - // (`[1 x %__string__*]`) to `__ref__virtual*` / i8* and fail module verify - // (#36382 Slim/nyholm; php-src zend_array_destroy walks buckets). - if (0 !== ($var->type & Variable::IS_NATIVE_ARRAY)) { - $var->free(); - $released[$name] = true; - - return; - } - if ($var->type & Variable::IS_REFCOUNTED) { - if (null !== $var->objectPropertySlot) { - return; - } - $ptr = Variable::KIND_VALUE === $var->kind - ? $var->value - : $this->context->helper->loadValue($var); - if ($this->context->type->object->hasUserDestructors()) { - \PHPCompiler\JIT\Builtin\GcCollectCyclesRuntime::ensureLinked($this->context); - $this->context->builder->call( - $this->context->lookupFunction('phpc_destruct_try_invoke'), - $this->context->builder->pointerCast( - $ptr, - $this->context->getTypeFromString('int8*') - ) - ); - } - JIT\Builtin\WeakRefRuntime::ensureLinked($this->context); - $this->context->builder->call( - $this->context->lookupFunction('phpc_weakref_clear_object'), - $this->context->builder->pointerCast( - $ptr, - $this->context->getTypeFromString('int8*') - ) - ); - $this->context->refcount->delref($ptr); - if (Variable::KIND_VARIABLE === $var->kind && null !== $var->value) { - $slotTy = $var->value->typeOf(); - if (\PHPLLVM\Type::KIND_POINTER === $slotTy->getKind()) { - $this->context->builder->store( - $slotTy->getElementType()->constNull(), - $var->value - ); - } - } - $released[$name] = true; - } - } - - /** - * @param array $byRefParamNames - * @param array $released - */ - private function releaseJitNamedLocalAtReturn( - Block $returnBlock, - string $name, - int $slotIdx, - Block $scopeBlock, - array $byRefParamNames, - array &$released - ): void { - if ('this' === $name || isset($released[$name])) { - return; - } - if (isset($byRefParamNames[$name])) { - return; - } - $resolved = $this->context->resolveRefAliasName($name); - $var = $this->context->namedVariableBindings[$resolved] ?? null; - if (null !== $var) { - $this->releaseJitCanonicalNamedLocalAtReturn($name, $var, $byRefParamNames, $released); - - return; - } - if ($slotIdx < 0) { - return; - } - $scopedOp = $scopeBlock->operandForScopeSlot($slotIdx); - if (null === $scopedOp) { - return; - } - try { - $var = $this->context->getVariableFromOp($scopedOp); - } catch (\LogicException) { - return; - } - $this->releaseJitCanonicalNamedLocalAtReturn($name, $var, $byRefParamNames, $released); - } - - /** - * @return list<\PHPCfg\Operand> - */ - private function jitFunctionAssignTargets(Block $returnBlock): array - { - /** @var list<\PHPCfg\Operand> $targets */ - $targets = []; - $seen = new \SplObjectStorage(); - foreach ($this->jitFunctionNamedScopeSlots($returnBlock) as [, , $scopeBlock]) { - foreach ($this->listUnpackAssignTargetsInBlock($scopeBlock) as $dest) { - if ($seen->contains($dest)) { - continue; - } - $seen[$dest] = true; - $targets[] = $dest; - } - } - - return $targets; - } - - /** - * All named CV slots in the returning function — return-block scope alone omits - * live-at-return locals php-cfg already marked dead (#36245 make_pair). - * - * @return \Generator - */ - private function jitFunctionNamedScopeSlots(Block $returnBlock): \Generator - { - $root = $this->context->jitFunctionRootBlock ?? $returnBlock; - /** @var array $seenBlocks */ - $seenBlocks = []; - /** @var list $queue */ - $queue = [$root]; - while ([] !== $queue) { - $scan = array_shift($queue); - $blockId = spl_object_id($scan); - if (isset($seenBlocks[$blockId])) { - continue; - } - $seenBlocks[$blockId] = true; - foreach ($scan->eachNamedScopeSlot() as [$name, $slotIdx]) { - yield [$name, $slotIdx, $scan]; - } - foreach ($scan->opCodes as $op) { - foreach ([$op->block1 ?? null, $op->block2 ?? null, $op->block3 ?? null] as $target) { - if ($target instanceof Block && !isset($seenBlocks[spl_object_id($target)])) { - $queue[] = $target; - } - } - } - } - } - - /** - * unset($var) on boxed locals: run __destruct before nulling when {main} defers delref destroy (#4096). - * Also clear WeakMap/WeakReference immediately — {main} may defer __ref__delref free (#27621 / #26795). - */ - private function jitWriteNullForUnset(\PHPLLVM\Value $valueBoxPtr): void - { - $map = $this->context->structFieldMap['__value__']; - $i8 = $this->context->getTypeFromString('int8'); - $i8p = $this->context->getTypeFromString('int8*'); - $typeByte = $this->context->builder->load( - $this->context->builder->structGep($valueBoxPtr, $map['type']) - ); - $isObject = $this->context->builder->icmp( - \PHPLLVM\Builder::INT_EQ, - $typeByte, - $i8->constInt(Variable::TYPE_OBJECT, false) - ); - $objBlock = JIT\BasicBlockHelper::append($this->context, 'unset_object_side'); - $doneBlock = JIT\BasicBlockHelper::append($this->context, 'unset_object_done'); - $this->context->builder->branchIf($isObject, $objBlock, $doneBlock); - $this->context->builder->positionAtEnd($objBlock); - $obj = $this->context->builder->call( - $this->context->lookupFunction('__value__readObject'), - $valueBoxPtr - ); - $objI8 = $this->context->builder->pointerCast($obj, $i8p); - if ($this->context->type->object->hasUserDestructors()) { - \PHPCompiler\JIT\Builtin\GcCollectCyclesRuntime::ensureLinked($this->context); - $this->context->builder->call( - $this->context->lookupFunction('phpc_destruct_try_invoke'), - $objI8 - ); - } - // WeakMap keys must drop before count() even when delref destroy is deferred (#27621). - // Save insert point — WeakRefRuntime::ensureLinked clears the builder (#27621). - $insertBefore = $this->context->builder->getInsertBlock(); - JIT\Builtin\WeakRefRuntime::ensureLinked($this->context); - if (null !== $insertBefore) { - $this->context->builder->positionAtEnd($insertBefore); - } - $this->context->builder->call( - $this->context->lookupFunction('phpc_weakref_clear_object'), - $objI8 - ); - // Zend decrements refcount on unset — valueDelref alone leaves extra GC roots (#36245). - $this->context->refcount->delref($obj); - $this->context->builder->branch($doneBlock); - $this->context->builder->positionAtEnd($doneBlock); - $this->jitNoteMemoryReleaseForUnset($valueBoxPtr); - $this->context->builder->call( - $this->context->lookupFunction('__value__writeNull'), - $valueBoxPtr - ); - } - - /** - * Named-storage `$a = new T` keeps the object in the NEW/ASSIGN result - * {@see __object__**} alloca as well as the CV value-box. Unset (and null - * assign) must null those mirrors without delref — otherwise the next - * loop-body NEW freeObjectMirrorUnlessNull double-delrefs the orphan and - * GC sees roots=0 (#36245 loop_unset). Distinct Operand instances share a - * CFG slot, so clear via getOperand (assign's operand) not only - * operandForScopeSlot (prologue). - */ - private function jitClearAssignResultObjectMirrorForNamedUnset(Block $block, ?int $unsetArgSlot): void - { - if (null === $unsetArgSlot) { - return; - } - $targetSlot = (int) $unsetArgSlot; - $seen = new \SplObjectStorage(); - foreach ($block->opCodes as $assignOp) { - if (OpCode::TYPE_ASSIGN !== $assignOp->type || null === $assignOp->arg2) { - continue; - } - if ((int) $assignOp->arg2 !== $targetSlot) { - continue; - } - // Property/dim assigns use arg1 === arg2; still clear RHS object mirrors. - $slots = []; - if (null !== $assignOp->arg1 && $assignOp->arg1 !== $assignOp->arg2) { - $slots[] = (int) $assignOp->arg1; - } - try { - $rhs = $this->assignRhsSlot($assignOp); - if ($rhs !== $targetSlot) { - $slots[] = $rhs; - } - } catch (\LogicException $e) { - // Missing RHS slot — named unset still clears assign-result mirrors. - } - foreach ($slots as $slot) { - $this->jitNullObjectMirrorForScopeSlot($block, $slot, $seen); - } - } - } - - /** - * Null every {@see __object__**} alloca bound to $slot (map + all Operand aliases). - * - * @param \SplObjectStorage<\PHPLLVM\Value, mixed> $seen - */ - private function jitNullObjectMirrorForScopeSlot(Block $block, int $slot, \SplObjectStorage $seen): void - { - $nullObj = $this->context->getTypeFromString('__object__*')->constNull(); - if (isset($this->context->scopeSlotObjectMirrorLlvmBySlot[$slot])) { - $llvmMirror = $this->context->scopeSlotObjectMirrorLlvmBySlot[$slot]; - if (!$seen->contains($llvmMirror)) { - $seen[$llvmMirror] = true; - $this->context->builder->store($nullObj, $llvmMirror); - } - } - $operands = []; - $scoped = $block->operandForScopeSlot($slot); - if (null !== $scoped) { - $operands[] = $scoped; - } - // Prefer the exact Operand getOperand returns — assign/NEW lower against it (#36245). - $fromOpcode = $block->getOperand($slot); - if (null !== $fromOpcode) { - $operands[] = $fromOpcode; - } - foreach ($block->scopedOperands() as $scopedOp) { - if ($block->slotForOperand($scopedOp) === $slot) { - $operands[] = $scopedOp; - } - } - foreach ($operands as $op) { - if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { - continue; - } - $mirror = $this->context->hasVariableOp($op) - ? $this->context->getVariableFromOp($op) - : $this->context->scope->variables[$op]; - if ( - Variable::TYPE_OBJECT !== $mirror->type - || Variable::KIND_VARIABLE !== $mirror->kind - || null !== $mirror->objectPropertySlot - || $mirror->functionStaticGlobal - ) { - continue; - } - if (!str_contains($this->context->getStringFromType($mirror->value->typeOf()), '__object__')) { - continue; - } - if ($seen->contains($mirror->value)) { - continue; - } - $seen[$mirror->value] = true; - $this->context->builder->store($nullObj, $mirror->value); - $this->context->scopeSlotObjectMirrorLlvmBySlot[$slot] = $mirror->value; - } - } - - /** Delref an {@see __object__**} mirror only when it still holds a non-null pointer (#36245). */ - private function freeObjectMirrorUnlessNull(Variable $mirror): void - { - $nullObj = $this->context->getTypeFromString('__object__*')->constNull(); - $loaded = $this->context->builder->load($mirror->value); - $hasObj = $this->context->builder->icmp( - \PHPLLVM\Builder::INT_NE, - $loaded, - $nullObj - ); - $delrefBlock = JIT\BasicBlockHelper::append($this->context, 'obj_mirror_delref'); - $skipBlock = JIT\BasicBlockHelper::append($this->context, 'obj_mirror_skip'); - $this->context->builder->branchIf($hasObj, $delrefBlock, $skipBlock); - $this->context->builder->positionAtEnd($delrefBlock); - $this->context->refcount->delref($loaded); - $this->context->builder->branch($skipBlock); - $this->context->builder->positionAtEnd($skipBlock); - // Always clear — unset may have nulled without delref; next NEW must not - // load a stale pointer (#36245 / Variable::free peer). - $this->context->builder->store($nullObj, $mirror->value); - } - - /** - * After === / !==, drop anonymous Temporary value boxes (call results). Named - * locals stay; freeDeadVariables at block edges is too late for unset (#27118). - */ - private function jitReleaseTempValueBoxAfterCompare(Block $block, Operand $op): void - { - $this->context->aliasVariableOpFromSlot($block, $op); - if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { - $slot = $block->slotForOperand($op); - if (null === $slot) { - return; - } - $scoped = $block->operandForScopeSlot($slot); - if (null === $scoped) { - return; - } - $op = $scoped; - } - if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { - return; - } - $var = $this->context->hasVariableOp($op) - ? $this->context->getVariableFromOp($op) - : $this->context->scope->variables[$op]; - $name = JIT\OperandName::resolve($op); - if (null !== $name && '' !== $name) { - // Named locals/params must survive identical/not-identical (#31101 MiniWebApp - // $route after !== "api/status"). Only anonymous temps are statement-end released - // for WeakReference::get (#27118). - return; - } - if ( - Variable::TYPE_VALUE !== $var->type - || $var->functionStaticGlobal - || $var->borrowedValueEntry - || null !== $var->superglobalName - || null !== $var->valueBoxAliasPtr - ) { - return; - } - if ( - Variable::KIND_VARIABLE !== $var->kind - && Variable::KIND_VALUE !== $var->kind - ) { - return; - } - $this->jitWriteNullForUnset( - JIT\JitValueBox::valuePtrFromVariable($this->context, $var) - ); - if ($this->context->scope->variables->contains($op)) { - $this->context->scope->variables->detach($op); - } - } - - private function jitReleasePendingWeakReferenceGetResult(): void - { - $op = $this->context->pendingWeakReferenceGetResult; - $this->context->pendingWeakReferenceGetResult = null; - if (null === $op) { - return; - } - if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { - return; - } - $var = $this->context->hasVariableOp($op) - ? $this->context->getVariableFromOp($op) - : $this->context->scope->variables[$op]; - if ( - Variable::TYPE_VALUE !== $var->type - || $var->functionStaticGlobal - || $var->borrowedValueEntry - ) { - return; - } - $this->jitWriteNullForUnset( - JIT\JitValueBox::valuePtrFromVariable($this->context, $var) - ); - if ($this->context->scope->variables->contains($op)) { - $this->context->scope->variables->detach($op); - } - } - - /** - * VM releaseVmJumpIfCondTemps (#14103) for JIT: drop anonymous TYPE_VALUE boxes - * that die in this block before branching so WeakReference::get() results do not - * keep referents across unset in a ternary-echo merge block (#27118). - * - * Only considers {@see Block::$orig} deadOperands for this block — after successor - * arms are compiled, scope also holds merge-block bindings that must not be freed here. - */ - private function jitReleaseJumpIfAnonValueBoxes(Block $block, OpCode $jumpIf): void - { - $keepOps = new \SplObjectStorage(); - if (null !== $jumpIf->arg1) { - $condOp = $this->operandAt($block, $jumpIf->arg1, 'branch condition'); - $keepOps[$condOp] = true; - } - foreach ($this->context->coalesceAssignTargets as $mergeOp) { - $keepOps[$mergeOp] = true; - } - $toFree = []; - $seen = new \SplObjectStorage(); - foreach ($block->orig->deadOperands as $deadOp) { - $candidates = [$deadOp]; - $slot = $block->slotForOperand($deadOp); - if (null !== $slot) { - $scoped = $block->operandForScopeSlot($slot); - if (null !== $scoped) { - $candidates[] = $scoped; - } - } - foreach ($candidates as $op) { - if ($seen->contains($op)) { - continue; - } - $seen[$op] = true; - if ($keepOps->contains($op)) { - continue; - } - $name = JIT\OperandName::resolve($op); - // Named CVs (params / locals) are never "anon" temps — nulling them at a - // JUMPIF edge clears live values still read in ternary/if arms (#27624: - // DNF `__value__*` param `$x` + `is_array($x) ? count($x) : …`). - if (null !== $name && '' !== $name) { - continue; - } - if (!$this->context->scope->variables->contains($op) && !$this->context->hasVariableOp($op)) { - continue; - } - $var = $this->context->hasVariableOp($op) - ? $this->context->getVariableFromOp($op) - : $this->context->scope->variables[$op]; - // Only owned KIND_VARIABLE allocas. KIND_VALUE often aliases a live CV / - // caller `__value__*` (DNF/mixed params); writeNull would clear storage - // still read in ternary arms (#27624). - if ( - Variable::TYPE_VALUE !== $var->type - || Variable::KIND_VARIABLE !== $var->kind - || $var->functionStaticGlobal - || $var->borrowedValueEntry - || null !== $var->superglobalName - || null !== $var->valueBoxAliasPtr - ) { - continue; - } - $toFree[] = $op; - } - } - foreach ($toFree as $op) { - $var = $this->context->hasVariableOp($op) - ? $this->context->getVariableFromOp($op) - : $this->context->scope->variables[$op]; - $this->jitWriteNullForUnset( - JIT\JitValueBox::valuePtrFromVariable($this->context, $var) - ); - if ($this->context->scope->variables->contains($op)) { - $this->context->scope->variables->detach($op); - } - } - } - - /** Zend emalloc parity: drop tracked bytes when unset frees a string (#7310). */ - private function jitNoteMemoryReleaseForUnset(\PHPLLVM\Value $valueBoxPtr): void - { - JIT\Builtin\MemoryRuntime::ensureLinked($this->context); - $map = $this->context->structFieldMap['__value__']; - $stringMap = $this->context->structFieldMap['__string__']; - $i8 = $this->context->getTypeFromString('int8'); - $i64 = $this->context->getTypeFromString('int64'); - $zero = $i64->constInt(0, false); - $typeByte = $this->context->builder->load( - $this->context->builder->structGep($valueBoxPtr, $map['type']) - ); - $isString = $this->context->builder->icmp( - \PHPLLVM\Builder::INT_EQ, - $typeByte, - $i8->constInt(Variable::TYPE_STRING, false) - ); - $doneBlock = JIT\BasicBlockHelper::append($this->context, 'unset_mem_done'); - $stringBlock = JIT\BasicBlockHelper::append($this->context, 'unset_mem_string'); - $this->context->builder->branchIf($isString, $stringBlock, $doneBlock); - $this->context->builder->positionAtEnd($stringBlock); - $strPtr = $this->context->builder->call( - $this->context->lookupFunction('__value__readString'), - $valueBoxPtr - ); - $len = $this->context->builder->load( - $this->context->builder->structGep($strPtr, $stringMap['length']) - ); - $negLen = $this->context->builder->sub($zero, $len); - JIT\Builtin\MemoryRuntime::noteAlloc($this->context, $negLen); - $this->context->builder->branch($doneBlock); - $this->context->builder->positionAtEnd($doneBlock); - } - - /** Drop assign RHS / result temps so block-end dead-operand free cannot re-delref (#4096). */ - private function jitClearAssignTempOperand(Operand $op): void - { - $this->jitWriteNullOperand($op); - if ($this->context->scope->variables->contains($op)) { - $this->context->scope->variables->detach($op); - } - } - - /** Mirror VM assign: clear dead assign-result / RHS temps (#4096). */ - private function jitWriteNullOperand(Operand $op): void - { - if (!$this->context->hasVariableOp($op)) { - return; - } - $var = $this->context->getVariableFromOp($op); - if (Variable::KIND_VARIABLE === $var->kind && Variable::TYPE_VALUE === $var->type) { - $this->context->builder->call( - $this->context->lookupFunction('__value__writeNull'), - $var->value - ); - - return; - } - if ( - Variable::TYPE_OBJECT === $var->type - && Variable::KIND_VARIABLE === $var->kind - && null !== $var->value - && \in_array($var->value, $this->context->scopeSlotObjectMirrorLlvmBySlot, true) - ) { - $isCanonicalCv = false; - foreach ($this->context->namedVariableBindings as $bound) { - if ($bound === $var) { - $isCanonicalCv = true; - break; - } - } - if (!$isCanonicalCv) { - $slotTy = $var->value->typeOf(); - if (\PHPLLVM\Type::KIND_POINTER === $slotTy->getKind()) { - $this->context->builder->store( - $slotTy->getElementType()->constNull(), - $var->value - ); - } - - return; - } - } - $var->free(); - } - - /** - * php-cfg ASSIGN(resultTemp, namedAlias, rhs) — mark the named CV, not only the dead temp (#36405). - */ - private function propagateAssignAliasBinding( - ?Operand $destOp, - Operand $aliasOp, - bool $namedAliasReceivedAssign - ): void { - $aliasName = JIT\OperandName::resolve($aliasOp); - if (null === $aliasName || '' === $aliasName) { - return; - } - if ($namedAliasReceivedAssign) { - if ($this->context->hasVariableOp($aliasOp)) { - JIT\UndefinedVariableHelper::markAssigned( - $this->context, - $aliasOp, - $this->context->getVariableFromOp($aliasOp) - ); - } - - return; - } - if (null !== $destOp && $this->context->hasVariableOp($destOp)) { - $destVar = $this->context->getVariableFromOp($destOp); - $this->context->setVariableOp($aliasOp, $destVar); - $this->context->bindVariableByName( - $this->context->resolveRefAliasName($aliasName), - $destVar - ); - JIT\UndefinedVariableHelper::markAssigned($this->context, $aliasOp, $destVar); - } - } - - private function maybeBindNamedVariable(Operand $op): void - { - if (!$this->context->hasVariableOp($op)) { - return; - } - $name = JIT\OperandName::resolve($op); - if (null === $name || '' === $name) { - return; - } - $var = $this->context->getVariableFromOp($op); - $this->context->bindVariableByName($name, $var); - // TYPE_ASSIGN dest is a defined CV for later ZEND_CHECK_UNDEFINED_VAR (#32041). - JIT\UndefinedVariableHelper::markAssigned($this->context, $op, $var); - } - - /** - * After KIND_VALUE→alloca CONCAT promotion, rebind every Operand for the dest - * scope slot (named local + unnamed Temporary) so in-place `$out .=` loads and - * stores the same alloca across loop iterations (#22845). - */ - private function bindPromotedStringConcatDest(Block $block, Operand $destOp, Variable $promoted): void - { - $names = []; - $destName = JIT\OperandName::resolve($destOp); - if (null !== $destName && '' !== $destName) { - $names[$destName] = true; - } - $slot = $block->slotForOperand($destOp); - if (null !== $slot) { - foreach ($block->scopedOperands() as $scopeOp) { - if ($block->slotForOperand($scopeOp) !== $slot) { - continue; - } - $this->context->setVariableOp($scopeOp, $promoted); - $scopeName = JIT\OperandName::resolve($scopeOp); - if (null !== $scopeName && '' !== $scopeName) { - $names[$scopeName] = true; - } - } - } - foreach ($names as $name => $_) { - $this->context->bindVariableByName((string) $name, $promoted); - } - $this->markScopeVariableAssignedIfTracked($destOp, $promoted); - if (null !== $slot) { - foreach ($block->scopedOperands() as $scopeOp) { - if ($block->slotForOperand($scopeOp) !== $slot) { - continue; - } - $this->markScopeVariableAssignedIfTracked($scopeOp, $promoted); - } - } - } - - /** - * When php-cfg assigns through a named temporary with no downstream usages, the name slot - * may still be skipped by assignOperand; fold from the matching TYPE_ASSIGN constant (#1226). - */ - private function foldVarFetchNameFromAssign(Block $block, int $nameSlot, Variable $nameVar): void - { - if (null !== $nameVar->compileTimeString) { - return; - } - if (isset($block->constants[$nameSlot])) { - $nameVar->compileTimeString = $block->constants[$nameSlot]->toString(); - - return; - } - foreach ($block->opCodes as $prior) { - if (OpCode::TYPE_ASSIGN !== $prior->type) { - continue; - } - if (!\in_array($prior->arg2, $this->jitNamedScopeSlotAliases($block, $nameSlot), true)) { - continue; - } - if (!isset($block->constants[$prior->arg3])) { - continue; - } - $nameVar->compileTimeString = $block->constants[$prior->arg3]->toString(); - - return; - } - } - - private function varFetchDestUsedAsAssignLvalue(Block $block, int $opIndex, int $destSlot): bool - { - // Immediate next only — later ASSIGN is often dead-temp reuse, not a write (#23986). - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next) { - return false; - } - if (!OpCode::destSlotUsedAsAssignLvalue($next, $destSlot)) { - return false; - } - // php-cfg folds `($o->prop . '=')` into in-place CONCAT on the ?: echo phi slot. - // That CONCAT writes the stack phi, not the property — a write-mode fetch empties - // virtual DOM props (nodeName) and AOT prints "=" then after= is blank (#33849). - if ( - OpCode::TYPE_CONCAT === $next->type - && isset($this->context->coalesceMergeSlotOperands[$destSlot]) - ) { - return false; - } - - return true; - } - - /** - * True when fetch dest is the operand of an immediate TYPE_RETURN in a by-ref function - * (`function &f(){ return C::$x; }` → ZEND_FETCH_STATIC_PROP_W, #34727). - */ - private function varFetchDestUsedAsByRefReturn(Block $block, int $opIndex, int $destSlot): bool - { - if (!$this->cfgFunctionReturnsByRef($block->func)) { - return false; - } - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next || OpCode::TYPE_RETURN !== $next->type) { - return false; - } - - return (int) $next->arg1 === $destSlot; - } - - /** - * True when the fetch dest is the LHS of the immediately following TYPE_ASSIGN - * (`$this->x = $rhs`). Skip the VALUE-slot load for those writes (#32349). - */ - private function varFetchDestUsedAsPlainAssignStore(Block $block, int $opIndex, int $destSlot): bool - { - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next || OpCode::TYPE_ASSIGN !== $next->type) { - return false; - } - - return OpCode::destSlotUsedAsAssignLvalue($next, $destSlot); - } - - /** True when fetch dest is lhs of a following compound assign ($a[$k] += …, #31991). */ - private function varFetchDestUsedAsCompoundAssign(Block $block, int $opIndex, int $destSlot): bool - { - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next) { - return false; - } - - return OpCode::destSlotUsedAsCompoundAssignRead($next, $destSlot) - || OpCode::destSlotUsedAsInPlaceCompoundAssign($next, $destSlot); - } - - /** True when fetch dest is lhs of a following compound read ($prop += …, #30077). */ - private function varFetchDestUsedAsCompoundAssignRead(Block $block, int $opIndex, int $destSlot): bool - { - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next) { - return false; - } - - return OpCode::destSlotUsedAsCompoundAssignRead($next, $destSlot); - } - - /** - * True when the next meaningful use of the fetch dest is TYPE_ISSET (?? / isset). - * Those are BP_VAR_IS and must not raise typed-uninit (#29688 / #33886). - */ - private function propertyFetchResultUsedOnlyAsIsset(Block $block, int $opIndex, int $destSlot): bool - { - $ops = $block->opCodes; - $n = \count($ops); - for ($i = $opIndex + 1; $i < $n; ++$i) { - $next = $ops[$i]; - if (OpCode::TYPE_ISSET === $next->type) { - return (int) $next->arg2 === $destSlot || (int) $next->arg1 === $destSlot; - } - // Any other consumer of this slot (echo, assign, call, …) is BP_VAR_R. - if ( - (int) $next->arg1 === $destSlot - || (int) ($next->arg2 ?? -1) === $destSlot - || (int) ($next->arg3 ?? -1) === $destSlot - ) { - return false; - } - } - - return false; - } - - /** - * True when fetch dest is the container for `$prop[]=` / `$prop[$k]=` / unset dim (#29748). - */ - private function varFetchDestUsedAsDimWriteContainer(Block $block, int $opIndex, int $destSlot): bool - { - $ops = $block->opCodes; - $n = \count($ops); - for ($i = $opIndex + 1; $i < $n; ++$i) { - $next = $ops[$i]; - if (OpCode::destSlotUsedAsDimWriteContainer($next, $destSlot)) { - return true; - } - if ( - OpCode::TYPE_PROPERTY_FETCH === $next->type - || OpCode::TYPE_PROPERTY_FETCH_WRITE === $next->type - ) { - if ((int) $next->arg1 === $destSlot) { - return false; - } - continue; - } - if ( - OpCode::TYPE_ARRAY_DIM_FETCH === $next->type - || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type - ) { - continue; - } - if (OpCode::TYPE_UNSET === $next->type) { - continue; - } - - return false; - } - - return false; - } - - /** - * True when fetch dest is the container of a later FETCH_DIM_W (`$a[i][j]` / #34745). - * - * @see php-src Zend/zend_execute.c ZEND_FETCH_DIM_W (nested dimension address) - */ - private function varFetchDestUsedAsNestedDimWriteContainer(Block $block, int $opIndex, int $destSlot): bool - { - $ops = $block->opCodes; - $n = \count($ops); - for ($i = $opIndex + 1; $i < $n; ++$i) { - $next = $ops[$i]; - if ( - OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type - && (int) $next->arg2 === $destSlot - ) { - return true; - } - } - - return false; - } - - /** - * Expected type for dimFetch: force TYPE_ARRAY on nested FETCH_DIM_W intermediates (#24011 / #34745) - * and on FETCH_DIM_W prefixes that feed unset($a[i][k]) (#36380). - * - * CFG often leaves `$a[0]` as mixed when `$a` is a by-ref formal; without TYPE_ARRAY the outer - * write returns a prepareIndexWrite orphan and the inner write/unset mutates a detached HT. - * - * php-src: Zend/zend_execute.c ZEND_FETCH_DIM_W (nested dimension address) + ZEND_UNSET_DIM. - */ - private function dimFetchExpectedType( - Block $block, - int $opIndex, - int $destSlot, - ?\PHPTypes\Type $resultType, - bool $forWrite - ): ?\PHPTypes\Type { - if ( - $forWrite - && ( - $this->varFetchDestUsedAsNestedDimWriteContainer($block, $opIndex, $destSlot) - || $this->varFetchDestUsedAsDimWriteContainer($block, $opIndex, $destSlot) - ) - ) { - return \PHPTypes\Type::fromDecl('array'); - } - - return $resultType; - } - - /** - * True when property fetch feeds dim RW (++/--/+=) — Zend BP_VAR_RW (#31784). - */ - private function varFetchDestUsedAsDimRwContainer(Block $block, int $opIndex, int $destSlot): bool - { - $ops = $block->opCodes; - $n = \count($ops); - for ($i = $opIndex + 1; $i < $n; ++$i) { - $next = $ops[$i]; - if ( - OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type - && (int) $next->arg2 === $destSlot - ) { - $dimSlot = (int) $next->arg1; - for ($j = $i + 1; $j < $n; ++$j) { - $consumer = $ops[$j]; - if (OpCode::dimSlotUsedAsRwOp($consumer, $dimSlot)) { - return true; - } - if ( - OpCode::TYPE_ASSIGN === $consumer->type - && (int) $consumer->arg2 === $dimSlot - && (int) $consumer->arg3 !== $dimSlot - ) { - return false; - } - if ((int) $consumer->arg1 === $dimSlot) { - if ( - OpCode::TYPE_PROPERTY_FETCH === $consumer->type - || OpCode::TYPE_PROPERTY_FETCH_WRITE === $consumer->type - || OpCode::TYPE_ARRAY_DIM_FETCH === $consumer->type - || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $consumer->type - ) { - return false; - } - } - } - - return false; - } - if ( - OpCode::TYPE_PROPERTY_FETCH === $next->type - || OpCode::TYPE_PROPERTY_FETCH_WRITE === $next->type - ) { - if ((int) $next->arg1 === $destSlot) { - return false; - } - continue; - } - if ( - OpCode::TYPE_ARRAY_DIM_FETCH === $next->type - || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type - ) { - continue; - } - if (OpCode::TYPE_UNSET === $next->type) { - continue; - } - - return false; - } - - return false; - } - - private function varFetchDestUsedAsIncDec(Block $block, int $opIndex, int $destSlot): bool - { - $next = $block->opCodes[$opIndex + 1] ?? null; - if (null === $next) { - return false; - } - - return \in_array($next->type, [ - OpCode::TYPE_PRE_INC, - OpCode::TYPE_POST_INC, - OpCode::TYPE_PRE_DEC, - OpCode::TYPE_POST_DEC, - ], true) && $next->arg3 === $destSlot; - } - /** * Resolve the JIT variable for a scope slot (issue #1226). * diff --git a/lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php b/lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php new file mode 100644 index 00000000000..dabcf4c98ec --- /dev/null +++ b/lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php @@ -0,0 +1,1062 @@ +func) { + return; + } + $fnName = $block->func->name; + if ('{main}' === $fnName || str_ends_with($fnName, '::__destruct')) { + return; + } + $byRefParamNames = []; + foreach ($block->paramByRef as $paramIdx => $_) { + if (isset($block->paramNames[$paramIdx]) && '' !== $block->paramNames[$paramIdx]) { + $byRefParamNames[$block->paramNames[$paramIdx]] = true; + } + } + /** @var array $released */ + $released = []; + /** @var array $localNames */ + $localNames = []; + foreach ($this->jitFunctionNamedScopeSlots($block) as [$name, ,]) { + if ('this' !== $name && '' !== $name) { + $localNames[$name] = true; + } + } + foreach ($this->jitFunctionAssignTargets($block) as $destOp) { + $name = JIT\OperandName::resolve($destOp); + if (null !== $name && '' !== $name) { + $localNames[$name] = true; + } + } + foreach ($block->orig->deadOperands ?? [] as $deadOp) { + $name = JIT\OperandName::resolve($deadOp); + if (null !== $name && '' !== $name) { + $localNames[$name] = true; + } + } + foreach (array_keys($localNames) as $name) { + if (isset($released[$name])) { + continue; + } + $resolved = $this->context->resolveRefAliasName($name); + $var = $this->context->namedVariableBindings[$resolved] ?? null; + if (null === $var) { + continue; + } + $this->releaseJitCanonicalNamedLocalAtReturn( + $name, + $var, + $byRefParamNames, + $released + ); + } + } + + /** + * @param array $byRefParamNames + * @param array $released + */ + private function releaseJitCanonicalNamedLocalAtReturn( + string $name, + Variable $var, + array $byRefParamNames, + array &$released + ): void { + if ('this' === $name || isset($released[$name])) { + return; + } + if (isset($byRefParamNames[$name])) { + return; + } + if (Variable::KIND_VARIABLE !== $var->kind) { + return; + } + if ($var->borrowedValueEntry || null !== $var->valueBoxAliasPtr) { + return; + } + if (Variable::TYPE_VALUE === $var->type) { + $this->jitWriteNullForUnset(JIT\JitValueBox::valuePtrFromVariable($this->context, $var)); + $released[$name] = true; + + return; + } + // Native packed arrays (e.g. `string[1]`) still have IS_REFCOUNTED on the + // element type. loadValue+delref would bitcast the array aggregate + // (`[1 x %__string__*]`) to `__ref__virtual*` / i8* and fail module verify + // (#36382 Slim/nyholm; php-src zend_array_destroy walks buckets). + if (0 !== ($var->type & Variable::IS_NATIVE_ARRAY)) { + $var->free(); + $released[$name] = true; + + return; + } + if ($var->type & Variable::IS_REFCOUNTED) { + if (null !== $var->objectPropertySlot) { + return; + } + $ptr = Variable::KIND_VALUE === $var->kind + ? $var->value + : $this->context->helper->loadValue($var); + if ($this->context->type->object->hasUserDestructors()) { + \PHPCompiler\JIT\Builtin\GcCollectCyclesRuntime::ensureLinked($this->context); + $this->context->builder->call( + $this->context->lookupFunction('phpc_destruct_try_invoke'), + $this->context->builder->pointerCast( + $ptr, + $this->context->getTypeFromString('int8*') + ) + ); + } + JIT\Builtin\WeakRefRuntime::ensureLinked($this->context); + $this->context->builder->call( + $this->context->lookupFunction('phpc_weakref_clear_object'), + $this->context->builder->pointerCast( + $ptr, + $this->context->getTypeFromString('int8*') + ) + ); + $this->context->refcount->delref($ptr); + if (Variable::KIND_VARIABLE === $var->kind && null !== $var->value) { + $slotTy = $var->value->typeOf(); + if (\PHPLLVM\Type::KIND_POINTER === $slotTy->getKind()) { + $this->context->builder->store( + $slotTy->getElementType()->constNull(), + $var->value + ); + } + } + $released[$name] = true; + } + } + + /** + * @param array $byRefParamNames + * @param array $released + */ + private function releaseJitNamedLocalAtReturn( + Block $returnBlock, + string $name, + int $slotIdx, + Block $scopeBlock, + array $byRefParamNames, + array &$released + ): void { + if ('this' === $name || isset($released[$name])) { + return; + } + if (isset($byRefParamNames[$name])) { + return; + } + $resolved = $this->context->resolveRefAliasName($name); + $var = $this->context->namedVariableBindings[$resolved] ?? null; + if (null !== $var) { + $this->releaseJitCanonicalNamedLocalAtReturn($name, $var, $byRefParamNames, $released); + + return; + } + if ($slotIdx < 0) { + return; + } + $scopedOp = $scopeBlock->operandForScopeSlot($slotIdx); + if (null === $scopedOp) { + return; + } + try { + $var = $this->context->getVariableFromOp($scopedOp); + } catch (\LogicException) { + return; + } + $this->releaseJitCanonicalNamedLocalAtReturn($name, $var, $byRefParamNames, $released); + } + + /** + * @return list<\PHPCfg\Operand> + */ + private function jitFunctionAssignTargets(Block $returnBlock): array + { + /** @var list<\PHPCfg\Operand> $targets */ + $targets = []; + $seen = new \SplObjectStorage(); + foreach ($this->jitFunctionNamedScopeSlots($returnBlock) as [, , $scopeBlock]) { + foreach ($this->listUnpackAssignTargetsInBlock($scopeBlock) as $dest) { + if ($seen->contains($dest)) { + continue; + } + $seen[$dest] = true; + $targets[] = $dest; + } + } + + return $targets; + } + + /** + * All named CV slots in the returning function — return-block scope alone omits + * live-at-return locals php-cfg already marked dead (#36245 make_pair). + * + * @return \Generator + */ + private function jitFunctionNamedScopeSlots(Block $returnBlock): \Generator + { + $root = $this->context->jitFunctionRootBlock ?? $returnBlock; + /** @var array $seenBlocks */ + $seenBlocks = []; + /** @var list $queue */ + $queue = [$root]; + while ([] !== $queue) { + $scan = array_shift($queue); + $blockId = spl_object_id($scan); + if (isset($seenBlocks[$blockId])) { + continue; + } + $seenBlocks[$blockId] = true; + foreach ($scan->eachNamedScopeSlot() as [$name, $slotIdx]) { + yield [$name, $slotIdx, $scan]; + } + foreach ($scan->opCodes as $op) { + foreach ([$op->block1 ?? null, $op->block2 ?? null, $op->block3 ?? null] as $target) { + if ($target instanceof Block && !isset($seenBlocks[spl_object_id($target)])) { + $queue[] = $target; + } + } + } + } + } + + /** + * unset($var) on boxed locals: run __destruct before nulling when {main} defers delref destroy (#4096). + * Also clear WeakMap/WeakReference immediately — {main} may defer __ref__delref free (#27621 / #26795). + */ + private function jitWriteNullForUnset(\PHPLLVM\Value $valueBoxPtr): void + { + $map = $this->context->structFieldMap['__value__']; + $i8 = $this->context->getTypeFromString('int8'); + $i8p = $this->context->getTypeFromString('int8*'); + $typeByte = $this->context->builder->load( + $this->context->builder->structGep($valueBoxPtr, $map['type']) + ); + $isObject = $this->context->builder->icmp( + \PHPLLVM\Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_OBJECT, false) + ); + $objBlock = JIT\BasicBlockHelper::append($this->context, 'unset_object_side'); + $doneBlock = JIT\BasicBlockHelper::append($this->context, 'unset_object_done'); + $this->context->builder->branchIf($isObject, $objBlock, $doneBlock); + $this->context->builder->positionAtEnd($objBlock); + $obj = $this->context->builder->call( + $this->context->lookupFunction('__value__readObject'), + $valueBoxPtr + ); + $objI8 = $this->context->builder->pointerCast($obj, $i8p); + if ($this->context->type->object->hasUserDestructors()) { + \PHPCompiler\JIT\Builtin\GcCollectCyclesRuntime::ensureLinked($this->context); + $this->context->builder->call( + $this->context->lookupFunction('phpc_destruct_try_invoke'), + $objI8 + ); + } + // WeakMap keys must drop before count() even when delref destroy is deferred (#27621). + // Save insert point — WeakRefRuntime::ensureLinked clears the builder (#27621). + $insertBefore = $this->context->builder->getInsertBlock(); + JIT\Builtin\WeakRefRuntime::ensureLinked($this->context); + if (null !== $insertBefore) { + $this->context->builder->positionAtEnd($insertBefore); + } + $this->context->builder->call( + $this->context->lookupFunction('phpc_weakref_clear_object'), + $objI8 + ); + // Zend decrements refcount on unset — valueDelref alone leaves extra GC roots (#36245). + $this->context->refcount->delref($obj); + $this->context->builder->branch($doneBlock); + $this->context->builder->positionAtEnd($doneBlock); + $this->jitNoteMemoryReleaseForUnset($valueBoxPtr); + $this->context->builder->call( + $this->context->lookupFunction('__value__writeNull'), + $valueBoxPtr + ); + } + + /** + * Named-storage `$a = new T` keeps the object in the NEW/ASSIGN result + * {@see __object__**} alloca as well as the CV value-box. Unset (and null + * assign) must null those mirrors without delref — otherwise the next + * loop-body NEW freeObjectMirrorUnlessNull double-delrefs the orphan and + * GC sees roots=0 (#36245 loop_unset). Distinct Operand instances share a + * CFG slot, so clear via getOperand (assign's operand) not only + * operandForScopeSlot (prologue). + */ + private function jitClearAssignResultObjectMirrorForNamedUnset(Block $block, ?int $unsetArgSlot): void + { + if (null === $unsetArgSlot) { + return; + } + $targetSlot = (int) $unsetArgSlot; + $seen = new \SplObjectStorage(); + foreach ($block->opCodes as $assignOp) { + if (OpCode::TYPE_ASSIGN !== $assignOp->type || null === $assignOp->arg2) { + continue; + } + if ((int) $assignOp->arg2 !== $targetSlot) { + continue; + } + // Property/dim assigns use arg1 === arg2; still clear RHS object mirrors. + $slots = []; + if (null !== $assignOp->arg1 && $assignOp->arg1 !== $assignOp->arg2) { + $slots[] = (int) $assignOp->arg1; + } + try { + $rhs = $this->assignRhsSlot($assignOp); + if ($rhs !== $targetSlot) { + $slots[] = $rhs; + } + } catch (\LogicException $e) { + // Missing RHS slot — named unset still clears assign-result mirrors. + } + foreach ($slots as $slot) { + $this->jitNullObjectMirrorForScopeSlot($block, $slot, $seen); + } + } + } + + /** + * Null every {@see __object__**} alloca bound to $slot (map + all Operand aliases). + * + * @param \SplObjectStorage<\PHPLLVM\Value, mixed> $seen + */ + private function jitNullObjectMirrorForScopeSlot(Block $block, int $slot, \SplObjectStorage $seen): void + { + $nullObj = $this->context->getTypeFromString('__object__*')->constNull(); + if (isset($this->context->scopeSlotObjectMirrorLlvmBySlot[$slot])) { + $llvmMirror = $this->context->scopeSlotObjectMirrorLlvmBySlot[$slot]; + if (!$seen->contains($llvmMirror)) { + $seen[$llvmMirror] = true; + $this->context->builder->store($nullObj, $llvmMirror); + } + } + $operands = []; + $scoped = $block->operandForScopeSlot($slot); + if (null !== $scoped) { + $operands[] = $scoped; + } + // Prefer the exact Operand getOperand returns — assign/NEW lower against it (#36245). + $fromOpcode = $block->getOperand($slot); + if (null !== $fromOpcode) { + $operands[] = $fromOpcode; + } + foreach ($block->scopedOperands() as $scopedOp) { + if ($block->slotForOperand($scopedOp) === $slot) { + $operands[] = $scopedOp; + } + } + foreach ($operands as $op) { + if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { + continue; + } + $mirror = $this->context->hasVariableOp($op) + ? $this->context->getVariableFromOp($op) + : $this->context->scope->variables[$op]; + if ( + Variable::TYPE_OBJECT !== $mirror->type + || Variable::KIND_VARIABLE !== $mirror->kind + || null !== $mirror->objectPropertySlot + || $mirror->functionStaticGlobal + ) { + continue; + } + if (!str_contains($this->context->getStringFromType($mirror->value->typeOf()), '__object__')) { + continue; + } + if ($seen->contains($mirror->value)) { + continue; + } + $seen[$mirror->value] = true; + $this->context->builder->store($nullObj, $mirror->value); + $this->context->scopeSlotObjectMirrorLlvmBySlot[$slot] = $mirror->value; + } + } + + /** Delref an {@see __object__**} mirror only when it still holds a non-null pointer (#36245). */ + private function freeObjectMirrorUnlessNull(Variable $mirror): void + { + $nullObj = $this->context->getTypeFromString('__object__*')->constNull(); + $loaded = $this->context->builder->load($mirror->value); + $hasObj = $this->context->builder->icmp( + \PHPLLVM\Builder::INT_NE, + $loaded, + $nullObj + ); + $delrefBlock = JIT\BasicBlockHelper::append($this->context, 'obj_mirror_delref'); + $skipBlock = JIT\BasicBlockHelper::append($this->context, 'obj_mirror_skip'); + $this->context->builder->branchIf($hasObj, $delrefBlock, $skipBlock); + $this->context->builder->positionAtEnd($delrefBlock); + $this->context->refcount->delref($loaded); + $this->context->builder->branch($skipBlock); + $this->context->builder->positionAtEnd($skipBlock); + // Always clear — unset may have nulled without delref; next NEW must not + // load a stale pointer (#36245 / Variable::free peer). + $this->context->builder->store($nullObj, $mirror->value); + } + + /** + * After === / !==, drop anonymous Temporary value boxes (call results). Named + * locals stay; freeDeadVariables at block edges is too late for unset (#27118). + */ + private function jitReleaseTempValueBoxAfterCompare(Block $block, Operand $op): void + { + $this->context->aliasVariableOpFromSlot($block, $op); + if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { + $slot = $block->slotForOperand($op); + if (null === $slot) { + return; + } + $scoped = $block->operandForScopeSlot($slot); + if (null === $scoped) { + return; + } + $op = $scoped; + } + if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { + return; + } + $var = $this->context->hasVariableOp($op) + ? $this->context->getVariableFromOp($op) + : $this->context->scope->variables[$op]; + $name = JIT\OperandName::resolve($op); + if (null !== $name && '' !== $name) { + // Named locals/params must survive identical/not-identical (#31101 MiniWebApp + // $route after !== "api/status"). Only anonymous temps are statement-end released + // for WeakReference::get (#27118). + return; + } + if ( + Variable::TYPE_VALUE !== $var->type + || $var->functionStaticGlobal + || $var->borrowedValueEntry + || null !== $var->superglobalName + || null !== $var->valueBoxAliasPtr + ) { + return; + } + if ( + Variable::KIND_VARIABLE !== $var->kind + && Variable::KIND_VALUE !== $var->kind + ) { + return; + } + $this->jitWriteNullForUnset( + JIT\JitValueBox::valuePtrFromVariable($this->context, $var) + ); + if ($this->context->scope->variables->contains($op)) { + $this->context->scope->variables->detach($op); + } + } + + private function jitReleasePendingWeakReferenceGetResult(): void + { + $op = $this->context->pendingWeakReferenceGetResult; + $this->context->pendingWeakReferenceGetResult = null; + if (null === $op) { + return; + } + if (!$this->context->hasVariableOp($op) && !$this->context->scope->variables->contains($op)) { + return; + } + $var = $this->context->hasVariableOp($op) + ? $this->context->getVariableFromOp($op) + : $this->context->scope->variables[$op]; + if ( + Variable::TYPE_VALUE !== $var->type + || $var->functionStaticGlobal + || $var->borrowedValueEntry + ) { + return; + } + $this->jitWriteNullForUnset( + JIT\JitValueBox::valuePtrFromVariable($this->context, $var) + ); + if ($this->context->scope->variables->contains($op)) { + $this->context->scope->variables->detach($op); + } + } + + /** + * VM releaseVmJumpIfCondTemps (#14103) for JIT: drop anonymous TYPE_VALUE boxes + * that die in this block before branching so WeakReference::get() results do not + * keep referents across unset in a ternary-echo merge block (#27118). + * + * Only considers {@see Block::$orig} deadOperands for this block — after successor + * arms are compiled, scope also holds merge-block bindings that must not be freed here. + */ + private function jitReleaseJumpIfAnonValueBoxes(Block $block, OpCode $jumpIf): void + { + $keepOps = new \SplObjectStorage(); + if (null !== $jumpIf->arg1) { + $condOp = $this->operandAt($block, $jumpIf->arg1, 'branch condition'); + $keepOps[$condOp] = true; + } + foreach ($this->context->coalesceAssignTargets as $mergeOp) { + $keepOps[$mergeOp] = true; + } + $toFree = []; + $seen = new \SplObjectStorage(); + foreach ($block->orig->deadOperands as $deadOp) { + $candidates = [$deadOp]; + $slot = $block->slotForOperand($deadOp); + if (null !== $slot) { + $scoped = $block->operandForScopeSlot($slot); + if (null !== $scoped) { + $candidates[] = $scoped; + } + } + foreach ($candidates as $op) { + if ($seen->contains($op)) { + continue; + } + $seen[$op] = true; + if ($keepOps->contains($op)) { + continue; + } + $name = JIT\OperandName::resolve($op); + // Named CVs (params / locals) are never "anon" temps — nulling them at a + // JUMPIF edge clears live values still read in ternary/if arms (#27624: + // DNF `__value__*` param `$x` + `is_array($x) ? count($x) : …`). + if (null !== $name && '' !== $name) { + continue; + } + if (!$this->context->scope->variables->contains($op) && !$this->context->hasVariableOp($op)) { + continue; + } + $var = $this->context->hasVariableOp($op) + ? $this->context->getVariableFromOp($op) + : $this->context->scope->variables[$op]; + // Only owned KIND_VARIABLE allocas. KIND_VALUE often aliases a live CV / + // caller `__value__*` (DNF/mixed params); writeNull would clear storage + // still read in ternary arms (#27624). + if ( + Variable::TYPE_VALUE !== $var->type + || Variable::KIND_VARIABLE !== $var->kind + || $var->functionStaticGlobal + || $var->borrowedValueEntry + || null !== $var->superglobalName + || null !== $var->valueBoxAliasPtr + ) { + continue; + } + $toFree[] = $op; + } + } + foreach ($toFree as $op) { + $var = $this->context->hasVariableOp($op) + ? $this->context->getVariableFromOp($op) + : $this->context->scope->variables[$op]; + $this->jitWriteNullForUnset( + JIT\JitValueBox::valuePtrFromVariable($this->context, $var) + ); + if ($this->context->scope->variables->contains($op)) { + $this->context->scope->variables->detach($op); + } + } + } + + /** Zend emalloc parity: drop tracked bytes when unset frees a string (#7310). */ + private function jitNoteMemoryReleaseForUnset(\PHPLLVM\Value $valueBoxPtr): void + { + JIT\Builtin\MemoryRuntime::ensureLinked($this->context); + $map = $this->context->structFieldMap['__value__']; + $stringMap = $this->context->structFieldMap['__string__']; + $i8 = $this->context->getTypeFromString('int8'); + $i64 = $this->context->getTypeFromString('int64'); + $zero = $i64->constInt(0, false); + $typeByte = $this->context->builder->load( + $this->context->builder->structGep($valueBoxPtr, $map['type']) + ); + $isString = $this->context->builder->icmp( + \PHPLLVM\Builder::INT_EQ, + $typeByte, + $i8->constInt(Variable::TYPE_STRING, false) + ); + $doneBlock = JIT\BasicBlockHelper::append($this->context, 'unset_mem_done'); + $stringBlock = JIT\BasicBlockHelper::append($this->context, 'unset_mem_string'); + $this->context->builder->branchIf($isString, $stringBlock, $doneBlock); + $this->context->builder->positionAtEnd($stringBlock); + $strPtr = $this->context->builder->call( + $this->context->lookupFunction('__value__readString'), + $valueBoxPtr + ); + $len = $this->context->builder->load( + $this->context->builder->structGep($strPtr, $stringMap['length']) + ); + $negLen = $this->context->builder->sub($zero, $len); + JIT\Builtin\MemoryRuntime::noteAlloc($this->context, $negLen); + $this->context->builder->branch($doneBlock); + $this->context->builder->positionAtEnd($doneBlock); + } + + /** Drop assign RHS / result temps so block-end dead-operand free cannot re-delref (#4096). */ + private function jitClearAssignTempOperand(Operand $op): void + { + $this->jitWriteNullOperand($op); + if ($this->context->scope->variables->contains($op)) { + $this->context->scope->variables->detach($op); + } + } + + /** Mirror VM assign: clear dead assign-result / RHS temps (#4096). */ + private function jitWriteNullOperand(Operand $op): void + { + if (!$this->context->hasVariableOp($op)) { + return; + } + $var = $this->context->getVariableFromOp($op); + if (Variable::KIND_VARIABLE === $var->kind && Variable::TYPE_VALUE === $var->type) { + $this->context->builder->call( + $this->context->lookupFunction('__value__writeNull'), + $var->value + ); + + return; + } + if ( + Variable::TYPE_OBJECT === $var->type + && Variable::KIND_VARIABLE === $var->kind + && null !== $var->value + && \in_array($var->value, $this->context->scopeSlotObjectMirrorLlvmBySlot, true) + ) { + $isCanonicalCv = false; + foreach ($this->context->namedVariableBindings as $bound) { + if ($bound === $var) { + $isCanonicalCv = true; + break; + } + } + if (!$isCanonicalCv) { + $slotTy = $var->value->typeOf(); + if (\PHPLLVM\Type::KIND_POINTER === $slotTy->getKind()) { + $this->context->builder->store( + $slotTy->getElementType()->constNull(), + $var->value + ); + } + + return; + } + } + $var->free(); + } + + /** + * php-cfg ASSIGN(resultTemp, namedAlias, rhs) — mark the named CV, not only the dead temp (#36405). + */ + private function propagateAssignAliasBinding( + ?Operand $destOp, + Operand $aliasOp, + bool $namedAliasReceivedAssign + ): void { + $aliasName = JIT\OperandName::resolve($aliasOp); + if (null === $aliasName || '' === $aliasName) { + return; + } + if ($namedAliasReceivedAssign) { + if ($this->context->hasVariableOp($aliasOp)) { + JIT\UndefinedVariableHelper::markAssigned( + $this->context, + $aliasOp, + $this->context->getVariableFromOp($aliasOp) + ); + } + + return; + } + if (null !== $destOp && $this->context->hasVariableOp($destOp)) { + $destVar = $this->context->getVariableFromOp($destOp); + $this->context->setVariableOp($aliasOp, $destVar); + $this->context->bindVariableByName( + $this->context->resolveRefAliasName($aliasName), + $destVar + ); + JIT\UndefinedVariableHelper::markAssigned($this->context, $aliasOp, $destVar); + } + } + + private function maybeBindNamedVariable(Operand $op): void + { + if (!$this->context->hasVariableOp($op)) { + return; + } + $name = JIT\OperandName::resolve($op); + if (null === $name || '' === $name) { + return; + } + $var = $this->context->getVariableFromOp($op); + $this->context->bindVariableByName($name, $var); + // TYPE_ASSIGN dest is a defined CV for later ZEND_CHECK_UNDEFINED_VAR (#32041). + JIT\UndefinedVariableHelper::markAssigned($this->context, $op, $var); + } + + /** + * After KIND_VALUE→alloca CONCAT promotion, rebind every Operand for the dest + * scope slot (named local + unnamed Temporary) so in-place `$out .=` loads and + * stores the same alloca across loop iterations (#22845). + */ + private function bindPromotedStringConcatDest(Block $block, Operand $destOp, Variable $promoted): void + { + $names = []; + $destName = JIT\OperandName::resolve($destOp); + if (null !== $destName && '' !== $destName) { + $names[$destName] = true; + } + $slot = $block->slotForOperand($destOp); + if (null !== $slot) { + foreach ($block->scopedOperands() as $scopeOp) { + if ($block->slotForOperand($scopeOp) !== $slot) { + continue; + } + $this->context->setVariableOp($scopeOp, $promoted); + $scopeName = JIT\OperandName::resolve($scopeOp); + if (null !== $scopeName && '' !== $scopeName) { + $names[$scopeName] = true; + } + } + } + foreach ($names as $name => $_) { + $this->context->bindVariableByName((string) $name, $promoted); + } + $this->markScopeVariableAssignedIfTracked($destOp, $promoted); + if (null !== $slot) { + foreach ($block->scopedOperands() as $scopeOp) { + if ($block->slotForOperand($scopeOp) !== $slot) { + continue; + } + $this->markScopeVariableAssignedIfTracked($scopeOp, $promoted); + } + } + } + + /** + * When php-cfg assigns through a named temporary with no downstream usages, the name slot + * may still be skipped by assignOperand; fold from the matching TYPE_ASSIGN constant (#1226). + */ + private function foldVarFetchNameFromAssign(Block $block, int $nameSlot, Variable $nameVar): void + { + if (null !== $nameVar->compileTimeString) { + return; + } + if (isset($block->constants[$nameSlot])) { + $nameVar->compileTimeString = $block->constants[$nameSlot]->toString(); + + return; + } + foreach ($block->opCodes as $prior) { + if (OpCode::TYPE_ASSIGN !== $prior->type) { + continue; + } + if (!\in_array($prior->arg2, $this->jitNamedScopeSlotAliases($block, $nameSlot), true)) { + continue; + } + if (!isset($block->constants[$prior->arg3])) { + continue; + } + $nameVar->compileTimeString = $block->constants[$prior->arg3]->toString(); + + return; + } + } + + private function varFetchDestUsedAsAssignLvalue(Block $block, int $opIndex, int $destSlot): bool + { + // Immediate next only — later ASSIGN is often dead-temp reuse, not a write (#23986). + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next) { + return false; + } + if (!OpCode::destSlotUsedAsAssignLvalue($next, $destSlot)) { + return false; + } + // php-cfg folds `($o->prop . '=')` into in-place CONCAT on the ?: echo phi slot. + // That CONCAT writes the stack phi, not the property — a write-mode fetch empties + // virtual DOM props (nodeName) and AOT prints "=" then after= is blank (#33849). + if ( + OpCode::TYPE_CONCAT === $next->type + && isset($this->context->coalesceMergeSlotOperands[$destSlot]) + ) { + return false; + } + + return true; + } + + /** + * True when fetch dest is the operand of an immediate TYPE_RETURN in a by-ref function + * (`function &f(){ return C::$x; }` → ZEND_FETCH_STATIC_PROP_W, #34727). + */ + private function varFetchDestUsedAsByRefReturn(Block $block, int $opIndex, int $destSlot): bool + { + if (!$this->cfgFunctionReturnsByRef($block->func)) { + return false; + } + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next || OpCode::TYPE_RETURN !== $next->type) { + return false; + } + + return (int) $next->arg1 === $destSlot; + } + + /** + * True when the fetch dest is the LHS of the immediately following TYPE_ASSIGN + * (`$this->x = $rhs`). Skip the VALUE-slot load for those writes (#32349). + */ + private function varFetchDestUsedAsPlainAssignStore(Block $block, int $opIndex, int $destSlot): bool + { + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next || OpCode::TYPE_ASSIGN !== $next->type) { + return false; + } + + return OpCode::destSlotUsedAsAssignLvalue($next, $destSlot); + } + + /** True when fetch dest is lhs of a following compound assign ($a[$k] += …, #31991). */ + private function varFetchDestUsedAsCompoundAssign(Block $block, int $opIndex, int $destSlot): bool + { + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next) { + return false; + } + + return OpCode::destSlotUsedAsCompoundAssignRead($next, $destSlot) + || OpCode::destSlotUsedAsInPlaceCompoundAssign($next, $destSlot); + } + + /** True when fetch dest is lhs of a following compound read ($prop += …, #30077). */ + private function varFetchDestUsedAsCompoundAssignRead(Block $block, int $opIndex, int $destSlot): bool + { + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next) { + return false; + } + + return OpCode::destSlotUsedAsCompoundAssignRead($next, $destSlot); + } + + /** + * True when the next meaningful use of the fetch dest is TYPE_ISSET (?? / isset). + * Those are BP_VAR_IS and must not raise typed-uninit (#29688 / #33886). + */ + private function propertyFetchResultUsedOnlyAsIsset(Block $block, int $opIndex, int $destSlot): bool + { + $ops = $block->opCodes; + $n = \count($ops); + for ($i = $opIndex + 1; $i < $n; ++$i) { + $next = $ops[$i]; + if (OpCode::TYPE_ISSET === $next->type) { + return (int) $next->arg2 === $destSlot || (int) $next->arg1 === $destSlot; + } + // Any other consumer of this slot (echo, assign, call, …) is BP_VAR_R. + if ( + (int) $next->arg1 === $destSlot + || (int) ($next->arg2 ?? -1) === $destSlot + || (int) ($next->arg3 ?? -1) === $destSlot + ) { + return false; + } + } + + return false; + } + + /** + * True when fetch dest is the container for `$prop[]=` / `$prop[$k]=` / unset dim (#29748). + */ + private function varFetchDestUsedAsDimWriteContainer(Block $block, int $opIndex, int $destSlot): bool + { + $ops = $block->opCodes; + $n = \count($ops); + for ($i = $opIndex + 1; $i < $n; ++$i) { + $next = $ops[$i]; + if (OpCode::destSlotUsedAsDimWriteContainer($next, $destSlot)) { + return true; + } + if ( + OpCode::TYPE_PROPERTY_FETCH === $next->type + || OpCode::TYPE_PROPERTY_FETCH_WRITE === $next->type + ) { + if ((int) $next->arg1 === $destSlot) { + return false; + } + continue; + } + if ( + OpCode::TYPE_ARRAY_DIM_FETCH === $next->type + || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type + ) { + continue; + } + if (OpCode::TYPE_UNSET === $next->type) { + continue; + } + + return false; + } + + return false; + } + + /** + * True when fetch dest is the container of a later FETCH_DIM_W (`$a[i][j]` / #34745). + * + * @see php-src Zend/zend_execute.c ZEND_FETCH_DIM_W (nested dimension address) + */ + private function varFetchDestUsedAsNestedDimWriteContainer(Block $block, int $opIndex, int $destSlot): bool + { + $ops = $block->opCodes; + $n = \count($ops); + for ($i = $opIndex + 1; $i < $n; ++$i) { + $next = $ops[$i]; + if ( + OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type + && (int) $next->arg2 === $destSlot + ) { + return true; + } + } + + return false; + } + + /** + * Expected type for dimFetch: force TYPE_ARRAY on nested FETCH_DIM_W intermediates (#24011 / #34745) + * and on FETCH_DIM_W prefixes that feed unset($a[i][k]) (#36380). + * + * CFG often leaves `$a[0]` as mixed when `$a` is a by-ref formal; without TYPE_ARRAY the outer + * write returns a prepareIndexWrite orphan and the inner write/unset mutates a detached HT. + * + * php-src: Zend/zend_execute.c ZEND_FETCH_DIM_W (nested dimension address) + ZEND_UNSET_DIM. + */ + private function dimFetchExpectedType( + Block $block, + int $opIndex, + int $destSlot, + ?\PHPTypes\Type $resultType, + bool $forWrite + ): ?\PHPTypes\Type { + if ( + $forWrite + && ( + $this->varFetchDestUsedAsNestedDimWriteContainer($block, $opIndex, $destSlot) + || $this->varFetchDestUsedAsDimWriteContainer($block, $opIndex, $destSlot) + ) + ) { + return \PHPTypes\Type::fromDecl('array'); + } + + return $resultType; + } + + /** + * True when property fetch feeds dim RW (++/--/+=) — Zend BP_VAR_RW (#31784). + */ + private function varFetchDestUsedAsDimRwContainer(Block $block, int $opIndex, int $destSlot): bool + { + $ops = $block->opCodes; + $n = \count($ops); + for ($i = $opIndex + 1; $i < $n; ++$i) { + $next = $ops[$i]; + if ( + OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type + && (int) $next->arg2 === $destSlot + ) { + $dimSlot = (int) $next->arg1; + for ($j = $i + 1; $j < $n; ++$j) { + $consumer = $ops[$j]; + if (OpCode::dimSlotUsedAsRwOp($consumer, $dimSlot)) { + return true; + } + if ( + OpCode::TYPE_ASSIGN === $consumer->type + && (int) $consumer->arg2 === $dimSlot + && (int) $consumer->arg3 !== $dimSlot + ) { + return false; + } + if ((int) $consumer->arg1 === $dimSlot) { + if ( + OpCode::TYPE_PROPERTY_FETCH === $consumer->type + || OpCode::TYPE_PROPERTY_FETCH_WRITE === $consumer->type + || OpCode::TYPE_ARRAY_DIM_FETCH === $consumer->type + || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $consumer->type + ) { + return false; + } + } + } + + return false; + } + if ( + OpCode::TYPE_PROPERTY_FETCH === $next->type + || OpCode::TYPE_PROPERTY_FETCH_WRITE === $next->type + ) { + if ((int) $next->arg1 === $destSlot) { + return false; + } + continue; + } + if ( + OpCode::TYPE_ARRAY_DIM_FETCH === $next->type + || OpCode::TYPE_ARRAY_DIM_FETCH_WRITE === $next->type + ) { + continue; + } + if (OpCode::TYPE_UNSET === $next->type) { + continue; + } + + return false; + } + + return false; + } + + private function varFetchDestUsedAsIncDec(Block $block, int $opIndex, int $destSlot): bool + { + $next = $block->opCodes[$opIndex + 1] ?? null; + if (null === $next) { + return false; + } + + return \in_array($next->type, [ + OpCode::TYPE_PRE_INC, + OpCode::TYPE_POST_INC, + OpCode::TYPE_PRE_DEC, + OpCode::TYPE_POST_DEC, + ], true) && $next->arg3 === $destSlot; + } +} diff --git a/lib/JIT/SpineChunkRuntimeMethodDemote.php b/lib/JIT/SpineChunkRuntimeMethodDemote.php index f7487a1edd8..dd3a026687f 100644 --- a/lib/JIT/SpineChunkRuntimeMethodDemote.php +++ b/lib/JIT/SpineChunkRuntimeMethodDemote.php @@ -48,7 +48,7 @@ * OOM at 1536M without demote (~148s) — hollow + demote emits under SPINE_CHUNK (#36387). * - Func\* / Cfg\* / Lint\* / Visitor\*: NestedJIT SEGV on Internal/PHP/Linter/ * OpSubBlockAccess/VoidCastResolver — measured 2026-09-04. - * - Top-level JIT Concern traits ({@see CompileBlockInternal}, AssignOperand, InitJitMethodCall, InitJitStaticCall, CompileIncDecAndConcatFlatten, DateTimeConstructAndMutationMeta, DomCompileTimeTagMeta, CoerceReturnPropertyDeclaringAndByRef, PropertyIncDecCompile, CallResultCompileTimePropagate, PropertyFetchCoalesceAndCompileTimeString, CallResultOperandAssign, ByRefFormalAssignAndCallArgAdapt, ResolveJitOutgoingCall) +* - Top-level JIT Concern traits ({@see CompileBlockInternal}, AssignOperand, InitJitMethodCall, InitJitStaticCall, CompileIncDecAndConcatFlatten, DateTimeConstructAndMutationMeta, DomCompileTimeTagMeta, CoerceReturnPropertyDeclaringAndByRef, PropertyIncDecCompile, CallResultCompileTimePropagate, PropertyFetchCoalesceAndCompileTimeString, CallResultOperandAssign, ByRefFormalAssignAndCallArgAdapt, ResolveJitOutgoingCall, LocalReleaseUnsetAndVarFetchDest) * live in namespace PHPCompiler (not PHPCompiler\JIT\Concern). Host CFG of CompileBlockInternal * (470 KB) OOMs at 1536M unless {@see rewriteSource()} hollows **trait** bodies (T_TRAIT) — * measured 2026-09-04: rc=255 before; emits after T_TRAIT + demote target. @@ -106,6 +106,7 @@ public static function isDemoteTarget(string $displayClassLc): bool || 'phpcompiler\\callresultoperandassign' === $lc || 'phpcompiler\\byrefformalassignandcallargadapt' === $lc || 'phpcompiler\\resolvejitoutgoingcall' === $lc + || 'phpcompiler\\localreleaseunsetandvarfetchdest' === $lc || str_starts_with($lc, 'phpcompiler\\builtin') ) { return true; diff --git a/prelinked/bootstrap-gen0/.m3_compiler_lib_sidecar.sha b/prelinked/bootstrap-gen0/.m3_compiler_lib_sidecar.sha index de21c1b7476..d24e3abd8e8 100755 --- a/prelinked/bootstrap-gen0/.m3_compiler_lib_sidecar.sha +++ b/prelinked/bootstrap-gen0/.m3_compiler_lib_sidecar.sha @@ -1 +1 @@ -5ab39d9b4a50571c3145a4a81d4e51ef2c9d3fd4 \ No newline at end of file +cdb2cfbbaa0239b0b45425e9ae573ab0a7b2aa54 diff --git a/script/size-budgets.json b/script/size-budgets.json index d9b60215272..bc1ab4108bf 100644 --- a/script/size-budgets.json +++ b/script/size-budgets.json @@ -9,9 +9,9 @@ "note": "Concern trait splits (#36230/#36387/#36403); CompileFuncCfgReturnAndGlobalImport + peers" }, "lib/JIT.php": { - "budget": 12739, + "budget": 11704, "target": 20000, - "note": "Concern trait splits (#36387/#36403); ResolveJitOutgoingCall + ByRefFormalAssignAndCallArgAdapt + CallResultOperandAssign" + "note": "Concern trait splits (#36387/#36403); LocalReleaseUnsetAndVarFetchDest + ResolveJitOutgoingCall + peers" }, "lib/VM.php": { "budget": 21186, diff --git a/test/selfhost/compiler_lib_spine_smoke/main.php b/test/selfhost/compiler_lib_spine_smoke/main.php index f2640a63c50..6e451590a0e 100644 --- a/test/selfhost/compiler_lib_spine_smoke/main.php +++ b/test/selfhost/compiler_lib_spine_smoke/main.php @@ -5399,6 +5399,7 @@ require_once __DIR__.'/../../../lib/JIT/Concern/CallResultOperandAssign.php'; require_once __DIR__.'/../../../lib/JIT/Concern/ByRefFormalAssignAndCallArgAdapt.php'; require_once __DIR__.'/../../../lib/JIT/Concern/ResolveJitOutgoingCall.php'; +require_once __DIR__.'/../../../lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php'; require_once __DIR__.'/../../../lib/JIT/Concern/InitJitMethodCall.php'; require_once __DIR__.'/../../../lib/Compiler/CompileTimeNew.php'; require_once __DIR__.'/../../../lib/Compiler/DeprecatedMetadata.php'; diff --git a/test/unit/BootstrapSelfhostBundleTest.php b/test/unit/BootstrapSelfhostBundleTest.php index f8c2c75887c..c72b3485f53 100644 --- a/test/unit/BootstrapSelfhostBundleTest.php +++ b/test/unit/BootstrapSelfhostBundleTest.php @@ -564,7 +564,7 @@ public function testCompilerLibSpineSmokeBundleUnitCountAndKeyUnits(): void $contents = (string) file_get_contents($entry); $count = bootstrap_spine_counts(self::$root)['spine']; // Spine ratio 8017/8002 — Phase A inventory in compiler_lib_spine_smoke (#2202, #31835 typed parent assign). - $this->assertSame(8217, $count, 'M2 spine require_once units track Phase A inventory (#8559, #9234, #11629, #18550, #36079); coverage sync defers PregJitHelperThinAot (#24115) + NetworkServicesNameLookupThinAot (#27103)'); + $this->assertSame(8218, $count, 'M2 spine require_once units track Phase A inventory (#8559, #9234, #11629, #18550, #36079); coverage sync defers PregJitHelperThinAot (#24115) + NetworkServicesNameLookupThinAot (#27103)'); foreach (self::LIB_SPINE_SMOKE_NEW_UNITS as $unit) { $this->assertStringContainsString( "require_once __DIR__.'/../../../{$unit}';", diff --git a/test/unit/SpineChunkRuntimeMethodDemoteTest.php b/test/unit/SpineChunkRuntimeMethodDemoteTest.php index 36685957983..c210b67d814 100644 --- a/test/unit/SpineChunkRuntimeMethodDemoteTest.php +++ b/test/unit/SpineChunkRuntimeMethodDemoteTest.php @@ -99,6 +99,7 @@ public function testShouldDemoteHubCapacityClassesUnderSpineChunk(): void $this->assertTrue(SpineChunkRuntimeMethodDemote::shouldDemote('PHPCompiler\\CallResultOperandAssign')); $this->assertTrue(SpineChunkRuntimeMethodDemote::shouldDemote('PHPCompiler\\ByRefFormalAssignAndCallArgAdapt')); $this->assertTrue(SpineChunkRuntimeMethodDemote::shouldDemote('PHPCompiler\\ResolveJitOutgoingCall')); + $this->assertTrue(SpineChunkRuntimeMethodDemote::shouldDemote('PHPCompiler\\LocalReleaseUnsetAndVarFetchDest')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\CompileBlockInternal')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\CompileClassAndTraitUses')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\InitJitStaticCall')); @@ -112,6 +113,7 @@ public function testShouldDemoteHubCapacityClassesUnderSpineChunk(): void $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\CallResultOperandAssign')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\ByRefFormalAssignAndCallArgAdapt')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\ResolveJitOutgoingCall')); + $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\LocalReleaseUnsetAndVarFetchDest')); $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/CompileBlockInternal.php')); $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/CompileClassAndTraitUses.php')); $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/InitJitStaticCall.php')); @@ -125,6 +127,7 @@ public function testShouldDemoteHubCapacityClassesUnderSpineChunk(): void $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/CallResultOperandAssign.php')); $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/ByRefFormalAssignAndCallArgAdapt.php')); $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/ResolveJitOutgoingCall.php')); + $this->assertTrue(SpineChunkRuntimeMethodDemote::oversizeSingletonCanEmit('lib/JIT/Concern/LocalReleaseUnsetAndVarFetchDest.php')); // Doctor.php — NestedJIT OOM without demote; hollow + demote emits (#36387). $this->assertTrue(SpineChunkRuntimeMethodDemote::shouldDemote('PHPCompiler\\Doctor')); $this->assertTrue(SpineChunkRuntimeMethodDemote::isDemoteTarget('PHPCompiler\\Doctor'));
MetricValue
Spine / inventory8217 / 8217
Spine / inventory8218 / 8218
Capability matrix rows4054 (VM 4021 / JIT 1883 / AOT 1737)
Differential cases323
Differential cases324
Apps corpus ready1 / 12
VM driver probe target~20 ms