#![cfg(all(feature = "z3-solver", feature = "arm"))]
use synth_core::WasmOp;
use synth_synthesis::rules::Condition;
use synth_synthesis::{ArmOp, Operand2, Reg};
use thiserror::Error;
use z3::ast::{BV, Bool};
use z3::{SatResult, Solver};
#[derive(Debug, Clone)]
pub struct CertifiedSelection<W, A> {
pub wasm: W,
pub arm: Vec<A>,
pub witness: Option<Witness>,
}
impl<W, A> CertifiedSelection<W, A> {
pub fn new(wasm: W, arm: Vec<A>) -> Self {
Self {
wasm,
arm,
witness: None,
}
}
pub fn with_witness(mut self, witness: Witness) -> Self {
self.witness = Some(witness);
self
}
pub fn is_certified(&self) -> bool {
self.witness.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Witness {
pub wasm_op_label: String,
pub arm_op_count: usize,
pub solver_result: SolverResultKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SolverResultKind {
Unsat,
Sat,
Unknown,
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("counterexample for {wasm_op_label}: {description}")]
Counterexample {
wasm_op_label: String,
description: String,
},
#[error("validator does not support {0:?}")]
UnsupportedOp(WasmOp),
#[error("solver returned unknown: {0}")]
SolverUnknown(String),
#[error("internal validator error: {0}")]
Internal(String),
}
pub trait Validator<W, A> {
fn validate(&self, sel: &CertifiedSelection<W, A>) -> Result<Witness, ValidationError>;
}
fn sym32(name: &str) -> BV {
BV::new_const(name, 32)
}
fn k32(v: i64) -> BV {
BV::from_i64(v, 32)
}
fn bool_to_i32(b: &Bool) -> BV {
b.ite(&k32(1), &k32(0))
}
#[derive(Clone)]
struct I64Pair {
lo: BV,
hi: BV,
}
impl I64Pair {
fn eq_pair(&self, other: &I64Pair) -> Bool {
Bool::and(&[&self.lo.eq(&other.lo), &self.hi.eq(&other.hi)])
}
fn join64(&self) -> BV {
self.hi.concat(&self.lo)
}
fn from_u64_bv(v: &BV) -> I64Pair {
I64Pair {
lo: v.extract(31, 0),
hi: v.extract(63, 32),
}
}
}
fn i64_add(a: &I64Pair, b: &I64Pair) -> I64Pair {
let lo = a.lo.bvadd(&b.lo);
let carry = lo.bvult(&a.lo).ite(&k32(1), &k32(0));
let hi = a.hi.bvadd(&b.hi).bvadd(&carry);
I64Pair { lo, hi }
}
fn i64_sub(a: &I64Pair, b: &I64Pair) -> I64Pair {
let lo = a.lo.bvsub(&b.lo);
let borrow = a.lo.bvult(&b.lo).ite(&k32(1), &k32(0));
let hi = a.hi.bvsub(&b.hi).bvsub(&borrow);
I64Pair { lo, hi }
}
fn i64_mul(a: &I64Pair, b: &I64Pair) -> I64Pair {
I64Pair::from_u64_bv(&a.join64().bvmul(b.join64()))
}
fn shift_amount64(b: &I64Pair) -> BV {
b.lo.bvand(k32(63))
}
fn i64_shl(a: &I64Pair, s: &BV) -> I64Pair {
let s_lt_32 = s.bvult(k32(32));
let s_is_zero = s.eq(k32(0));
let lo_small = a.lo.bvshl(s);
let hi_small = a.hi.bvshl(s).bvor(a.lo.bvlshr(k32(32).bvsub(s)));
let lo_big = k32(0);
let hi_big = a.lo.bvshl(s.bvsub(k32(32)));
let lo = s_is_zero.ite(&a.lo, &s_lt_32.ite(&lo_small, &lo_big));
let hi = s_is_zero.ite(&a.hi, &s_lt_32.ite(&hi_small, &hi_big));
I64Pair { lo, hi }
}
fn i64_shr_u(a: &I64Pair, s: &BV) -> I64Pair {
let s_lt_32 = s.bvult(k32(32));
let s_is_zero = s.eq(k32(0));
let lo_small = a.lo.bvlshr(s).bvor(a.hi.bvshl(k32(32).bvsub(s)));
let hi_small = a.hi.bvlshr(s);
let lo_big = a.hi.bvlshr(s.bvsub(k32(32)));
let hi_big = k32(0);
let lo = s_is_zero.ite(&a.lo, &s_lt_32.ite(&lo_small, &lo_big));
let hi = s_is_zero.ite(&a.hi, &s_lt_32.ite(&hi_small, &hi_big));
I64Pair { lo, hi }
}
fn i64_shr_s(a: &I64Pair, s: &BV) -> I64Pair {
let s_lt_32 = s.bvult(k32(32));
let s_is_zero = s.eq(k32(0));
let sign = a.hi.bvashr(k32(31));
let lo_small = a.lo.bvlshr(s).bvor(a.hi.bvshl(k32(32).bvsub(s)));
let hi_small = a.hi.bvashr(s);
let lo_big = a.hi.bvashr(s.bvsub(k32(32)));
let hi_big = sign.clone();
let lo = s_is_zero.ite(&a.lo, &s_lt_32.ite(&lo_small, &lo_big));
let hi = s_is_zero.ite(&a.hi, &s_lt_32.ite(&hi_small, &hi_big));
I64Pair { lo, hi }
}
fn i64_rotl(a: &I64Pair, s: &BV) -> I64Pair {
let left = i64_shl(a, s);
let comp = k32(64).bvsub(s).bvand(k32(63));
let right = i64_shr_u(a, &comp);
I64Pair {
lo: left.lo.bvor(&right.lo),
hi: left.hi.bvor(&right.hi),
}
}
fn i64_rotr(a: &I64Pair, s: &BV) -> I64Pair {
let right = i64_shr_u(a, s);
let comp = k32(64).bvsub(s).bvand(k32(63));
let left = i64_shl(a, &comp);
I64Pair {
lo: right.lo.bvor(&left.lo),
hi: right.hi.bvor(&left.hi),
}
}
fn i64_lt_u(a: &I64Pair, b: &I64Pair) -> Bool {
let hi_lt = a.hi.bvult(&b.hi);
let hi_eq = a.hi.eq(&b.hi);
let lo_lt = a.lo.bvult(&b.lo);
Bool::or(&[&hi_lt, &Bool::and(&[&hi_eq, &lo_lt])])
}
fn i64_lt_s(a: &I64Pair, b: &I64Pair) -> Bool {
let hi_lt = a.hi.bvslt(&b.hi);
let hi_eq = a.hi.eq(&b.hi);
let lo_lt = a.lo.bvult(&b.lo);
Bool::or(&[&hi_lt, &Bool::and(&[&hi_eq, &lo_lt])])
}
#[derive(Clone)]
struct Flags {
n: Bool, z: Bool, c: Bool, v: Bool, }
impl Flags {
fn unconstrained() -> Self {
Self {
n: Bool::new_const("flag_n"),
z: Bool::new_const("flag_z"),
c: Bool::new_const("flag_c"),
v: Bool::new_const("flag_v"),
}
}
fn from_cmp(a: &BV, b: &BV) -> Self {
let result = a.bvsub(b);
let n = result.bvslt(k32(0));
let z = a.eq(b);
let c = a.bvuge(b);
let a_neg = a.bvslt(k32(0));
let b_neg = b.bvslt(k32(0));
let r_neg = result.bvslt(k32(0));
let signs_differ = a_neg.eq(&b_neg).not();
let result_sign_wrong = r_neg.eq(&a_neg).not();
let v = Bool::and(&[&signs_differ, &result_sign_wrong]);
Self { n, z, c, v }
}
fn holds(&self, cond: &Condition) -> Bool {
match cond {
Condition::EQ => self.z.clone(),
Condition::NE => self.z.not(),
Condition::LT => self.n.eq(&self.v).not(),
Condition::GE => self.n.eq(&self.v),
Condition::LE => Bool::or(&[&self.z, &self.n.eq(&self.v).not()]),
Condition::GT => Bool::and(&[&self.z.not(), &self.n.eq(&self.v)]),
Condition::LO => self.c.not(),
Condition::HS => self.c.clone(),
Condition::LS => Bool::or(&[&self.c.not(), &self.z.clone()]),
Condition::HI => Bool::and(&[&self.c, &self.z.not()]),
}
}
}
enum OpResult {
Word(BV),
Pair(I64Pair),
}
pub struct Z3ArmValidator;
impl Default for Z3ArmValidator {
fn default() -> Self {
Self::new()
}
}
impl Z3ArmValidator {
pub fn new() -> Self {
Self
}
fn wasm_reference(&self, op: &WasmOp, a: &I64Pair, b: &I64Pair) -> Option<OpResult> {
use WasmOp::*;
let word = |bv: BV| Some(OpResult::Word(bv));
let pair = |p: I64Pair| Some(OpResult::Pair(p));
match op {
I32Add => word(a.lo.bvadd(&b.lo)),
I32Sub => word(a.lo.bvsub(&b.lo)),
I32Mul => word(a.lo.bvmul(&b.lo)),
I32And => word(a.lo.bvand(&b.lo)),
I32Or => word(a.lo.bvor(&b.lo)),
I32Xor => word(a.lo.bvxor(&b.lo)),
I32Shl => word(a.lo.bvshl(b.lo.bvand(k32(31)))),
I32ShrU => word(a.lo.bvlshr(b.lo.bvand(k32(31)))),
I32ShrS => word(a.lo.bvashr(b.lo.bvand(k32(31)))),
I32Rotl => word(a.lo.bvrotl(b.lo.bvand(k32(31)))),
I32Rotr => word(a.lo.bvrotr(b.lo.bvand(k32(31)))),
I32DivS => word(a.lo.bvsdiv(&b.lo)),
I32DivU => word(a.lo.bvudiv(&b.lo)),
I32Eq => word(bool_to_i32(&a.lo.eq(&b.lo))),
I32Ne => word(bool_to_i32(&a.lo.eq(&b.lo).not())),
I32LtS => word(bool_to_i32(&a.lo.bvslt(&b.lo))),
I32LtU => word(bool_to_i32(&a.lo.bvult(&b.lo))),
I32LeS => word(bool_to_i32(&a.lo.bvsle(&b.lo))),
I32LeU => word(bool_to_i32(&a.lo.bvule(&b.lo))),
I32GtS => word(bool_to_i32(&a.lo.bvsgt(&b.lo))),
I32GtU => word(bool_to_i32(&a.lo.bvugt(&b.lo))),
I32GeS => word(bool_to_i32(&a.lo.bvsge(&b.lo))),
I32GeU => word(bool_to_i32(&a.lo.bvuge(&b.lo))),
I32Eqz => word(bool_to_i32(&a.lo.eq(k32(0)))),
I64Add => pair(i64_add(a, b)),
I64Sub => pair(i64_sub(a, b)),
I64Mul => pair(i64_mul(a, b)),
I64And => pair(I64Pair {
lo: a.lo.bvand(&b.lo),
hi: a.hi.bvand(&b.hi),
}),
I64Or => pair(I64Pair {
lo: a.lo.bvor(&b.lo),
hi: a.hi.bvor(&b.hi),
}),
I64Xor => pair(I64Pair {
lo: a.lo.bvxor(&b.lo),
hi: a.hi.bvxor(&b.hi),
}),
I64Shl => pair(i64_shl(a, &shift_amount64(b))),
I64ShrU => pair(i64_shr_u(a, &shift_amount64(b))),
I64ShrS => pair(i64_shr_s(a, &shift_amount64(b))),
I64Rotl => pair(i64_rotl(a, &shift_amount64(b))),
I64Rotr => pair(i64_rotr(a, &shift_amount64(b))),
I64Eq => word(bool_to_i32(&a.eq_pair(b))),
I64Ne => word(bool_to_i32(&a.eq_pair(b).not())),
I64LtU => word(bool_to_i32(&i64_lt_u(a, b))),
I64LtS => word(bool_to_i32(&i64_lt_s(a, b))),
I64GtU => word(bool_to_i32(&i64_lt_u(b, a))),
I64GtS => word(bool_to_i32(&i64_lt_s(b, a))),
I64LeU => word(bool_to_i32(&i64_lt_u(b, a).not())),
I64LeS => word(bool_to_i32(&i64_lt_s(b, a).not())),
I64GeU => word(bool_to_i32(&i64_lt_u(a, b).not())),
I64GeS => word(bool_to_i32(&i64_lt_s(a, b).not())),
I64Eqz => word(bool_to_i32(&Bool::and(&[
&a.lo.eq(k32(0)),
&a.hi.eq(k32(0)),
]))),
_ => None,
}
}
fn arity(&self, op: &WasmOp) -> Option<usize> {
use WasmOp::*;
match op {
I32Add | I32Sub | I32Mul | I32And | I32Or | I32Xor | I32Shl | I32ShrU | I32ShrS
| I32Rotl | I32Rotr | I32DivS | I32DivU | I32Eq | I32Ne | I32LtS | I32LtU | I32LeS
| I32LeU | I32GtS | I32GtU | I32GeS | I32GeU | I64Add | I64Sub | I64Mul | I64And
| I64Or | I64Xor | I64Shl | I64ShrU | I64ShrS | I64Rotl | I64Rotr | I64Eq | I64Ne
| I64LtU | I64LtS | I64GtU | I64GtS | I64LeU | I64LeS | I64GeU | I64GeS => Some(2),
I32Eqz | I64Eqz => Some(1),
_ => None,
}
}
fn is_i64(op: &WasmOp) -> bool {
use WasmOp::*;
matches!(
op,
I64Add
| I64Sub
| I64Mul
| I64And
| I64Or
| I64Xor
| I64Shl
| I64ShrU
| I64ShrS
| I64Rotl
| I64Rotr
| I64Eq
| I64Ne
| I64LtU
| I64LtS
| I64GtU
| I64GtS
| I64LeU
| I64LeS
| I64GeU
| I64GeS
| I64Eqz
)
}
fn result_is_pair(op: &WasmOp) -> bool {
use WasmOp::*;
matches!(
op,
I64Add
| I64Sub
| I64Mul
| I64And
| I64Or
| I64Xor
| I64Shl
| I64ShrU
| I64ShrS
| I64Rotl
| I64Rotr
)
}
fn is_div_rem(op: &WasmOp) -> bool {
use WasmOp::*;
matches!(op, I32DivS | I32DivU)
}
fn div_rem_precondition(&self, op: &WasmOp, a: &I64Pair, b: &I64Pair) -> Bool {
use WasmOp::*;
match op {
I32DivU => b.lo.eq(k32(0)).not(),
I32DivS => {
let div_nonzero = b.lo.eq(k32(0)).not();
let int_min = a.lo.eq(k32(i32::MIN as i64));
let neg_one = b.lo.eq(k32(-1));
let overflow = Bool::and(&[&int_min, &neg_one]);
Bool::and(&[&div_nonzero, &overflow.not()])
}
_ => Bool::from_bool(true),
}
}
fn execute_arm(
&self,
op: &WasmOp,
arm: &[ArmOp],
a: &I64Pair,
b: &I64Pair,
) -> Result<OpResult, ValidationError> {
let mut regs: Vec<BV> = (0..16).map(|i| sym32(&format!("r{i}"))).collect();
let mut flags = Flags::unconstrained();
let is64 = Self::is_i64(op);
let arity = self.arity(op).unwrap_or(0);
if is64 {
regs[0] = a.lo.clone();
regs[1] = a.hi.clone();
if arity == 2 {
regs[2] = b.lo.clone();
regs[3] = b.hi.clone();
}
} else {
regs[0] = a.lo.clone();
if arity == 2 {
regs[1] = b.lo.clone();
}
}
let idx = reg_index;
let eval_op2 = |regs: &[BV], op2: &Operand2| -> Result<BV, ValidationError> {
match op2 {
Operand2::Reg(r) => Ok(regs[idx(r)].clone()),
Operand2::Imm(v) => Ok(k32(*v as i64)),
other => Err(ValidationError::Internal(format!(
"unsupported Operand2 in lowering: {other:?}"
))),
}
};
for instr in arm {
match instr {
ArmOp::Add { rd, rn, op2 } => {
regs[idx(rd)] = regs[idx(rn)].bvadd(&eval_op2(®s, op2)?);
}
ArmOp::Sub { rd, rn, op2 } => {
regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(®s, op2)?);
}
ArmOp::Adds { rd, rn, op2 } => {
let rn_v = regs[idx(rn)].clone();
let v = rn_v.bvadd(&eval_op2(®s, op2)?);
flags.c = v.bvult(&rn_v);
regs[idx(rd)] = v;
}
ArmOp::Adc { rd, rn, op2 } => {
let c = flags.c.ite(&k32(1), &k32(0));
regs[idx(rd)] = regs[idx(rn)].bvadd(&eval_op2(®s, op2)?).bvadd(&c);
}
ArmOp::Subs { rd, rn, op2 } => {
let rn_v = regs[idx(rn)].clone();
let op2_v = eval_op2(®s, op2)?;
flags.c = rn_v.bvuge(&op2_v);
regs[idx(rd)] = rn_v.bvsub(&op2_v);
}
ArmOp::Sbc { rd, rn, op2 } => {
let borrow = flags.c.ite(&k32(0), &k32(1));
regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(®s, op2)?).bvsub(&borrow);
}
ArmOp::Mul { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvmul(®s[idx(rm)]);
}
ArmOp::And { rd, rn, op2 } => {
regs[idx(rd)] = regs[idx(rn)].bvand(&eval_op2(®s, op2)?);
}
ArmOp::Orr { rd, rn, op2 } => {
regs[idx(rd)] = regs[idx(rn)].bvor(&eval_op2(®s, op2)?);
}
ArmOp::Eor { rd, rn, op2 } => {
regs[idx(rd)] = regs[idx(rn)].bvxor(&eval_op2(®s, op2)?);
}
ArmOp::Mov { rd, op2 } => {
regs[idx(rd)] = eval_op2(®s, op2)?;
}
ArmOp::Mvn { rd, op2 } => {
regs[idx(rd)] = eval_op2(®s, op2)?.bvnot();
}
ArmOp::LslReg { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvshl(®s[idx(rm)]);
}
ArmOp::LsrReg { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvlshr(®s[idx(rm)]);
}
ArmOp::AsrReg { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvashr(®s[idx(rm)]);
}
ArmOp::RorReg { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvrotr(®s[idx(rm)]);
}
ArmOp::Lsl { rd, rn, shift } => {
regs[idx(rd)] = regs[idx(rn)].bvshl(k32(*shift as i64));
}
ArmOp::Lsr { rd, rn, shift } => {
regs[idx(rd)] = regs[idx(rn)].bvlshr(k32(*shift as i64));
}
ArmOp::Asr { rd, rn, shift } => {
regs[idx(rd)] = regs[idx(rn)].bvashr(k32(*shift as i64));
}
ArmOp::Ror { rd, rn, shift } => {
regs[idx(rd)] = regs[idx(rn)].bvrotr(k32(*shift as i64));
}
ArmOp::Rsb { rd, rn, imm } => {
regs[idx(rd)] = k32(*imm as i64).bvsub(®s[idx(rn)]);
}
ArmOp::Sdiv { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvsdiv(®s[idx(rm)]);
}
ArmOp::Udiv { rd, rn, rm } => {
regs[idx(rd)] = regs[idx(rn)].bvudiv(®s[idx(rm)]);
}
ArmOp::Mls { rd, rn, rm, ra } => {
regs[idx(rd)] = regs[idx(ra)].bvsub(regs[idx(rn)].bvmul(®s[idx(rm)]));
}
ArmOp::Cmp { rn, op2 } => {
let rn_v = regs[idx(rn)].clone();
let op2_v = eval_op2(®s, op2)?;
flags = Flags::from_cmp(&rn_v, &op2_v);
}
ArmOp::SetCond { rd, cond } => {
regs[idx(rd)] = bool_to_i32(&flags.holds(cond));
}
ArmOp::I64SetCond {
rd,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
cond,
} => {
let n = I64Pair {
lo: regs[idx(rn_lo)].clone(),
hi: regs[idx(rn_hi)].clone(),
};
let m = I64Pair {
lo: regs[idx(rm_lo)].clone(),
hi: regs[idx(rm_hi)].clone(),
};
let holds = match cond {
Condition::EQ => n.eq_pair(&m),
Condition::NE => n.eq_pair(&m).not(),
Condition::LT => i64_lt_s(&n, &m),
Condition::GE => i64_lt_s(&n, &m).not(),
Condition::GT => i64_lt_s(&m, &n),
Condition::LE => i64_lt_s(&m, &n).not(),
Condition::LO => i64_lt_u(&n, &m),
Condition::HS => i64_lt_u(&n, &m).not(),
Condition::HI => i64_lt_u(&m, &n),
Condition::LS => i64_lt_u(&m, &n).not(),
};
regs[idx(rd)] = bool_to_i32(&holds);
}
ArmOp::I64Mul {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
} => {
let n = I64Pair {
lo: regs[idx(rn_lo)].clone(),
hi: regs[idx(rn_hi)].clone(),
};
let m = I64Pair {
lo: regs[idx(rm_lo)].clone(),
hi: regs[idx(rm_hi)].clone(),
};
let prod = i64_mul(&n, &m);
regs[idx(rd_lo)] = prod.lo;
regs[idx(rd_hi)] = prod.hi;
}
ArmOp::I64Shl {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi: _,
} => {
let n = I64Pair {
lo: regs[idx(rn_lo)].clone(),
hi: regs[idx(rn_hi)].clone(),
};
let amt = regs[idx(rm_lo)].bvand(k32(63));
let r = i64_shl(&n, &amt);
regs[idx(rd_lo)] = r.lo;
regs[idx(rd_hi)] = r.hi;
}
ArmOp::I64ShrU {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi: _,
} => {
let n = I64Pair {
lo: regs[idx(rn_lo)].clone(),
hi: regs[idx(rn_hi)].clone(),
};
let amt = regs[idx(rm_lo)].bvand(k32(63));
let r = i64_shr_u(&n, &amt);
regs[idx(rd_lo)] = r.lo;
regs[idx(rd_hi)] = r.hi;
}
ArmOp::I64ShrS {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi: _,
} => {
let n = I64Pair {
lo: regs[idx(rn_lo)].clone(),
hi: regs[idx(rn_hi)].clone(),
};
let amt = regs[idx(rm_lo)].bvand(k32(63));
let r = i64_shr_s(&n, &amt);
regs[idx(rd_lo)] = r.lo;
regs[idx(rd_hi)] = r.hi;
}
ArmOp::I64Rotl {
rdlo,
rdhi,
rnlo,
rnhi,
shift,
} => {
let n = I64Pair {
lo: regs[idx(rnlo)].clone(),
hi: regs[idx(rnhi)].clone(),
};
let amt = regs[idx(shift)].bvand(k32(63));
let r = i64_rotl(&n, &amt);
regs[idx(rdlo)] = r.lo;
regs[idx(rdhi)] = r.hi;
}
ArmOp::I64Rotr {
rdlo,
rdhi,
rnlo,
rnhi,
shift,
} => {
let n = I64Pair {
lo: regs[idx(rnlo)].clone(),
hi: regs[idx(rnhi)].clone(),
};
let amt = regs[idx(shift)].bvand(k32(63));
let r = i64_rotr(&n, &amt);
regs[idx(rdlo)] = r.lo;
regs[idx(rdhi)] = r.hi;
}
ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
let is_zero =
Bool::and(&[®s[idx(rn_lo)].eq(k32(0)), ®s[idx(rn_hi)].eq(k32(0))]);
regs[idx(rd)] = bool_to_i32(&is_zero);
}
ArmOp::Nop => {}
other => {
return Err(ValidationError::Internal(format!(
"ARM op not modeled by validator: {other:?}"
)));
}
}
}
if is64 && Self::result_is_pair(op) {
Ok(OpResult::Pair(I64Pair {
lo: regs[0].clone(),
hi: regs[1].clone(),
}))
} else {
Ok(OpResult::Word(regs[0].clone()))
}
}
}
fn reg_index(r: &Reg) -> usize {
match r {
Reg::R0 => 0,
Reg::R1 => 1,
Reg::R2 => 2,
Reg::R3 => 3,
Reg::R4 => 4,
Reg::R5 => 5,
Reg::R6 => 6,
Reg::R7 => 7,
Reg::R8 => 8,
Reg::R9 => 9,
Reg::R10 => 10,
Reg::R11 => 11,
Reg::R12 => 12,
Reg::SP => 13,
Reg::LR => 14,
Reg::PC => 15,
}
}
impl Validator<WasmOp, ArmOp> for Z3ArmValidator {
fn validate(
&self,
sel: &CertifiedSelection<WasmOp, ArmOp>,
) -> Result<Witness, ValidationError> {
let label = format!("{:?}", sel.wasm);
self.arity(&sel.wasm)
.ok_or_else(|| ValidationError::UnsupportedOp(sel.wasm.clone()))?;
let solver = Solver::new();
let a = I64Pair {
lo: sym32("a_lo"),
hi: sym32("a_hi"),
};
let b = I64Pair {
lo: sym32("b_lo"),
hi: sym32("b_hi"),
};
if Self::is_div_rem(&sel.wasm) {
solver.assert(self.div_rem_precondition(&sel.wasm, &a, &b));
}
let wasm_result = self
.wasm_reference(&sel.wasm, &a, &b)
.ok_or_else(|| ValidationError::UnsupportedOp(sel.wasm.clone()))?;
let arm_result = self.execute_arm(&sel.wasm, &sel.arm, &a, &b)?;
let differ = match (&wasm_result, &arm_result) {
(OpResult::Word(w), OpResult::Word(r)) => w.eq(r).not(),
(OpResult::Pair(w), OpResult::Pair(r)) => w.eq_pair(r).not(),
_ => {
return Err(ValidationError::Internal(format!(
"result-shape mismatch for {label}: WASM and ARM disagree on word vs pair"
)));
}
};
solver.assert(&differ);
match solver.check() {
SatResult::Unsat => Ok(Witness {
wasm_op_label: label,
arm_op_count: sel.arm.len(),
solver_result: SolverResultKind::Unsat,
}),
SatResult::Sat => {
let description = match solver.get_model() {
Some(model) => {
let mut parts = Vec::new();
for (name, bv) in [
("a_lo", &a.lo),
("a_hi", &a.hi),
("b_lo", &b.lo),
("b_hi", &b.hi),
] {
if let Some(v) = model.eval(bv, true).and_then(|v| v.as_i64()) {
parts.push(format!("{name}={v}"));
}
}
parts.join(", ")
}
None => "no model available".to_string(),
};
Err(ValidationError::Counterexample {
wasm_op_label: label,
description,
})
}
SatResult::Unknown => Err(ValidationError::SolverUnknown(label)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::with_z3_context;
fn assert_certifies(wasm: WasmOp, arm: Vec<ArmOp>) {
let validator = Z3ArmValidator::new();
let sel = CertifiedSelection::new(wasm.clone(), arm);
match validator.validate(&sel) {
Ok(w) => {
assert_eq!(
w.solver_result,
SolverResultKind::Unsat,
"{wasm:?} did not certify"
);
assert_eq!(w.wasm_op_label, format!("{wasm:?}"));
}
other => panic!("expected {wasm:?} to certify, got {other:?}"),
}
}
fn dp_r0_r0_r1(make: fn(Reg, Reg, Operand2) -> ArmOp) -> Vec<ArmOp> {
vec![make(Reg::R0, Reg::R0, Operand2::Reg(Reg::R1))]
}
fn i32_cmp(cond: Condition) -> Vec<ArmOp> {
vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond { rd: Reg::R0, cond },
]
}
fn i64_cmp(cond: Condition) -> Vec<ArmOp> {
vec![ArmOp::I64SetCond {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
cond,
}]
}
fn i64_logic(make: fn(Reg, Reg, Operand2) -> ArmOp) -> Vec<ArmOp> {
vec![
make(Reg::R0, Reg::R0, Operand2::Reg(Reg::R2)),
make(Reg::R1, Reg::R1, Operand2::Reg(Reg::R3)),
]
}
#[test]
fn i32_add_certifies() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
let correct = CertifiedSelection::new(
WasmOp::I32Add,
vec![ArmOp::Add {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}],
);
let witness = validator
.validate(&correct)
.expect("validator must accept I32Add → ADD");
assert_eq!(witness.wasm_op_label, "I32Add");
assert_eq!(witness.arm_op_count, 1);
assert_eq!(witness.solver_result, SolverResultKind::Unsat);
let wrong = CertifiedSelection::new(
WasmOp::I32Add,
vec![ArmOp::Sub {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}],
);
match validator.validate(&wrong) {
Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
assert_eq!(wasm_op_label, "I32Add");
}
other => panic!("expected Counterexample for SUB, got {other:?}"),
}
});
}
#[test]
fn unsupported_op_returns_structured_error() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
let sel = CertifiedSelection::<WasmOp, ArmOp>::new(WasmOp::Drop, vec![]);
match validator.validate(&sel) {
Err(ValidationError::UnsupportedOp(op)) => assert_eq!(op, WasmOp::Drop),
other => panic!("expected UnsupportedOp, got {other:?}"),
}
});
}
#[test]
fn certified_selection_witness_roundtrip() {
let sel = CertifiedSelection::<WasmOp, ArmOp>::new(
WasmOp::I32Add,
vec![ArmOp::Add {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}],
);
assert!(!sel.is_certified());
let witness = Witness {
wasm_op_label: "I32Add".to_string(),
arm_op_count: 1,
solver_result: SolverResultKind::Unsat,
};
let certified = sel.with_witness(witness.clone());
assert!(certified.is_certified());
assert_eq!(certified.witness, Some(witness));
}
#[test]
fn i32_arith_logic_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I32Add,
dp_r0_r0_r1(|rd, rn, op2| ArmOp::Add { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I32Sub,
dp_r0_r0_r1(|rd, rn, op2| ArmOp::Sub { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I32Mul,
vec![ArmOp::Mul {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}],
);
assert_certifies(
WasmOp::I32And,
dp_r0_r0_r1(|rd, rn, op2| ArmOp::And { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I32Or,
dp_r0_r0_r1(|rd, rn, op2| ArmOp::Orr { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I32Xor,
dp_r0_r0_r1(|rd, rn, op2| ArmOp::Eor { rd, rn, op2 }),
);
});
}
#[test]
fn i32_shifts_certify() {
with_z3_context(|| {
let mask_then = |shift: ArmOp| {
vec![
ArmOp::And {
rd: Reg::R1,
rn: Reg::R1,
op2: Operand2::Imm(31),
},
shift,
]
};
assert_certifies(
WasmOp::I32Shl,
mask_then(ArmOp::LslReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}),
);
assert_certifies(
WasmOp::I32ShrU,
mask_then(ArmOp::LsrReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}),
);
assert_certifies(
WasmOp::I32ShrS,
mask_then(ArmOp::AsrReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}),
);
assert_certifies(
WasmOp::I32Rotr,
vec![ArmOp::RorReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}],
);
assert_certifies(
WasmOp::I32Rotl,
vec![
ArmOp::Rsb {
rd: Reg::R1,
rn: Reg::R1,
imm: 0,
},
ArmOp::RorReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
},
],
);
});
}
#[test]
fn i32_comparisons_certify() {
with_z3_context(|| {
assert_certifies(WasmOp::I32Eq, i32_cmp(Condition::EQ));
assert_certifies(WasmOp::I32Ne, i32_cmp(Condition::NE));
assert_certifies(WasmOp::I32LtS, i32_cmp(Condition::LT));
assert_certifies(WasmOp::I32LeS, i32_cmp(Condition::LE));
assert_certifies(WasmOp::I32GtS, i32_cmp(Condition::GT));
assert_certifies(WasmOp::I32GeS, i32_cmp(Condition::GE));
assert_certifies(WasmOp::I32LtU, i32_cmp(Condition::LO));
assert_certifies(WasmOp::I32LeU, i32_cmp(Condition::LS));
assert_certifies(WasmOp::I32GtU, i32_cmp(Condition::HI));
assert_certifies(WasmOp::I32GeU, i32_cmp(Condition::HS));
});
}
#[test]
fn i32_eqz_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I32Eqz,
vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Imm(0),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::EQ,
},
],
);
});
}
#[test]
fn i32_div_certify() {
with_z3_context(|| {
assert_certifies(
WasmOp::I32DivS,
vec![ArmOp::Sdiv {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}],
);
assert_certifies(
WasmOp::I32DivU,
vec![ArmOp::Udiv {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R1,
}],
);
});
}
#[test]
fn i32_rem_is_scoped_out() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
for op in [WasmOp::I32RemS, WasmOp::I32RemU] {
let sel = CertifiedSelection::<WasmOp, ArmOp>::new(op.clone(), vec![]);
match validator.validate(&sel) {
Err(ValidationError::UnsupportedOp(got)) => assert_eq!(got, op),
other => panic!("expected {op:?} to be UnsupportedOp, got {other:?}"),
}
}
});
}
#[test]
fn i64_add_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Add,
vec![
ArmOp::Adds {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R2),
},
ArmOp::Adc {
rd: Reg::R1,
rn: Reg::R1,
op2: Operand2::Reg(Reg::R3),
},
],
);
});
}
#[test]
fn i64_sub_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Sub,
vec![
ArmOp::Subs {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R2),
},
ArmOp::Sbc {
rd: Reg::R1,
rn: Reg::R1,
op2: Operand2::Reg(Reg::R3),
},
],
);
});
}
#[test]
fn i64_mul_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Mul,
vec![ArmOp::I64Mul {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}],
);
});
}
#[test]
fn i64_logic_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64And,
i64_logic(|rd, rn, op2| ArmOp::And { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I64Or,
i64_logic(|rd, rn, op2| ArmOp::Orr { rd, rn, op2 }),
);
assert_certifies(
WasmOp::I64Xor,
i64_logic(|rd, rn, op2| ArmOp::Eor { rd, rn, op2 }),
);
});
}
fn prove_shift_reference(
limb_fn: fn(&I64Pair, &BV) -> I64Pair,
native: fn(&BV, &BV) -> BV,
label: &str,
) {
let solver = Solver::new();
let a = I64Pair {
lo: sym32("sa_lo"),
hi: sym32("sa_hi"),
};
let amt = sym32("samt");
solver.assert(amt.bvult(k32(64)));
let limb = limb_fn(&a, &amt);
let limb64 = limb.hi.concat(&limb.lo);
let a64 = a.hi.concat(&a.lo);
let amt64 = k32(0).concat(&amt);
let truth = native(&a64, &amt64);
solver.assert(limb64.eq(&truth).not());
assert_eq!(
solver.check(),
SatResult::Unsat,
"{label} limb reference must equal the native 64-bit shift for all inputs"
);
}
#[test]
fn i64_shift_references_match_native() {
with_z3_context(|| {
prove_shift_reference(i64_shl, |x, s| x.bvshl(s), "i64.shl");
prove_shift_reference(i64_shr_u, |x, s| x.bvlshr(s), "i64.shr_u");
prove_shift_reference(i64_shr_s, |x, s| x.bvashr(s), "i64.shr_s");
});
}
#[test]
fn i64_shifts_certify() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Shl,
vec![ArmOp::I64Shl {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}],
);
assert_certifies(
WasmOp::I64ShrU,
vec![ArmOp::I64ShrU {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}],
);
assert_certifies(
WasmOp::I64ShrS,
vec![ArmOp::I64ShrS {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}],
);
});
}
#[test]
fn i64_rotates_certify() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Rotl,
vec![ArmOp::I64Rotl {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R0,
rnhi: Reg::R1,
shift: Reg::R2,
}],
);
assert_certifies(
WasmOp::I64Rotr,
vec![ArmOp::I64Rotr {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R0,
rnhi: Reg::R1,
shift: Reg::R2,
}],
);
});
}
#[test]
fn i64_compare_references_match_native() {
with_z3_context(|| {
let solver = Solver::new();
let a = I64Pair {
lo: sym32("ca_lo"),
hi: sym32("ca_hi"),
};
let b = I64Pair {
lo: sym32("cb_lo"),
hi: sym32("cb_hi"),
};
let a64 = a.hi.concat(&a.lo);
let b64 = b.hi.concat(&b.lo);
let lt_u_wrong = i64_lt_u(&a, &b).eq(a64.bvult(&b64)).not();
let lt_s_wrong = i64_lt_s(&a, &b).eq(a64.bvslt(&b64)).not();
solver.assert(Bool::or(&[<_u_wrong, <_s_wrong]));
assert_eq!(
solver.check(),
SatResult::Unsat,
"i64 lexicographic compare references must match native 64-bit comparisons"
);
});
}
#[test]
fn i64_comparisons_certify() {
with_z3_context(|| {
assert_certifies(WasmOp::I64Eq, i64_cmp(Condition::EQ));
assert_certifies(WasmOp::I64Ne, i64_cmp(Condition::NE));
assert_certifies(WasmOp::I64LtS, i64_cmp(Condition::LT));
assert_certifies(WasmOp::I64LeS, i64_cmp(Condition::LE));
assert_certifies(WasmOp::I64GtS, i64_cmp(Condition::GT));
assert_certifies(WasmOp::I64GeS, i64_cmp(Condition::GE));
assert_certifies(WasmOp::I64LtU, i64_cmp(Condition::LO));
assert_certifies(WasmOp::I64LeU, i64_cmp(Condition::LS));
assert_certifies(WasmOp::I64GtU, i64_cmp(Condition::HI));
assert_certifies(WasmOp::I64GeU, i64_cmp(Condition::HS));
});
}
#[test]
fn i64_eqz_certifies() {
with_z3_context(|| {
assert_certifies(
WasmOp::I64Eqz,
vec![ArmOp::I64SetCondZ {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
}],
);
});
}
#[test]
fn wrong_i64_add_lowering_rejected() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
let wrong = CertifiedSelection::new(
WasmOp::I64Add,
vec![
ArmOp::Adds {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R2),
},
ArmOp::Add {
rd: Reg::R1,
rn: Reg::R1,
op2: Operand2::Reg(Reg::R3),
},
],
);
match validator.validate(&wrong) {
Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
assert_eq!(wasm_op_label, "I64Add");
}
other => {
panic!("expected Counterexample for carry-dropping i64.add, got {other:?}")
}
}
});
}
#[test]
fn wrong_i32_lt_s_lowering_rejected() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
let wrong = CertifiedSelection::new(
WasmOp::I32LtS,
vec![
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
},
ArmOp::SetCond {
rd: Reg::R0,
cond: Condition::LO,
},
],
);
match validator.validate(&wrong) {
Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
assert_eq!(wasm_op_label, "I32LtS");
}
other => panic!("expected Counterexample for lt_s-as-LO, got {other:?}"),
}
});
}
#[test]
fn wrong_i64_shl_lowering_rejected() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
let wrong = CertifiedSelection::new(
WasmOp::I64Shl,
vec![
ArmOp::LslReg {
rd: Reg::R0,
rn: Reg::R0,
rm: Reg::R2,
},
],
);
match validator.validate(&wrong) {
Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
assert_eq!(wasm_op_label, "I64Shl");
}
other => panic!("expected Counterexample for partial i64.shl, got {other:?}"),
}
});
}
#[test]
fn i64_div_is_scoped_out() {
with_z3_context(|| {
let validator = Z3ArmValidator::new();
for op in [
WasmOp::I64DivS,
WasmOp::I64DivU,
WasmOp::I64RemS,
WasmOp::I64RemU,
] {
let sel = CertifiedSelection::<WasmOp, ArmOp>::new(op.clone(), vec![]);
match validator.validate(&sel) {
Err(ValidationError::UnsupportedOp(got)) => assert_eq!(got, op),
other => panic!("expected {op:?} to be UnsupportedOp, got {other:?}"),
}
}
});
}
}