Skip to content

wasm_to_ir: inst_id overloaded as both IR-id and vreg-slot — decouple via explicit slot_stack #121

@avrabe

Description

@avrabe

Context

`OptimizerBridge::wasm_to_ir` (`crates/synth-synthesis/src/optimizer_bridge.rs`) uses a single `inst_id` counter for two distinct concepts:

  1. Unique IR instruction id (every Instruction has a unique `id`).
  2. Vreg-slot index — binary/unary ops look up operands via `OptReg(inst_id.saturating_sub(N))`.

When any wasm op consumes a slot WITHOUT producing a vreg (Drop, LocalSet, GlobalSet, Stores, Br, BrIf, BrTable, Block, Loop, End, plus the now-fixed Nop/Unreachable/Return), the slot-as-vreg-position model breaks: subsequent binary/unary ops' back-references land on unmapped slots and trigger the defensive panic at line 1670.

How PR #117 found this

The PR's gating fuzz harness `wasm_ops_lower_or_error` surfaced six progressively-shallower crash inputs to the same panic site:

  1. `[I32DivS]` — fixed by adding pre-flight underflow check.
  2. `[Unreachable, I32GeS]` — pre-flight Unreachable was Bail.
  3. `[Return, I64Eqz, I32Const(0)]` — pre-flight Return was Bail.
  4. `[LocalGet(0), Nop, I64ExtendI32U]` — wasm_to_ir Nop consumed slot.
  5. `[LocalGet(0), Unreachable, I64ExtendI32U]` — wasm_to_ir Unreachable consumed slot.
  6. `[LocalGet(1), LocalGet(1), Drop, I32Popcnt]` — wasm_to_ir Drop consumed slot.

Rounds 4-5 were fixed by adding `WasmOp::Nop | Unreachable | Return => continue` arms to wasm_to_ir. Round 6 surfaces a DIFFERENT structural class — Drop genuinely consumes a wasm value, so simply `continue`ing on Drop would cause silent miscompilation (back-references would jump over Drop and read the consumed producer).

Repro for round 6

```rust
use synth_core::WasmOp;
use synth_synthesis::OptimizerBridge;

let ops = vec![
WasmOp::LocalGet(1),
WasmOp::LocalGet(1),
WasmOp::Drop,
WasmOp::I32Popcnt,
];
let b = OptimizerBridge::new();
b.optimize_full(&ops).unwrap(); // panics inside ir_to_arm
```

Also affects the same shape with: LocalSet, GlobalSet, I32Store, I64Store family, Br, BrIf, BrTable, Block, Loop, End — any non-producer that the optimized path passes through. The non-optimized path (`select_with_stack`) is unaffected because it uses a real value stack.

Proposed fix

Decouple producer-slot tracking from inst_id. Add an explicit `slot_stack: Vec` parallel to inst_id:

  • Producers push the inst_id of their dest onto slot_stack.
  • Consumers pop from slot_stack.
  • Binary/unary ops use `slot_stack[len-N]` instead of `inst_id.saturating_sub(N)`.

Scope: every binary/unary/consumer/producer handler in wasm_to_ir (~30 ops). 136 `inst_id.saturating_sub` call sites would migrate to `slot_stack[...]`. Significant but mechanical.

Workaround in #117

The five fixes shipped in #117 close the panic class for Nop/Unreachable/Return. The harness still finds Drop-class crashes. To unblock #117's merge, the gating-vs-exploration matrix entry will be flipped temporarily:

```yaml

  • target: wasm_ops_lower_or_error
    gating: false # demoted pending issue #NNN — wasm_to_ir slot model
    ```

This will be reverted to `gating: true` once this issue lands.

References

  • Panic site: `crates/synth-synthesis/src/optimizer_bridge.rs:1670` (defensive panic in `get_arm_reg` closure inside `ir_to_arm`).
  • Slot computation example: line 472 (`I32DivS` handler uses `inst_id.saturating_sub(2)` / `saturating_sub(1)`).
  • LocalSet handler: line 646.
  • Drop has no explicit handler — falls through `_ => Opcode::Nop` at line 1380.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsilicon-affectedConfirmed broken on real hardware (Gale)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions