You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Written 2026-07-26. Every claim below was measured this session; sources are named so each can be
re-checked rather than believed.
Diagnosis
The project has a great deal of verification ceremony — gates, stamps, spine sync, generated-doc
checks — but the load-bearing checks do not run on the code that matters. So defects and
non-functional artifacts ship while the gates report green.
Evidence, all verified today:
finding
evidence
No CI on the compiler itself
.github/workflows/bootstrap-spine-gate.yml is the only workflow, and its pull_request trigger carries a paths: filter excluding lib/JIT/**, lib/VM/**, ext/**. A PR touching lowering gets zero checks, and gh pr view reports MERGEABLE/CLEAN — which means "no checks configured", not "checks passed".
Silent wrong output on ordinary PHP
24 of 43 mundane programs mismatched Zend. f($x+1, $x+2) printed 12 12; str_replace($p['from'], $p['to'], 'xy!') returned xy!. None caught by the compliance suite. Fixed in #23356/#23424.
The shipped compiler does not work
prelinked/bootstrap-gen0/bin-compile-aot fails parseAndCompile on every input including hello-world (#23468). Driver bytes are from 2026-06-15; 6441 lowering commits since; manifest restamped 272 times, provenance unverified-restamp.
A core cache was silently off for 20 days
Committed helper-runtime fingerprint 1771b935… vs live 6497e66d…. On mismatch every unit is skipped, so a two-line script recompiled ~257 units across 13 cores. Nothing noticed. Refreshed in #23457.
The AOT backend is wrong on its own benchmarks
mandelbrot renders all _ (#23471); Ack(3,8) segfaults (#23472). Both AOT-only — Zend and the VM are correct. These are the benchmarks the project quotes speedups from.
The test suites are too slow to run
VMTest ≈ 4 h serial; JITTest did not finish in 40 min. So they are not run per-change, and both carry large pre-existing failure sets (407 and 472).
Tooling could hang forever
script/bench.php had no timeout: bin/vm.php on Ack(3,10) ran 38 min at 100% CPU (Zend: 1.9 s) and blocked the whole suite. Fixed today.
The through-line: it is currently cheaper to make a gate green than to make the thing work.
Restamping satisfies freshness; a copy satisfies "native emit"; an empty filter satisfies CI.
Strategy
Make truth cheap and lies expensive. Concretely: every claim the repo makes about itself should be
produced by a check that fails when the claim stops being true, and no claim should be satisfiable
by restamping or copying.
Work in this order. Each phase exists because the next one is worthless without it.
Phase 1 — Make correctness observable (days, highest leverage)
Nothing else matters while defects can land unnoticed.
CI on lib/**, ext/**. Today: zero. This is the single highest-value change in the plan.
Start with the cheap tier — php -l, unit suite, nikic preflight (18 s), differential sweep —
so it fits a PR turnaround.
Make the suites runnable. Sharding takes VMTest from ~4 h to ~20 min (24 shards, per-test
TeamCity output so a stalled shard still reports). This session's harness works; land it as a
script rather than leaving it in a scratchpad.
OPEN: does any consumer actually need the gen-0 driver per-commit, or only per-release? If the
latter (likely), the per-commit freshness gate is the wrong shape entirely — see Phase 3.
Phase 3 — Break the artifact treadmill (structural, weeks)
The recurring failure is a committed artifact keyed on a fingerprint that changes several times a
day. Master merges ~4 commits/hour, ~56% touching lowering sources. Any artifact costing more than
~7 minutes to rebuild is stale on arrival, so it gets restamped instead.
Treat gen-0 as a release artifact, not a per-commit invariant. Rebuild under a merge freeze,
verify by function, tag, ship. Between releases, report staleness honestly
(bootstrap-gen0-staleness.php already does) instead of restamping to green.
Phase 4 — Performance, with evidence (after 1–3)
Benchmarks that cannot hang — done today: per-run cap, separate build cap, n/a with a
reason rather than a stall.
Only quote numbers traceable to a committed table where every runtime produced identical
output. The current published AOT column was measured on binaries that are wrong or crash.
Then optimise, driven by profiles. The known hot spots are already measured: the php-cfg Simplifier quadratic path is 57% of gen-0 compile samples with a linear path sitting behind
an opt-in env var, and helper-unit emit is O(units × transitive closure).
DECISION: no optimisation work before Phase 2 closes. Optimising a binary that renders mandelbrot wrong is effort spent making a wrong answer arrive sooner.
Release criteria for v1.1.0
Falsifiable, and each one is a command someone can run:
script/differential-sweep.sh green on VM and AOT.
Committed gen-0 driver compiles an unseen script and matches Zend.
Compliance suites: no regressions vs the previous release by case name, flaky set quarantined
and named.
benchmarks/README.md regenerated with no n/a in any column, every runtime output-verified.
No open defect of class "silent wrong output".
What not to do
Do not optimise before Phase 2. Wrong-but-fast is worse than slow-but-right.
Do not chase the gen-0 rebuild as a per-commit invariant. Arithmetic says it cannot be one.
Do not trust CLEAN on a PR as evidence of anything until Phase 1 item 1 lands.
Sequencing note
Phase 1 items 1–3 are days of work and unblock everything else, because they turn "did I break
something?" from a 4-hour question into a 20-minute one. That single change is what makes the rest
of this plan executable at the project's actual merge velocity.
php-compiler → release: steering plan
Written 2026-07-26. Every claim below was measured this session; sources are named so each can be
re-checked rather than believed.
Diagnosis
The project has a great deal of verification ceremony — gates, stamps, spine sync, generated-doc
checks — but the load-bearing checks do not run on the code that matters. So defects and
non-functional artifacts ship while the gates report green.
Evidence, all verified today:
.github/workflows/bootstrap-spine-gate.ymlis the only workflow, and itspull_requesttrigger carries apaths:filter excludinglib/JIT/**,lib/VM/**,ext/**. A PR touching lowering gets zero checks, andgh pr viewreportsMERGEABLE/CLEAN— which means "no checks configured", not "checks passed".f($x+1, $x+2)printed12 12;str_replace($p['from'], $p['to'], 'xy!')returnedxy!. None caught by the compliance suite. Fixed in #23356/#23424.prelinked/bootstrap-gen0/bin-compile-aotfailsparseAndCompileon every input including hello-world (#23468). Driver bytes are from 2026-06-15; 6441 lowering commits since; manifest restamped 272 times, provenanceunverified-restamp.1771b935…vs live6497e66d…. On mismatch every unit is skipped, so a two-line script recompiled ~257 units across 13 cores. Nothing noticed. Refreshed in #23457.mandelbrotrenders all_(#23471);Ack(3,8)segfaults (#23472). Both AOT-only — Zend and the VM are correct. These are the benchmarks the project quotes speedups from.VMTest≈ 4 h serial;JITTestdid not finish in 40 min. So they are not run per-change, and both carry large pre-existing failure sets (407 and 472).script/bench.phphad no timeout:bin/vm.phponAck(3,10)ran 38 min at 100% CPU (Zend: 1.9 s) and blocked the whole suite. Fixed today.The through-line: it is currently cheaper to make a gate green than to make the thing work.
Restamping satisfies freshness; a copy satisfies "native emit"; an empty filter satisfies CI.
Strategy
Make truth cheap and lies expensive. Concretely: every claim the repo makes about itself should be
produced by a check that fails when the claim stops being true, and no claim should be satisfiable
by restamping or copying.
Work in this order. Each phase exists because the next one is worthless without it.
Phase 1 — Make correctness observable (days, highest leverage)
Nothing else matters while defects can land unnoticed.
lib/**,ext/**. Today: zero. This is the single highest-value change in the plan.Start with the cheap tier —
php -l, unit suite, nikic preflight (18 s), differential sweep —so it fits a PR turnaround.
script/differential-sweep.sh, merged Test: differential sweep against Zend (#23354) #23444).It found all 24 argument defects and would have caught AOT: mandelbrot renders all '_' — escape condition wrong in native code (VM correct) #23471/AOT: Ack(3,n) binary segfaults (exit 139) — Zend and VM return correctly #23472. Extend cases toward the
shapes users actually write.
VMTestfrom ~4 h to ~20 min (24 shards, per-testTeamCity output so a stalled shard still reports). This session's harness works; land it as a
script rather than leaving it in a scratchpad.
hrtime_*(Regression: hrtime(true)/hrtime() pair non-monotonic — microtime refine walks ns backwards (re-#12225/#12279, ext/standard/hrtime.c) #23420),proc_get_status_basic,interface_abstract_static_call,dnf_return_type_errorflip between runs and make regressioncomparison noisy — one appeared on the fixed list in one comparison and the regressed list in
the next. Name them in a quarantine list; treat entry to that list as a bug to fix, not a
dumping ground.
DECISION: compare suites by set difference of failing case names, never by count — neither
suite is green on master, so a count is meaningless.
Phase 2 — Stop shipping artifacts that do not work (weeks)
closed, no speed claim about generated code is defensible.
a script it has never seen, produce a runnable binary, and match Zend. A driver that starts and
fails
parseAndCompilecurrently satisfies every existing check.BOOTSTRAP_GEN0_ALLOW_UNVERIFIED_STAMPis still exercised — two
Trust: restamp gen-0 fingerprintcommits landed today alone (Trust: restamp gen-0 fingerprint after lowering drift (#10533) #23429,Trust: restamp gen-0 fingerprint after lowering drift (#10533) #23438). Either the escape hatch requires a recorded reason and shows as degraded in
release-readiness, or it goes.OPEN: does any consumer actually need the gen-0 driver per-commit, or only per-release? If the
latter (likely), the per-commit freshness gate is the wrong shape entirely — see Phase 3.
Phase 3 — Break the artifact treadmill (structural, weeks)
The recurring failure is a committed artifact keyed on a fingerprint that changes several times a
day. Master merges ~4 commits/hour, ~56% touching lowering sources. Any artifact costing more than
~7 minutes to rebuild is stale on arrival, so it gets restamped instead.
core_fingerprintcoverslib/JIT.php,lib/Runtime.php, … so one edit invalidates all 257 units. Key each unit on theclosure it actually reached at emit time — the emitter already walks it.
intra-spine class references cross a directory boundary). A single 6,519-file translation unit
running ~4.6 h and OOMing becomes incremental, parallel and resumable — the model already exists
in
HelperRuntimeCache+emit-helper-runtime-object.php.verify by function, tag, ship. Between releases, report staleness honestly
(
bootstrap-gen0-staleness.phpalready does) instead of restamping to green.Phase 4 — Performance, with evidence (after 1–3)
n/awith areason rather than a stall.
output. The current published AOT column was measured on binaries that are wrong or crash.
Simplifierquadratic path is 57% of gen-0 compile samples with a linear path sitting behindan opt-in env var, and helper-unit emit is O(units × transitive closure).
DECISION: no optimisation work before Phase 2 closes. Optimising a binary that renders
mandelbrotwrong is effort spent making a wrong answer arrive sooner.Release criteria for v1.1.0
Falsifiable, and each one is a command someone can run:
script/differential-sweep.shgreen on VM and AOT.and named.
benchmarks/README.mdregenerated with non/ain any column, every runtime output-verified.What not to do
emit_path=native, copy-fallback reported as native emit, substring match onnative-prelinked-sidecar) is the template for what to avoid.CLEANon a PR as evidence of anything until Phase 1 item 1 lands.Sequencing note
Phase 1 items 1–3 are days of work and unblock everything else, because they turn "did I break
something?" from a 4-hour question into a 20-minute one. That single change is what makes the rest
of this plan executable at the project's actual merge velocity.