Repository navigation
CI: select the compliance suite; add a spine-chunk split-TU probe - #24835
Conversation
Two independent pieces, both about measuring things that were previously
measured by hand and got the wrong answer.
1. compliance-nightly.yml takes a `suite` input (VMTest | JITTest).
The cron nightly still runs VMTest, which is the suite with a committed
baseline. JITTest (~472 failures) has none, so `--suite=JITTest --diff` is
expected to refuse until a collect run has been reviewed and landed by a
human. `schedule` supplies no inputs, so every expression falls back to
VMTest. The concurrency group is keyed on the suite as well, so a JITTest
collect cannot cancel a VMTest nightly on the same ref.
2. script/spine-chunk-probe.sh replaces the hand-built chunk fixtures under
build/micro/chunk/ used to test whether the spine can be split into
separately-compiled translation units (RELEASE-PLAN Phase 3 item 9).
The hand-built fixtures gave two wrong answers, and the script encodes
both:
- They were not valid PHP programs. A bare require_once list over ext/<n>/
references classes declared elsewhere (ModuleAbstract, VmClassMethod,
Func\Internal), so Zend fatals on all seven with rc=255. "The compiler
failed to build this chunk" could not be distinguished from "this chunk
is not a program". The generated fixture prepends the autoloader and the
Zend baseline is now checked rather than assumed.
- rc=0 plus a non-empty binary is not a pass. ext/ds compiles to an
18.5 MB binary that exits 0 while the compiler itself reports 61 method
calls — including hexdec, substr, is_string, str_starts_with — lowered
to a silent null because their class is not in the module (#579,
#24227). Against the bare fixture that binary exits 0 where Zend exits
255: silent wrong output, which is the characteristic failure of this
project (AGENTS.md 3). An unbound stub is therefore a failure here, not
a warning.
Measured at fd3b1fe with PHP_COMPILER_SPINE_CHUNK=1 (#24496):
ds compiles, 18.5 MB, 61 unbound stubs -> HOLLOW
types LLVMAbstract/Module.php exception, rc=255
msgpack unimplemented TYPE_BITWISE binary op
sockets Static call class must be a literal (JIT.php:18097)
vm same, at 202s
spl SIGSEGV (rc=139) at 71s
In the sockets/vm case Block::getOperand() returns null for BOTH the class
and name slots, so the class operand is absent rather than dynamic — the
throw message describes a different condition than the one occurring.
Exit 0 only when every chunk compiles with every call bound. Today that is
never, which is the status quo being tracked; a zero exit is real news.
Validated by making it fail: exit 1 on a real chunk failure, exit 1 on a
bogus --ext, and the pass path exercised via ext/ds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Validated by running it, not by reading itThis PR has no checks — Dispatched the changed workflow from this branch: run 30455848639 —
So the suite plumbs through the matrix, the shard script, the artifact paths and the baseline collector. This matters because the last CI gate I added failed on arrival for want of exactly this check.
Not landing a JITTest baseline from this run. 489 names from a single run is one measurement; the two-run protocol requires a second collect to quarantine what flips. Run 2 is dispatched. A single-run baseline was measured wrong in 9 places on VMTest (#24672). |
… collects The two-run baseline protocol needs two collects of the SAME commit. Two things broke that, and both are fixed here. cancel-in-progress killed one of any two overlapping manual runs, leaving a single result and no way to tell flaky from changed. It now applies only to scheduled runs, which should still supersede each other. The quarantine landed in #24778 was derived from two collects 15 commits apart, so every flip it recorded was a CHANGE rather than flakiness: 0ae0f94 (highlight_string HTML wire format) fixed four highlight_* cases and broke three; 6af97e0 (property hooks on the default profile) made six property_hook_* cases start executing at all, because VMTest.php gates them on CompilerVersion::supportsPropertyHooks() and the provider had never yielded them before. A case that goes from unyielded to failing looks exactly like a flaky one in a set difference of failing NAMES. 18 real failures were quarantined into invisibility as a result — including three highlight_* regressions from 0ae0f94 that still fail at b114e97 (#24726). workflow_dispatch resolves a branch to its head at dispatch time, and master merges ~4 commits/hour, so two dispatches minutes apart are not the same tree. Documented: cut a ref that does not move and dispatch both against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…longer run" The baseline recorded only failing NAMES. That cannot distinguish a case that started passing from one the provider stopped yielding, and the two need opposite responses: regenerate for the first, investigate for the second. VMTest::providePHPTests() gates cases on CompilerVersion::supports*(), and the default profile has flipped repeatedly this week — 6af97e0 enabled property hooks, 9108205 re-rejected them on the 8.2 reference profile, 270fea4 re-gated asymmetric visibility, 7236ecd typed class constants. Each flip changes which cases exist as far as the diff is concerned. Measured on the 2026-07-29 nightly (run 30458065584) against a634624, four cases it reported as FIXED do not execute at all: language/asymmetric_visibility_write language/asymmetric_visibility_public_protected_set language/static_asymmetric_visibility language/match_typed_class_const Regenerating the baseline from that report would have dropped them from tracking permanently. The same mechanism already buried 18 real failures in the quarantine (#24726), including three highlight_* regressions. shard-compliance.sh writes <suite>-<i>-of-<N>.executed alongside .failed compliance-baseline.sh --collect writes baselines/<suite>.executed --diff splits FIXED / DROPPED / NEWLY EXECUTED Splitting FIXED from DROPPED needs only the current run's executed set; flagging NEWLY EXECUTED needs the executed baseline. The two are gated independently — conflating them printed "executed, passing now" over a list whose execution had never been checked. Degrades honestly: with no .executed files the heading becomes "FIXED OR DROPPED — execution NOT verified" and says not to regenerate from it, rather than asserting something it did not measure. Validated on synthetic fixtures covering all four paths: real fix reported as FIXED, unexecuted case reported as DROPPED and NOT as fixed, newly-executed failing case flagged before its regression line, and the no-.executed fallback relabelled. Exit stays 1 on regressions, 0 otherwise. SHARD_DIR / BASELINE_DIR / QUARANTINE are now overridable so this is testable without touching committed baselines (#24498 does the same for the corpus). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d pair The quarantine had 26 entries. Exactly one of them is flaky. The previous contents came from two collects taken 15 commits apart (1d57e9d vs d646332), so every "flip" they recorded was a CHANGE rather than nondeterminism (#24726). Re-measured from two complete 24-shard CI collects at the SAME commit a3f3266, on the same runner image — runs 30461221640 and 30461258793: 8,112 executed cases, and the two runs disagreed about exactly ONE. Both the failing set and the executed set were otherwise identical between the runs. Held constant, the suite is deterministic. Verdict on all 26 quarantined names, from that pair: FLAKY 1 stdlib/hrtime_nanosecond_precision (#24870) — kept stable-FAIL 6 real failures the quarantine was HIDING — now baselined stable-pass 13 never flaky; pure noise — removed not executed 6 property_hook_* not yielded at this tree — removed The six that were hidden: language/builtin_attribute_classes language/final_hooked_property stdlib/array_all_internal_callback stdlib/highlight_file_br stdlib/highlight_file_multiline_br stdlib/highlight_string_pre_wrapper The last three were introduced by 0ae0f94, which fixed four other highlight_* cases and broke these three. Quarantining them meant the gate could never report them again — the exact failure mode the quarantine exists to prevent, inverted. Baseline is now 428 names, plus the first VMTest.executed at 8,112 names. Validated the way that counts: the baseline collected from run 30461258793 diffs CLEAN (exit 0) against run 30461221640 — the run it was not built from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The executed-set change paid for itself immediatelyPushed two more commits and validated the whole thing end-to-end on real CI runs. The gate had a hole, and the nightly walked into itNightly 30458065584 reported a FIXED list. Four of those cases do not execute at all at The prescribed response to a FIXED entry is "regenerate the baseline", which would have deleted them from tracking permanently. It degrades honestly: with no First properly pinned pair, and the quarantine was wrongTwo complete 24-shard collects at the SAME commit 8,112 executed cases. The two runs disagreed about exactly one. Verdict on all 26 quarantined names:
The six that were hidden include Baseline is now 428 names plus the first Gate I ranThis PR gets no CI checks (
|
…d-spine-probe # Conflicts: # test/compliance/quarantine.txt
#24933) The executed set I added in #24835 counted skips as executions, so the very confusion it was built to prevent survived in a second form. PHPUnit emits testStarted for a skipped case AND then testIgnored. The set was built from testStarted alone, so once --SKIPIF-- started being honoured (#24888) a case that became skipped left the failing set, stayed in the executed set, and read as FIXED. Measured on the first post-SKIPIF collect (runs 30472500277 / 30472527097 at 90c654c): failing fell 428 -> 370, while executed ROSE 8112 -> 8121 and exactly ZERO cases stopped executing. The 58 cases that stopped failing were skips wearing a fix. shard-compliance.sh writes <suite>-<i>-of-<N>.skipped from testIgnored, and .executed is now started MINUS ignored compliance-baseline.sh --diff splits FIXED / SKIPPED / DROPPED / NEWLY EXECUTED SKIPPED and DROPPED are kept apart because they are different problems: a skipped case declares itself inapplicable to this host/profile, an unyielded case was gated out by VMTest::providePHPTests(). Both are coverage losses, neither is a repair, and a skip guard that started firing wrongly would look identical to a correct one. The zero-executed guard deliberately still keys on testStarted: it means "the filter matched nothing", and a shard whose cases all legitimately skip is not that failure. Verified on a real mixed teamcity log (4 started, 2 skipped, 2 ran, 1 failed — the two skips correctly excluded from "ran"), and on synthetic fixtures for all four classifications. Degrades conservatively: with no .skipped files a skipped case falls into DROPPED, never into FIXED. Co-authored-by: PurHur <tedyyyyy@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) The compiler gate has produced no master verdict at all. Measured over the 60 most recent runs of compiler-gate.yml: cancelled 31 in flight 29 completed 0 Every master push shared one concurrency group with cancel-in-progress, and master takes ~12 commits/hour against a gate that needs ~25 min — so each run was cancelled by the next merge before it could reach a conclusion. The gate appeared to be running constantly and never reported anything. That is the same failure as a check that reports without running, which this repo has now hit four times (#24888 dead SKIPIF, #24726 quarantine from two different commits, #24835 baseline that could not tell fixed from dropped, #24933 skips counted as executions). Cancelled-forever is absence wearing a busy spinner. Master runs are now keyed on the SHA, so each merge gets its own verdict. PR runs keep superseding each other on the ref, which is correct — once a branch is pushed again the previous tree is nobody's concern. Cost: master runs may overlap. With the paths filter (lib, ext, bin, patches, the sweep scripts) and ~56% of commits touching lowering, that is roughly 7 runs/hour at ~25 min each, so ~3 concurrent. GitHub queues the excess. Found while investigating why #24988 merged with a red gate. That failure was `aot-smoke: 7 passed, 1 failed` — `echo` hitting rc=124, a 900s COMPILE TIMEOUT under CI load, not a wrong result. Verified on current master (e5bf674, which contains that patch): aot-smoke 8 passed, 0 failed locally, including echo. So the red was infrastructure slowness, and master is intact — but nobody could have known that from the gate, because it never finishes. Co-authored-by: PurHur <tedyyyyy@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two independent pieces, both about measuring things that were previously measured by hand and got the wrong answer.
1.
compliance-nightly.ymltakes asuiteinput (VMTest|JITTest)The cron nightly still runs VMTest, the suite with a committed baseline. JITTest (~472 failures) has none, so
--suite=JITTest --diffis expected to refuse until a collect run has been reviewed and landed by a human — that is the script doing its job, not a break.schedulesupplies no inputs, so every expression falls back toVMTest. The concurrency group is keyed on the suite too, so a JITTest collect cannot cancel a VMTest nightly on the same ref.2.
script/spine-chunk-probe.shReplaces the hand-built fixtures under
build/micro/chunk/used to test whether the spine can be split into separately-compiled TUs (RELEASE-PLAN Phase 3 item 9). This matters because the gen-1 bootstrap compiles all 6,519 spine files as one TU and has now failed four times purely on memory — OOM at 6, 24 and 48 GiB, and a fourth run pinned at a 12 GiB cap for 4h55m in direct reclaim (memory.events max=1426,oom_kill=0— it survived by swapping, not by fitting). Capacity is not the constraint; unbounded growth in a single TU is.The hand-built fixtures gave two wrong answers. The script encodes both.
They were not valid PHP programs. A bare
require_oncelist overext/<name>/references classes declared elsewhere (ModuleAbstract,VmClassMethod,Func\Internal), so Zend fatals on all seven with rc=255. "The compiler failed to build this chunk" could not be distinguished from "this chunk is not a program" — which undercuts the premise of #24429's title. The generated fixture prepends the autoloader, and the Zend baseline is checked rather than assumed.rc=0 plus a non-empty binary is not a pass.
ext/dscompiles to an 18.5 MB binary that exits 0, while the compiler itself reports 61 method calls — includinghexdec,substr,is_string,str_starts_with— lowered to a silent null because their class is not in the module (#579, #24227). Against the bare fixture that binary exits 0 where Zend exits 255. Silent wrong output is the characteristic failure of this project (AGENTS.md §3), so an unbound stub is a failure here, not a warning.Measured at
fd3b1fedcwithPHP_COMPILER_SPINE_CHUNK=1(#24496)dstypesLLVMAbstract/Module.phpexception, rc=255msgpackTYPE_BITWISEbinary opsocketsStatic call class must be a literal(lib/JIT.php:18097)vmsplSo #24496 did what it claimed — the chunks get past the old
Call to undefined method object::…abort — but no chunk is yet usable, and the blocker moved from "compile aborts" to "compiles, silently wrong", which is the more dangerous state.One detail worth a follow-up: in the
sockets/vmcaseBlock::getOperand()returns null for both the class and name slots, so the class operand is absent rather than dynamic. The throw message describes a different condition than the one occurring.Validation
Validated by making it fail: exit 1 on a real chunk failure, exit 1 on a bogus
--ext, and the pass path exercised viaext/ds.🤖 Generated with Claude Code