use crate::term::{BV, Bool};
use ordeal::{BoolTerm, CheckResult};
const DEFAULT_MAX_CONFLICTS: u64 = 1_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckOutcome {
Unsat,
Sat,
Unknown(String),
}
pub trait BvSolver {
fn name(&self) -> &'static str;
fn assert(&mut self, cond: &Bool);
fn check(&mut self) -> CheckOutcome;
fn value(&self, var: &BV) -> Option<u128>;
}
pub fn new_solver() -> Box<dyn BvSolver> {
#[cfg(feature = "z3-solver")]
if std::env::var("SYNTH_SOLVER_DIFF").as_deref() == Ok("1") {
return Box::new(z3_backend::DifferentialSolver::new());
}
Box::new(OrdealSolver::new())
}
pub struct OrdealSolver {
assertions: Vec<BoolTerm>,
max_conflicts: u64,
model: Option<ordeal::Model>,
}
impl OrdealSolver {
pub fn new() -> Self {
let max_conflicts = std::env::var("SYNTH_ORDEAL_MAX_CONFLICTS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MAX_CONFLICTS);
Self {
assertions: Vec::new(),
max_conflicts,
model: None,
}
}
}
impl Default for OrdealSolver {
fn default() -> Self {
Self::new()
}
}
impl BvSolver for OrdealSolver {
fn name(&self) -> &'static str {
"ordeal"
}
fn assert(&mut self, cond: &Bool) {
self.assertions.push(cond.term().clone());
}
fn check(&mut self) -> CheckOutcome {
let mut solver = ordeal::Solver::new();
for a in &self.assertions {
solver.assert(a.clone());
}
let result = if self.max_conflicts == 0 {
solver.check()
} else {
solver.check_with_limit(self.max_conflicts)
};
match result {
CheckResult::Unsat(_certificate) => CheckOutcome::Unsat,
CheckResult::Sat(model) => {
self.model = Some(model);
CheckOutcome::Sat
}
CheckResult::Unknown => CheckOutcome::Unknown(format!(
"ordeal returned unknown (conflict budget {})",
self.max_conflicts
)),
}
}
fn value(&self, var: &BV) -> Option<u128> {
let model = self.model.as_ref()?;
let name = var.var_name()?;
Some(
model
.assignments
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
.unwrap_or(0),
)
}
}
#[cfg(feature = "z3-solver")]
mod z3_backend {
use super::*;
use ordeal::BvTerm;
use z3::SatResult;
use z3::ast::Ast;
fn bv_to_z3(t: &BvTerm) -> z3::ast::BV {
match t {
BvTerm::Const { value, sort } => {
if sort.width <= 64 {
z3::ast::BV::from_u64(*value as u64, sort.width)
} else {
let hi = z3::ast::BV::from_u64((value >> 64) as u64, sort.width - 64);
let lo = z3::ast::BV::from_u64(*value as u64, 64);
hi.concat(&lo)
}
}
BvTerm::Var { name, sort } => z3::ast::BV::new_const(name.clone(), 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),
BvTerm::Ite { cond, then_, else_ } => {
bool_to_z3(cond).ite(&bv_to_z3(then_), &bv_to_z3(else_))
}
}
}
fn bool_to_z3(t: &BoolTerm) -> z3::ast::Bool {
match t {
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(a) => bool_to_z3(a).not(),
BoolTerm::And(a, b) => z3::ast::Bool::and(&[&bool_to_z3(a), &bool_to_z3(b)]),
BoolTerm::Or(a, b) => z3::ast::Bool::or(&[&bool_to_z3(a), &bool_to_z3(b)]),
}
}
pub struct Z3Solver {
assertions: Vec<BoolTerm>,
model: Option<z3::Model>,
}
impl Z3Solver {
pub fn new() -> Self {
Self {
assertions: Vec::new(),
model: None,
}
}
}
impl BvSolver for Z3Solver {
fn name(&self) -> &'static str {
"z3"
}
fn assert(&mut self, cond: &Bool) {
self.assertions.push(cond.term().clone());
}
fn check(&mut self) -> CheckOutcome {
let solver = z3::Solver::new();
for a in &self.assertions {
solver.assert(bool_to_z3(a));
}
match solver.check() {
SatResult::Unsat => CheckOutcome::Unsat,
SatResult::Sat => {
self.model = solver.get_model();
if self.model.is_some() {
CheckOutcome::Sat
} else {
CheckOutcome::Unknown("z3: SAT but no model available".to_string())
}
}
SatResult::Unknown => CheckOutcome::Unknown("z3 returned unknown".to_string()),
}
}
fn value(&self, var: &BV) -> Option<u128> {
let model = self.model.as_ref()?;
let z3_var = bv_to_z3(var.term());
model
.eval(&z3_var, true)
.and_then(|v| v.as_u64())
.map(u128::from)
}
}
#[derive(Clone, Copy, PartialEq)]
enum ModelSource {
Ordeal,
Z3,
}
pub struct DifferentialSolver {
ordeal: OrdealSolver,
z3: Z3Solver,
model_source: ModelSource,
}
impl DifferentialSolver {
pub fn new() -> Self {
Self {
ordeal: OrdealSolver::new(),
z3: Z3Solver::new(),
model_source: ModelSource::Ordeal,
}
}
}
impl BvSolver for DifferentialSolver {
fn name(&self) -> &'static str {
"ordeal+z3-differential"
}
fn assert(&mut self, cond: &Bool) {
self.ordeal.assert(cond);
self.z3.assert(cond);
}
fn check(&mut self) -> CheckOutcome {
let ordeal_verdict = self.ordeal.check();
let z3_verdict = self.z3.check();
match (&ordeal_verdict, &z3_verdict) {
(CheckOutcome::Unknown(reason), _) => {
eprintln!(
"[synth-verify differential] ordeal unknown ({reason}); \
falling through to z3 verdict: {z3_verdict:?}"
);
self.model_source = ModelSource::Z3;
z3_verdict
}
(_, CheckOutcome::Unknown(reason)) => {
eprintln!(
"[synth-verify differential] z3 unknown ({reason}); \
keeping ordeal verdict: {ordeal_verdict:?}"
);
self.model_source = ModelSource::Ordeal;
ordeal_verdict
}
(CheckOutcome::Unsat, CheckOutcome::Unsat) => CheckOutcome::Unsat,
(CheckOutcome::Sat, CheckOutcome::Sat) => {
self.model_source = ModelSource::Ordeal;
CheckOutcome::Sat
}
(o, z) => panic!(
"SOLVER DISAGREEMENT (#553 differential oracle): \
ordeal={o:?} z3={z:?} on the same query — one engine is \
unsound on this fragment; refusing to proceed"
),
}
}
fn value(&self, var: &BV) -> Option<u128> {
match self.model_source {
ModelSource::Ordeal => self.ordeal.value(var),
ModelSource::Z3 => self.z3.value(var),
}
}
}
}
#[cfg(feature = "z3-solver")]
pub use z3_backend::{DifferentialSolver, Z3Solver};
#[cfg(test)]
mod tests {
use super::*;
fn x32() -> BV {
BV::new_const("x", 32)
}
fn y32() -> BV {
BV::new_const("y", 32)
}
fn corpus() -> Vec<(&'static str, Bool, bool)> {
let x = x32();
let y = y32();
let one = BV::from_i64(1, 32);
vec![
("add-comm", x.bvadd(&y).eq(y.bvadd(&x)).not(), true),
("mul-comm", x.bvmul(&y).eq(y.bvmul(&x)).not(), true),
("and-comm", x.bvand(&y).eq(y.bvand(&x)).not(), true),
(
"shl1-vs-mul2",
x.bvshl(&one).eq(x.bvmul(BV::from_i64(2, 32))).not(),
true,
),
(
"sub-not-comm",
x.bvsub(&y).eq(y.bvsub(&x)).not(),
false, ),
(
"ite-select",
{
let c = x.eq(BV::from_i64(0, 32)).not();
c.ite(&y, &one).eq(c.ite(&y, &one)).not()
},
true,
),
(
"add-vs-sub-bug",
x.bvadd(&y).eq(x.bvsub(&y)).not(),
false, ),
]
}
#[test]
fn ordeal_decides_the_corpus() {
for (label, query, expect_unsat) in corpus() {
let mut solver = OrdealSolver::new();
solver.assert(&query);
let outcome = solver.check();
if expect_unsat {
assert_eq!(outcome, CheckOutcome::Unsat, "{label}");
} else {
assert_eq!(outcome, CheckOutcome::Sat, "{label}");
assert!(solver.value(&x32()).is_some(), "{label}: no model value");
}
}
}
#[test]
fn ordeal_finds_counterexample_with_model() {
let x = x32();
let mut solver = OrdealSolver::new();
solver.assert(
&x.bvadd(BV::from_i64(1, 32))
.eq(x.bvsub(BV::from_i64(1, 32))),
);
assert_eq!(solver.check(), CheckOutcome::Unsat);
let mut solver = OrdealSolver::new();
solver.assert(&x.eq(BV::from_i64(7, 32)));
assert_eq!(solver.check(), CheckOutcome::Sat);
assert_eq!(solver.value(&x), Some(7));
}
#[test]
fn bool_var_bridge_is_symbolic() {
let flag = Bool::new_const("flag");
let one = BV::from_i64(1, 32);
let zero = BV::from_i64(0, 32);
for (cond, expected) in [(flag.clone(), 1u128), (flag.not(), 0u128)] {
let mut solver = OrdealSolver::new();
let out = flag.ite(&one, &zero);
let probe = BV::new_const("probe", 32);
solver.assert(&probe.eq(&out));
solver.assert(&cond);
assert_eq!(solver.check(), CheckOutcome::Sat);
assert_eq!(solver.value(&probe), Some(expected));
}
}
#[cfg(feature = "z3-solver")]
#[test]
fn differential_zero_disagreements_on_corpus() {
crate::with_z3_context(|| {
for (label, query, expect_unsat) in corpus() {
let mut solver = DifferentialSolver::new();
solver.assert(&query);
let outcome = solver.check();
if expect_unsat {
assert_eq!(outcome, CheckOutcome::Unsat, "{label}");
} else {
assert_eq!(outcome, CheckOutcome::Sat, "{label}");
}
}
});
}
}