Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .github/workflows/fuzz-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ jobs:
# error` is taken from the `gating` flag. Promote an exploration
# harness to gating once its bug-list stabilises.
include:
# `wasm_ops_lower_or_error` temporarily demoted from gating per
# issue #121 — wasm_to_ir's inst_id-as-vreg-slot model breaks for
# Drop / LocalSet / Store / Block / etc. The five rounds of fixes
# in PR #117 close the Nop/Unreachable/Return slot crashes; the
# remaining Drop-class shapes need the proper slot_stack refactor
# tracked in #121. Will re-promote to `gating: true` when that
# lands.
# `wasm_ops_lower_or_error` is gating again. It was demoted in
# PR #117 because wasm_to_ir's inst_id-as-vreg-slot model crashed
# on Drop / LocalSet / Store / Block shapes; the slot_stack
# refactor (issue #121) closed those. The last panic site in the
# optimized path was `ir_to_arm`'s defensive `get_arm_reg`, which
# now returns `Result` instead of `panic!`-ing — the whole
# `optimize_full` + `ir_to_arm` path is panic-free, so this
# harness (contract: "lower or Err, never panic") gates again.
- target: wasm_ops_lower_or_error
gating: false # demoted pending issue #121
gating: true
- target: wasm_to_ir_roundtrip_op_coverage
gating: true
- target: i64_lowering_doesnt_clobber_params
Expand Down
26 changes: 15 additions & 11 deletions crates/synth-backend/src/arm_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,21 @@ fn compile_wasm_to_arm(
};

