Context
`OptimizerBridge::wasm_to_ir` (`crates/synth-synthesis/src/optimizer_bridge.rs`) uses a single `inst_id` counter for two distinct concepts:
- Unique IR instruction id (every Instruction has a unique `id`).
- 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:
- `[I32DivS]` — fixed by adding pre-flight underflow check.
- `[Unreachable, I32GeS]` — pre-flight Unreachable was Bail.
- `[Return, I64Eqz, I32Const(0)]` — pre-flight Return was Bail.
- `[LocalGet(0), Nop, I64ExtendI32U]` — wasm_to_ir Nop consumed slot.
- `[LocalGet(0), Unreachable, I64ExtendI32U]` — wasm_to_ir Unreachable consumed slot.
- `[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.
Context
`OptimizerBridge::wasm_to_ir` (`crates/synth-synthesis/src/optimizer_bridge.rs`) uses a single `inst_id` counter for two distinct concepts:
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:
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:
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
gating: false # demoted pending issue #NNN — wasm_to_ir slot model
```
This will be reverted to `gating: true` once this issue lands.
References