Skip to content

fix(opt): wasm_to_ir missing result vreg for WasmOp::Call#109

Merged
avrabe merged 1 commit into
mainfrom
fix/wasm-to-ir-call-result-vreg
May 14, 2026
Merged

fix(opt): wasm_to_ir missing result vreg for WasmOp::Call#109
avrabe merged 1 commit into
mainfrom
fix/wasm-to-ir-call-result-vreg

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 13, 2026

Closes the last latent unmapped-vreg gap surfaced by the systematic AAPCS audit (PR #108) when compiling examples/wat/simple_add.wat — the recursive fib function caused synth compile --cortex-m to panic on vreg v13 (the second Call result, consumed by the outer i32.add).

Root cause

optimizer_bridge::wasm_to_ir's match on WasmOp had no arm for WasmOp::Call(idx), so it fell through to Opcode::Nop. The IR therefore had no record that inst_id N defined a vreg; any downstream consumer (here, the outer i32.add) resolved its src via get_arm_reg → unmapped → defensive panic (PR #101) or, under the silent R0 fallback that's in main today, silently consumed whatever value happened to be in R0.

Fix

  • New Opcode::Call { dest, func_idx } in synth-opt
  • WasmOp::Call → Opcode::Call in wasm_to_ir
  • Opcode::Call → ArmOp::Bl { label: "func_<idx>" } in ir_to_arm, with dest → R0 registered in vreg_to_arm (AAPCS: R0 holds the return value)

Scope (narrow)

This is the "compile cleanly, don't panic on lawful WASM containing call" fix. The IR still does NOT:

  • Model call-clobber-R0..R3 invalidation of surviving vregs across the BL
  • Pop arg slots from the value-tracking stack

Both are deferred to a broader call-boundary regalloc rework.

Tests

  • fib_compiles_through_optimized_path — full fib WAT through optimize_full + ir_to_arm; asserts two BL func_1 are emitted plus the outer i32.add lowering
  • call_emits_bl_in_optimized_path — minimal standalone WasmOp::Call → BL func_5 round-trip

A third end-to-end test through ArmBackend was removed during local review because synth-synthesis sits below synth-backend in the dep graph (would close a cycle). The existing synth-cli/tests/wast_compile.rs test suite already exercises the full pipeline.

Verification

  • cargo test --workspace — all pass
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo fmt --check — clean
  • synth compile examples/wat/simple_add.wat --cortex-m -o /tmp/sa.elf — succeeds (no panic)

Unblocks

PR #101 (defensive panic) — once this lands, the defensive panic can ship as a permanent guard against the wasm_to_ir-gap class.

🤖 Generated with Claude Code

Closes the last latent unmapped-vreg gap surfaced by the systematic AAPCS
audit (PR #108) when compiling examples/wat/simple_add.wat — the
recursive fib function caused `synth compile --cortex-m` to panic on
vreg v13 (the second `Call` result, consumed by the outer `i32.add`).

Root cause: `optimizer_bridge::wasm_to_ir`'s match on `WasmOp` had no
arm for `WasmOp::Call(idx)`, so it fell through to `Opcode::Nop`. The
IR therefore had no record that `inst_id N` defined a vreg; any
downstream consumer (here, the outer `i32.add`) resolved its `src`
via `get_arm_reg` -> unmapped -> defensive panic (PR #101) or, under
the silent R0 fallback that's in main today, silently consumed
whatever value happened to be in R0.

Fix: added `Opcode::Call { dest, func_idx }` to `synth-opt`, mapped
`WasmOp::Call -> Opcode::Call` in `wasm_to_ir`, and lowered it in
`ir_to_arm` to `BL func_<idx>` with `dest -> R0` registered in
`vreg_to_arm` (AAPCS: R0 holds the return value).

Scope note: this is the narrow "compile cleanly, don't panic" fix.
The IR still does NOT model call-clobber-R0..R3 invalidation of
surviving vregs across the BL, nor does it pop arg slots from the
value-tracking stack — both are deferred to a broader call-boundary
regalloc rework.

Two regression tests:
* `fib_compiles_through_optimized_path` — full fib WAT through
  optimize_full + ir_to_arm; asserts two `BL func_1` are emitted plus
  the outer `i32.add` lowering.
* `call_emits_bl_in_optimized_path` — minimal standalone `WasmOp::Call`
  -> `BL func_5` round-trip.

A third end-to-end test through `ArmBackend` was removed during review
because synth-synthesis sits below synth-backend in the dep graph
(would close a cycle). The existing synth-cli/tests/wast_compile.rs
test suite already exercises the full pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avrabe avrabe merged commit 1e6ef38 into main May 14, 2026
6 of 8 checks passed
@avrabe avrabe deleted the fix/wasm-to-ir-call-result-vreg branch May 14, 2026 04:50
avrabe added a commit that referenced this pull request May 14, 2026
…ack (#101)

With PR #109 (WasmOp::Call wasm_to_ir handler) merged, every known wasm_to_ir gap is closed. The silent R0 fallback in `get_arm_reg` is now safe to replace with a diagnostic panic — future 'wasm op silently dropped' bugs in the #93 class will surface at the boundary instead of producing miscompiled firmware.

A compiler crash with diagnostic context is strictly better than a hung MCU.
avrabe added a commit that referenced this pull request May 14, 2026
Two harnesses (i64_lowering_doesnt_clobber_params, encoder_no_panic)
keep finding new AAPCS-class bugs at a rate faster than we close them
in a single release cycle. Mark them as non-gating (continue-on-error)
so the harness infrastructure can ship as part of #100 while the
bug-hunting continues in v0.3.x patches.

Promotion criteria for moving a harness from exploration → gating: 60s
smoke runs report zero findings on main for 2 consecutive weeks.

Open bugs tracked: #103 (fixed via #106), #104 (fixed via #107), the
class-wide sweep #108, the WasmOp::Call gap #109/#101, the I32WrapI64
preassign #111, and the new #112 (i64-extend chain + Movw R0).
avrabe added a commit that referenced this pull request May 15, 2026
Closes #82.

Adds a workspace-excluded `fuzz/` crate with 4 harnesses + CI smoke gate:

* `wasm_ops_lower_or_error` — fuzz arbitrary Vec<WasmOp> through both optimized and no-optimize paths; assert no panic / no unencodable instruction. (gating)
* `wasm_to_ir_roundtrip_op_coverage` — value-producing wasm ops must produce live IR (catches the #93-class silent-drop bug). (gating)
* `i64_lowering_doesnt_clobber_params` — random i64/i32-param mixes; assert no ARM instr writes R{p} before LocalGet(p), with LocalSet(p)/LocalTee(p) carved out as wasm-intended. (exploration)
* `encoder_no_panic` — random ArmOp values; assert ArmEncoder doesn't panic. (exploration)

CI matrix splits gating from exploration: gating harnesses block merge, exploration harnesses upload crash artifacts but use `continue-on-error: true` until their bug-lists stabilise (promotion criterion documented in fuzz-smoke.yml).

This harness suite caught the bugs subsequently fixed in #103/#104/#108/#109/#111 and surfaced #112 for v0.3.x follow-up. The infrastructure value is real even with exploration harnesses non-gating today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant