Summary
On the RISC-V rv32 backend, i32.rem_s(INT_MIN, -1) spuriously traps. WASM Core §4.3.2 (irem_s) defines the remainder as 0 in this case — it must NOT trap (only div_s traps on the INT_MIN/-1 overflow). The rv32 selector copies the div_s INT_MIN/-1 overflow ebreak guard into the rem_s lowering, so rem_s(INT_MIN,-1) faults instead of returning 0. The Thumb-2 backend is correct here; this is rv32-selector-specific.
- synth
0.34.0, -b riscv -t rv32imac
- oracle: wasmtime 42.0.1
Reproducer
(module (func (export "f") (param i32 i32) (result i32)
(i32.rem_s (local.get 0) (local.get 1))))
wasmtime --invoke f -2147483648 -1 → 0 (correct; rem_s overflow result is 0, no trap).
- synth rv32 under qemu-system-riscv32 → traps (
ebreak), never returns.
Root cause (rv32 disassembly of f)
bnez t1, .divok
ebreak ; divisor == 0 -> trap (correct for both div_s and rem_s)
.divok:
lui t3, 0x80000 ; t3 = INT_MIN
bne t0, t3, .ok
li t4, -1
bne t1, t4, .ok
ebreak ; INT_MIN / -1 -> trap <-- WRONG for rem_s: irem_s(INT_MIN,-1)=0, no trap
.ok:
rem ...
The INT_MIN, -1 -> ebreak overflow guard is correct for div_s (idiv_s traps) but must NOT be present for rem_s (irem_s returns 0). The rv32 selector shares the div overflow guard with the rem path. RISC-V hardware rem already returns the correct INT_MIN % -1 = 0 without any guard, so the fix is simply to omit the INT_MIN/-1 ebreak for rem_s (keep the divide-by-zero ebreak).
Thumb-2 i32.rem_s(INT_MIN,-1) correctly returns 0; rv32 i64.rem_s is unaffected — this is the rv32 i32 rem_s path only. Distinct from the closed #317 (rv32 div_s/rem_s sign computation).
Found via the pulseengine challenge harness (rv32 disasm + wasmtime oracle; qemu-system-riscv32 execution).
Summary
On the RISC-V rv32 backend,
i32.rem_s(INT_MIN, -1)spuriously traps. WASM Core §4.3.2 (irem_s) defines the remainder as 0 in this case — it must NOT trap (onlydiv_straps on the INT_MIN/-1 overflow). The rv32 selector copies thediv_sINT_MIN/-1 overflowebreakguard into therem_slowering, sorem_s(INT_MIN,-1)faults instead of returning 0. The Thumb-2 backend is correct here; this is rv32-selector-specific.0.34.0,-b riscv -t rv32imacReproducer
wasmtime --invoke f -2147483648 -1→ 0 (correct;rem_soverflow result is 0, no trap).ebreak), never returns.Root cause (rv32 disassembly of
f)The
INT_MIN, -1 -> ebreakoverflow guard is correct fordiv_s(idiv_s traps) but must NOT be present forrem_s(irem_s returns 0). The rv32 selector shares the div overflow guard with the rem path. RISC-V hardwareremalready returns the correctINT_MIN % -1 = 0without any guard, so the fix is simply to omit the INT_MIN/-1ebreakforrem_s(keep the divide-by-zeroebreak).Thumb-2
i32.rem_s(INT_MIN,-1)correctly returns 0; rv32i64.rem_sis unaffected — this is the rv32 i32rem_spath only. Distinct from the closed #317 (rv32 div_s/rem_s sign computation).Found via the pulseengine challenge harness (rv32 disasm + wasmtime oracle; qemu-system-riscv32 execution).