Skip to content

test(vcr-sel-005): universe-complete cross-backend op-parity gate — surface 16 silent ARM/RV32 op-gaps (#242, #223, #232)#816

Merged
avrabe merged 5 commits into
mainfrom
feat/49-cross-backend-parity
Jul 17, 2026
Merged

test(vcr-sel-005): universe-complete cross-backend op-parity gate — surface 16 silent ARM/RV32 op-gaps (#242, #223, #232)#816
avrabe merged 5 commits into
mainfrom
feat/49-cross-backend-parity

Conversation

@avrabe

@avrabe avrabe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What & why

The "selector missed an op" bug class (#223, #232) was never a silent miscompile — both selectors fail honestly on an op they don't lower. The real defect is a silent cross-backend divergence: ARM lowers op X, RV32 loud-declines X, so a function compiles on Cortex-M but is skipped on RISC-V, and nothing catches it until gale runs it on qemu.

A parity oracle already existed (crates/synth-backend-riscv/tests/cross_backend_op_parity.rs, VCR-SEL-005) but had two vacuity gaps: it probed a curated 71-op list (a new WasmOp variant is silently never probed) and it was not wired as a named CI job. This PR hardens the existing gate to universe-completeness, surfaces the gaps that hiding was masking, adds a red-first non-vacuity proof, and CI-wires it. It does not build a gate from scratch.

The gate (VCR-SEL-005 shape)

  • Every WasmOp variant is assigned a ParityClass by a no-wildcard match (classify()). A new op fails to compile until classified — the A32 encoder silently NOPs i64 mul/shl/shr/rotl/rotr/compare/eqz (--target cortex-r5) — returns garbage instead of computing or rejecting #615 no-wildcard tripwire. (Honest scope: the classify match is compiler-enforced; the representative Vec is hand-maintained because WasmOp's Vec/Box/f32 fields rule out a derive iterator — but a new variant's compile error lands in classify() and points the author to add both.)
  • IntegerCore ops carry a self-contained probe; both the ARM (select_with_stack, no-FPU Cortex-M4) and RV32 selectors are lowered against it and must be at parity — both lower, both decline, or a ledgered one-sided divergence with a written reason.
  • StructurallyExcluded (float/f64/SIMD): NOT asserted. Their ARM lowering is target-parameterized — measured: f32.add declines with fpu=None, lowers with FPUPrecision::Single/Double; f64.add needs Double. Asserting "ARM lowers float" at a fixed no-FPU probe would be false; float/SIMD parity is a separately-tracked gap.
  • Fails in both directions: a new un-ledgered one-sided gap (the v0.11.21 RISC-V backend missing wasm ops: Select + LocalTee (blocks control_step/controller_step/flat_flight; filter_axis OK) #223 class) fails; a ledgered gap that has closed fails as stale.

What universe-completeness surfaced

Enumerating the full WasmOp universe found 16 real ARM-lowers / RV32-loud-declines one-sided gaps the curated list never probed — each confirmed end-to-end via synth compile -b riscv (unsupported wasm op for RV32 skeleton: …):

  • global.get, global.set
  • memory.size, memory.grow, memory.copy, memory.fill
  • br_table
  • the nine sub-word i64 loads/stores: i64.load8_s/u, i64.load16_s/u, i64.load32_s/u, i64.store8/16/32 (the full-word i64.load/i64.store DO lower on both — only the sub-word extend/truncate variants are the gap)

All 16 are now ledgered VCR-SEL-005 RV32-selector deferrals (a gated, owned fact instead of a silicon surprise), each with a concrete reason. These are the #223 silent-divergence class made visible.

Allowlist (KNOWN_DIVERGENCES) — 21 entries = 5 Zbb + 16 new

  • 5 Zbb bit-manip (measured 2026-06-20): i32.rotl, i32.rotr, i32.clz, i32.ctz, i32.popcnt — RV32IMAC/rv32imc lack Zbb; ARM lowers via ROR/CLZ/RBIT + software popcount, so they are sequence-lowerable on RV32 too (the follow-up target).
  • 16 new (measured 2026-07-17, above): globals, memory intrinsics, bulk memory, br_table, sub-word i64 mem.

Every entry names the RV32-selector reason and points at VCR-SEL-005 sequence-lowering as the follow-up; a landed lowering is forced to retire its ledger line by the stale-entry check.

Red-first evidence (non-vacuity)

red_first_unledgered_one_sided_gap_is_caught drops the real i32.rotl ledger entry and asserts the gate reports it as an unexpected divergence on the real backends — no synthetic op, no shipping-code change. With the correct ledger the main test is green; drop one load-bearing entry and it reddens. ledger_labels_are_live_integer_core_ops guards ledger drift.

RV32 allocation-validator (2nd deliverable) — scoped to follow-up #815

The VCR-RA-003 allocation validator (liveness::validate_final_allocation, #808) is deeply ARM-coupled: &[ArmInstruction], callee-saved [R4..R8], ARM Push/Pop prologue detection, reg_effect(&ArmOp). RV32 needs a parallel re-implementation (different RiscVOp enum, s0s11 callee set, addi sp/sw ra prologue, a new RV32 reg_effect), not "also run it." Per the task's "don't half-do a soundness checker," this is filed as #815 with the coupling and RV32 shapes named, rather than half-implemented here.

Frozen / gates

  • Test-only + CI/roadmap prose — zero codegen source touched. Frozen 10/10 green (control_step 0x00210A55), RV32 oracle unchanged.
  • claim_check.py: 25/25 (no new countable claim; status.json in sync post-merge).
  • rivet validate (0 non-xref errors) + rivet coverage (exit 0), re-run after merging origin/main.
  • clippy -D warnings clean, fmt clean, touched crates + synth-cli tests green.
  • CI-wired as the named job vcr-sel-005-cross-backend-op-parity-gate (appended, not rewritten).

Refs #223 #232 #242. Follow-up: #815.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 5 commits July 17, 2026 16:16
…, #223, #232)

Upgrade the curated 71-op parity oracle to a COMPILER-ENFORCED universe-complete
gate. Every WasmOp variant is now assigned a ParityClass by a no-wildcard match
(classify()); a new op fails to compile until classified (the #615 no-wildcard
tripwire). Integer-core ops are probed on both backends; float/f64/SIMD are
StructurallyExcluded with a documented reason (their ARM lowering is
target-parameterized — measured: f32.add declines with fpu=None, lowers with
Single/Double — so asserting "ARM lowers float" would be false at this probe).

Enumerating the full universe surfaced 16 REAL one-sided gaps the curated list
never probed (ARM lowers, RV32 loud-declines, all confirmed end-to-end via
`synth compile -b riscv`): global.get/set, memory.size/grow/copy/fill, br_table,
and the nine sub-word i64 loads/stores (load8/16/32_s/u, store8/16/32). Each is
now a ledgered, reasoned VCR-SEL-005 deferral — a gated, owned fact instead of a
silicon surprise. Ledger: 5 Zbb + 12 new = 17 known divergences.

Red-first non-vacuity: red_first_unledgered_one_sided_gap_is_caught drops the
i32.rotl ledger entry and asserts the gate reports it as an unexpected
divergence on the REAL backends (not a synthetic op, no shipping-code change).
ledger_labels_are_live_integer_core_ops guards ledger drift.

Test-only; no codegen change (.text byte-identical, frozen 10/10 unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…job (#242)

Append a dedicated vcr-sel-005-cross-backend-op-parity-gate job that runs the
universe-complete parity oracle (cross_backend_integer_op_parity_242), the
red-first non-vacuity proof, and the ledger-liveness guard. Previously the test
only ran implicitly under `cargo test --workspace`; a named job makes a
cross-backend op-gap regression visible on its own failing check name (mirrors
the VCR-RA-003 job it sits beside). Static command; no untrusted-input interp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…gered gaps (#49)

Document the hardening of the cross-backend op-parity oracle from a curated
integer list to a compiler-enforced universe-complete gate (no-wildcard WasmOp
match), the 16 newly-surfaced ARM-lowers/RV32-declines one-sided gaps now
ledgered as VCR-SEL-005 deferrals (globals, memory intrinsics, bulk memory,
br_table, sub-word i64 mem — all end-to-end confirmed), the red-first
non-vacuity companion test, and the new named CI job. Update the
verification-criteria (floor 30->65, universe-completeness, red-first, CI-wire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…t completeness

Reconcile the entry counts across all three spots (the ledger actually holds 21
tuples): the test MEASURED block said "SEVEN more" (stale — the 9 sub-word i64
entries were added after that prose); the roadmap said "12 new = 17" (wrong).
Both now read 16 new / 21 total, matching known_divergences().

Also tighten the universe-completeness wording: the classify() no-wildcard match
is compiler-enforced, but all_wasm_op_representatives() is a hand-maintained Vec
(WasmOp's Vec/Box/f32 fields rule out a derive iterator) — say so, and note that
a new variant's compile error lands in classify() and points the author to add
both the arm and the representative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe merged commit 4997393 into main Jul 17, 2026
4 checks passed
@avrabe
avrabe deleted the feat/49-cross-backend-parity branch July 17, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant