use crate::solver::{CheckOutcome, new_solver};
use crate::term::{BV, Bool};
use crate::wasm_semantics::WasmSemantics;
use std::collections::HashMap;
use synth_core::WasmOp;
use synth_core::wsc_facts::{FactKind, WscFact};
#[derive(Debug)]
pub struct FactSpecResult {
pub ops: Vec<WasmOp>,
pub block_arity: Vec<(u8, u8)>,
pub kept: Vec<usize>,
pub admitted: Vec<String>,
pub declined: Vec<String>,
}
impl FactSpecResult {
pub fn changed(&self) -> bool {
!self.admitted.is_empty()
}
}
#[derive(Clone)]
struct Val {
bv: BV,
start: Option<usize>,
created: usize,
}
pub fn specialize_function(
func_name: &str,
ops: &[WasmOp],
block_arity: &[(u8, u8)],
facts: &[WscFact],
) -> FactSpecResult {
let mut pass = Pass::new(func_name, ops, block_arity, facts);
pass.walk();
pass.finish()
}
struct Pass<'a> {
func: &'a str,
ops: &'a [WasmOp],
block_arity: &'a [(u8, u8)],
opener_ordinal: HashMap<usize, usize>,
range_facts: HashMap<usize, (i32, i32)>,
sem: WasmSemantics,
stack: Vec<Val>,
locals: HashMap<u32, BV>,
vars: Vec<BV>,
fresh: u32,
premises: Vec<Bool>,
premise_desc: Vec<String>,
deletions: Vec<(usize, usize)>,
admitted: Vec<String>,
declined: Vec<String>,
}
impl<'a> Pass<'a> {
fn new(
func: &'a str,
ops: &'a [WasmOp],
block_arity: &'a [(u8, u8)],
facts: &'a [WscFact],
) -> Self {
let mut opener_ordinal = HashMap::new();
let mut ord = 0usize;
for (i, op) in ops.iter().enumerate() {
if matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If) {
opener_ordinal.insert(i, ord);
ord += 1;
}
}
let mut range_facts = HashMap::new();
for f in facts {
if let FactKind::ValueRange { lo, hi } = f.kind {
let lo = lo.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
let hi = hi.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
if lo <= hi && (f.value_id as usize) < ops.len() {
range_facts.insert(f.value_id as usize, (lo, hi));
}
}
}
Self {
func,
ops,
block_arity,
opener_ordinal,
range_facts,
sem: WasmSemantics::new_with_memory(Vec::new()),
stack: Vec::new(),
locals: HashMap::new(),
vars: Vec::new(),
fresh: 0,
premises: Vec::new(),
premise_desc: Vec::new(),
deletions: Vec::new(),
admitted: Vec::new(),
declined: Vec::new(),
}
}
fn fresh_var(&mut self, name: String) -> BV {
let v = BV::new_const(name, 32);
self.vars.push(v.clone());
v
}
fn local_bv(&mut self, idx: u32) -> BV {
if let Some(bv) = self.locals.get(&idx) {
return bv.clone();
}
let v = self.fresh_var(format!("fs_l{idx}"));
self.locals.insert(idx, v.clone());
v
}
fn attach_fact(&mut self, i: usize, bv: &BV) {
if let Some(&(lo, hi)) = self.range_facts.get(&i) {
let lo_bv = BV::from_i64(i64::from(lo), 32);
let hi_bv = BV::from_i64(i64::from(hi), 32);
let p = Bool::and(&[&bv.bvsge(&lo_bv), &bv.bvsle(&hi_bv)]);
self.premises.push(p);
self.premise_desc
.push(format!("value(op#{i}) ∈ [{lo}, {hi}] (signed)"));
}
}
fn push(&mut self, bv: BV, start: Option<usize>, created: usize) {
self.stack.push(Val { bv, start, created });
}
fn matching_end(&self, i: usize) -> Option<(usize, bool)> {
let mut depth = 0usize;
let mut has_else = false;
for (j, op) in self.ops.iter().enumerate().skip(i + 1) {
match op {
WasmOp::Block | WasmOp::Loop | WasmOp::If => depth += 1,
WasmOp::Else if depth == 0 => has_else = true,
WasmOp::End => {
if depth == 0 {
return Some((j, has_else));
}
depth -= 1;
}
_ => {}
}
}
None
}
fn havoc_region(&mut self, i: usize, end: usize, arity: (u8, u8)) {
let ops = self.ops;
for op in &ops[i + 1..end] {
if let WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) = op {
let n = self.fresh;
self.fresh += 1;
let v = self.fresh_var(format!("fs_h{n}"));
self.locals.insert(*idx, v);
}
}
for _ in 0..arity.0 {
self.stack.pop();
}
for k in 0..arity.1 {
let n = self.fresh;
self.fresh += 1;
let v = self.fresh_var(format!("fs_r{n}_{k}"));
self.push(v, None, end);
}
}
fn decline(&mut self, msg: String) {
self.declined
.push(format!("{}: {} — general lowering emitted", self.func, msg));
}
fn try_elide(&mut self, i: usize, end: usize, cond: &Val) -> bool {
if self.premises.is_empty() {
self.decline(format!(
"op#{i} `if` — no premise reaches this site (no usable value-range fact)"
));
return false;
}
let mut solver = new_solver();
for p in &self.premises {
solver.assert(p);
}
let taken = cond.bv.ne(BV::from_i64(0, 32));
solver.assert(&taken);
match solver.check() {
CheckOutcome::Unsat => {
let Some(start) = cond.start else {
self.decline(format!(
"op#{i} `if` proven dead (UNSAT) but its condition slice is not \
erasable (impure or non-contiguous producer)"
));
return false;
};
self.deletions.push((start, end));
self.admitted.push(format!(
"{}: op#{i} `if` (+condition slice) — ops [{start}..={end}] elided \
({} ops): UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; \
every Unsat carries an LRAT proof validated by ordeal-lrat); \
P = {{{}}}; cond = {}",
self.func,
end - start + 1,
solver.name(),
self.premise_desc.join(" ∧ "),
cond.bv,
));
true
}
CheckOutcome::Sat => {
let cex: Vec<String> = self
.vars
.iter()
.filter_map(|v| {
let name = format!("{v}");
solver
.value(v)
.map(|x| format!("{name}={}", x as u32 as i32))
})
.collect();
self.decline(format!(
"op#{i} `if` — obligation Sat (branch reachable under P; \
counterexample: {})",
if cex.is_empty() {
"<no model>".to_string()
} else {
cex.join(", ")
}
));
false
}
CheckOutcome::Unknown(reason) => {
self.decline(format!(
"op#{i} `if` — obligation Unknown ({reason}); conservative decline"
));
false
}
}
}
fn walk(&mut self) {
if self.range_facts.is_empty() {
self.decline("no usable value-range fact targets this function".to_string());
return;
}
let ops = self.ops;
let mut i = 0usize;
while i < ops.len() {
let op = &ops[i];
match op {
WasmOp::Nop => {}
WasmOp::I32Const(v) => {
let bv = self.sem.encode_op(&WasmOp::I32Const(*v), &[]);
self.attach_fact(i, &bv);
self.push(bv, Some(i), i);
}
WasmOp::LocalGet(idx) => {
let bv = self.local_bv(*idx);
self.attach_fact(i, &bv);
self.push(bv, Some(i), i);
}
WasmOp::LocalSet(idx) => {
let Some(v) = self.stack.pop() else {
self.decline(format!("op#{i} local.set on empty symbolic stack"));
return;
};
self.locals.insert(*idx, v.bv);
}
WasmOp::LocalTee(idx) => {
let Some(top) = self.stack.last_mut() else {
self.decline(format!("op#{i} local.tee on empty symbolic stack"));
return;
};
top.start = None;
top.created = i;
let bv = top.bv.clone();
self.locals.insert(*idx, bv.clone());
self.attach_fact(i, &bv);
}
WasmOp::Drop => {
if self.stack.pop().is_none() {
self.decline(format!("op#{i} drop on empty symbolic stack"));
return;
}
}
WasmOp::I32Eqz => {
let Some(a) = self.stack.pop() else {
self.decline(format!("op#{i} unary op on empty symbolic stack"));
return;
};
let bv = self.sem.encode_op(op, &[a.bv]);
self.attach_fact(i, &bv);
let start = a.start.filter(|_| a.created + 1 == i);
self.push(bv, start, i);
}
WasmOp::I32Add
| WasmOp::I32Sub
| WasmOp::I32Mul
| WasmOp::I32And
| WasmOp::I32Or
| WasmOp::I32Xor
| WasmOp::I32Shl
| WasmOp::I32ShrS
| WasmOp::I32ShrU
| WasmOp::I32Rotl
| WasmOp::I32Rotr
| WasmOp::I32Eq
| WasmOp::I32Ne
| WasmOp::I32LtS
| WasmOp::I32LtU
| WasmOp::I32LeS
| WasmOp::I32LeU
| WasmOp::I32GtS
| WasmOp::I32GtU
| WasmOp::I32GeS
| WasmOp::I32GeU => {
let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
self.decline(format!("op#{i} binop on underflowing symbolic stack"));
return;
};
let bv = self.sem.encode_op(op, &[a.bv, b.bv]);
self.attach_fact(i, &bv);
let start = match (a.start, b.start) {
(Some(sa), Some(sb)) if a.created + 1 == sb && b.created + 1 == i => {
Some(sa)
}
_ => None,
};
self.push(bv, start, i);
}
WasmOp::If => {
let Some(cond) = self.stack.pop() else {
self.decline(format!("op#{i} `if` on empty symbolic stack"));
return;
};
let Some((end, has_else)) = self.matching_end(i) else {
self.decline(format!("op#{i} `if` without matching `end`"));
return;
};
let Some(&ord) = self.opener_ordinal.get(&i) else {
self.decline(format!("op#{i} `if` missing from the opener ordinal map"));
return;
};
let Some(&arity) = self.block_arity.get(ord) else {
self.decline(format!(
"op#{i} `if` has no block_arity entry (side-table desync)"
));
return;
};
if has_else {
self.decline(format!(
"op#{i} `if`/`else` — only no-else `if` is in Phase-2 scope"
));
self.havoc_region(i, end, arity);
} else if self.try_elide(i, end, &cond) {
} else {
self.havoc_region(i, end, arity);
}
i = end + 1;
continue;
}
WasmOp::End => break,
WasmOp::Return => break,
other => {
self.decline(format!(
"op#{i} {other:?} is outside the tracked i32 fragment — \
fact tracking stops here (no further elisions in this function)"
));
return;
}
}
i += 1;
}
}
fn finish(self) -> FactSpecResult {
let Pass {
ops,
block_arity,
deletions,
admitted,
declined,
..
} = self;
if deletions.is_empty() {
return FactSpecResult {
ops: ops.to_vec(),
block_arity: block_arity.to_vec(),
kept: (0..ops.len()).collect(),
admitted,
declined,
};
}
let deleted = |i: usize| deletions.iter().any(|&(s, e)| i >= s && i <= e);
let mut out_ops = Vec::with_capacity(ops.len());
let mut out_arity = Vec::with_capacity(block_arity.len());
let mut kept = Vec::with_capacity(ops.len());
let mut ord = 0usize;
for (i, op) in ops.iter().enumerate() {
let is_opener = matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If);
if !deleted(i) {
out_ops.push(op.clone());
kept.push(i);
if is_opener && let Some(&a) = block_arity.get(ord) {
out_arity.push(a);
}
}
if is_opener {
ord += 1;
}
}
FactSpecResult {
ops: out_ops,
block_arity: out_arity,
kept,
admitted,
declined,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use WasmOp::*;
fn fact(value_id: u32, lo: i64, hi: i64) -> WscFact {
WscFact {
func_index: 0,
value_id,
kind: FactKind::ValueRange { lo, hi },
}
}
fn clamp_ops() -> Vec<WasmOp> {
vec![
LocalGet(0), I32Const(476), I32Add, LocalSet(1), LocalGet(1), I32Const(1000), I32LtS, If, I32Const(1000), LocalSet(1), End, LocalGet(1), I32Const(2000), I32GtS, If, I32Const(2000), LocalSet(1), End, LocalGet(1), End, ]
}
const CLAMP_ARITY: &[(u8, u8)] = &[(0, 0), (0, 0)];
#[test]
fn clamp_shape_elides_both_branches_under_the_proven_bound_494() {
let ops = clamp_ops();
let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)]);
assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
assert!(r.changed());
assert_eq!(
r.ops,
vec![
LocalGet(0),
I32Const(476),
I32Add,
LocalSet(1),
LocalGet(1),
End
],
"both clamp comparisons + branches + bodies must be gone"
);
assert_eq!(r.block_arity, vec![], "both If arity entries removed");
assert_eq!(r.kept, vec![0, 1, 2, 3, 18, 19]);
for line in &r.admitted {
assert!(line.contains("UNSAT"), "{line}");
assert!(line.contains("certificate-checked"), "{line}");
assert!(line.contains("[524, 1524]"), "{line}");
}
}
#[test]
fn wrong_wide_bound_is_sat_and_declines_loudly_494() {
let ops = clamp_ops();
let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 0, 4000)]);
assert_eq!(r.admitted.len(), 0);
assert!(!r.changed());
assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
assert_eq!(r.block_arity, CLAMP_ARITY.to_vec());
assert!(
r.declined
.iter()
.any(|d| d.contains("Sat") && d.contains("counterexample")),
"declines must be loud and carry a model: {:?}",
r.declined
);
}
#[test]
fn partially_dead_bound_elides_only_the_proven_branch_494() {
let ops = clamp_ops();
let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 4000)]);
assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
assert_eq!(r.declined.len(), 1);
assert_eq!(
r.ops,
vec![
LocalGet(0),
I32Const(476),
I32Add,
LocalSet(1),
LocalGet(1),
I32Const(2000),
I32GtS,
If,
I32Const(2000),
LocalSet(1),
End,
LocalGet(1),
End,
]
);
assert_eq!(r.block_arity, vec![(0, 0)], "one If survives");
}
#[test]
fn no_facts_changes_nothing_494() {
let ops = clamp_ops();
let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[]);
assert!(!r.changed());
assert_eq!(r.ops, ops);
assert!(!r.declined.is_empty(), "the no-fact case is a loud decline");
}
#[test]
fn declined_if_havocs_its_locals_no_false_admit_downstream_494() {
let ops = vec![
LocalGet(0), I32Const(476), I32Add, LocalSet(1), LocalGet(2), If, I32Const(-9), LocalSet(1), End, LocalGet(1), I32Const(2000), I32GtS, If, I32Const(2000), LocalSet(1), End, LocalGet(1), End, ];
let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)]);
assert_eq!(
r.admitted.len(),
0,
"havocked local must block the downstream elision: {:?}",
r.admitted
);
assert_eq!(r.ops, ops);
}
#[test]
fn if_with_else_declines_494() {
let ops = vec![
LocalGet(0), If, I32Const(1), LocalSet(1), Else, I32Const(2), LocalSet(1), End, LocalGet(1), End, ];
let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 0, 0)]);
assert_eq!(r.admitted.len(), 0);
assert!(
r.declined.iter().any(|d| d.contains("else")),
"{:?}",
r.declined
);
assert_eq!(r.ops, ops);
}
#[test]
fn nested_opener_inside_elided_body_fixes_block_arity_ordinals_494() {
let ops = vec![
LocalGet(0), I32Eqz, If, LocalGet(0), If, I32Const(7), LocalSet(1), End, End, Block, End, End, ];
let arity = &[(0, 0), (0, 0), (0, 1)];
let r = specialize_function("f", &ops, arity, &[fact(0, 5, 5)]);
assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
assert_eq!(r.ops, vec![Block, End, End]);
assert_eq!(r.kept, vec![9, 10, 11]);
assert_eq!(
r.block_arity,
vec![(0, 1)],
"only the surviving Block's entry"
);
}
#[test]
fn tee_condition_slice_is_not_erasable_494() {
let ops = vec![
LocalGet(0), LocalTee(1), I32Eqz, If, I32Const(9), LocalSet(2), End, End, ];
let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 1, 1)]);
assert_eq!(r.admitted.len(), 0);
assert!(
r.declined
.iter()
.any(|d| d.contains("not") && d.contains("erasable")),
"{:?}",
r.declined
);
assert_eq!(r.ops, ops);
}
#[test]
fn untracked_op_stops_tracking_loudly_494() {
let ops = vec![
LocalGet(0), I64ExtendI32S, Drop, End, ];
let r = specialize_function("f", &ops, &[], &[fact(0, 1, 2)]);
assert!(!r.changed());
assert!(
r.declined.iter().any(|d| d.contains("outside the tracked")),
"{:?}",
r.declined
);
}
#[test]
fn out_of_range_value_id_is_vacuous_494() {
let ops = clamp_ops();
let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(999, 524, 1524)]);
assert!(!r.changed());
assert_eq!(r.ops, ops);
}
}