RQ-61-MVPANIC (#1093): parameter-taking block types decline loudly on ARM and RV32 — the aarch64 VCR-A64-CF-001 refusal ported - #1096
Conversation
…omething worse than the reported panic The status flip rides ON THIS BRANCH rather than following the merge, because R4 is evaluated over first-parent history: once #1096's commits land on main they ARE first-parent, and an id-first delivery commit with an unacknowledged status turns main's own status-evidence gate red. Doing the flip afterwards would mean main is red in the gap. (Verified: the gate reports exactly that FAIL on the branch tree before this commit, 0 failures after.) VERIFIED BY THE COORDINATOR AGAINST BOTH BINARIES, not from the lane's report: * The reported panic converts. All 12 (shape x path) combinations that exited 101 now exit 1 with a named decline. * THE CLASS SWEEP IS THE REAL FINDING. An else-less `if (param i32) (result i32)` COMPILED CLEANLY (exit 0) on all three paths before this change, emitting on the direct ARM path: movw r1, #7 <- the block PARAMETER cmp r0, #0 beq +8 <- false path skips the then-branch add r3, r1, #42 <- r3 written ONLY on the true path mov r0, r3 <- result = r3 On the false path r3 is NEVER WRITTEN: the function returns an UNINITIALIZED REGISTER and the block parameter in r1 is dropped. wasmtime says 7. That is a silent wrong-answer miscompile on valid wasm — strictly worse than the panic that was actually reported, because a panic at least stops. rv32 `block (param..)`+br_if and `loop (param..)`+back-edge are the same shape. All now decline. * NO REGRESSION: an independent byte-identity sweep over scripts/repro/*.wat x 3 legs gives 339 byte-identical, 0 differing, 0 previously-compiling modules newly declined. NOT CLAIMED: parameter-taking block types are still not LOWERED. This converts one panic and three silent miscompiles into loud declines; multi-value lowering is a separate capability question with its own design, and the aarch64 decline message already states why it is not free. Refs #1093, refs #242. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
|
Coordinator verification — widened the reach-regression check beyond fixtures. The lane's byte-identity evidence covers That is the property that actually matters for this change: a new decline is only safe if it declines nothing that previously compiled correctly. Confirmed on real input, not just the fixtures the guard was written against. Separately verified the class conversion myself, from the emitted bytes rather than the lane's report. The else-less
Holding the merge for all nine required contexts green by name — currently 3/9 with six queued on |
|
Rebased onto
Verified the real post-merge tree locally before pushing: Commit content is unchanged — only the base moved. Same treatment as #1099. |
f0ac9aa to
e141bae
Compare
…omething worse than the reported panic The status flip rides ON THIS BRANCH rather than following the merge, because R4 is evaluated over first-parent history: once #1096's commits land on main they ARE first-parent, and an id-first delivery commit with an unacknowledged status turns main's own status-evidence gate red. Doing the flip afterwards would mean main is red in the gap. (Verified: the gate reports exactly that FAIL on the branch tree before this commit, 0 failures after.) VERIFIED BY THE COORDINATOR AGAINST BOTH BINARIES, not from the lane's report: * The reported panic converts. All 12 (shape x path) combinations that exited 101 now exit 1 with a named decline. * THE CLASS SWEEP IS THE REAL FINDING. An else-less `if (param i32) (result i32)` COMPILED CLEANLY (exit 0) on all three paths before this change, emitting on the direct ARM path: movw r1, #7 <- the block PARAMETER cmp r0, #0 beq +8 <- false path skips the then-branch add r3, r1, #42 <- r3 written ONLY on the true path mov r0, r3 <- result = r3 On the false path r3 is NEVER WRITTEN: the function returns an UNINITIALIZED REGISTER and the block parameter in r1 is dropped. wasmtime says 7. That is a silent wrong-answer miscompile on valid wasm — strictly worse than the panic that was actually reported, because a panic at least stops. rv32 `block (param..)`+br_if and `loop (param..)`+back-edge are the same shape. All now decline. * NO REGRESSION: an independent byte-identity sweep over scripts/repro/*.wat x 3 legs gives 339 byte-identical, 0 differing, 0 previously-compiling modules newly declined. NOT CLAIMED: parameter-taking block types are still not LOWERED. This converts one panic and three silent miscompiles into loud declines; multi-value lowering is a separate capability question with its own design, and the aarch64 decline message already states why it is not free. Refs #1093, refs #242. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… ARM and RV32 — the aarch64 VCR-A64-CF-001 refusal ported Valid multi-value wasm with an `if (param ..) .. else` PANICKED both the ARM direct selector (select_with_stack.rs:3674) and the RV32 selector (selector.rs:3164) — "`at` split index (is 2) should be <= len (is 1)", exit 101 on 7 of 8 invocation paths. The frame-entry operand-stack checkpoint cannot represent block params consumed BELOW it. Measured worse than the report: the else-less `if (param ..)` and the RV32 br-edge block/loop-param shapes were SILENT miscompiles (exit 0, wrong value — ipe(0) returned 0, want 7, on all four ARM/RV32 legs). One predicate + one message, defined once in synth-core (find_param_block_type / param_block_decline_msg), called from: - compile_wasm_to_arm — the choke point BOTH ARM codegen paths cross - select_with_stack — pre-flight for direct library callers - rv32 compile_function_with_opts (+ the module path now threads func.block_arity into the per-function config) DO-NOT: this is a loud decline, not multi-value support. Ratchet: selector_lines_code 19199 -> 19213 (+14, waivered); selector_lines_total 29845 -> 29859. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ttribution corrected Tests (all red on v0.60.0 by panic or silent wrong value): - synth-synthesis issue_1093_param_block_decline: the repro if/else (was panic), the else-less if (was SILENT wrong value), block/loop params, and the negative control ((0,1) if/else + empty side-table still compile) - synth-backend param_block_decline_1093: both ARM paths decline by name at the compile_wasm_to_arm choke point; void reading preserved - synth-backend-riscv param_block_decline_1093: the panic shape and the measured silent loop-param br miscompile (lpb(3) -> 2, want 3) decline - issue_509_br_value_carry: the loop-param case now declines at frame OPEN (#1093 pre-flight, subsuming the #509 branch-site decline); the loud-Err property it pins is unchanged run_supported.sh: the summary claimed panics were 'optimizer regalloc — all pass with --no-optimize'. FALSE for this class: the split_off panic lives in instruction selection, before the optimizer, on both ARM paths and RV32 — --no-optimize changed nothing (measured, 7 of 8 invocation paths). Panics now report as compiler defects with no mode attribution. Byte-identity: 835 (fixture,leg) pairs — scripts/repro/*.wat x 5 legs — compiled with the origin/main binary and this branch's binary: 0 differ. Spec suite: if.wast panicked (exit 101) on all three legs pre-fix; post: 0 panics, 8/7/9 named param-declines on if/block/loop.wast. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…omething worse than the reported panic The status flip rides ON THIS BRANCH rather than following the merge, because R4 is evaluated over first-parent history: once #1096's commits land on main they ARE first-parent, and an id-first delivery commit with an unacknowledged status turns main's own status-evidence gate red. Doing the flip afterwards would mean main is red in the gap. (Verified: the gate reports exactly that FAIL on the branch tree before this commit, 0 failures after.) VERIFIED BY THE COORDINATOR AGAINST BOTH BINARIES, not from the lane's report: * The reported panic converts. All 12 (shape x path) combinations that exited 101 now exit 1 with a named decline. * THE CLASS SWEEP IS THE REAL FINDING. An else-less `if (param i32) (result i32)` COMPILED CLEANLY (exit 0) on all three paths before this change, emitting on the direct ARM path: movw r1, #7 <- the block PARAMETER cmp r0, #0 beq +8 <- false path skips the then-branch add r3, r1, #42 <- r3 written ONLY on the true path mov r0, r3 <- result = r3 On the false path r3 is NEVER WRITTEN: the function returns an UNINITIALIZED REGISTER and the block parameter in r1 is dropped. wasmtime says 7. That is a silent wrong-answer miscompile on valid wasm — strictly worse than the panic that was actually reported, because a panic at least stops. rv32 `block (param..)`+br_if and `loop (param..)`+back-edge are the same shape. All now decline. * NO REGRESSION: an independent byte-identity sweep over scripts/repro/*.wat x 3 legs gives 339 byte-identical, 0 differing, 0 previously-compiling modules newly declined. NOT CLAIMED: parameter-taking block types are still not LOWERED. This converts one panic and three silent miscompiles into loud declines; multi-value lowering is a separate capability question with its own design, and the aarch64 decline message already states why it is not free. Refs #1093, refs #242. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
|
Rebased again onto Verified the combined tree locally before pushing:
Second rebase for this PR. That is the real cost of the queue latency in #1062 — while a PR waits, siblings merge and invalidate the green it already earned. Commit content is unchanged both times. |
e141bae to
4ee7d81
Compare
|
Holding the merge. All nine required contexts are green and the base is current — but Only Baseline first, before attributing it to this PRNot environmental drift. This PR causes it. Diagnosis, from the gate's own design
That is precisely the function this PR modifies — the rv32 arity guard was added there (and the module path now threads The gate is working exactly as intended. The pin is what makes the MC/DC number non-vacuous: if a function's branch population changes and nobody re-pins it, the coverage figure describes a function that no longer exists. What is neededRe-pin I verified the two host pre-steps pass locally ( Recording the near-miss plainly: my merge rule is "all nine required contexts green by name, plus |
…H_POPULATION repin 9 -> 10 The #1096 MC/DC red, diagnosed from the evidence (run 33120589927), not the summary: 1. BRANCH_POPULATION compile_function_with_opts 9 -> 10 — a TRUE source change: the #1093 guard added one br_if (manifest id 2571, instr 7) at the top of the function. Repinned to 10, value taken from the gate's own REPIN block. 2. dead 51 > 50 — witness#208 layout reshuffle, measured precisely: the NEW branch is not in any scored decision; the reconstruction regrouped the function's scored set ({2098 gap, 2099 gap} -> {old-2099 gap, old-2104 dead}), pulling a never-executed EXISTING branch into scoring. The fix is still rows, not a ceiling raise: rv_param_block_gate drives the decline BOTH ways through Backend::compile_function (params=0 void block compiles; params=1/2 decline — 2 is the #1093 repro's arity), with the driver asserted non-vacuous in the host sanity test. Local A/B under CI's pinned witness 0.42.0 (macOS reconstruction differs from ubuntu, so deltas not absolutes): baseline 19 dec / 129 cond / 55 proved / 51 dead -> with rows 20 / 131 / 56 / 49. Dead falls 2 under the ceiling; every floor-relevant count moves up. CI (ubuntu) is the platform of record for the absolute floors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
|
MC/DC red addressed (commit a3b0173) — diagnosed from the run-33120589927 evidence, reproduced locally with 1. Branch population 9 → 10 — repinned to 10. True source change: the #1093 guard added one 2. dead 51 → fixed with rows, ceiling untouched. One mechanism note from the manifest/report diff: the new branch is not itself the dead condition — it is not in any scored decision. witness#208's layout-sensitive reconstruction regrouped the function's scored set (main: Local A/B under CI's pinned witness 0.42.0 (macOS reconstruction differs from ubuntu, so deltas not absolutes): baseline 19 dec / 129 cond / 55 proved / 51 dead → with rows 20 / 131 / 56 / 49 dead. Dead falls 2 below the ceiling; every floor-relevant count rises. |
|
Diagnosis: (a), proven by the manifest diff — and the disposition is (c). No number touched: The manifest diff (instrument side — the stable surface)Reproduced both columns from the evidence artifacts (33120546295 main, 33126354714 branch). Per-function branch population: identical for all 20 scored functions except the intended Where proved 60 → 56 actually wentAll of it lands on functions this PR does not touch, whose populations did not move:
Net −4. Decisions vanish and appear over unchanged code in both directions — that is layout, not rows. Your compound-predicate question — checked, and it is not the story
Why not chase a driver shapeThe FIRST red on this PR — before any harness change, the 14-line backend guard alone — already moved dec 32 → 29 / dead 48 → 51, in different functions than this red. The perturbation is a property of adding code anywhere, not of Filed, per (c)
The decision that is yoursThe gate's #990 block documents the remedy for exactly this state: "a re-statement WITH evidence and not a floor lowered to go green" — its own floors were re-derived 26→21 that way. The evidence above is that re-statement's justification, ready to cite. If you want it applied in this PR, the branch-measured baseline is |
…vidence protocol — the second instance of witness#208 The gate's own text says a floor miss WITHOUT a branch-population mismatch is witness's layout-sensitive reconstruction, that the manifest pins are what say whether anything real was lost, and that such a move is a RE-STATEMENT WITH EVIDENCE rather than a floor lowered to go green. This is that case, and the protocol was followed rather than re-derived. THE DISCRIMINATING MEASUREMENT — run by the coordinator on the instrument side with this script's own extraction, main b2abf95 vs this branch: main 175 scored branches across 20 functions branch 176 scored branches across 20 functions populations that DIFFER: exactly 1 — synth_backend_riscv::backend::compile_function_with_opts 9 -> 10 of the 19 equal-count functions, 19/19 byte-identical (kind, instr_index) branch signatures Nothing deleted, nothing unreachable: one condition added exactly where the #1093 arity guard was added, and BRANCH_POPULATION re-pinned to 10 in a3b0173. The report side nonetheless moved 32/141/60 -> 27/132/56, and the loss decomposes ENTIRELY onto functions this PR never touched (sp_slot_store -2 with its full_mcdc decision vanishing, validate_final_allocation_rv32 -4 on a 13->12 regrouping, build_options -1, validate_reloc_resolutions -1, offset +4 from validate_served_image going 0 -> 4 scored conditions). THE OBVIOUS ALTERNATIVE WAS KILLED WITH EVIDENCE, not assumed. "The new guard just needs more rows": the guard's branch has hits: 7 in run.json and its driver flips both outcomes (asserted in the harness host test), yet the report places it in NO decision — no row count can prove a condition witness never scores. And the FIRST red, the guard alone before any harness change existed, already moved dec 32->29 / dead 48->51 in DIFFERENT functions, so the perturbation is a property of adding code anywhere. 56 is the MEASURED value with ZERO slack, deliberately. The previous 57 sat 5 below its own 62 baseline; widening that gap would absorb exactly the noise that needs to stay visible. This floor will very likely red again on the next unrelated-code PR until witness#208 is fixed — that recurrence is the argument, filed as #1100, for moving the real assertion onto the stable surface rather than re-stating a report-side number each time. NOT TOUCHED: CEILING_DEAD stays 50 (the new row brought dead back under it), FLOOR_DECISIONS 21, FLOOR_CONDITIONS 130, FLOOR_FULL 3, and every BRANCH_POPULATION entry except the intended one. Gate on this tree: PASS: all MC/DC floors met (27 dec / 9 full / 132 cond / 56 proved / 26 gap / 50 dead). test_mcdc_gate 6 OK, claim_check 52/52, status-evidence 0 failures. Refs #1093, refs #1100, refs pulseengine/witness#208, refs #990. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… idle rust-cpu pool (#1101) The smallest possible first step, and deliberately a NON-REQUIRED job. MEASURED, not argued. Four PRs opened within ~30 minutes each reached EXACTLY 3/9 required contexts within minutes and then stalled for hours. The three that completed were the same three every time — Format, Version Pin Sweep, Rivet Validation — and those are precisely the three that run SELF-HOSTED. The six that stalled are the six on `ubuntu-latest`. Same commits, same moment, same queue; the split is not explained by job cost (Format is not cheaper than Clippy) but entirely by which pool the job asks for. runs-on census: 56 ubuntu-latest · 4 rust-cpu · 2 light · 1 macos org runners: 12 online, 4 busy (OTHER repos), 8 IDLE (7 rust-cpu, 1 light) WHY THIS JOB. `synth-provenance-v1 reconciliation gate` needs only `dtolnay/rust-toolchain` — no apt, no Nix, no `cargo install`, no network package fetch. That is the same shape as `Rivet Validation`, which already runs on `rust-cpu` successfully, so the pool is proven for cargo-shaped work and this move tests scheduling rather than toolchain. WHY NOT A REQUIRED JOB FIRST, even though the required six are what actually blocks. A REQUIRED context naming a label no online runner satisfies does not fail — it NEVER RUNS, and a non-running required check deadlocks every merge in the repo. A non-required job that fails is a red someone can read and revert; a required job that never starts is a repo that cannot merge its own revert. One job per PR, each with its own full green cycle, in the risk order posted to #1062: Claim Check ... checkout + python ONLY .... next, after this cycle Clippy ........ rust-toolchain + cache .... then Kani .......... cargo install ............. then Test / Z3 ..... sudo apt-get .............. only after apt is confirmed Bazel ......... nix + setup-bazel ......... last or never — an environmental failure there looks exactly like a real proof failure THE COST BEING PAID, measured this wave: queue latency converts directly into wasted CI cycles. Two PRs each accumulated green against a base that moved underneath them (#1096 at 8/9, #1099 at 9/9) because GitHub re-runs checks when the BRANCH moves, never when the BASE does. Both had to be rebased and re-run from scratch; #1096 twice. The longer a PR waits, the likelier its green describes a tree that no longer exists. Change is one `runs-on` line: ci.yml ubuntu-latest 50 -> 49, all workflows 56 -> 55. Nothing else moved. Refs #1062, refs #242. Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #1093.
What this is
Valid multi-value wasm —
(if (param i32 i32) (result i32) ...)— panicked the ARM direct selector (select_with_stack.rs:3674) and the RV32 selector (selector.rs:3164):`at` split index (is 2) should be <= len (is 1), exit 101 on 7 of 8 invocation paths. Only aarch64 declined cleanly (the issue's table said rv32 declined too — it did not; reproduced before touching anything). Both selectors checkpoint the operand stack at frame entry and a parameter-taking block type consumes operands below the checkpoint.Port, not a third variant: the predicate and message live once in synth-core (
find_param_block_type/param_block_decline_msg— the aarch64open_slotparams != 0refusal ported; the shared needlePARAMETER-taking block typematches aarch64's message so one parity probe covers all three backends). Call sites:compile_wasm_to_arm(the choke point BOTH ARM codegen paths cross),select_with_stackpre-flight (direct library callers), rv32compile_function_with_opts(+ the module path now threadsfunc.block_arityinto the per-function config). aarch64 keeps its in-selector check: it is entangled with reconciliation-slot reservation and also covers theresults > 1case, and its message is pinned bycross_backend_op_parity.rs.DO-NOT honored: this is a loud decline, NOT multi-value support.
The class sweep found the silent direction was worse (all measured, unicorn vs wasmtime)
if (param ..) .. elseif (param ..)no elseipe(0)→ 0, want 7, all 4 ARM/RV32 legsblock (param ..)+br_ifbpb(1)→ 10, want 15); ARM correctloop (param ..)+ back-edgelpb(3)→ 2, want 3); ARM declined (#509)block/loop (param ..)fall-through onlyThe last row is the one deliberate behavior change for previously-correct input: the branch-free fall-through shape is correct only by accident of the vstack flowing through, telling it apart from the broken shapes would be a NEW predicate with its own proof burden, and aarch64 already refuses it — so the whole class declines, exactly like aarch64 (#1013 policy: match the existing refusal).
Gates
if.wast); post-fix 0 compile panics on both--cortex-mand rv32 legs.if/block/loop.wastproduce 8/7/9 named param-declines.scripts/repro/*.watx 5 legs (cortex-m image, ARM plain, ARM relocatable, rv32 plain, rv32 relocatable): 835 (fixture, leg) pairs, 0 differ (exit codes and output bytes).arm_corpus_sweep_973.pyPASS — compiled 154/167 (floor 144), 2502/2502 vectors, 0 mismatches;EXPECTED_DECLINESunchanged (no repo fixture contains a parameter-taking block type — grepped).issue_509_br_value_carryupdated — the loop-param shape now declines at frame OPEN (subsumes the Direct selector miscompiles value-returning br_table — carried value dropped (affects --relocatable/shipped path) #509 branch-site decline; the loud-Err property is unchanged, the Direct selector miscompiles value-returning br_table — carried value dropped (affects --relocatable/shipped path) #509 arm stays as defense-in-depth).run_supported.shno longer claims panics are "optimizer regalloc — all pass with--no-optimize" (FALSE for this class: the panic is in instruction selection, before the optimizer,--no-optimizechanged nothing).cargo fmt/clippy -D warnings/cargo test --workspacegreen;claim_check52/52 (selector_lines_code 19199 → 19213, +14 waivered in the same commit; selector_lines_total 29845 → 29859; status.json regenerated).Honest residuals
status_evidence_check.pyR4 fires on this branch only (the lane commit is first-parent here). On PR CI the checkout is the synthetic merge commit — lane subjects are not first-parent — and on main the coordinator flips the v0.61 artifact status at merge (release files untouched per the lane contract).select_with_signatures) still has no arity channel; every real driver (CLI per-function loop and the module-levelcompile) passes through the guarded backend entry.results > 1arm and slot rollback).🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L