#![cfg(not(target_family = "wasm"))]
use crate::eval::{self, Env};
use crate::sliver::{self, ArrayTerm, ExtBoolTerm, ExtBvTerm, ExtOp};
use crate::term::{BoolTerm, BvTerm, Sort};
use std::collections::BTreeMap;
use z3::ast::{Array, Ast, BV, Bool};
use z3::{FuncDecl, Params, SatResult, Solver, Sort as Z3Sort};
fn mask(width: u32) -> u128 {
if width >= 128 {
u128::MAX
} else {
(1u128 << width) - 1
}
}
fn bv_const(value: u128, width: u32) -> BV {
let masked = value & mask(width);
if width <= 64 {
BV::from_u64(masked as u64, width)
} else {
BV::from_str(width, &masked.to_string()).expect("decimal bitvector numeral")
}
}
pub fn bv_to_z3(term: &BvTerm) -> BV {
match term {
BvTerm::Const { value, sort } => bv_const(*value, sort.width),
BvTerm::Var { name, sort } => BV::new_const(name.as_str(), sort.width),
BvTerm::Add(a, b) => bv_to_z3(a).bvadd(bv_to_z3(b)),
BvTerm::Sub(a, b) => bv_to_z3(a).bvsub(bv_to_z3(b)),
BvTerm::Mul(a, b) => bv_to_z3(a).bvmul(bv_to_z3(b)),
BvTerm::Udiv(a, b) => bv_to_z3(a).bvudiv(bv_to_z3(b)),
BvTerm::And(a, b) => bv_to_z3(a).bvand(bv_to_z3(b)),
BvTerm::Or(a, b) => bv_to_z3(a).bvor(bv_to_z3(b)),
BvTerm::Xor(a, b) => bv_to_z3(a).bvxor(bv_to_z3(b)),
BvTerm::Shl(a, b) => bv_to_z3(a).bvshl(bv_to_z3(b)),
BvTerm::Lshr(a, b) => bv_to_z3(a).bvlshr(bv_to_z3(b)),
BvTerm::Ashr(a, b) => bv_to_z3(a).bvashr(bv_to_z3(b)),
BvTerm::Rotr(a, b) => bv_to_z3(a).bvrotr(bv_to_z3(b)),
BvTerm::Extract { hi, lo, arg } => bv_to_z3(arg).extract(*hi, *lo),
BvTerm::Concat(a, b) => bv_to_z3(a).concat(bv_to_z3(b)),
BvTerm::ZeroExt { by, arg } => bv_to_z3(arg).zero_ext(*by),
BvTerm::SignExt { by, arg } => bv_to_z3(arg).sign_ext(*by),
}
}
pub fn to_z3(term: &BoolTerm) -> Bool {
match term {
BoolTerm::Eq(a, b) => bv_to_z3(a).eq(bv_to_z3(b)),
BoolTerm::Ne(a, b) => bv_to_z3(a).eq(bv_to_z3(b)).not(),
BoolTerm::Ult(a, b) => bv_to_z3(a).bvult(bv_to_z3(b)),
BoolTerm::Ule(a, b) => bv_to_z3(a).bvule(bv_to_z3(b)),
BoolTerm::Ugt(a, b) => bv_to_z3(a).bvugt(bv_to_z3(b)),
BoolTerm::Uge(a, b) => bv_to_z3(a).bvuge(bv_to_z3(b)),
BoolTerm::Slt(a, b) => bv_to_z3(a).bvslt(bv_to_z3(b)),
BoolTerm::Sle(a, b) => bv_to_z3(a).bvsle(bv_to_z3(b)),
BoolTerm::Sgt(a, b) => bv_to_z3(a).bvsgt(bv_to_z3(b)),
BoolTerm::Sge(a, b) => bv_to_z3(a).bvsge(bv_to_z3(b)),
BoolTerm::Not(t) => to_z3(t).not(),
BoolTerm::And(a, b) => Bool::and(&[to_z3(a), to_z3(b)]),
BoolTerm::Or(a, b) => Bool::or(&[to_z3(a), to_z3(b)]),
}
}
fn array_to_z3(array: &ArrayTerm) -> Array {
match array {
ArrayTerm::Var { name } => {
Array::new_const(name.as_str(), &Z3Sort::bitvector(32), &Z3Sort::bitvector(8))
}
ArrayTerm::Store {
array,
index,
value,
} => array_to_z3(array).store(&ext_bv_to_z3(index), &ext_bv_to_z3(value)),
}
}
fn ext_bv_to_z3(term: &ExtBvTerm) -> BV {
match term {
ExtBvTerm::Core(bv) => bv_to_z3(bv),
ExtBvTerm::Op(op) => ext_op_to_z3(op),
ExtBvTerm::Select { array, index } => array_to_z3(array)
.select(&ext_bv_to_z3(index))
.as_bv()
.expect("array range is a bitvector sort"),
ExtBvTerm::PureCall { name, args, sort } => {
let zargs: Vec<BV> = args.iter().map(ext_bv_to_z3).collect();
let domain: Vec<Z3Sort> = zargs
.iter()
.map(|a| Z3Sort::bitvector(a.get_size()))
.collect();
let domain_refs: Vec<&Z3Sort> = domain.iter().collect();
let f = FuncDecl::new(name.as_str(), &domain_refs, &Z3Sort::bitvector(sort.width));
let arg_refs: Vec<&dyn Ast> = zargs.iter().map(|b| b as &dyn Ast).collect();
f.apply(&arg_refs)
.as_bv()
.expect("pure_call result is a bitvector sort")
}
}
}
fn ext_op_to_z3(op: &ExtOp) -> BV {
match op {
ExtOp::Add(a, b) => ext_bv_to_z3(a).bvadd(ext_bv_to_z3(b)),
ExtOp::Sub(a, b) => ext_bv_to_z3(a).bvsub(ext_bv_to_z3(b)),
ExtOp::Mul(a, b) => ext_bv_to_z3(a).bvmul(ext_bv_to_z3(b)),
ExtOp::Udiv(a, b) => ext_bv_to_z3(a).bvudiv(ext_bv_to_z3(b)),
ExtOp::And(a, b) => ext_bv_to_z3(a).bvand(ext_bv_to_z3(b)),
ExtOp::Or(a, b) => ext_bv_to_z3(a).bvor(ext_bv_to_z3(b)),
ExtOp::Xor(a, b) => ext_bv_to_z3(a).bvxor(ext_bv_to_z3(b)),
ExtOp::Shl(a, b) => ext_bv_to_z3(a).bvshl(ext_bv_to_z3(b)),
ExtOp::Lshr(a, b) => ext_bv_to_z3(a).bvlshr(ext_bv_to_z3(b)),
ExtOp::Ashr(a, b) => ext_bv_to_z3(a).bvashr(ext_bv_to_z3(b)),
ExtOp::Rotr(a, b) => ext_bv_to_z3(a).bvrotr(ext_bv_to_z3(b)),
ExtOp::Extract { hi, lo, arg } => ext_bv_to_z3(arg).extract(*hi, *lo),
ExtOp::Concat(a, b) => ext_bv_to_z3(a).concat(ext_bv_to_z3(b)),
ExtOp::ZeroExt { by, arg } => ext_bv_to_z3(arg).zero_ext(*by),
ExtOp::SignExt { by, arg } => ext_bv_to_z3(arg).sign_ext(*by),
}
}
fn ext_bool_to_z3(term: &ExtBoolTerm) -> Bool {
match term {
ExtBoolTerm::Eq(a, b) => ext_bv_to_z3(a).eq(ext_bv_to_z3(b)),
ExtBoolTerm::Ne(a, b) => ext_bv_to_z3(a).eq(ext_bv_to_z3(b)).not(),
ExtBoolTerm::Ult(a, b) => ext_bv_to_z3(a).bvult(ext_bv_to_z3(b)),
ExtBoolTerm::Ule(a, b) => ext_bv_to_z3(a).bvule(ext_bv_to_z3(b)),
ExtBoolTerm::Ugt(a, b) => ext_bv_to_z3(a).bvugt(ext_bv_to_z3(b)),
ExtBoolTerm::Uge(a, b) => ext_bv_to_z3(a).bvuge(ext_bv_to_z3(b)),
ExtBoolTerm::Slt(a, b) => ext_bv_to_z3(a).bvslt(ext_bv_to_z3(b)),
ExtBoolTerm::Sle(a, b) => ext_bv_to_z3(a).bvsle(ext_bv_to_z3(b)),
ExtBoolTerm::Sgt(a, b) => ext_bv_to_z3(a).bvsgt(ext_bv_to_z3(b)),
ExtBoolTerm::Sge(a, b) => ext_bv_to_z3(a).bvsge(ext_bv_to_z3(b)),
ExtBoolTerm::Not(t) => ext_bool_to_z3(t).not(),
ExtBoolTerm::And(a, b) => Bool::and(&[ext_bool_to_z3(a), ext_bool_to_z3(b)]),
ExtBoolTerm::Or(a, b) => Bool::or(&[ext_bool_to_z3(a), ext_bool_to_z3(b)]),
}
}
pub fn sliver_z3_check(assertions: &[ExtBoolTerm]) -> OracleVerdict {
let solver = Solver::new();
let mut params = Params::new();
params.set_u32("timeout", 5_000);
solver.set_params(¶ms);
for a in assertions {
solver.assert(ext_bool_to_z3(a));
}
match solver.check() {
SatResult::Unsat => OracleVerdict::Unsat,
SatResult::Unknown => OracleVerdict::Unknown,
SatResult::Sat => OracleVerdict::Sat(Env::new()),
}
}
#[derive(Clone, Debug)]
pub struct SliverDisagreement {
pub engine: EngineVerdict,
pub oracle: OracleVerdict,
pub assertions: Vec<ExtBoolTerm>,
}
pub fn sliver_differential(assertions: &[ExtBoolTerm]) -> Option<SliverDisagreement> {
let core = match sliver::lower(assertions) {
Ok(c) => c,
Err(_) => return None,
};
let engine = ordeal_engine(&core);
if engine == EngineVerdict::Unknown {
return None;
}
let oracle = sliver_z3_check(assertions);
let disagrees = match (&engine, &oracle) {
(EngineVerdict::Unknown, _) | (_, OracleVerdict::Unknown) => false,
(EngineVerdict::Sat(_), OracleVerdict::Unsat)
| (EngineVerdict::Unsat, OracleVerdict::Sat(_)) => true,
(EngineVerdict::Sat(_), OracleVerdict::Sat(_))
| (EngineVerdict::Unsat, OracleVerdict::Unsat) => false,
};
disagrees.then(|| SliverDisagreement {
engine,
oracle,
assertions: assertions.to_vec(),
})
}
const ARRAYS: [&str; 2] = ["a", "b"];
const SLIVER_VARS: [&str; 2] = ["x", "y"];
fn gen_ext_bv(rng: &mut XorShift64, depth: u32) -> ExtBvTerm {
if depth == 0 || rng.below(2) == 0 {
return match rng.below(4) {
0 => ExtBvTerm::Core(BvTerm::Const {
value: rng.below(256) as u128,
sort: Sort::new(8),
}),
1 => ExtBvTerm::Core(BvTerm::Var {
name: SLIVER_VARS[rng.below(2) as usize].into(),
sort: Sort::new(8),
}),
2 => {
let d = rng.below(3) as u32;
let arr = gen_ext_array(rng, d);
ExtBvTerm::Select {
array: Box::new(arr),
index: Box::new(concrete_index(rng)),
}
}
_ => {
if rng.below(2) == 0 {
ExtBvTerm::PureCall {
name: "f".into(),
args: vec![gen_ext_bv(rng, depth.saturating_sub(1))],
sort: Sort::new(8),
}
} else {
ExtBvTerm::PureCall {
name: "g".into(),
args: vec![
gen_ext_bv(rng, depth.saturating_sub(1)),
gen_ext_bv(rng, depth.saturating_sub(1)),
],
sort: Sort::new(8),
}
}
}
};
}
let a = gen_ext_bv(rng, depth - 1);
let b = gen_ext_bv(rng, depth - 1);
ExtBvTerm::Op(Box::new(match rng.below(4) {
0 => ExtOp::Add(a, b),
1 => ExtOp::Sub(a, b),
2 => ExtOp::Xor(a, b),
_ => ExtOp::And(a, b),
}))
}
fn concrete_index(rng: &mut XorShift64) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Const {
value: rng.below(4) as u128,
sort: Sort::new(32),
})
}
fn gen_ext_array(rng: &mut XorShift64, depth: u32) -> ArrayTerm {
let base = ArrayTerm::Var {
name: ARRAYS[rng.below(2) as usize].into(),
};
(0..depth).fold(base, |acc, _| ArrayTerm::Store {
array: Box::new(acc),
index: Box::new(concrete_index(rng)),
value: Box::new(gen_ext_bv(rng, 1)),
})
}
fn gen_ext_bool(rng: &mut XorShift64, depth: u32) -> ExtBoolTerm {
if depth == 0 {
let (a, b) = (gen_ext_bv(rng, 2), gen_ext_bv(rng, 2));
return match rng.below(6) {
0 => ExtBoolTerm::Eq(a, b),
1 => ExtBoolTerm::Ne(a, b),
2 => ExtBoolTerm::Ult(a, b),
3 => ExtBoolTerm::Ule(a, b),
4 => ExtBoolTerm::Slt(a, b),
_ => ExtBoolTerm::Sle(a, b),
};
}
match rng.below(3) {
0 => ExtBoolTerm::Not(Box::new(gen_ext_bool(rng, depth - 1))),
1 => ExtBoolTerm::And(
Box::new(gen_ext_bool(rng, depth - 1)),
Box::new(gen_ext_bool(rng, depth - 1)),
),
_ => ExtBoolTerm::Or(
Box::new(gen_ext_bool(rng, depth - 1)),
Box::new(gen_ext_bool(rng, depth - 1)),
),
}
}
pub fn gen_sliver_corpus(seed: u64, n: usize) -> Vec<Vec<ExtBoolTerm>> {
let mut rng = XorShift64::new(seed);
(0..n)
.map(|_| {
let k = 1 + rng.below(3) as usize;
(0..k).map(|_| gen_ext_bool(&mut rng, 2)).collect()
})
.collect()
}
fn collect_bv_vars(term: &BvTerm, out: &mut BTreeMap<String, u32>) {
match term {
BvTerm::Const { .. } => {}
BvTerm::Var { name, sort } => {
out.insert(name.clone(), sort.width);
}
BvTerm::Add(a, b)
| BvTerm::Sub(a, b)
| BvTerm::Mul(a, b)
| BvTerm::Udiv(a, b)
| BvTerm::And(a, b)
| BvTerm::Or(a, b)
| BvTerm::Xor(a, b)
| BvTerm::Shl(a, b)
| BvTerm::Lshr(a, b)
| BvTerm::Ashr(a, b)
| BvTerm::Rotr(a, b)
| BvTerm::Concat(a, b) => {
collect_bv_vars(a, out);
collect_bv_vars(b, out);
}
BvTerm::Extract { arg, .. } | BvTerm::ZeroExt { arg, .. } | BvTerm::SignExt { arg, .. } => {
collect_bv_vars(arg, out);
}
}
}
fn collect_bool_vars(term: &BoolTerm, out: &mut BTreeMap<String, u32>) {
match term {
BoolTerm::Eq(a, b)
| BoolTerm::Ne(a, b)
| BoolTerm::Ult(a, b)
| BoolTerm::Ule(a, b)
| BoolTerm::Ugt(a, b)
| BoolTerm::Uge(a, b)
| BoolTerm::Slt(a, b)
| BoolTerm::Sle(a, b)
| BoolTerm::Sgt(a, b)
| BoolTerm::Sge(a, b) => {
collect_bv_vars(a, out);
collect_bv_vars(b, out);
}
BoolTerm::Not(t) => collect_bool_vars(t, out),
BoolTerm::And(a, b) | BoolTerm::Or(a, b) => {
collect_bool_vars(a, out);
collect_bool_vars(b, out);
}
}
}
fn free_vars(assertions: &[BoolTerm]) -> BTreeMap<String, u32> {
let mut out = BTreeMap::new();
for a in assertions {
collect_bool_vars(a, &mut out);
}
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OracleVerdict {
Sat(Env),
Unsat,
Unknown,
}
pub fn z3_check(assertions: &[BoolTerm]) -> OracleVerdict {
let solver = Solver::new();
let mut params = Params::new();
params.set_u32("timeout", 2_000);
solver.set_params(¶ms);
for a in assertions {
solver.assert(to_z3(a));
}
match solver.check() {
SatResult::Unsat => OracleVerdict::Unsat,
SatResult::Unknown => OracleVerdict::Unknown,
SatResult::Sat => {
let model = match solver.get_model() {
Some(m) => m,
None => return OracleVerdict::Unknown,
};
let mut env = Env::new();
for (name, width) in free_vars(assertions) {
assert!(width <= 64, "model extraction supports widths <= 64");
let var = BV::new_const(name.as_str(), width);
let value = model
.eval(&var, true)
.and_then(|v| v.as_u64())
.expect("completed model must value every variable");
env.insert(name, value as u128);
}
OracleVerdict::Sat(env)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EngineVerdict {
Sat(Env),
Unsat,
Unknown,
}
#[derive(Clone, Debug)]
pub struct Disagreement {
pub engine: EngineVerdict,
pub oracle: OracleVerdict,
pub assertions: Vec<BoolTerm>,
}
fn model_checks_out(env: &Env, assertions: &[BoolTerm]) -> bool {
assertions
.iter()
.all(|a| eval::eval_bool(a, env) == Ok(true))
}
pub fn differential_check(
assertions: &[BoolTerm],
engine: impl Fn(&[BoolTerm]) -> EngineVerdict,
) -> Option<Disagreement> {
let engine_verdict = engine(assertions);
if engine_verdict == EngineVerdict::Unknown {
return None;
}
let oracle_verdict = z3_check(assertions);
let disagrees = match (&engine_verdict, &oracle_verdict) {
(EngineVerdict::Unknown, _) | (_, OracleVerdict::Unknown) => false,
(EngineVerdict::Sat(_), OracleVerdict::Unsat)
| (EngineVerdict::Unsat, OracleVerdict::Sat(_)) => true,
(EngineVerdict::Unsat, OracleVerdict::Unsat) => false,
(EngineVerdict::Sat(engine_env), OracleVerdict::Sat(oracle_env)) => {
!model_checks_out(engine_env, assertions) || !model_checks_out(oracle_env, assertions)
}
};
disagrees.then(|| Disagreement {
engine: engine_verdict,
oracle: oracle_verdict,
assertions: assertions.to_vec(),
})
}
pub fn ordeal_engine(assertions: &[BoolTerm]) -> EngineVerdict {
let mut solver = crate::solver::Solver::new();
for a in assertions {
solver.assert(a.clone());
}
match solver.check_raw() {
crate::solver::RawVerdict::Sat(env) => EngineVerdict::Sat(env),
crate::solver::RawVerdict::Unsat => EngineVerdict::Unsat,
crate::solver::RawVerdict::Unknown => EngineVerdict::Unknown,
}
}
const WIDTHS: [u32; 3] = [8, 32, 64];
const MAX_DEPTH: u32 = 5;
struct XorShift64 {
state: u64,
}
impl XorShift64 {
fn new(seed: u64) -> Self {
Self {
state: if seed == 0 {
0x2545_F491_4F6C_DD1D
} else {
seed
},
}
}
fn next(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn width(&mut self) -> u32 {
WIDTHS[self.below(WIDTHS.len() as u64) as usize]
}
}
fn gen_leaf(rng: &mut XorShift64, width: u32, vars: &[(String, u32)]) -> BvTerm {
let candidates: Vec<&(String, u32)> = vars.iter().filter(|(_, w)| *w == width).collect();
if !candidates.is_empty() && rng.below(2) == 0 {
let (name, w) = candidates[rng.below(candidates.len() as u64) as usize];
BvTerm::Var {
name: name.clone(),
sort: Sort::new(*w),
}
} else {
BvTerm::Const {
value: rng.next() as u128,
sort: Sort::new(width),
}
}
}
fn gen_bv(rng: &mut XorShift64, width: u32, depth: u32, vars: &[(String, u32)]) -> BvTerm {
if depth == 0 {
return gen_leaf(rng, width, vars);
}
let bin = |rng: &mut XorShift64| {
let a = Box::new(gen_bv(rng, width, depth - 1, vars));
let b = Box::new(gen_bv(rng, width, depth - 1, vars));
(a, b)
};
match rng.below(15) {
0 => {
let (a, b) = bin(rng);
BvTerm::Add(a, b)
}
1 => {
let (a, b) = bin(rng);
BvTerm::Sub(a, b)
}
2 => {
let (a, b) = bin(rng);
BvTerm::Mul(a, b)
}
3 => {
let (a, b) = bin(rng);
BvTerm::Udiv(a, b)
}
4 => {
let (a, b) = bin(rng);
BvTerm::And(a, b)
}
5 => {
let (a, b) = bin(rng);
BvTerm::Or(a, b)
}
6 => {
let (a, b) = bin(rng);
BvTerm::Xor(a, b)
}
7 => {
let (a, b) = bin(rng);
BvTerm::Shl(a, b)
}
8 => {
let (a, b) = bin(rng);
BvTerm::Lshr(a, b)
}
9 => {
let (a, b) = bin(rng);
BvTerm::Ashr(a, b)
}
10 => {
let (a, b) = bin(rng);
BvTerm::Rotr(a, b)
}
11 => {
let sources: Vec<u32> = WIDTHS.iter().copied().filter(|w| *w >= width).collect();
let src = sources[rng.below(sources.len() as u64) as usize];
let lo = rng.below((src - width + 1) as u64) as u32;
BvTerm::Extract {
hi: lo + width - 1,
lo,
arg: Box::new(gen_bv(rng, src, depth - 1, vars)),
}
}
12 if width == 64 => {
BvTerm::Concat(
Box::new(gen_bv(rng, 32, depth - 1, vars)),
Box::new(gen_bv(rng, 32, depth - 1, vars)),
)
}
13 | 14 if width > 8 => {
let sources: Vec<u32> = WIDTHS.iter().copied().filter(|w| *w < width).collect();
let src = sources[rng.below(sources.len() as u64) as usize];
let arg = Box::new(gen_bv(rng, src, depth - 1, vars));
if rng.below(2) == 0 {
BvTerm::ZeroExt {
by: width - src,
arg,
}
} else {
BvTerm::SignExt {
by: width - src,
arg,
}
}
}
_ => gen_leaf(rng, width, vars),
}
}
fn gen_bool(rng: &mut XorShift64, depth: u32, vars: &[(String, u32)]) -> BoolTerm {
let cmp = |rng: &mut XorShift64| {
let w = rng.width();
let a = Box::new(gen_bv(rng, w, depth.saturating_sub(1), vars));
let b = Box::new(gen_bv(rng, w, depth.saturating_sub(1), vars));
(a, b)
};
let choices = if depth == 0 { 10 } else { 13 };
match rng.below(choices) {
0 => {
let (a, b) = cmp(rng);
BoolTerm::Eq(a, b)
}
1 => {
let (a, b) = cmp(rng);
BoolTerm::Ne(a, b)
}
2 => {
let (a, b) = cmp(rng);
BoolTerm::Ult(a, b)
}
3 => {
let (a, b) = cmp(rng);
BoolTerm::Ule(a, b)
}
4 => {
let (a, b) = cmp(rng);
BoolTerm::Ugt(a, b)
}
5 => {
let (a, b) = cmp(rng);
BoolTerm::Uge(a, b)
}
6 => {
let (a, b) = cmp(rng);
BoolTerm::Slt(a, b)
}
7 => {
let (a, b) = cmp(rng);
BoolTerm::Sle(a, b)
}
8 => {
let (a, b) = cmp(rng);
BoolTerm::Sgt(a, b)
}
9 => {
let (a, b) = cmp(rng);
BoolTerm::Sge(a, b)
}
10 => BoolTerm::Not(Box::new(gen_bool(rng, depth - 1, vars))),
11 => BoolTerm::And(
Box::new(gen_bool(rng, depth - 1, vars)),
Box::new(gen_bool(rng, depth - 1, vars)),
),
_ => BoolTerm::Or(
Box::new(gen_bool(rng, depth - 1, vars)),
Box::new(gen_bool(rng, depth - 1, vars)),
),
}
}
fn gen_corpus_with(seed: u64, n: usize, n_vars: usize) -> Vec<Vec<BoolTerm>> {
let mut rng = XorShift64::new(seed);
(0..n)
.map(|_| {
let vars: Vec<(String, u32)> = (0..n_vars)
.map(|i| (format!("v{i}"), rng.width()))
.collect();
let n_assertions = 1 + rng.below(3);
(0..n_assertions)
.map(|_| gen_bool(&mut rng, MAX_DEPTH, &vars))
.collect()
})
.collect()
}
pub fn gen_corpus(seed: u64, n: usize) -> Vec<Vec<BoolTerm>> {
let mut rng = XorShift64::new(seed);
(0..n)
.map(|_| {
let n_vars = 2 + rng.below(2) as usize;
let seed_i = rng.next();
gen_corpus_with(seed_i, 1, n_vars).pop().expect("one query")
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::eval_bool;
fn const_query_truth(query: &[BoolTerm]) -> bool {
query
.iter()
.all(|a| eval_bool(a, &Env::new()).expect("well-sorted constant query"))
}
#[test]
fn translation_matches_eval_on_constants() {
let corpus = gen_corpus_with(0x0DDEA1, 150, 0);
for (i, query) in corpus.iter().enumerate() {
let expected_sat = const_query_truth(query);
match z3_check(query) {
OracleVerdict::Sat(_) => {
assert!(expected_sat, "query {i}: z3 Sat but eval says false")
}
OracleVerdict::Unsat => {
assert!(!expected_sat, "query {i}: z3 Unsat but eval says true")
}
OracleVerdict::Unknown => panic!("query {i}: z3 Unknown on a constant query"),
}
}
}
#[test]
fn sat_models_satisfy_every_assertion() {
let corpus = gen_corpus(0x0D1F_FCEC, 100);
let mut sats = 0usize;
for (i, query) in corpus.iter().enumerate() {
if let OracleVerdict::Sat(env) = z3_check(query) {
sats += 1;
for (j, a) in query.iter().enumerate() {
assert_eq!(
eval_bool(a, &env),
Ok(true),
"query {i} assertion {j}: model does not satisfy it\nenv: {env:?}"
);
}
}
}
assert!(
sats > 0,
"corpus produced no SAT queries — generator broken"
);
}
#[test]
fn corpus_exercises_every_variant() {
fn mark_bv(t: &BvTerm, bv: &mut [bool; 16]) {
let (idx, children): (usize, Vec<&BvTerm>) = match t {
BvTerm::Const { .. } => (0, vec![]),
BvTerm::Var { .. } => (1, vec![]),
BvTerm::Add(a, b) => (2, vec![a, b]),
BvTerm::Sub(a, b) => (3, vec![a, b]),
BvTerm::Mul(a, b) => (4, vec![a, b]),
BvTerm::Udiv(a, b) => (5, vec![a, b]),
BvTerm::And(a, b) => (6, vec![a, b]),
BvTerm::Or(a, b) => (7, vec![a, b]),
BvTerm::Xor(a, b) => (8, vec![a, b]),
BvTerm::Shl(a, b) => (9, vec![a, b]),
BvTerm::Lshr(a, b) => (10, vec![a, b]),
BvTerm::Ashr(a, b) => (11, vec![a, b]),
BvTerm::Rotr(a, b) => (12, vec![a, b]),
BvTerm::Extract { arg, .. } => (13, vec![arg]),
BvTerm::Concat(a, b) => (14, vec![a, b]),
BvTerm::ZeroExt { arg, .. } | BvTerm::SignExt { arg, .. } => (15, vec![arg]),
};
bv[idx] = true;
for c in children {
mark_bv(c, bv);
}
}
fn mark_bool(t: &BoolTerm, bl: &mut [bool; 13], bv: &mut [bool; 16], zs: &mut [bool; 2]) {
let (idx, bvs, bools): (usize, Vec<&BvTerm>, Vec<&BoolTerm>) = match t {
BoolTerm::Eq(a, b) => (0, vec![a, b], vec![]),
BoolTerm::Ne(a, b) => (1, vec![a, b], vec![]),
BoolTerm::Ult(a, b) => (2, vec![a, b], vec![]),
BoolTerm::Ule(a, b) => (3, vec![a, b], vec![]),
BoolTerm::Ugt(a, b) => (4, vec![a, b], vec![]),
BoolTerm::Uge(a, b) => (5, vec![a, b], vec![]),
BoolTerm::Slt(a, b) => (6, vec![a, b], vec![]),
BoolTerm::Sle(a, b) => (7, vec![a, b], vec![]),
BoolTerm::Sgt(a, b) => (8, vec![a, b], vec![]),
BoolTerm::Sge(a, b) => (9, vec![a, b], vec![]),
BoolTerm::Not(t) => (10, vec![], vec![t]),
BoolTerm::And(a, b) => (11, vec![], vec![a, b]),
BoolTerm::Or(a, b) => (12, vec![], vec![a, b]),
};
bl[idx] = true;
for t in bvs {
mark_bv(t, bv);
mark_zs(t, zs);
}
for t in bools {
mark_bool(t, bl, bv, zs);
}
}
fn mark_zs(t: &BvTerm, zs: &mut [bool; 2]) {
match t {
BvTerm::ZeroExt { arg, .. } => {
zs[0] = true;
mark_zs(arg, zs);
}
BvTerm::SignExt { arg, .. } => {
zs[1] = true;
mark_zs(arg, zs);
}
BvTerm::Const { .. } | BvTerm::Var { .. } => {}
BvTerm::Add(a, b)
| BvTerm::Sub(a, b)
| BvTerm::Mul(a, b)
| BvTerm::Udiv(a, b)
| BvTerm::And(a, b)
| BvTerm::Or(a, b)
| BvTerm::Xor(a, b)
| BvTerm::Shl(a, b)
| BvTerm::Lshr(a, b)
| BvTerm::Ashr(a, b)
| BvTerm::Rotr(a, b)
| BvTerm::Concat(a, b) => {
mark_zs(a, zs);
mark_zs(b, zs);
}
BvTerm::Extract { arg, .. } => mark_zs(arg, zs),
}
}
let mut bv = [false; 16];
let mut bl = [false; 13];
let mut zs = [false; 2];
for query in gen_corpus(0xC0FFEE, 150) {
for a in &query {
mark_bool(a, &mut bl, &mut bv, &mut zs);
}
}
assert!(bv.iter().all(|&c| c), "uncovered BvTerm variant: {bv:?}");
assert!(bl.iter().all(|&c| c), "uncovered BoolTerm variant: {bl:?}");
assert!(
zs.iter().all(|&c| c),
"ZeroExt/SignExt not both covered: {zs:?}"
);
}
#[test]
fn corpus_is_reproducible() {
let a = format!("{:?}", gen_corpus(42, 10));
let b = format!("{:?}", gen_corpus(42, 10));
assert_eq!(a, b);
}
fn x_is_five() -> Vec<BoolTerm> {
vec![BoolTerm::Eq(
Box::new(BvTerm::Var {
name: "x".into(),
sort: Sort::new(8),
}),
Box::new(BvTerm::Const {
value: 5,
sort: Sort::new(8),
}),
)]
}
#[test]
fn unknown_engine_never_disagrees() {
for query in gen_corpus(0xA6BEE, 20) {
assert!(
differential_check(&query, |_| EngineVerdict::Unknown).is_none(),
"Unknown must never disagree"
);
}
}
#[test]
fn wrong_sat_model_disagrees() {
let query = x_is_five();
let good = Env::from([("x".to_string(), 5u128)]);
assert!(differential_check(&query, |_| EngineVerdict::Sat(good.clone())).is_none());
let bad = Env::from([("x".to_string(), 4u128)]);
let d = differential_check(&query, |_| EngineVerdict::Sat(bad.clone()))
.expect("bad model must disagree");
assert_eq!(d.engine, EngineVerdict::Sat(bad));
let d = differential_check(&query, |_| EngineVerdict::Unsat)
.expect("Unsat vs Sat must disagree");
assert!(matches!(d.oracle, OracleVerdict::Sat(_)));
}
#[test]
fn differential_ordeal_vs_z3_on_corpus() {
let corpus = gen_corpus(0xC0FF_EE00_5EED_0001, 300);
let (mut sats, mut unsats, mut unknowns) = (0u32, 0u32, 0u32);
for (i, query) in corpus.iter().enumerate() {
match ordeal_engine(query) {
EngineVerdict::Sat(_) => sats += 1,
EngineVerdict::Unsat => unsats += 1,
EngineVerdict::Unknown => unknowns += 1,
}
if let Some(d) = differential_check(query, ordeal_engine) {
panic!("corpus query {i}: ordeal-vs-Z3 disagreement — {d:?}");
}
}
assert!(sats > 0, "corpus produced no engine-SAT verdicts");
assert!(unsats > 0, "corpus produced no engine-UNSAT verdicts");
assert_eq!(unknowns, 0, "unexpected Unknown verdicts: {unknowns}");
}
}
#[cfg(test)]
mod sliver_oracle_tests {
use super::*;
use crate::sliver::{ArrayTerm, ExtBoolTerm, ExtBvTerm, SliverError};
fn c8(v: u128) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Const {
value: v,
sort: Sort::new(8),
})
}
fn v8(name: &str) -> ExtBvTerm {
ExtBvTerm::Core(BvTerm::Var {
name: name.into(),
sort: Sort::new(8),
})
}
fn call(name: &str, args: Vec<ExtBvTerm>) -> ExtBvTerm {
ExtBvTerm::PureCall {
name: name.into(),
args,
sort: Sort::new(8),
}
}
#[test]
fn sliver_diff() {
let corpus = gen_sliver_corpus(0x5111_5EED_A55A_0001, 300);
let (mut sats, mut unsats) = (0u32, 0u32);
for (i, query) in corpus.iter().enumerate() {
match ordeal_engine(&crate::sliver::lower(query).expect("in-sliver")) {
EngineVerdict::Sat(_) => sats += 1,
EngineVerdict::Unsat => unsats += 1,
EngineVerdict::Unknown => {}
}
if let Some(d) = sliver_differential(query) {
panic!("sliver corpus query {i}: ordeal-vs-Z3 disagreement — {d:?}");
}
}
assert!(sats > 0, "corpus produced no SAT verdicts");
assert!(unsats > 0, "corpus produced no UNSAT verdicts");
}
#[test]
fn sliver_diff_rejects_out_of_sliver() {
let sym = ExtBoolTerm::Eq(
ExtBvTerm::Select {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(v8("i_is_bv8")), },
c8(0),
);
assert_eq!(
crate::sliver::lower(&[sym]).err(),
Some(SliverError::BadArraySort)
);
let sym32 = ExtBoolTerm::Eq(
ExtBvTerm::Select {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(ExtBvTerm::Core(BvTerm::Var {
name: "i".into(),
sort: Sort::new(32),
})),
},
c8(0),
);
assert_eq!(
crate::sliver::lower(&[sym32]).err(),
Some(SliverError::NonConcreteIndex)
);
assert!(sliver_differential(&[sym32_query()]).is_none());
}
fn sym32_query() -> ExtBoolTerm {
ExtBoolTerm::Eq(
ExtBvTerm::Select {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(ExtBvTerm::Core(BvTerm::Var {
name: "i".into(),
sort: Sort::new(32),
})),
},
c8(0),
)
}
#[test]
fn sliver_uf() {
let unsat = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(5)),
ExtBoolTerm::Eq(call("f", vec![v8("y")]), c8(7)),
ExtBoolTerm::Eq(v8("x"), v8("y")),
];
assert!(sliver_differential(&unsat).is_none());
assert_eq!(
ordeal_engine(&crate::sliver::lower(&unsat).unwrap()),
EngineVerdict::Unsat
);
assert_eq!(sliver_z3_check(&unsat), OracleVerdict::Unsat);
let sat = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(5)),
ExtBoolTerm::Eq(call("f", vec![v8("y")]), c8(7)),
ExtBoolTerm::Ne(v8("x"), v8("y")),
];
assert!(sliver_differential(&sat).is_none());
assert!(matches!(
ordeal_engine(&crate::sliver::lower(&sat).unwrap()),
EngineVerdict::Sat(_)
));
let corpus = gen_sliver_corpus(0x5111_0DFF_1CE0_0002, 250);
let mut uf_seen = 0u32;
for (i, query) in corpus.iter().enumerate() {
if mentions_call(query) {
uf_seen += 1;
}
if let Some(d) = sliver_differential(query) {
panic!("sliver_uf corpus query {i}: disagreement — {d:?}");
}
}
assert!(uf_seen > 0, "corpus exercised no pure_call sites");
}
#[test]
fn sliver_uf_rejects_inconsistent_call() {
let q = vec![
ExtBoolTerm::Eq(call("f", vec![v8("x")]), c8(1)),
ExtBoolTerm::Eq(call("f", vec![v8("x"), v8("y")]), c8(2)),
];
assert_eq!(
crate::sliver::lower(&q).err(),
Some(SliverError::InconsistentCall { name: "f".into() })
);
assert!(sliver_differential(&q).is_none());
}
fn mentions_call(query: &[ExtBoolTerm]) -> bool {
fn in_bv(t: &ExtBvTerm) -> bool {
match t {
ExtBvTerm::PureCall { .. } => true,
ExtBvTerm::Core(_) => false,
ExtBvTerm::Select { index, .. } => in_bv(index),
ExtBvTerm::Op(op) => in_op(op),
}
}
fn in_op(op: &crate::sliver::ExtOp) -> bool {
use crate::sliver::ExtOp::*;
match op {
Add(a, b)
| Sub(a, b)
| Mul(a, b)
| Udiv(a, b)
| And(a, b)
| Or(a, b)
| Xor(a, b)
| Shl(a, b)
| Lshr(a, b)
| Ashr(a, b)
| Rotr(a, b)
| Concat(a, b) => in_bv(a) || in_bv(b),
Extract { arg, .. } | ZeroExt { arg, .. } | SignExt { arg, .. } => in_bv(arg),
}
}
fn in_bool(t: &ExtBoolTerm) -> bool {
use ExtBoolTerm::*;
match t {
Eq(a, b)
| Ne(a, b)
| Ult(a, b)
| Ule(a, b)
| Ugt(a, b)
| Uge(a, b)
| Slt(a, b)
| Sle(a, b)
| Sgt(a, b)
| Sge(a, b) => in_bv(a) || in_bv(b),
Not(x) => in_bool(x),
And(a, b) | Or(a, b) => in_bool(a) || in_bool(b),
}
}
query.iter().any(in_bool)
}
}