fix(#973): ARM select on an i64-comparison returns the then-arm — and an ARM leg for the corpus CI never compiled - #992
Conversation
… the WEEKLY API limit Committed by the hub coordinator so the work survives. NOT reviewed and NOT complete: the lane stopped mid-task. This is a restore point, not a claim that anything is verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…4/360) The fixture + unicorn-vs-wasmtime differential for RQ-58-SELECT973, landed BEFORE the fix so the red is on the record. Measured on 60ab227 (pre-fix), `--target cortex-m4`, BOTH ARM lowering legs: [relocatable] 132/180 vectors match wasmtime [self-contained] 132/180 vectors match wasmtime #973 CHECKS=264/360 96 wrong vectors, and every one returns the THEN-arm where the else-arm was due: BUG sel_i64_lt_s(7,5): want=205 got=107 BUG sel_i64_lt_s(0,0): want=200 got=100 BUG sel_i64_lt_s(100,100): want=300 got=200 BUG sel_i64_eq(-1,1): want=201 got=99 MECHANISM RE-DERIVED HERE, not taken from the issue (capstone over the ELF .text, never `synth disasm` text): add.w r5, r1, #0xc8 ; else-arm -> r5, LIVE str.w r3, [sp] ; then-arm SPILLED to free an i64 pair ... ; the 64-bit compare ldr.w r5, [sp] ; RELOADED INTO r5 == the live else-arm movne r6, r5 moveq r6, r5 ; both `it` arms move the SAME register The spill is NOT the opt-in SYNTH_SPILL_ON_EXHAUST rung — `alloc_consecutive_pair` spills the deepest register-resident vstack entry unconditionally when no free consecutive pair remains, which the i64 comparison's two sign-extended pairs guarantee. `pop_operand`'s reload then allocates against the REMAINING vstack plus `live_params`; a value already popped for this same op is on neither list, so the allocator is free to hand out exactly the register the select still needs. Two facts the issue did not have: * the SELF-CONTAINED (optimized) leg emits a BYTE-IDENTICAL body (sha256 1bef7e189ae8e090 both legs) — the i64 ops delegate this shape to `select_with_stack`, so this is one defect on two nominal paths, not two. * `sel_wide_i64cmp` (i64 ARMS under an i64 comparison) is GREEN pre-fix: the wide path already reserves its destination pair across the pops, which is the shape of the fix the narrow path needs. Guards are in the fixture for the two NON-signatures: `guard_const_arms` (constant arms spill nothing, so the bug is invisible) and `guard_same_arm` (two conditional moves reading one register is CORRECT when both arms are the same value — so "same source" is not by itself the defect).
…loading pop
RQ-58-SELECT973. `select` on an i64-comparison condition with COMPUTED arms
returned the then-arm on every vector where the else-arm was due.
MECHANISM (verified by execution and by the emitted bytes, not taken from the
issue). `pop_operand`'s reload allocates against the REMAINING vstack plus
`reserved`. A value the SAME op popped a moment ago is on neither list — it
left the vstack when it was popped, and the caller has to thread it through by
hand. The narrow `Select` arm passed only `live_params`, so the reload was free
to hand back the register the select still needed:
add.w r5, r1, #0xc8 ; else-arm -> r5, LIVE
str.w r3, [sp] ; then-arm spilled: the i64 compare needs a PAIR
ldr.w r5, [sp] ; reloaded INTO the live else-arm
movne r6, r5
moveq r6, r5 ; both `it` arms move the same register
The spill is NOT the opt-in SYNTH_SPILL_ON_EXHAUST rung — `alloc_consecutive_pair`
spills the deepest register-resident entry unconditionally when no free pair
remains, which an i64 comparison's two sign-extended operands guarantee. That is
why the bug needs an i64 CONDITION and COMPUTED arms: constant arms spill
nothing.
THE FIX is the reservation discipline #677 already established for bulk-memory
("every popped operand of the op"), generalized into `pop_operand_committed` so
the next multi-pop site cannot forget it: the condition register is committed
across both pops, and `val2` across the second. The wide (i64-arms) path gets
the same treatment for `val2`'s PAIR — that path measured GREEN pre-fix because
it already reserved its destination pair, so this is the identical latent shape
closed rather than a second live bug.
BYTES. The reservation can only change an allocation the OLD code would have
answered with a committed register — which is the miscompile — so everything
else is byte-identical:
* 10/10 frozen anchors UNCHANGED (`cargo test -p synth-cli --test
frozen_codegen_bytes`), control_step and flight_algo included. No re-freeze
was needed and none was done.
* full `cargo test --workspace` green (142 test binaries), clippy -D warnings
clean.
Where bytes DO move, they move to correct AND smaller code — `sel_i64_lt_s`
68 B -> 64 B, because with `val1 != val2` restored the #209 in-place select
applies and one conditional move disappears:
ldr.w r6, [sp] ; reload lands clear of the live else-arm
cmp r4, #0
it ne
movne r5, r6 ; single in-place move; r5 already holds the else-arm
EXECUTION EVIDENCE. `scripts/repro/select_i64cmp_973_arm_differential.py`
264/360 -> 360/360 against wasmtime, on BOTH ARM lowering legs (relocatable and
self-contained, whose bodies are byte-identical here).
The defensive `Err` in `pop_operand_committed` is a TRIPWIRE for future edits,
not a live path: with the reservation in place it is unreachable by
construction. It is unit-tested directly rather than left as untested prose.
…ith floors The second half of RQ-58-SELECT973, and the larger half. #973 was found only because a lane compiled `scripts/repro/*.wat` for ARM BY HAND. Nothing in CI did that: the `repro-sweep-*` jobs run hand-listed oracles, each pinned to the one fixture its issue was about, so a fixture written for RV32 or aarch64 never reached the ARM backend even when its shape is backend-independent. `rv32_cmp_select_472.wat` has carried the #973 shape as `sel_cmp_i64` since v0.11 and no ARM build ever saw it. RED-FIRST FOR THE GATE ITSELF, not only for the fix. Run against the PRE-fix binary, this sweep reports 7 wrong vectors for `rv32_cmp_select_472.wat:sel_cmp_i64` — the already-in-tree fixture — and 55 across the #973 shapes in total. Post-fix those are gone. The leg would have caught the defect it was built for, with no new fixture required. TWO PHASES, because compiling alone would not have caught #973 (it compiled perfectly and returned the wrong number): A. COMPILE CENSUS over all 156 fixtures at `--target cortex-m4f --relocatable --all-exports`. Measured 144 compile / 12 decline / 0 hard error. The 12 are LOUD `#952` declines listed by name and reason, ratcheted BOTH ways: an undeclared failure is red, and a listed fixture that starts compiling is red too ("remove the entry"), so the list cannot rot into a permanent excuse. B. EXECUTION DIFFERENTIAL vs wasmtime over every all-i32 export of every module pure enough to emulate faithfully. Measured 43 modules, 2327 compared vectors, 2335 emulator entries. WHAT IT SURFACED — 48 pre-existing wrong vectors that have nothing to do with #973, identical on binaries built before and after the fix. Reported, not narrowed away, and both classes proven from the emitted bytes: * #989 — a `local.set`/`local.tee` clobbers an earlier `local.get` of the SAME local still on the operand stack. `war_set` returns 200 for EVERY input (`mov r0,r0; movw r1,#0x64; mov r0,r1; adds r2,r0,r0`); `get_set_get_param_no_alias` never reads its argument at all. RV32 has the protection (`snapshot_aliases`) and its own fixture comment predicts this miscompile; ARM has no equivalent. 44 vectors, 4 exports. * #990 — a local written on only ONE arm of a `br_if` is never zero-inited, so the merge reads uninitialised stack (`bne` jumps past the only `str.w`, then `ldr.w` reads the slot). Provable rather than inferred: the returned values are this harness's 0xDEADBEEF poison plus/minus one, the two `select` arms at the merge. #970 closed the parameter half of this class; this is the local-on-a-br_if half. 3 vectors, information-disclosure shape. They are listed in `KNOWN_ARM_MISMATCHES` against their issue number, ratcheted in both directions — an entry that stops mismatching must be deleted, which is how each fix will record itself — and SUPPRESSED rather than skipped, so listing debt cannot buy slack in the comparison floor. FLOORS ARE MEASURED, NOT GUESSED (this hub found existing floors at half their real value). Both scripts were calibrated through the real CI invocation path, `scripts/oracle_run.py --report-only`: select_i64cmp_973_arm_differential.py emulations = 360 (floor 360) arm_corpus_sweep_973.py emulations = 2335 (floor 2335) The sweep carries a SECOND floor the driver cannot express: `emulations` counts emulator ENTRIES, and a run that entered the emulator and then discarded every result on budget grounds would still clear it. `MIN_COMPARED = 2327` is the floor on vectors actually compared against wasmtime. HONEST GAPS, all counted and printed rather than assumed away: 91 modules are excluded from phase B for declaring a memory/table/global/data/elem/import section (their images are `repro-sweep-memory-oracle`'s job — a differential that reported its own setup gaps as miscompiles would train people to ignore it); 37 vectors trap under wasmtime; 36 exceed a budget. Both engines get one — wasmtime runs on FUEL, unicorn on an instruction count — because `aarch64_ctrlflow_851.wat:countdown(-1)` is a 2^32-iteration loop that costs 1.4 s per vector in the interpreter, and a leg that takes an hour gets deleted. Ledger moved with the docs, as the #910 ritual requires: ORACLE_WIRING.md 144 -> 146 oracles and 296,059 -> 298,754 entries, feature_matrix.md.tmpl, claims.yaml (both the verbatim texts and the emulations count-min), and ci.yml's `--min-emulation-floor`. Also corrected two pre-existing drifts found in the same table: the stdout floor total read 458 against a measured 460, and the doc's `--min-emulation-floor 295621` disagreed with ci.yml's 295726. claim_check 43/43.
Follow-up: the
|
Automated review for PR #992pulseengine/synth: Verdict: 💬 Comment Summary: The commit message does not clearly explain why the number of assertions has increased from 144 to 146. It is also unclear what changes were made to the code that caused this increase. Findings: 0 mechanical (rivet) · 1 from local AI model. Findings (1):
Generated by a local AI model and post-validated against a strict JSON contract. Each finding includes the verbatim line being criticised — verify by reading the file at the cited location. Reviewed at |
…me itself Pre-emptive fix for the class this job would otherwise have discovered the hard way. The ARM corpus sweep shelled out to `wat2wasm --enable-all`, which makes the EXECUTED POPULATION depend on the runner's wabt build: several fixtures use post-MVP proposals (multi-memory, bulk-memory), and a wabt that rejects the flag or a proposal would have refused every fixture, collapsed the comparison count, and reported it as a coverage failure rather than as the toolchain problem it is. That is the #850/#881 host-dependency class, and a never-before-run job is exactly where it hides — this repo's own #890 job comments say so verbatim. `wasmtime.wat2wasm` is the SAME library that then instantiates the module, so what parses and what runs cannot disagree, and there is no apt package in the path at all (the step is gone; the #973 differential never used wat2wasm either, it loads the `.wat` through `Module.from_file`). MEASURED EQUIVALENT, not assumed: across all 156 fixtures wabt and wasmtime accept exactly the same set (0 refusals each), and the section-id purity classification is identical for every one of them. The sweep's numbers are unchanged — compiled 144/156, 2327 compared vectors, emulations 2335, so no floor moved. Also: a mass reference-refusal now names ITSELF. Without it, a host whose assembler cannot read the corpus reds on the comparison floor, which reads as "the sweep lost coverage" and sends the next reader to the compiler. Fail-closed either way; the difference is the diagnostic. Gates re-run after the change, each without a pipe: sweep PASS (floor 2335, measured 2335), #973 differential 360/360 (floor 360, measured 360), oracle_wiring_check --min-emulation-floor 298754 rc=0, claim_check 43/43.
Both found by re-reading the checkers rather than the checked code — v0.57's lesson was that 5 of its 10 defects lived in instruments. 1. The #973 differential did not assert that the emulation REACHED the return pad. The instruction budget is ~500x what these straight-line functions need, so it cannot be hit today; the point is that if it ever were, `run()` would return whatever R0 happened to hold and the vector could COMPARE EQUAL by accident. A differential that passes on a value it never finished computing is precisely the shape being guarded against elsewhere in this lane. The corpus sweep already checked PC; now both do. 2. The sweep leaked its temp dir (`mkdtemp` with no cleanup). Re-verified after the change, each without a pipe: differential 360/360 (floor 360, measured 360), sweep PASS with compiled 144/156 and 2327 compared vectors (floor 2335 emulations, measured 2335). No number moved.
Runner reconciliation: ubuntu-latest matches macOS exactlyThe floors in this PR sit at the measured value with no headroom, and this Every counter is identical to the local measurement — compiled 144/156, 43 Two later commits tightened the harnesses after that run — the reference |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
… defeated gates Conflict resolution (claims.yaml, scripts/claim_check.py): union of the two lanes — main's RQ-58-METRIC ratchet engine + this branch's RQ-58-MIRRORS fields (the retired aarch64_selector_ops derivation stays deleted; the branch-only fields-equal evidence kind is ported into main's _check_evidence dispatcher). Two gates were silently defeated on main and are re-armed here: 1. MC/DC rustc pin (#984): dependabot bumped dtolnay/rust-toolchain 1.96.1 -> 1.100.0 — for that action the ref IS the compiler, so the MC/DC job's deliberate measurement pin was changed with no re-measure of the floors. Restored to 1.96.1 and excluded from dependabot with the reason in both files. 2. Subtraction ratchet stale-base cross (#991 x #992): #992 merged 19:50 adding 102 code-region selector lines and one _ => None helper arm; #991 pinned the ratchet values at 20:07 from a base that predated it, so main itself derives 18582/29839/63/106 against a ledger saying 18480/29616/62/105 and is red on its own queued push CI. This PR is the first to carry the union; the pins move here with waivers attributing the growth to #992's #973 miscompile fix, not to this lane. mirror_marker_files 57 -> 58 is waived as the surviving single count_params copy's own RQ-58-MIRRORS retirement-provenance comment — the heuristic's documented prose over-count, not a new mirror. status.json + FEATURE_MATRIX.md regenerated via --emit-status; 49/49 claims hold on the merged tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…DSL rules replaced (-1,312 lines, byte-identical) (#999) * retire(#242): i32.add — delete the hand-written select_default arm The Rocq-proved rule_i32_add is the shipped lowering since the default-on flip; the superseded arm is deleted. Byte-identity: 688-row corpus manifest (156 fixtures x 4 configs incl. declines) identical to origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.sub — delete the hand-written select_default arm Same evidence as i32.add: 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.mul — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.and — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.or — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.xor — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.shl — delete the hand-written select_default arm The Rocq-proved rule carries the #682 mod-32 R12 mask. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.shr_s — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.shr_u — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.rotl — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.rotr — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.clz — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.ctz — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.popcnt — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.add — delete both hand-written arms (default + direct) One op, both selectors: the select_default fixed-pair arm and the select_with_stack allocated-pair arm each collapse to the Rocq-proved rule_i64_add call that has been the shipped path since the flip. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.sub — delete both hand-written arms (default + direct) 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.and — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.or — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.xor — delete the hand-written select_default arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.{or,and,xor} — delete the hand-written direct-selector arm One grouped select_with_stack arm serves the three i64 bitwise ops via the shared i64_pair_rule dispatch, so the deletion unit is the group (its select_default counterparts were retired per-op in the three previous commits). Also removes the group's '_ => unreachable!()' wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.eqz — delete both hand-written arms (default + direct) 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64 comparisons x10 — delete the hand-written select_default arm One grouped arm serves the ten binary i64 comparisons via the shared i64_setcond_rule dispatch, so the deletion unit is the group. Also removes the group's '_ => unreachable!()' condition-map wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.{shl,shr_u,shr_s} — delete the hand-written direct-selector arm One grouped select_with_stack arm serves the three i64 variable shifts via the shared i64_pair_bin_rule dispatch; the deletion unit is the group. Also removes the group's '_ => unreachable!()' wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32 comparisons x10 — retire the flag conditioning on the direct selector PARTIAL by design, and named as residual: this grouped arm is dual-use. The reg-reg case now takes the Rocq-proved i32_cmp_rule unconditionally; the #258 cmp/cmn imm-fold case has no DSL rule (no CmpImm-shaped rule exists), so its hand-written Cmp+SetCond fallback emission STAYS and is enumerated in the RQ-58-SPLIT residual set rather than deleted. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.eqz — delete the hand-written direct-selector arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.{shl,shr_s,shr_u,rotr} — delete the hand-written direct-selector arm One grouped select_with_stack arm serves the four register shifts/rotates via the shared i32_shift_rule dispatch; the deletion unit is the group. The generated rules carry the #682 mod-32 R12 mask themselves. Also removes the group's '_ => unreachable!()' wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.clz — delete the hand-written direct-selector arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.ctz — delete the hand-written direct-selector arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i32.popcnt — delete the hand-written direct-selector arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64 comparisons x10 — delete the hand-written direct-selector arm One grouped select_with_stack arm serves the ten binary i64 comparisons via the shared i64_setcond_rule dispatch; the deletion unit is the group. Also removes the group's '_ => unreachable!()' condition-map wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.mul — delete the hand-written direct-selector arm 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.{rotl,rotr} — delete the hand-written direct-selector arm One grouped select_with_stack arm serves both rotates via the shared i64_rot_rule dispatch; the deletion unit is the group. Also removes the group's '_ => unreachable!()' wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): i64.{clz,ctz,popcnt} — delete the hand-written direct-selector arm One grouped select_with_stack arm serves the three i64 bit-counts via the shared i64_unary_count_rule dispatch; the deletion unit is the group. Also removes the group's '_ => unreachable!()' wildcard arm. 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * retire(#242): the SYNTH_SEL_DSL lever itself + the now-vacuous mirror-pin gates With every superseded arm deleted there is no second implementation for the flag to select: the sel_dsl field, sel_dsl_from_env (SYNTH_SEL_DSL / SYNTH_NO_SEL_DSL), and set_sel_dsl are dead code and go. The four mirror-pin tests (sd + sws + i64-pair + defaults-on) compared the two paths byte-for-byte; with one path left they are vacuous — retired on the same grounds as the VcrSelRulesGenCheck reflexivity gate (see coq/STATUS.md). The #258 imm-fold holdout test is REWRITTEN, not deleted: the residual hand-written Cmp/Cmn+SetCond emission it pins survived the retirement (cmp_imm_fold_residual_path_stays_handwritten_258). The RULES table — including its Delegation wiring metadata that the Rocq generation and the manifest gates consume — is untouched. Byte-identity: 688-row corpus manifest identical to the origin/main baseline; all 10 frozen anchors green; synth-synthesis suite 732+ green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * docs+ledger(#242): re-bank the subtraction ratchet for RQ-58-RETIRE claims.yaml: selector_lines_code 18582 -> 17897 (banked), wildcard_arms_code 63 -> 56 (banked), totals tracked 29839 -> 28495 / 106 -> 91; the two former #992 waivers deleted with the re-bank (a waiver above the new baseline is a standing licence to grow back). status.json regenerated via --emit-status. CLAUDE.md / coq/STATUS.md / verified-codegen-roadmap.yaml no longer claim the SYNTH_NO_SEL_DSL opt-out exists; rivet RQ-58-RETIRE -> implemented with evidence + the measured residual set. claim_check 49/49. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… can run (#1001) #1000, from a toolchain-wide CLI survey: `synth verify` exists in the tree and in `--help`, fails closed with an exemplary message — and is in NO artifact a user can obtain. Reproduced by the reporter on the layer build (0.55.0) AND the current release (0.57.0), whose four platform tarballs all lack the feature. THE ISSUE'S OWN ESCAPE HATCH IS DEAD, AND I MEASURED IT RATHER THAN ASSUMING. #1000 offers: "or, if the Z3 dependency makes that undesirable for the default artifact, publish a variant". There is no Z3 dependency: cargo tree -p synth-cli --features verify -> 0 z3 nodes cargo tree -p synth-cli --features verify,…/z3-solver -> 2 z3 nodes `verify = ["synth-verify"]` and nothing more. `z3-solver = ["z3"]` is a separate opt-in with `optional = true`, and even enabled it links the SYSTEM libz3 rather than bundling it (#553). Ordeal (pure Rust QF_BV) has been the default engine since v0.27.0. Shipping `--features verify` costs no Z3, no libz3, no C++ toolchain — so this is a release-workflow change, not a trade-off. README.md:98 IS THE OTHER HALF OF THE FINDING, and it is corrected here. It said the CLI `verify` feature "currently also enables the feature-gated Z3 differential oracle (statically linked)". False since v0.27.0 — and it is exactly the sentence that would make a maintainer accept #1000's escape hatch and NOT ship the feature. Prose that was true once, quietly stopped being, and stayed load-bearing on a release decision: the class the v0.57 cold review kept turning up, found again by a user asking a good question about a stale premise. Artifact scope, three parts, with the middle one carrying the weight: 1. build the released binaries (four tarballs + npm) with `--features verify` 2. a NON-VACUITY GATE — a released-artifact smoke test that actually RUNS `synth verify` on a freshly compiled module and asserts a VERDICT, not the capability-missing error. Red-first against a binary built without the feature. Without this, "we shipped the feature" regresses silently to "we shipped the help text", which is the class this whole release closes. 3. move the capability check AHEAD of the banner — today the four `Translation validation:` lines including `Strategy: Per-rule SMT verification (ASIL D path)` print BEFORE the tool discovers it cannot verify, so a log-scraper finds the ASIL-D line in a run that verified nothing. Exit code is already correct. Out of scope, named not dropped: #1000's `synthesize`-vs-`compile` description overlap, `--format json`, help-line width. Also re-graded RQ-58-MIRRORS (#993) and RQ-58-SELECT973 (#992) proposed -> implemented; both merged and the ledger had not caught up. rivet: 50 errors before AND after (unchanged); warnings +2, the standard pair every artifact carries. claim_check 49/49. Refs #1000, #242 Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…11/11, version pin sweep All 11 RQ-58-* artifacts are implemented and merged, so the release-readiness query over artifacts/release-v0.58.yaml is satisfied. Every number in the CHANGELOG was re-derived from MERGED CODE at this commit, not from PR bodies (v0.57's cold review found 4 factual errors in release prose written the same way): DSL rules 50 -> 74 (coq/vcr_sel_rules.manifest, tag vs HEAD) 1:1 Qed theorems 50 -> 74 (VcrSelRules.v) Rocq Qed (suite) 592 -> 617 (2 Admitted, 2 admit.) selector code 18,480 -> 17,961 selector wildcards 62 -> 55 selector total 29,616 -> 28,582 TWO CORRECTIONS the cold review caught in my own draft: 1. The v0.57.0 code-region baseline is 18,480 / 62 wildcards, NOT 18,582 / 63. The latter pair is the METRIC pin's CREATION value — measured mid-v0.58 after #992 grew the file — not the v0.57.0 release value. Re-measured at the tag with the ratchet's own marker ('#[cfg(test)]\nmod tests', asserted unique) so the release-over-release delta is like-for-like. 2. The SPLIT bullet originally read 'the root file went 29,616 -> 18,284', which is true release-over-release but credits SPLIT with a drop that RETIRE (-1,312) and SELDSL (-79) partly produced. Corrected to SPLIT's own effect: 10,298 lines moved out, 28,533 -> 18,284. Locally-true sentence, wrong attribution — the same shape as all four v0.57 errors. Version pin sweep across all four surfaces + Cargo.lock: check_version_pins.py exits 0 at 0.58.0. claim_check 49/49, model_coverage_audit --check ok, fmt clean. Refs #242.
…11/11, version pin sweep (#1010) All 11 RQ-58-* artifacts are implemented and merged, so the release-readiness query over artifacts/release-v0.58.yaml is satisfied. Every number in the CHANGELOG was re-derived from MERGED CODE at this commit, not from PR bodies (v0.57's cold review found 4 factual errors in release prose written the same way): DSL rules 50 -> 74 (coq/vcr_sel_rules.manifest, tag vs HEAD) 1:1 Qed theorems 50 -> 74 (VcrSelRules.v) Rocq Qed (suite) 592 -> 617 (2 Admitted, 2 admit.) selector code 18,480 -> 17,961 selector wildcards 62 -> 55 selector total 29,616 -> 28,582 TWO CORRECTIONS the cold review caught in my own draft: 1. The v0.57.0 code-region baseline is 18,480 / 62 wildcards, NOT 18,582 / 63. The latter pair is the METRIC pin's CREATION value — measured mid-v0.58 after #992 grew the file — not the v0.57.0 release value. Re-measured at the tag with the ratchet's own marker ('#[cfg(test)]\nmod tests', asserted unique) so the release-over-release delta is like-for-like. 2. The SPLIT bullet originally read 'the root file went 29,616 -> 18,284', which is true release-over-release but credits SPLIT with a drop that RETIRE (-1,312) and SELDSL (-79) partly produced. Corrected to SPLIT's own effect: 10,298 lines moved out, 28,533 -> 18,284. Locally-true sentence, wrong attribution — the same shape as all four v0.57 errors. Version pin sweep across all four surfaces + Cargo.lock: check_version_pins.py exits 0 at 0.58.0. claim_check 49/49, model_coverage_audit --check ok, fmt clean. Refs #242.
…ery compile path — the decoder had no StartSection arm at all The section fell through the catch-all and was discarded outright: all three backends compiled a (start)-carrying module, exited 0, printed no warning, and the start function was not even in the object. wasmtime runs it at instantiation (WASM Core §4.5.5) and returns 42 on the repro; synth-compiled code read never-initialized memory and returned 0. - wasm_decoder: Payload::StartSection arm + DecodedModule.start_function - main.rs: refuse_dropped_start_function at all three compile decode sites (module path, single-function path, WAST merge path) — non-zero exit + a reason naming the start section, the #851/#1041 refusal shape - deliberately NO escape hatch: the start function's code is not in the artifact, so no embedder contract can honor it (unlike #1041's --embedder-data-init, where the embedder holds the bytes) - start-function INVOCATION is a capability follow-on, not this fix 0 fixtures/harnesses affected (no .wat/.wast/.wasm in the repo carries a start section — swept textually AND by binary section id); emitted bytes for start-free modules are unchanged (guard only fires on Some), frozen anchors untouched; EXPECTED_DECLINES (#992) unchanged (keys on scripts/repro/*.wat, none of which carry (start)). Red-first: the 7 refusal tests in start_section_refusal_1046.rs failed on the parent commit and pass now; the start-free control passes throughout. Gates: fmt, clippy -D warnings, cargo test --workspace (true exit 0), claim_check 50/50, model_coverage_audit ok, rivet validate 50 errors (= main baseline, #1012). Refs #1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… by ALL THREE backends — decode it and refuse loudly; fourth silent drop (globals inits, #1052) found and filed (#1053) * test(#1046): RED-FIRST — every backend must refuse a (start ...) module, asserted as a loud refusal naming the start section All 7 refusal cases FAIL on current main (exit 0, no warning, start function absent from the object); the start-free control passes. The assertions deliberately name the refusal (non-zero exit + reason string containing 'start function' and '#1046'), NOT the absence of invocation — which was already true on the broken behaviour and is exactly why the bug survived every existing test. Refs #1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L * fix(#1046): decode the (start ...) section and REFUSE it loudly on every compile path — the decoder had no StartSection arm at all The section fell through the catch-all and was discarded outright: all three backends compiled a (start)-carrying module, exited 0, printed no warning, and the start function was not even in the object. wasmtime runs it at instantiation (WASM Core §4.5.5) and returns 42 on the repro; synth-compiled code read never-initialized memory and returned 0. - wasm_decoder: Payload::StartSection arm + DecodedModule.start_function - main.rs: refuse_dropped_start_function at all three compile decode sites (module path, single-function path, WAST merge path) — non-zero exit + a reason naming the start section, the #851/#1041 refusal shape - deliberately NO escape hatch: the start function's code is not in the artifact, so no embedder contract can honor it (unlike #1041's --embedder-data-init, where the embedder holds the bytes) - start-function INVOCATION is a capability follow-on, not this fix 0 fixtures/harnesses affected (no .wat/.wast/.wasm in the repo carries a start section — swept textually AND by binary section id); emitted bytes for start-free modules are unchanged (guard only fires on Some), frozen anchors untouched; EXPECTED_DECLINES (#992) unchanged (keys on scripts/repro/*.wat, none of which carry (start)). Red-first: the 7 refusal tests in start_section_refusal_1046.rs failed on the parent commit and pass now; the start-free control passes throughout. Gates: fmt, clippy -D warnings, cargo test --workspace (true exit 0), claim_check 50/50, model_coverage_audit ok, rivet validate 50 errors (= main baseline, #1012). Refs #1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #973. RQ-58-SELECT973, both halves.
1. The miscompile
selecton an i64-comparison condition with computed arms returned thethen-arm on every vector where the else-arm was due.
Mechanism, re-derived here rather than taken from the issue (capstone over
the ELF
.text, neversynth disasmtext):pop_operand's reload allocates against the remaining vstack plusreserved. A value the same op popped a moment ago is on neither list, so theallocator was free to hand back the register the select still needed. The spill
is not the opt-in
SYNTH_SPILL_ON_EXHAUSTrung —alloc_consecutive_pairspills the deepest register-resident entry unconditionally when no free pair
remains, which an i64 comparison's two sign-extended operands guarantee. Hence
the shape: i64 condition (creates the pressure) plus computed arms
(constant arms spill nothing).
Three things the issue did not have, all measured:
(
sha256 1bef7e18…on both legs) — one defect on two nominal paths, not two;destination pair, which is the shape of the fix the narrow path needed. Its
val2-pair omission is closed here as the identical latent shape;guard_same_armshows the issue's suggested structural assertion ("a select'stwo conditional moves must never have the same source") is not sound as
stated —
select (local.get 0) (local.get 0) clegitimately moves oneregister twice. The real invariant is the reservation, so that is what landed.
The fix generalizes the reservation discipline #677 already established for
bulk-memory ("every popped operand of the op") into
pop_operand_committed, sothe next multi-pop site cannot forget it.
Red-first
scripts/repro/select_i64cmp_973_arm_differential.py, landed before the fix(commit 07bceef):
96 wrong vectors, every one returning the then-arm:
Plus four allocator unit tests, including a red one that reproduces the
hazard directly (
pop_operand_red_973_reload_lands_on_the_live_operand: withthe reservation the old code passed, the reload takes R5 while the op still
needs it).
Bytes
10/10 frozen anchors UNCHANGED — no re-freeze was needed and none was done.
The reservation can only change an allocation the old code would have answered
with a committed register, which is the miscompile. Where bytes do move they
get smaller and correct:
sel_i64_lt_s68 B → 64 B, because withval1 != val2restored the #209 in-place select applies and one conditionalmove disappears.
2. The CI gap — the larger half
#973 surfaced only because someone compiled the repro corpus for ARM by hand.
CI never did. The
repro-sweep-*jobs run hand-listed oracles each pinned toone fixture; a fixture written for RV32 or aarch64 never reached the ARM backend.
rv32_cmp_select_472.wathas carried this exact shape assel_cmp_i64sincev0.11 and no ARM build ever saw it.
New job
repro-sweep-arm-corpus-oraclerunsscripts/repro/arm_corpus_sweep_973.py:0 hard errors. The 12 are loud
#952declines listed by name and reason,ratcheted both ways (an undeclared failure is red; a listed fixture that
starts compiling is red too).
vectors, 2335 emulator entries.
The leg is red-first against its own defect. Run against the pre-fix binary
it reports 7 wrong vectors for
rv32_cmp_select_472.wat:sel_cmp_i64— thefixture already in the tree — and 55 across the #973 shapes overall.
Floors are measured through the real CI path (
oracle_run.py --report-only),not guessed:
emulations >= 360andemulations >= 2335, both exact. The sweepcarries a second floor the driver cannot express (
MIN_COMPARED = 2327),because
emulationscounts emulator entries and a run that discarded everyresult on budget grounds would still clear it.
3. What the leg newly surfaced — 48 vectors, two classes, both filed
Reported, not narrowed away. Identical on binaries built before and after the
#973 fix, and both proven from the emitted bytes.
#989 — a
local.set/local.teeclobbers a livelocal.getof the samelocal (44 vectors, 4 exports).
war_setreturns 200 for every input:get_set_get_param_no_aliasnever reads its argument at all. RV32 has theprotection (
snapshot_aliases) and its own fixture comment predicts this exactnumber; ARM has no equivalent.
#990 — a local written on only one arm of a
br_ifis never zero-initialised(3 vectors), reading uninitialised stack. Provable rather than inferred: the
returned values are the harness's
0xDEADBEEFpoison ±1, the twoselectarms at the merge. #970 closed the parameter half of this class; this is the
local-on-a-
br_ifhalf — same information-disclosure shape.Both are in
KNOWN_ARM_MISMATCHESagainst their issue number, suppressed notskipped (so listing debt cannot buy slack in the comparison floor) and
ratcheted in both directions — an entry that stops mismatching must be deleted,
which is how each fix will record itself.
A third finding, negative: VCR-RA-003's four invariants (callee-saved,
spill-slot aliasing, caller-saved across calls, availability across joins) do not
cover intra-operation operand liveness, which is why the whole-function
allocation validator was silent on #973. Named, not fixed here.
4. Honest gaps
memory/table/global/data/elem/import section. Their images are
repro-sweep-memory-oracle's job; a differential that reported its own setupgaps as miscompiles would train people to ignore it.
selectwas probed, not proven absent: sixadversarial binop shapes (one of which genuinely spills and reloads) matched
wasmtime 72/72.
pop_operand_committedexists so the next site can adopt itcheaply.
Gates
cargo test --workspace,cargo clippy --workspace --all-targets -D warnings,cargo fmt --check,python3 scripts/claim_check.py claims.yaml(43/43),oracle_wiring_check.py --min-emulation-floor 298754— each run without a pipe.Ledger moved with the docs per the #910 ritual: ORACLE_WIRING.md 144→146 oracles
and 296,059→298,754 entries,
feature_matrix.md.tmpl,claims.yaml(verbatimtexts and the emulations
count-min),ci.yml's--min-emulation-floor.Two pre-existing drifts in that same table were corrected: the stdout floor read
458 against a measured 460, and the doc's
--min-emulation-floor 295621disagreed with ci.yml's 295726.
No
[Unreleased]edit, no version bump, nostatus.jsonregeneration.docs/status/FEATURE_MATRIX.mdis regenerated because the claim-check gaterequires it when the template changes (one line).
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L