fix(trap-semantics): unreachable traps on every backend (#665); rv32 rem_s drops the spurious INT_MIN/-1 guard (#666) - #668
Merged
Conversation
…rem_s drops the spurious INT_MIN/-1 guard (#666) #665 — wasm `unreachable` compiled to a NO-OP on thumb-2 AND rv32 (falls through instead of trapping, WASM Core §4.4.5). Root cause was ONE decode drop: `convert_operator` returned `None` for `Unreachable` and `is_intentionally_ignored` whitelisted it alongside `Nop`, so no backend ever received the op — the selector trap arms (ARM `UDF #0`, RV32 `ebreak`) already existed but were dead code. Fix per path: - decoder (synth-core): `Unreachable` now decodes to `WasmOp::Unreachable`; only `Nop` stays intentionally ignorable. - ARM direct (`select_with_stack`) + `select_default`: existing `UDF #0` arms now fire (no change needed). - ARM optimized path (optimizer_bridge): previously lumped `Unreachable` in with Nop as an IR placeholder — now a typed loud-DECLINE to the direct selector (the bridge `Opcode` enum has no trap opcode; adding one would ripple through the #513 mirror-pinned reg_effect/rewrite_op machinery). Same decline-don't-drop pattern as #120 floats / #500 non-tail return. No new ArmOp on the optimized path, so the #511 estimator oracle is untouched (Udf was already covered by the div-zero guards). - RV32: existing `ebreak` arm now fires (no change needed). - aarch64: new `brk #0` encoder + selector arm (was a loud-decline). #666 — rv32 `i32.rem_s(INT_MIN,-1)` spuriously trapped: the selector shared div_s's INT_MIN/-1 overflow `ebreak` guard with rem_s via `bin_with_signed_div_traps`. WASM §4.3.2 defines irem_s(INT_MIN,-1) = 0 with NO trap, and RISC-V M-ext `rem` already returns 0 for the overflow case (unprivileged spec §7.2), so rem_s now takes plain `bin_with_zero_trap` — zero-divisor guard KEPT, bare `rem` is exactly wasm-correct. The pre-existing test `rv32_signed_rem_also_gets_overflow_guard` pinned the BUG; it is rewritten as the #633-twin fix-guard pins (`rv32_signed_rem_carries_only_zero_guard_666` + `rv32_signed_div_still_carries_both_guards_666`), mirroring ARM's `test_633_i64_rems_has_no_overflow_guard` and the existing i64 RV32 pin. Oracles (red on origin/main, green here; CI job trap-semantics-oracle): - scripts/repro/unreachable_665_differential.py — thumb2 + rv32 under unicorn vs wasmtime: bare `unreachable` traps, guarded `unreachable` taken traps, NOT taken returns normally (non-vacuity). Red on main: boom(7,9) "returned" 7 (arg fall-through) on both ISAs. Known gap kept visible: rv32 loud-declines the if/else-result-with-unreachable shape (#343 arity check) — contract-compliant (never falls through). - scripts/repro/rem_s_666_differential.py — rv32 trap table: rems(INT_MIN,-1)→0 no-trap (red on main: spurious ebreak), rems(INT_MIN,1)→0, rems(7,3)→1, rems(-7,3)→-1, rems(7,0) traps, divs(INT_MIN,-1) traps, divs(7,0) traps, divs(7,3)→2. Frozen anchors 10/10 bit-identical (no fixture contains `unreachable` — verified control_step/flight_seam/flight_seam_flat/signed_div_const). Workspace tests 108/108 suites green; fmt + clippy -D warnings clean. Fixes #665 Fixes #666 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jul 8, 2026
…ed rules, 81 bridge Qed (#673) * chore(release): v0.36.0 — unreachable traps + sparse tables + 40 rules + 81 bridge Qed (#668/#669/#670/#671) Pin sweep 0.35.0 -> 0.36.0 + CHANGELOG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: fold #672 (post-exhaustion quality) into the v0.36.0 changelog — merged ahead of the tag Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One lane, two trap-semantics bugs — same theme, disjoint files.
#665 —
unreachablecompiled to a NO-OP on thumb-2 AND rv32Root cause was ONE decode drop, not per-backend misses:
convert_operator(synth-corewasm_decoder.rs) returnedNoneforUnreachableandis_intentionally_ignoredwhitelisted it alongsideNop, so no backend ever received the op. The selector trap arms (ARMUDF #0in bothselect_with_stackandselect_default, RV32ebreak) already existed as dead code.Per-path status after the fix:
WasmOp::Unreachableselect_with_stack)UDF #0arm firesselect_defaultUDF #0arm firesUDF #0(same pattern as #120 floats / #500 non-tail return; no newOpcodethrough the #513 mirror-pinned reg_effect/rewrite_op machinery, no new ArmOp so the #511 estimator oracle is untouched —Udfwas already covered by the div-zero guards)ebreakarm firesbrk #0encoder + selector armKnown gap kept visible (contract-compliant, never falls through): rv32 loud-declines the
if/else-with-result shape whoseelsearm is pureunreachable(#343 arity check) — noted in the oracle.#666 — rv32
i32.rem_s(INT_MIN,-1)spuriously trappedThe selector shared div_s's INT_MIN/-1 overflow
ebreakguard with rem_s viabin_with_signed_div_traps. WASM §4.3.2:irem_s(INT_MIN,-1) = 0, NO trap — onlyidiv_straps there. RISC-V M-extremalready returns 0 for the overflow case (unprivileged spec §7.2), so rem_s now takes plainbin_with_zero_trap: zero-divisor guard KEPT, bareremis exactly wasm-correct. Disasm-verified:rems= 1 ebreak,divs= 2 ebreaks.The pre-existing test
rv32_signed_rem_also_gets_overflow_guardpinned the bug; rewritten as the #633-twin fix-guard pins (rv32_signed_rem_carries_only_zero_guard_666+rv32_signed_div_still_carries_both_guards_666), mirroring ARM'stest_633_i64_rems_has_no_overflow_guard.Oracles — red on origin/main, green here (new CI job
trap-semantics-oracle)scripts/repro/unreachable_665_differential.py(thumb2 + rv32 under unicorn vs wasmtime):boom(7,9)"returned" 7 (arg fall-through) on BOTH ISAs;guarded(-5)returned 0;guarded_br(-5)fell through — 5 FAILsguarded(5)/guarded_br(5)return 1 normallyscripts/repro/rem_s_666_differential.py(rv32 trap table):rems(INT_MIN,-1)spurious TRAP (wasmtime: 0)rems(INT_MIN,-1)→0,rems(INT_MIN,1)→0,rems(7,3)→1,rems(-7,3)→-1,rems(7,0)traps,divs(INT_MIN,-1)traps,divs(7,0)traps,divs(7,3)→2— 8/8Gates
unreachable)cargo test --workspace: 108/108 suites greencargo fmt --check+cargo clippy --workspace --all-targets -- -D warningscleanFixes #665
Fixes #666
🤖 Generated with Claude Code