You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v0.11.23 RISC-V: regalloc clobbers a live value (reuses t-register mid-live-range) — controller_step miscompiles (updates byte lost) #226
v0.11.23 RISC-V: register allocator clobbers a live value (reuses a t-register still in use) — controller_step_decide miscompiles
With #223's Select lowering in, controller_step_decide now compiles for RV32 but produces the wrong result on qemu_riscv32: got 0x00CE0001, expected 0x05CE7F9C (native gcc in the same harness is correct; ARM + wasmtime are correct). Decode: rudder byte correct (0xCE), but aileron/elevator and the updates byte are wrong.
Root cause — live-range violation in the RV32 register allocator. Disasm of controller_step_decide:
24: sll t2, a6, 24 ; t2 = updates << 24 <-- LIVE until the final pack at 0x9c
...
5c: slt t2, t1, t0 ; t2 = (aileron clamp compare) <-- CLOBBERS t2 while updates<<24 still needed
...
9c: or s5, t2, s4 ; pack: uses t2 expecting updates<<24, but t2 now holds the slt boolean (0/1)
t2 is assigned updates<<24 at 0x24 and is live until the packing or at 0x9c, but the allocator reuses t2 as scratch for the aileron-clamp slt at 0x5c. So the final or reads the comparison boolean instead of updates<<24 → the updates byte is lost and the pack is corrupted. (Same live-range/regalloc class as the ARM register issues — the RV32 selector's temp allocator isn't honoring the live range of an earlier-computed value.)
Scope:filter_axis is fine (no select, short live ranges). controller_step has three select-clamps + the updates<<24 value that stays live across all of them — that long live range across the select sequences is what gets clobbered. control_step (your unicorn diff says it's correct) packs only spark<<16|fuel with shorter live ranges, so it likely dodges this — but the allocator bug is general.
Fix: the RV32 temp/register allocator must not reuse a register that still holds a live value (track liveness / last-use, like the ARM backend).
Repro: controller.loom.wasm (loom-dissolved value-in controller), synth compile -b riscv -t rv32imac, run controller_step_decide(6400,0,-12800,0,3200,0,5) → should be 0x05CE7F9C. Env: synth v0.11.23, qemu-system-riscv32 virt. Happy to validate the fix (and I'll keep feeding RV32 functions — this is the regalloc analogue of the ARM #209 Opt-3 work, now a correctness bug on RISC-V).
Fixed in v0.11.25 (PR #228, just opened). Root cause confirmed exactly as you diagnosed: the RV32 alloc_temp recycled its 13-register pool round-robin with no awareness of the operand stack, so once it wrapped past the pool length it re-issued a register still pinning a live value — updates<<24 got reused as comparison scratch and the updates byte was lost.
Fix: alloc_temp now skips any register pinned by a live vstack entry (live_regs()); if every temp is pinned it flags exhaustion and the function is skipped (honest skip-and-continue) rather than miscompiled. This is the RISC-V analogue of the ARM #193 live-value reservation, as you noted.
Bundled a differential repro (controller_step_riscv_differential.py, a loom-dissolved-style controller that keeps updates<<24 live across three select-clamps). Before the fix all 5 vectors mismatched (your (6400,0,-12800,0,3200,0,5) → 0x000000ff, dropping the updates byte); after, all 5 match wasmtime with callee-saved regs preserved. control_step + filter_axis RV32 unaffected.
Please keep feeding RV32 functions — agreed this is the regalloc analogue of the ARM #209/Opt-3 work, now landed as a correctness fix. Next known RV32 gaps on the radar: i64 locals (2 slots) and non-leaf calls (ra + caller-saved-across-call, the ARM #188 equivalent).
Activity
avrabe commentedon Jun 3, 2026
Fixed in v0.11.25 (PR #228, just opened). Root cause confirmed exactly as you diagnosed: the RV32
alloc_temprecycled its 13-register pool round-robin with no awareness of the operand stack, so once it wrapped past the pool length it re-issued a register still pinning a live value —updates<<24got reused as comparison scratch and the updates byte was lost.Fix:
alloc_tempnow skips any register pinned by a livevstackentry (live_regs()); if every temp is pinned it flags exhaustion and the function is skipped (honest skip-and-continue) rather than miscompiled. This is the RISC-V analogue of the ARM #193 live-value reservation, as you noted.Bundled a differential repro (
controller_step_riscv_differential.py, a loom-dissolved-style controller that keepsupdates<<24live across three select-clamps). Before the fix all 5 vectors mismatched (your(6400,0,-12800,0,3200,0,5)→0x000000ff, dropping the updates byte); after, all 5 match wasmtime with callee-saved regs preserved.control_step+filter_axisRV32 unaffected.Please keep feeding RV32 functions — agreed this is the regalloc analogue of the ARM #209/Opt-3 work, now landed as a correctness fix. Next known RV32 gaps on the radar: i64 locals (2 slots) and non-leaf calls (ra + caller-saved-across-call, the ARM #188 equivalent).
fix(riscv): allocator must not reuse a register holding a live vstack…
fix(riscv): allocator must not reuse a register holding a live vstack…
cleanup(riscv): allocator prefers caller-saved via lowest-free, not r…
fix(riscv): signed-div overflow guard must not clobber the dividend (#…
docs(rivet): plan verified-codegen infrastructure as a phased program…
feat(synth-synthesis): precolouring — reserve registers in the alloca…
fix(riscv): i64 locals + per-op alloc pin scope — packed-u64 decide o…
test(vcr-oracle): extend frozen-codegen byte gate to RV32 (VCR-ORACLE…
docs(vcr-ra): local-promotion scoping spike — cost + de-risk the pari…
docs(vcr-ra): local-promotion scoping spike — cost + de-risk the pari…
docs(vcr-ra): local-promotion scoping spike — cost + de-risk the pari…
fix(selector): param liveness extends across loop back-edges — loop b…
fix(#752): RISC-V twin — software guard compares addr against the com…
fix(#752): wraparound-safe software bounds guard — the ADD-computed e…
feat(#890): wire 63 forgotten oracles + CI-wire the gate itself (step…
feat(#890): wire 63 forgotten oracles + CI-wire the gate itself (step…
fix(#890): oracle-wiring gate — 69 unwired oracles → 145 wired / 7 ma…