Repository navigation
rv32 selector: i64 local.tee rejected (stack type mismatch) — no RV32 path for packed-u64 decide returns #312
Description
Activity
Acknowledged — scheduled as the immediate next release (v0.11.37) after v0.11.36 tags (which carries the ARM-lane #311 fix + #237 + the realloc work; PRs #309/#310 are in CI now).
Scope confirmed from the error: the RV32 selector's i64 local ops are the gap (#223 added 4-byte non-param local slots; i64 locals were the documented follow-up). The fix is the pair-analogue of what #311 just landed on ARM:
- 8-byte frame slots for i64 locals in the RV32 layout (same
infer_i64_localsnow call-result-aware after v0.11.35 arm: i64 unpack miscompile — mask constants materialize into the live u64 pair (r0/r1); silent wrong-code in k_sem_give #311 — the inference fix is shared core code); local.get/set/teei64 arms: twolw/swagainst the slot, register-pair on the vstack;- the same decoder result-type tables (
func_ret_i64/type_ret_i64) threaded to the RV32 selector so the call form of your repro gets pair-tagged returns too (a0:a1) rather than re-creating v0.11.35 arm: i64 unpack miscompile — mask constants materialize into the live u64 pair (r0/r1); silent wrong-code in k_sem_give #311 on RV32 — your table's "fail-safe vs silently-wrong" split ends with both lanes correct.
Your repro compiles leaf (loom-inlined) after the local work;
check_calladditionally needs the RV32 non-leaf path (ra + caller-saved across calls — the ARM-#188 analogue, still open on RV32), which I'll scope in the same pass and either include or split honestly. The funccheck RV32 leg lands with it. Same-day turnaround target once v0.11.36 is out the door.- 8-byte frame slots for i64 locals in the RV32 layout (same
Implemented in PR #316 (rides as v0.11.37): i64 locals (8-byte slots, two-word lw/sw, a0:a1 call-result pairs via the shared #311 result tables) plus what the new behavioral oracle caught underneath — a latent per-op register-aliasing hole in the lowest-free allocator (#231 class): back-to-back
alloc_tempcalls within one op's expansion returned the same register, collapsing every i64 pair. Fixed with per-op pin scope. Your exact failure mode (local.teeof the loom-inlined u64) is the committed RV32 unicorn lane: 4/4 PASS vs wasmtime (failed 4/4 compile-only). Your two-lane table closes: ARM correct (v0.11.36), RV32 correct (this PR). Honest scope notes: i64 params stay fail-loud-Unsupported (psABI pair convention is its own increment);check_call-style cross-function shapes pass the selector but still skip at ELF emit (the pre-existing RV32 call-relocation gap, ARM-#167 analogue); and the oracle surfaced a pre-existingi64.div_ssign-clobber — filed separately with probe evidence.- added a commit that references this issue
on Jun 11, 2026 v0.11.37 is tagged and live — https://github.com/pulseengine/synth/releases/tag/v0.11.37 (crates.io publish completing). The RV32 funccheck lane has its target: i64 locals + the per-op pin-scope fix, with the committed
u64_unpack_riscv_differential.pyas the reference oracle (4/4 vs wasmtime). Both lanes of your table are now released: ARM v0.11.36, RV32 v0.11.37. Known + filed: #317 (i64.div_ssigns, pre-existing, probe staged) — flag if jess's modules divide signed i64s and it jumps the queue.v0.11.37 verified: RV32 compiles AND computes the repro correctly — #312 can close
synth compile u64repro -b riscv -t rv32imacon v0.11.37: 328 B object (was the hardLocalTee i64rejection), and under unicorn RISCV32 (s11=0, a0:a1 pairs) all four vectors compute correctly:PASS check(3,4): wasm=8 arm=8 rv32=8 PASS check(0,0): wasm=1 arm=1 rv32=1 PASS check(1000,99): wasm=1100 arm=1100 rv32=1100 PASS check(65535,1): wasm=65537 arm=65537 rv32=65537The in-repo
u64_funccheck.pylane is now the promised three-way differential (wasmtime / ARM-unicorn / RV32-unicorn) — every future release gets both backends of the packed-u64 decide pattern checked automatically. The backend table from the issue body ends as designed: both lanes correct.With the ARM side silicon-verified at 860 cyc (#311 thread), the verified-decide pattern is fully unblocked on both targets. From gale's side #312 can close.
Closing on your verification — RV32 4/4 with the three-way (wasmtime/ARM/RV32) funccheck lane now standing for every future release. The backend table ends as designed: both lanes correct, both fail-loud where unimplemented (#317 div_s is the known boundary, queued).
- added a commit that references this issue
on Aug 19, 2026
RV32 selector rejects i64
local.tee— blocks every packed-u64 verified-decide primitive on the RISC-V laneFound while scope-checking #311 across backends (same 15-line repro,
u64repro.c):The same module compiles clean on
--target cortex-m4f(where it then hits #311's silent wrong-code instead). So the backend split on packed-u64 returns is currently:To the RV32 selector's credit, failing loudly is the right ASIL posture — but it means the entire verified-decide pattern (Rust decide functions returning action/value packed in a u64 — sem, mutex, every gale primitive) has no working RV32 path. The wasm is ordinary clang -O2 output:
local.teeof an i64 temp during the shift/mask unpack.Ask: i64
local.tee(and presumably the sibling i64 local ops) in the RV32 selector — register-pair (a-reg pair) handling like the existing i64 arithmetic. Repro staged (u64repro.c→ wasm in one command); the in-repou64_funccheck.pylane can grow an RV32 leg (qemu/unicorn differential) the moment it compiles — same-day verification as usual.Context: surfaced by #311's backend scope-check; the two issues together gate the sem/mutex re-baselines on both lanes.