let bridge = OptimizerBridge::with_config(opt_config);
match bridge.optimize_full(wasm_ops) {
Ok((opt_ir, _cfg, _stats)) => {
let arm_ops = bridge.ir_to_arm(&opt_ir, num_params as usize);
arm_ops
.into_iter()
.map(|op| ArmInstruction {
op,
source_line: None,
})
.collect()
}
// `ir_to_arm` now returns `Result` — an `Err` means the optimized path
// hit an unmapped vreg (issue-#93-class). Treat it identically to an
// `optimize_full` failure: fall back to the direct selector rather
// than propagating, so the function still compiles correctly.
match bridge
.optimize_full(wasm_ops)
.and_then(|(opt_ir, _cfg, _stats)| bridge.ir_to_arm(&opt_ir, num_params as usize))
{
Ok(arm_ops) => arm_ops
.into_iter()
.map(|op| ArmInstruction {
op,
source_line: None,
})
.collect(),
// Issue #120: the optimized path declines modules it cannot lower
// (notably scalar f32/f64 ops — the IR has no float opcodes). Fall
// back to the direct instruction selector, which handles f32 via
Expand Down
447 changes: 234 additions & 213 deletions crates/synth-synthesis/src/optimizer_bridge.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/synth-synthesis/tests/audit_optimized_aapcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use synth_synthesis::{ArmOp, OptimizerBridge, Reg, WasmOp};
fn compile_optimized(wasm_ops: &[WasmOp], num_params: usize) -> Vec<ArmOp> {
let bridge = OptimizerBridge::new();
let (ir, _cfg, _stats) = bridge.optimize_full(wasm_ops).expect("optimize_full");
bridge.ir_to_arm(&ir, num_params)
bridge.ir_to_arm(&ir, num_params).expect("ir_to_arm")
}

fn writes(op: &ArmOp) -> Vec<Reg> {
Expand Down
4 changes: 3 additions & 1 deletion crates/synth-synthesis/tests/issue_104_i32_loadstore_cse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ fn compile_optimized(wasm: &[WasmOp]) -> Vec<ArmOp> {
let (ir, _cfg, _stats) = bridge
.optimize_full(wasm)
.expect("optimize_full should succeed for the store-load pattern");
bridge.ir_to_arm(&ir, /* num_params = */ 2)
bridge
.ir_to_arm(&ir, /* num_params = */ 2)
.expect("ir_to_arm should succeed for the store-load pattern")
}

/// Execute the swapped-operand variant: param 1 = addr (in R1),
Expand Down
4 changes: 3 additions & 1 deletion crates/synth-synthesis/tests/issue_93_memset_i64_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ fn compile_optimized(wasm_ops: &[WasmOp], num_params: usize) -> Vec<ArmOp> {
let (ir, _cfg, _stats) = bridge
.optimize_full(wasm_ops)
.expect("optimize_full should succeed for valid input");
bridge.ir_to_arm(&ir, num_params)
bridge
.ir_to_arm(&ir, num_params)
.expect("ir_to_arm should succeed for valid input")
}

/// Returns true if the ARM op is one of the 64-bit shift pseudo-ops (the
Expand Down
4 changes: 3 additions & 1 deletion crates/synth-synthesis/tests/regression_call_result_vreg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ fn compile_optimized(wasm_ops: &[WasmOp], num_params: usize) -> Vec<ArmOp> {
let (ir, _cfg, _stats) = bridge
.optimize_full(wasm_ops)
.expect("optimize_full should succeed for valid input");
bridge.ir_to_arm(&ir, num_params)
bridge
.ir_to_arm(&ir, num_params)
.expect("ir_to_arm should succeed for valid input")
}

/// The exact wasm-op sequence emitted by `wat2wasm` for the `fib` function
Expand Down
143 changes: 143 additions & 0 deletions crates/synth-synthesis/tests/regression_ir_to_arm_panic_free.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! Regression: `OptimizerBridge::ir_to_arm` must never panic.
//!
//! `ir_to_arm` hosts the `get_arm_reg` closure that maps an IR virtual
//! register to an ARM register. PR #101 made an unmapped vreg a hard
//! `panic!` ("synth internal compiler error: vreg vN has no assigned ARM
//! register ...") — correct at the time (a loud crash beats a silent
//! miscompilation), but it meant the optimized lowering path
//! (`optimize_full` -> `ir_to_arm`) could panic on malformed input.
//!
//! That panic was the last panic site in the optimized path and the reason
//! the `wasm_ops_lower_or_error` fuzz harness sat at `gating: false`. The
//! fix converts `get_arm_reg`'s `panic!` into `Err(Error::synthesis(...))`
//! with the same diagnostic text, and `ir_to_arm` now returns
//! `Result<Vec<ArmOp>>`. The defensive check is preserved — a genuine
//! `wasm_to_ir` bug still surfaces as a rich `Err`, it just no longer kills
//! the process.
//!
//! Contract enforced here: feeding a malformed `Vec<WasmOp>` through
//! `optimize_full` + `ir_to_arm` yields `Ok` or `Err`, never a panic.

use synth_core::WasmOp;
use synth_synthesis::OptimizerBridge;

/// Drive the full optimized pipeline. The `optimize_full` pre-flight
/// (`wasm_stack_check`) may reject the input as `Err`; if it accepts,
/// `ir_to_arm` runs and itself returns `Result`. Neither stage may panic.
fn lower_optimized(wasm_ops: &[WasmOp], num_params: usize) {
let bridge = OptimizerBridge::new();
if let Ok((instructions, _cfg, _stats)) = bridge.optimize_full(wasm_ops) {
// The point of the test: this returns `Ok` or `Err`, never panics.
// `ir_to_arm` is the function that previously hosted the
// process-killing `get_arm_reg` panic.
match bridge.ir_to_arm(&instructions, num_params) {
Ok(_arm) => {}
Err(_e) => {}
}
}
}

/// The motivating shape: `[I32Const, LocalGet, I32Const, I32Add, I32ShrS,
/// I32Const]`. A mixed arithmetic / shift sequence with a trailing constant
/// that historically could leave a vreg unmapped by the time `ir_to_arm`
/// runs.
#[test]
fn mixed_i32_arith_shift_sequence_does_not_panic() {
let wasm_ops = vec![
WasmOp::I32Const(7),
WasmOp::LocalGet(0),
WasmOp::I32Const(3),
WasmOp::I32Add,
WasmOp::I32ShrS,
WasmOp::I32Const(1),
];
lower_optimized(&wasm_ops, 4);
}

/// Type-mismatched i64 sequence: an i64 op consuming i32-shaped stack
/// slots. The kind of shape that, pre-fix, could produce IR referencing a
/// vreg `wasm_to_ir` never mapped, tripping `get_arm_reg`'s panic.
#[test]
fn type_mismatched_i64_sequence_does_not_panic() {
let wasm_ops = vec![
WasmOp::I32Const(1),
WasmOp::I32Const(2),
WasmOp::I64Add,
WasmOp::I64ShrU,
];
lower_optimized(&wasm_ops, 4);
}

/// An i64 extend feeding an i64 shift — issue-#93's exact class. With the
/// extend handled in `wasm_to_ir` this lowers cleanly, but the harness
/// contract holds regardless: `Ok` or `Err`, never a panic.
#[test]
fn i64_extend_into_shift_does_not_panic() {
let wasm_ops = vec![
WasmOp::LocalGet(0),
WasmOp::I64ExtendI32U,
WasmOp::I32Const(32),
WasmOp::I64ShrU,
];
lower_optimized(&wasm_ops, 4);
}

/// Confirms the new `Result` signature on a normal, well-formed program:
/// `ir_to_arm` returns `Ok` with a non-empty instruction stream. This is
/// the happy-path guard — if the `Result` conversion broke ordinary
/// lowering, this fails.
#[test]
fn well_formed_program_lowers_to_ok() {
let wasm_ops = vec![WasmOp::LocalGet(0), WasmOp::LocalGet(1), WasmOp::I32Add];

let bridge = OptimizerBridge::new();
let (instructions, _cfg, _stats) = bridge
.optimize_full(&wasm_ops)
.expect("well-formed program optimizes");
let arm = bridge
.ir_to_arm(&instructions, 2)
.expect("well-formed program lowers to ARM");
assert!(
!arm.is_empty(),
"a non-trivial program should emit at least one ARM op"
);
}

/// Exercise `ir_to_arm` directly with a hand-built IR instruction whose
/// source vreg has no producer — the exact unmapped-vreg condition the
/// `get_arm_reg` check guards. Pre-fix this panicked; post-fix it must be
/// a typed `Err` carrying the issue-#93 diagnostic.
#[test]
fn unmapped_vreg_yields_err_not_panic() {
use synth_opt::{Instruction, Opcode, Reg as OptReg};

let bridge = OptimizerBridge::new();
// A single i32 add whose operands (v100, v101) were never produced by
// any prior instruction — so `vreg_to_arm` has no mapping and there is
// no spill slot. This is precisely the state a `wasm_to_ir` gap leaves
// behind, and the state `get_arm_reg`'s defensive check exists to
// catch.
let instrs = vec![Instruction {
id: 0,
opcode: Opcode::Add {
dest: OptReg(0),
src1: OptReg(100),
src2: OptReg(101),
},
block_id: 0,
is_dead: false,
}];

// The contract: a typed `Err`, never a panic. v100 / v101 have no
// producer and no spill slot, so `get_arm_reg` must take its defensive
// branch — pre-fix that was a `panic!`, post-fix a recoverable `Err`.
let result = bridge.ir_to_arm(&instrs, 0);
let err = result.expect_err("unmapped src vregs must yield Err, not Ok or panic");
let msg = err.to_string();
// The diagnostic must survive the panic -> Err conversion: it is still
// the issue-#93-class bug-finder, just no longer process-killing.
assert!(
msg.contains("no assigned") && msg.contains("issue #93"),
"unmapped-vreg error lost its diagnostic content: {msg}"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ fn try_optimized(wasm_ops: &[WasmOp], num_params: usize) -> Result<usize, String
match bridge.optimize_full(wasm_ops) {
Ok((ir, _cfg, _stats)) => {
// ir_to_arm is the function that hosts the defensive get_arm_reg
// panic. Exercising it here proves no unmapped vreg is reached.
let arm = bridge.ir_to_arm(&ir, num_params);
Ok(arm.len())
// check. It now returns `Result` instead of panicking — an `Err`
// is a clean decline, not a crash. Exercising it here proves no
// unmapped vreg ever reaches a process-killing panic.
bridge
.ir_to_arm(&ir, num_params)
.map(|arm| arm.len())
.map_err(|e| e.to_string())
}
Err(e) => Err(e.to_string()),
}
Expand Down
8 changes: 6 additions & 2 deletions fuzz/fuzz_targets/wasm_ops_lower_or_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ fuzz_target!(|input: FuzzInput| {
// -----------------------------------------------------------------
let bridge = OptimizerBridge::new();
if let Ok((instructions, _cfg, _stats)) = bridge.optimize_full(&wasm_ops) {
let arm_ops = bridge.ir_to_arm(&instructions, input.num_params.min(4) as usize);
encode_each_or_typed_error(&arm_ops);
// `ir_to_arm` returns `Result`: `Ok` on success, `Err` for the
// issue-#93-class unmapped-vreg condition. Either is contract-
// compliant — only a panic is a crash.
if let Ok(arm_ops) = bridge.ir_to_arm(&instructions, input.num_params.min(4) as usize) {
encode_each_or_typed_error(&arm_ops);
}
}

// -----------------------------------------------------------------
Expand Down
Loading