Skip to content

v0.11.26 RV32: i32.div_s overflow guard clobbers dividend register (signed div-by-const → wrong result) #232

Description

@avrabe

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 -O2wasm-ldloom optimize --passes inlinesynth 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions