Skip to content

fix(selector): i64 rotl/rotr/div_u/rem_u compute real results — never silent 0 (#610) - #613

Merged
avrabe merged 1 commit into
mainfrom
fix/610-i64-rot-div
Jul 3, 2026
Merged

fix(selector): i64 rotl/rotr/div_u/rem_u compute real results — never silent 0 (#610)#613
avrabe merged 1 commit into
mainfrom
fix/610-i64-rot-div

Conversation

@avrabe

@avrabe avrabe commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Per-op root cause + verdict (all four: REAL FIX, no loud-rejects needed)

Filed by the challenge harness (#610): i64.rotl/rotr/div_u/rem_u compiled without error on the Cortex-M path and returned 0 for every input (rotl by 0 — the identity — returned 0). One disease in the Thumb-2 encoder's expansions, two forms:

op root cause verdict
i64.rotl / i64.rotr expansion used hardcoded R3/R4 scratch colliding with selector-assigned regs, then its own POP {R4} restored saved scratch over the computed result (rd_lo == R4 in the repro) → returns the caller's stale R4 = 0 under qemu reset state implemented — core rewritten to fixed regs (R0:R1 value, R2 amount, R3+R12 scratch) inside the new fixed-ABI wrapper
i64.div_u / i64.rem_u expansion ignored its register fields outright (rdlo: _, …; hardcoded R0:R1 / R2:R3 in, result to R0:R1) while the selector allocated rd = R4:R5, which the core's own POP {R4-R7} then clobbered implemented — shift-subtract cores byte-identical, wrapped in the fixed-ABI marshal/restore
i64.div_s / i64.rem_s (same disease, fixed together) identical fixed-ABI mismatch implemented

The fixed-ABI wrapper: save R0-R3 → marshal operand regs into the core's fixed inputs via the stack (permutation-safe: every source read before any fixed reg is written) → run the core (self-preserving for R4+; R12 is encoder scratch, never allocatable, #212) → MOV the result pair into the selector's rd (loud Err on the impossible swapped pair — the #554 honesty floor) → restore R0-R3, skipping registers the result occupies. Both codegen paths benefit (direct/--relocatable and optimized — both pass real registers).

Bonus per WASM semantics: divide-by-zero now traps (ORRS R12,R2,R3; BNE +0; UDF #0, matching the i32 guard) — previously div/0 silently returned 0.

Red→green

scripts/repro/i64_rot_div_610_differential.py — 55 vectors vs wasmtime under unicorn (-t cortex-m3 -n <fn> --relocatable, the issue's exact config): rot-by-0 identity, rot 32/63/≥64 (mod-64), _hi twins checking the upper result half, div by 1/self/0 (both sides trap), high-bit patterns, >32-bit divisors, signed variants, shl4 control.

  • v0.30.0 (pre-fix): 40/55 MISMATCH (every rot/div/rem vector wrong — issue rows reproduced exactly: rotl(1,8)=0 want 256, div_u(100,7)=0 want 14, …; controls OK)
  • post-fix: 55/55 OK, exit 0

Wired as an isolated CI oracle job (i64-rot-div-610-oracle), same shape as the #503/#587 job.

Gates

Closes #610

🤖 Generated with Claude Code

… silent 0 (#610)

All four ops (plus div_s/rem_s, same disease) compiled without error and
returned 0 for every input on the ARM Cortex-M path. Root cause was in the
Thumb-2 encoder's multi-instruction expansions, one disease in two forms:

* I64Rotl/I64Rotr: the expansion used hardcoded R3/R4 scratch that collided
  with selector-assigned registers, then its own `POP {R4}` restored the
  saved scratch OVER the computed result (rd_lo == R4 in the repro) — the op
  returned the caller's stale R4: 0 under qemu/unicorn reset state.
* I64DivU/I64RemU/I64DivS/I64RemS: the expansion IGNORED its register fields
  outright (`rdlo: _, ...` — hardcoded R0:R1 dividend, R2:R3 divisor, result
  to R0:R1) while the selector allocated rd elsewhere (R4:R5), which the
  core's own POP then clobbered with stale values.

Fix: a fixed-ABI wrapper around each core — save R0-R3, marshal the operand
registers into the core's fixed input regs via the stack (permutation-safe:
every source is read before any fixed reg is written), run the core
(self-preserving for R4+; R12 is encoder scratch, never allocatable #212),
MOV the result pair into the selector's rd (loud Err on the impossible
swapped pair), restore R0-R3 skipping the result registers. The rot cores are
rewritten to fixed regs (R0:R1 value, R2 amount, R3+R12 scratch); the div/rem
shift-subtract cores are byte-identical inside the wrapper. Divide-by-zero
now traps (`ORRS R12,R2,R3; BNE +0; UDF #0`), matching WASM semantics and the
i32 guard — previously div/0 silently returned 0.

Estimator kept in exact agreement (#498/#511 oracle): rot 74→102 bytes,
div_u/rem_u/div_s/rem_s 74/78/126/124 → 120/124/172/170; all sizes are
register-independent by construction. Frozen fixture hashes bit-identical
(these ops appear in no frozen anchor).

Red→green: scripts/repro/i64_rot_div_610_differential.py (55 vectors — rot
identity/32/63/>=64 + hi-half twins, div by 1/self/0-trap, high-bit patterns,
signed variants, shl control) vs wasmtime under unicorn: 40/55 MISMATCH on
v0.30.0, 55/55 OK after. Wired as an isolated CI oracle job. New encoder unit
tests pin the rd-landing tail, the zero-divisor guard, the rd∈R0-R3
skip-restore, and the swapped-pair loud reject.

Closes #610

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.84170% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend/src/arm_encoder.rs 98.81% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 76d99a6 into main Jul 3, 2026
29 checks passed
@avrabe
avrabe deleted the fix/610-i64-rot-div branch July 3, 2026 17:10
avrabe added a commit that referenced this pull request Jul 3, 2026
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jul 8, 2026
…rflow trap (#632, #633) (#634)

#632 — the I64Popcnt expansion's own scratch restore (POP {R3,R4,R5})
clobbered the freshly computed count whenever the allocator-assigned rd
landed inside the restore set (ADDS rd,R4,R5 one instruction before the
pop). Structural fix on both the Thumb-2 and A32 arms: the total is
carried ACROSS the restore in R12 (encoder scratch, never allocatable
per #212, never in any restore set) and moved into rd only after the
pop — no choice of rd can collide. The entry marshal also routes rnlo
through R12 so an operand pair living at (R3,R4) can no longer read a
clobbered R4, and the new MOV/MOV.W forms are total over rd/rnhi = R8
where the old 3-bit T1 fields silently corrupted the encoding.

Expansion-family audit (pop-restore clobber class): I64Popcnt was the
only affected op on either ISA. I64Div{S,U}/I64Rem{S,U} and
I64Rotl/Rotr stage their result in R0:R1 before their scratch pop and
route it through the #610 fixed-ABI exit (which skips restored
registers); I64Clz/I64Ctz and i32 Popcnt push no scratch.

#633 — the i64 signed-division expansion emitted only the
divide-by-zero guard: INT64_MIN/-1 negated the dividend onto itself and
silently returned INT64_MIN instead of trapping (WASM Core 4.3.2
idiv_s). Mirror the i32 path's overflow guard on the #610/#613
fixed-ABI wrapper path (dividend R0:R1, divisor R2:R3):
dividend==INT64_MIN && divisor==-1 -> UDF #0, on both the Thumb-2 and
A32 I64DivS arms. I64RemS deliberately stays guard-free —
rem_s(INT64_MIN,-1) is defined as 0 and must not trap (pinned by the
fix-guard twin vectors and unit tests).

Oracles (red -> green):
- scripts/repro/i64_popcnt_632_differential.py — unicorn-vs-wasmtime,
  symtab-based: 6/11 vectors MISMATCH (0 for every input) on main,
  11/11 OK post-fix.
- scripts/repro/i64_divs_overflow_633_differential.py — INT64_MIN/-1
  returned 0 instead of TRAP on main (2 MISMATCH), 16/16 OK post-fix
  including rem_s(INT64_MIN,-1)=0 no-trap and div-by-zero still-traps.
- estimator_encoder_agreement (the #511 pin): I64Popcnt 172->180,
  I64DivS 172->194, register-independent.
- frozen_codegen_bytes: all anchors untouched (no i64 popcnt /
  div_s-overflow shapes in the frozen fixtures).

Closes #632
Closes #633

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant