use std::collections::HashMap;
use num_bigint::BigInt;
use oxiz::core::TermKind;
use oxiz::{Solver, SolverResult, TermId, TermManager};
use super::encoder::{Encoded, Encoder, ValSort};
use super::types::{decode_bits, int_type_of, IntType};
use super::{Counterexample, Verdict};
struct Param {
name: String,
sort: ValSort,
}
pub struct EquivBuilder;
impl EquivBuilder {
pub fn prove(fn_a: &syn::ItemFn, fn_b: &syn::ItemFn) -> Verdict {
let params = match validate_signatures(fn_a, fn_b) {
Ok(p) => p,
Err(reason) => return Verdict::Unsupported { reason },
};
let mut tm = TermManager::new();
let mut solver = Solver::new();
solver.set_logic("QF_BV");
let mut env: HashMap<String, Encoded> = HashMap::new();
let mut param_terms: Vec<(String, IntType, TermId)> = Vec::new();
for param in ¶ms {
match param.sort {
ValSort::Int(ty) => {
let sort_id = tm.sorts.bitvec(ty.width);
let var = tm.mk_var(¶m.name, sort_id);
env.insert(
param.name.clone(),
Encoded {
term: var,
sort: param.sort,
},
);
param_terms.push((param.name.clone(), ty, var));
}
ValSort::Bool => {
let bool_sort = tm.sorts.bool_sort;
let var = tm.mk_var(¶m.name, bool_sort);
env.insert(
param.name.clone(),
Encoded {
term: var,
sort: param.sort,
},
);
}
}
}
if let Err(reason) = super::is_pure_supported_block(&fn_a.block) {
return Verdict::Unsupported { reason };
}
if let Err(reason) = super::is_pure_supported_block(&fn_b.block) {
return Verdict::Unsupported { reason };
}
let out_a = {
let mut enc = Encoder::new(&mut tm, env.clone());
match enc.encode_block(&fn_a.block, return_int_hint(fn_a)) {
Ok(e) => e,
Err(reason) => return Verdict::Unsupported { reason },
}
};
let out_b = {
let mut enc = Encoder::new(&mut tm, env);
match enc.encode_block(&fn_b.block, return_int_hint(fn_b)) {
Ok(e) => e,
Err(reason) => return Verdict::Unsupported { reason },
}
};
if out_a.sort != out_b.sort {
return Verdict::Unsupported {
reason: "the two functions produce results of different sorts".to_string(),
};
}
let eq_out = tm.mk_eq(out_a.term, out_b.term);
let diseq = tm.mk_not(eq_out);
solver.assert(diseq, &mut tm);
let check_result = solver.check(&mut tm);
match check_result {
SolverResult::Unsat => Verdict::Verified,
SolverResult::Sat => {
let cx = build_counterexample(
&solver,
&mut tm,
¶m_terms,
out_a.sort,
out_a.term,
out_b.term,
);
Verdict::Refuted(cx)
}
SolverResult::Unknown => Verdict::Unsupported {
reason: "solver returned Unknown".to_string(),
},
}
}
}
fn validate_signatures(fn_a: &syn::ItemFn, fn_b: &syn::ItemFn) -> Result<Vec<Param>, String> {
let params_a = collect_params(&fn_a.sig)?;
let params_b = collect_params(&fn_b.sig)?;
if params_a.len() != params_b.len() {
return Err(format!(
"arity mismatch: {} vs {} parameters",
params_a.len(),
params_b.len()
));
}
for (idx, (a, b)) in params_a.iter().zip(params_b.iter()).enumerate() {
if a.name != b.name {
return Err(format!(
"parameter {} name mismatch: `{}` vs `{}`",
idx, a.name, b.name
));
}
if a.sort != b.sort {
return Err(format!(
"parameter `{}` type mismatch between the two functions",
a.name
));
}
}
Ok(params_a)
}
fn collect_params(sig: &syn::Signature) -> Result<Vec<Param>, String> {
let mut out = Vec::new();
for input in &sig.inputs {
let syn::FnArg::Typed(pat_type) = input else {
return Err("`self` receiver is not modeled".to_string());
};
let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
return Err("only plain identifier parameters are modeled".to_string());
};
if pat_ident.by_ref.is_some() || pat_ident.subpat.is_some() {
return Err("`ref`/sub-pattern parameters are not modeled".to_string());
}
let name = pat_ident.ident.to_string();
let sort = sort_of_type(&pat_type.ty)
.ok_or_else(|| format!("parameter `{name}` has an unsupported type"))?;
out.push(Param { name, sort });
}
Ok(out)
}
fn sort_of_type(ty: &syn::Type) -> Option<ValSort> {
if let Some(int_ty) = int_type_of(ty) {
return Some(ValSort::Int(int_ty));
}
if is_bool_type(ty) {
return Some(ValSort::Bool);
}
None
}
fn is_bool_type(ty: &syn::Type) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
type_path.qself.is_none()
&& type_path.path.segments.len() == 1
&& type_path
.path
.segments
.first()
.is_some_and(|s| s.ident == "bool" && matches!(s.arguments, syn::PathArguments::None))
}
fn return_int_hint(item: &syn::ItemFn) -> Option<IntType> {
match &item.sig.output {
syn::ReturnType::Type(_, ty) => int_type_of(ty),
syn::ReturnType::Default => None,
}
}
fn build_counterexample(
solver: &Solver,
tm: &mut TermManager,
param_terms: &[(String, IntType, TermId)],
out_sort: ValSort,
out_a_term: TermId,
out_b_term: TermId,
) -> Counterexample {
let mut inputs = Vec::new();
let model = solver.model();
let mut pins: Vec<(TermId, u32, BigInt)> = Vec::new();
for (name, ty, term) in param_terms {
let raw = model.and_then(|m| model_bitvec_value(m, tm, *term));
let rendered = raw
.as_ref()
.map(|v| decode_bits(v, *ty))
.unwrap_or_else(|| "?".to_string());
if let Some(v) = raw {
pins.push((*term, ty.width, v));
}
inputs.push((name.clone(), rendered));
}
let (left_output, right_output) =
outputs_under_pins(tm, &pins, out_sort, out_a_term, out_b_term);
Counterexample {
inputs,
left_output,
right_output,
}
}
#[derive(Clone, Debug)]
enum ConstVal {
Bv(BigInt, u32),
Bool(bool),
}
fn bv_width(tm: &TermManager, term: TermId) -> Option<u32> {
let sort = tm.get(term)?.sort;
tm.sorts.get(sort).and_then(|s| s.bitvec_width())
}
fn modulus_of(width: u32) -> BigInt {
BigInt::from(1u64) << width
}
fn mask_to(value: BigInt, width: u32) -> BigInt {
let modulus = modulus_of(width);
let reduced = value % &modulus;
if reduced.sign() == num_bigint::Sign::Minus {
reduced + modulus
} else {
reduced
}
}
fn to_signed(value: &BigInt, width: u32) -> BigInt {
let half = BigInt::from(1u64) << (width - 1);
if *value >= half {
value - modulus_of(width)
} else {
value.clone()
}
}
fn shift_amount(value: &BigInt) -> Option<u32> {
match value.to_u32_digits() {
(num_bigint::Sign::Minus, _) => None,
(_, digits) => match digits.as_slice() {
[] => Some(0),
[single] => Some(*single),
_ => None,
},
}
}
fn eval_term_const(
tm: &TermManager,
term: TermId,
pins: &HashMap<TermId, BigInt>,
) -> Option<ConstVal> {
let kind = tm.get(term)?.kind.clone();
match kind {
TermKind::BitVecConst { value, width } => Some(ConstVal::Bv(mask_to(value, width), width)),
TermKind::Var(_) => {
let value = pins.get(&term)?.clone();
let width = bv_width(tm, term)?;
Some(ConstVal::Bv(mask_to(value, width), width))
}
TermKind::True => Some(ConstVal::Bool(true)),
TermKind::False => Some(ConstVal::Bool(false)),
TermKind::BvAdd(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(mask_to(va + vb, width), width))
}
TermKind::BvSub(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(mask_to(va - vb, width), width))
}
TermKind::BvMul(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(mask_to(va * vb, width), width))
}
TermKind::BvAnd(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(va & vb, width))
}
TermKind::BvOr(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(va | vb, width))
}
TermKind::BvXor(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bv(va ^ vb, width))
}
TermKind::BvNot(a) => {
let width = bv_width(tm, term)?;
let va = eval_bv(tm, a, pins)?;
let mask = modulus_of(width) - BigInt::from(1u64);
Some(ConstVal::Bv(mask ^ va, width))
}
TermKind::BvShl(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
let result = match shift_amount(&vb) {
Some(sh) if sh < width => va << sh,
_ => BigInt::from(0u64),
};
Some(ConstVal::Bv(mask_to(result, width), width))
}
TermKind::BvLshr(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
let result = match shift_amount(&vb) {
Some(sh) if sh < width => va >> sh,
_ => BigInt::from(0u64),
};
Some(ConstVal::Bv(result, width))
}
TermKind::BvAshr(a, b) => {
let width = bv_width(tm, term)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
let signed = to_signed(&va, width);
let negative = signed.sign() == num_bigint::Sign::Minus;
let result = match shift_amount(&vb) {
Some(sh) if sh < width => {
signed >> sh
}
_ if negative => -BigInt::from(1u64),
_ => BigInt::from(0u64),
};
Some(ConstVal::Bv(mask_to(result, width), width))
}
TermKind::BvExtract { high, low, arg } => {
let va = eval_bv(tm, arg, pins)?;
let out_width = high - low + 1;
let field_mask = modulus_of(out_width) - BigInt::from(1u64);
let extracted = (va >> low) & field_mask;
Some(ConstVal::Bv(extracted, out_width))
}
TermKind::BvConcat(high, low) => {
let (vh, low_width) = (eval_bv(tm, high, pins)?, bv_width(tm, low)?);
let vl = eval_bv(tm, low, pins)?;
let high_width = bv_width(tm, high)?;
let value = (vh << low_width) | vl;
Some(ConstVal::Bv(value, high_width + low_width))
}
TermKind::Eq(a, b) => {
match (eval_term_const(tm, a, pins)?, eval_term_const(tm, b, pins)?) {
(ConstVal::Bv(va, _), ConstVal::Bv(vb, _)) => Some(ConstVal::Bool(va == vb)),
(ConstVal::Bool(ba), ConstVal::Bool(bb)) => Some(ConstVal::Bool(ba == bb)),
_ => None,
}
}
TermKind::BvUlt(a, b) => {
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bool(va < vb))
}
TermKind::BvUle(a, b) => {
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bool(va <= vb))
}
TermKind::BvSlt(a, b) => {
let width = bv_width(tm, a)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bool(
to_signed(&va, width) < to_signed(&vb, width),
))
}
TermKind::BvSle(a, b) => {
let width = bv_width(tm, a)?;
let (va, vb) = (eval_bv(tm, a, pins)?, eval_bv(tm, b, pins)?);
Some(ConstVal::Bool(
to_signed(&va, width) <= to_signed(&vb, width),
))
}
TermKind::Not(a) => match eval_term_const(tm, a, pins)? {
ConstVal::Bool(b) => Some(ConstVal::Bool(!b)),
ConstVal::Bv(..) => None,
},
TermKind::And(args) => {
let mut all = true;
for arg in args {
match eval_term_const(tm, arg, pins)? {
ConstVal::Bool(b) => all &= b,
ConstVal::Bv(..) => return None,
}
}
Some(ConstVal::Bool(all))
}
TermKind::Or(args) => {
let mut any = false;
for arg in args {
match eval_term_const(tm, arg, pins)? {
ConstVal::Bool(b) => any |= b,
ConstVal::Bv(..) => return None,
}
}
Some(ConstVal::Bool(any))
}
TermKind::Ite(c, t, e) => match eval_term_const(tm, c, pins)? {
ConstVal::Bool(true) => eval_term_const(tm, t, pins),
ConstVal::Bool(false) => eval_term_const(tm, e, pins),
ConstVal::Bv(..) => None,
},
_ => None,
}
}
fn eval_bv(tm: &TermManager, term: TermId, pins: &HashMap<TermId, BigInt>) -> Option<BigInt> {
match eval_term_const(tm, term, pins)? {
ConstVal::Bv(value, _width) => Some(value),
ConstVal::Bool(_) => None,
}
}
fn render_const(value: ConstVal, out_sort: ValSort) -> Option<String> {
match (value, out_sort) {
(ConstVal::Bv(v, _w), ValSort::Int(ty)) => Some(decode_bits(&v, ty)),
(ConstVal::Bool(b), ValSort::Bool) => Some(b.to_string()),
(ConstVal::Bv(v, _w), ValSort::Bool) => Some(
if v == BigInt::from(0u64) {
"false"
} else {
"true"
}
.to_string(),
),
_ => None,
}
}
fn outputs_under_pins(
tm: &TermManager,
pins: &[(TermId, u32, BigInt)],
out_sort: ValSort,
out_a_term: TermId,
out_b_term: TermId,
) -> (Option<String>, Option<String>) {
let pin_map: HashMap<TermId, BigInt> = pins.iter().map(|(t, _w, v)| (*t, v.clone())).collect();
let left = eval_term_const(tm, out_a_term, &pin_map).and_then(|v| render_const(v, out_sort));
let right = eval_term_const(tm, out_b_term, &pin_map).and_then(|v| render_const(v, out_sort));
(left, right)
}
fn model_bitvec_value(
model: &oxiz::solver::Model,
tm: &mut TermManager,
term: TermId,
) -> Option<BigInt> {
let value_term = match model.get(term) {
Some(v) => v,
None => model.eval(term, tm),
};
match tm.get(value_term).map(|t| t.kind.clone())? {
TermKind::BitVecConst { value, .. } => Some(value),
_ => None,
}
}
#[cfg(test)]
mod eval_tests {
use super::*;
fn var_u32(tm: &mut TermManager, name: &str, value: u64) -> (TermId, (TermId, BigInt)) {
let bv32 = tm.sorts.bitvec(32);
let v = tm.mk_var(name, bv32);
(v, (v, BigInt::from(value)))
}
#[test]
fn folds_add_mul_and_sub() {
let mut tm = TermManager::new();
let (a, pin_a) = var_u32(&mut tm, "a", 5);
let (b, pin_b) = var_u32(&mut tm, "b", 3);
let two = tm.mk_bitvec(2i64, 32);
let sum = tm.mk_bv_add(a, b);
let prod = tm.mk_bv_mul(sum, two);
let diff = tm.mk_bv_sub(a, b);
let pins: HashMap<TermId, BigInt> = [pin_a, pin_b].into_iter().collect();
match eval_term_const(&tm, prod, &pins).expect("prod folds") {
ConstVal::Bv(v, w) => {
assert_eq!(v, BigInt::from(16));
assert_eq!(w, 32);
}
other => panic!("expected Bv, got {other:?}"),
}
match eval_term_const(&tm, diff, &pins).expect("diff folds") {
ConstVal::Bv(v, w) => {
assert_eq!(v, BigInt::from(2));
assert_eq!(w, 32);
}
other => panic!("expected Bv, got {other:?}"),
}
}
#[test]
fn sub_wraps_around() {
let mut tm = TermManager::new();
let (a, pin_a) = var_u32(&mut tm, "a", 3);
let (b, pin_b) = var_u32(&mut tm, "b", 5);
let diff = tm.mk_bv_sub(a, b);
let pins: HashMap<TermId, BigInt> = [pin_a, pin_b].into_iter().collect();
match eval_term_const(&tm, diff, &pins).expect("diff folds") {
ConstVal::Bv(v, _) => assert_eq!(v, BigInt::from(4_294_967_294u64)),
other => panic!("expected Bv, got {other:?}"),
}
}
#[test]
fn folds_left_shift() {
let mut tm = TermManager::new();
let (x, pin_x) = var_u32(&mut tm, "x", 0x4000_0001);
let one = tm.mk_bitvec(1i64, 32);
let shl = tm.mk_bv_shl(x, one);
let pins: HashMap<TermId, BigInt> = [pin_x].into_iter().collect();
match eval_term_const(&tm, shl, &pins).expect("shl folds") {
ConstVal::Bv(v, _) => assert_eq!(v, BigInt::from(0x8000_0002u64)),
other => panic!("expected Bv, got {other:?}"),
}
}
#[test]
fn folds_arithmetic_shift_right_negative() {
let mut tm = TermManager::new();
let (x, pin_x) = var_u32(&mut tm, "x", 0xFFFF_FFF0);
let one = tm.mk_bitvec(1i64, 32);
let ashr = tm.mk_bv_ashr(x, one);
let pins: HashMap<TermId, BigInt> = [pin_x].into_iter().collect();
match eval_term_const(&tm, ashr, &pins).expect("ashr folds") {
ConstVal::Bv(v, _) => assert_eq!(v, BigInt::from(0xFFFF_FFF8u64)),
other => panic!("expected Bv, got {other:?}"),
}
}
#[test]
fn folds_signed_less_than() {
let mut tm = TermManager::new();
let (a, pin_a) = var_u32(&mut tm, "a", 0xFFFF_FFFF); let (b, pin_b) = var_u32(&mut tm, "b", 1); let slt = tm.mk_bv_slt(a, b);
let ult = tm.mk_bv_ult(a, b);
let pins: HashMap<TermId, BigInt> = [pin_a, pin_b].into_iter().collect();
match eval_term_const(&tm, slt, &pins).expect("slt folds") {
ConstVal::Bool(b) => assert!(b, "-1 <_s 1 must be true"),
other => panic!("expected Bool, got {other:?}"),
}
match eval_term_const(&tm, ult, &pins).expect("ult folds") {
ConstVal::Bool(b) => assert!(!b, "0xFFFFFFFF <_u 1 must be false"),
other => panic!("expected Bool, got {other:?}"),
}
}
#[test]
fn folds_ite_and_eq() {
let mut tm = TermManager::new();
let (a, pin_a) = var_u32(&mut tm, "a", 7);
let (b, pin_b) = var_u32(&mut tm, "b", 9);
let eq = tm.mk_eq(a, b);
let ite = tm.mk_ite(eq, a, b);
let pins: HashMap<TermId, BigInt> = [pin_a, pin_b].into_iter().collect();
match eval_term_const(&tm, ite, &pins).expect("ite folds") {
ConstVal::Bv(v, _) => assert_eq!(v, BigInt::from(9)),
other => panic!("expected Bv, got {other:?}"),
}
}
#[test]
fn unbound_var_is_none() {
let mut tm = TermManager::new();
let bv32 = tm.sorts.bitvec(32);
let a = tm.mk_var("a", bv32);
let pins: HashMap<TermId, BigInt> = HashMap::new();
assert!(eval_term_const(&tm, a, &pins).is_none());
}
fn parse_fn(src: &str) -> syn::ItemFn {
syn::parse_str(src).expect("fn should parse")
}
#[test]
fn refute_populates_concrete_outputs() {
let original =
parse_fn("fn calc(a: u32, b: u32) -> u32 { let s = a + b; let t = s * 2; t }");
let corrupted = parse_fn("fn calc(a: u32, b: u32) -> u32 { a - b }");
let verdict = EquivBuilder::prove(&original, &corrupted);
let cx = match verdict {
Verdict::Refuted(cx) => cx,
other => panic!("expected Refuted, got {other:?}"),
};
let left = cx.left_output.expect("left output must be concrete");
let right = cx.right_output.expect("right output must be concrete");
let u32t = IntType {
width: 32,
signed: false,
};
let lookup = |name: &str| -> BigInt {
let raw = cx
.inputs
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.clone())
.unwrap_or_else(|| panic!("input `{name}` missing"));
BigInt::parse_bytes(raw.as_bytes(), 10).expect("decimal input")
};
let av = lookup("a");
let bv = lookup("b");
let mask = (BigInt::from(1u64) << 32) - BigInt::from(1u64);
let expected_left = decode_bits(&(((&av + &bv) * BigInt::from(2)) & &mask), u32t);
let modulus = BigInt::from(1u64) << 32;
let expected_right = decode_bits(&(((&av - &bv) % &modulus + &modulus) % &modulus), u32t);
assert_eq!(left, expected_left, "left output must equal (a+b)*2");
assert_eq!(
right, expected_right,
"right output must equal a-b (wrapping)"
);
assert_ne!(left, right, "a genuine counterexample must differ");
}
}