v0.11.26 RV32 regression: i32.div_s overflow guard clobbers the dividend register → wrong result
Severity: silent miscompile (wrong value, no error). Introduced by #231 (caller-saved-preference allocator), which merged + shipped in v0.11.26 before I could flag it. I left the detail on #231 post-merge (comment) but it shipped, so raising it here for visibility.
Repro
int32_t filter_axis_decide(int32_t prev, int32_t gyro, int32_t accel) {
int32_t gyro_term = prev + gyro;
return (gyro_term * 980 + accel * 20) / 1000; // signed div by constant
}
clang --target=wasm32 -O2 → wasm-ld → loom optimize --passes inline → synth compile -b riscv -t rv32imac --relocatable.
filter_axis_decide(1000, 100, 500) returns 0, expected 1088. Reproducible 3/3 on qemu_riscv32. The loom.wasm is correct (wasmtime --invoke → 1088), so it's RV32 codegen. ARM (--target cortex-m4f) is unaffected (only the RV32 pool changed in #231).
Root cause (v0.11.26 disasm)
20: add t0,t0,t1 # t0 = numerator (live, needed by the div)
24: li t1,1000 # divisor
28: bnez t1,30 # divisor != 0 -> ok
30: lui t0,0x80000 # INT_MIN const into t0 <-- CLOBBERS the numerator
34: bne t0,t0,44 # t0==t0 (always false) -- was meant to test numerator==INT_MIN
38: li t0,-1 # -1 const into t0 <-- clobbers again
3c: bne t1,t0,44 # 1000 != -1 -> taken
44: div t0,t0,t1 # t0 = (-1)/1000 = 0 <-- dividend gone
The signed-division numerator==INT_MIN && divisor==-1 overflow guard materializes its INT_MIN and −1 comparison constants into t0 — the register holding the live numerator. v0.11.25 kept the numerator in a callee-saved reg (s2) clear of the guard temps and was correct.
Diagnosis
The dividend's live range extends across the overflow-guard basic blocks to the div, but the new lowest-free allocator treats t0 as free after the add and reassigns it to the guard constants. Same class as #226 (liveness across a branch region), resurfaced by the lowest-free allocation order. The 5 differential fixtures stayed green because none is a signed-div-by-constant leaf — worth adding one as a fixture.
What would help
A fix that keeps the dividend live until the div (or sinks the guard-constant materialization into the taken edges, not the fall-through that reaches the div). I have all four wasm-cross-LTO leaves wired on qemu_riscv32 and will re-measure the instant a fix lands — the rest of #231 is a real win (filter/control/controller all drop to 0 callee-saved spills; control_step icount 141→129, controller_step 114→100), so this is the one thing standing between v0.11.26 and a clean RV32 leaf-codegen release.
v0.11.26 RV32 regression:
i32.div_soverflow guard clobbers the dividend register → wrong resultSeverity: silent miscompile (wrong value, no error). Introduced by #231 (caller-saved-preference allocator), which merged + shipped in v0.11.26 before I could flag it. I left the detail on #231 post-merge (comment) but it shipped, so raising it here for visibility.
Repro
clang --target=wasm32 -O2→wasm-ld→loom optimize --passes inline→synth compile -b riscv -t rv32imac --relocatable.filter_axis_decide(1000, 100, 500)returns 0, expected 1088. Reproducible 3/3 on qemu_riscv32. The loom.wasm is correct (wasmtime --invoke→ 1088), so it's RV32 codegen. ARM (--target cortex-m4f) is unaffected (only the RV32 pool changed in #231).Root cause (v0.11.26 disasm)
The signed-division
numerator==INT_MIN && divisor==-1overflow guard materializes its INT_MIN and −1 comparison constants into t0 — the register holding the live numerator. v0.11.25 kept the numerator in a callee-saved reg (s2) clear of the guard temps and was correct.Diagnosis
The dividend's live range extends across the overflow-guard basic blocks to the
div, but the new lowest-free allocator treats t0 as free after theaddand reassigns it to the guard constants. Same class as #226 (liveness across a branch region), resurfaced by the lowest-free allocation order. The 5 differential fixtures stayed green because none is a signed-div-by-constant leaf — worth adding one as a fixture.What would help
A fix that keeps the dividend live until the
div(or sinks the guard-constant materialization into the taken edges, not the fall-through that reaches the div). I have all four wasm-cross-LTO leaves wired on qemu_riscv32 and will re-measure the instant a fix lands — the rest of #231 is a real win (filter/control/controller all drop to 0 callee-saved spills; control_step icount 141→129, controller_step 114→100), so this is the one thing standing between v0.11.26 and a clean RV32 leaf-codegen release.