Skip to content

Plan: steering php-compiler to a defensible v1.1.0 release #23474

Description

@PurHur

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:

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.

  1. 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.
  2. Differential sweep as a gate, VM and AOT (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.
  3. 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.
  4. Quarantine flaky tests explicitly. 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_error flip between runs and make regression
    comparison 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)

  1. Fix the AOT correctness defects (AOT: mandelbrot renders all '_' — escape condition wrong in native code (VM correct) #23471 mandelbrot, AOT: Ack(3,n) binary segfaults (exit 139) — Zend and VM return correctly #23472 Ack segfault). Until these are
    closed, no speed claim about generated code is defensible.
  2. Gate the gen-0 driver on function, not on a stamp (Bootstrap: committed gen-0 driver cannot compile anything — every input fails at parseAndCompile #23468): the committed driver must compile
    a script it has never seen, produce a runnable binary, and match Zend. A driver that starts and
    fails parseAndCompile currently satisfies every existing check.
  3. Close the restamp loophole. Bootstrap: earn gen-0 provenance from the link, not the stamp (#22642) #22966 added build receipts, but BOOTSTRAP_GEN0_ALLOW_UNVERIFIED_STAMP
    is still exercised — two Trust: restamp gen-0 fingerprint commits 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.

  1. Helper cache: per-unit dependency keys (Perf: helper-runtime cache key is too coarse — one lowering edit invalidates all 257 units #23458). Today one global core_fingerprint covers
    lib/JIT.php, lib/Runtime.php, … so one edit invalidates all 257 units. Key each unit on the
    closure it actually reached at emit time — the emitter already walks it.
  2. Gen-0: split-TU build. The spine is ~96% partitionable (Bootstrap: measure how splittable the spine is before splitting it (#22642) #23018/Bootstrap: measure sub-splitting the oversized spine chunks — it does not work (#22642) #23033: only 3–4% of
    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.
  3. 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)

  1. Benchmarks that cannot hang — done today: per-run cap, separate build cap, n/a with a
    reason rather than a stall.
  2. 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.
  3. 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:

  1. script/differential-sweep.sh green on VM and AOT.
  2. Committed gen-0 driver compiles an unseen script and matches Zend.
  3. Compliance suites: no regressions vs the previous release by case name, flaky set quarantined
    and named.
  4. benchmarks/README.md regenerated with no n/a in any column, every runtime output-verified.
  5. No open defect of class "silent wrong output".

What not to do

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions