use crate::aig::{Aig, Lit, Word, word_input};
use crate::blast::{arith, bitwise, muldiv, shift, structural};
use crate::cnf::tseitin;
use crate::eval::{self, Env, EvalError};
use crate::sat::{SatResult, SatSolver};
use crate::term::{BoolTerm, BvTerm};
use std::collections::HashMap;
#[derive(Clone, Debug, Default)]
pub struct Certificate {
pub lrat: Vec<u8>,
pub cnf: Vec<Vec<i32>>,
}
#[derive(Debug)]
pub enum CertificateError {
NotText,
Rejected(ordeal_lrat::CheckError),
}
impl std::fmt::Display for CertificateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CertificateError::NotText => write!(f, "LRAT certificate is not valid UTF-8"),
CertificateError::Rejected(e) => write!(f, "checker rejected certificate: {e:?}"),
}
}
}
impl std::error::Error for CertificateError {}
impl Certificate {
pub fn lrat_text(&self) -> Option<&str> {
std::str::from_utf8(&self.lrat).ok()
}
pub fn recheck(&self) -> Result<(), CertificateError> {
let text = self.lrat_text().ok_or(CertificateError::NotText)?;
ordeal_lrat::check(&self.cnf, text).map_err(CertificateError::Rejected)
}
}
#[derive(Clone, Debug, Default)]
pub struct Model {
pub assignments: Vec<(String, u128)>,
}
#[derive(Clone, Debug)]
pub enum CheckResult {
Unsat(Certificate),
Sat(Model),
Unknown,
}
#[cfg(any(test, feature = "oracle"))]
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum RawVerdict {
Sat(Env),
Unsat,
Unknown,
}
const DISABLED_OPS: &[OpKind] = &[];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum OpKind {
Add,
Sub,
Mul,
Udiv,
And,
Or,
Xor,
Shl,
Lshr,
Ashr,
Rotr,
Extract,
Concat,
ZeroExt,
SignExt,
Eq,
Ne,
Ult,
Ule,
Ugt,
Uge,
Slt,
Sle,
Sgt,
Sge,
Ite,
BoolNot,
BoolAnd,
BoolOr,
}
fn bv_op(term: &BvTerm) -> Option<OpKind> {
Some(match term {
BvTerm::Const { .. } | BvTerm::Var { .. } => return None,
BvTerm::Add(..) => OpKind::Add,
BvTerm::Sub(..) => OpKind::Sub,
BvTerm::Mul(..) => OpKind::Mul,
BvTerm::Udiv(..) => OpKind::Udiv,
BvTerm::And(..) => OpKind::And,
BvTerm::Or(..) => OpKind::Or,
BvTerm::Xor(..) => OpKind::Xor,
BvTerm::Shl(..) => OpKind::Shl,
BvTerm::Lshr(..) => OpKind::Lshr,
BvTerm::Ashr(..) => OpKind::Ashr,
BvTerm::Rotr(..) => OpKind::Rotr,
BvTerm::Extract { .. } => OpKind::Extract,
BvTerm::Concat(..) => OpKind::Concat,
BvTerm::ZeroExt { .. } => OpKind::ZeroExt,
BvTerm::SignExt { .. } => OpKind::SignExt,
BvTerm::Ite { .. } => OpKind::Ite,
})
}
fn bv_uses_disabled(term: &BvTerm) -> bool {
if bv_op(term).is_some_and(|op| DISABLED_OPS.contains(&op)) {
return true;
}
match term {
BvTerm::Const { .. } | BvTerm::Var { .. } => false,
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) => bv_uses_disabled(a) || bv_uses_disabled(b),
BvTerm::Extract { arg, .. } | BvTerm::ZeroExt { arg, .. } | BvTerm::SignExt { arg, .. } => {
bv_uses_disabled(arg)
}
BvTerm::Ite { cond, then_, else_ } => {
bool_uses_disabled(cond) || bv_uses_disabled(then_) || bv_uses_disabled(else_)
}
}
}
fn bool_uses_disabled(term: &BoolTerm) -> bool {
let (op, kids_disabled) = match term {
BoolTerm::Eq(a, b) => (OpKind::Eq, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Ne(a, b) => (OpKind::Ne, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Ult(a, b) => (OpKind::Ult, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Ule(a, b) => (OpKind::Ule, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Ugt(a, b) => (OpKind::Ugt, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Uge(a, b) => (OpKind::Uge, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Slt(a, b) => (OpKind::Slt, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Sle(a, b) => (OpKind::Sle, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Sgt(a, b) => (OpKind::Sgt, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Sge(a, b) => (OpKind::Sge, bv_uses_disabled(a) || bv_uses_disabled(b)),
BoolTerm::Not(t) => (OpKind::BoolNot, bool_uses_disabled(t)),
BoolTerm::And(a, b) => (
OpKind::BoolAnd,
bool_uses_disabled(a) || bool_uses_disabled(b),
),
BoolTerm::Or(a, b) => (
OpKind::BoolOr,
bool_uses_disabled(a) || bool_uses_disabled(b),
),
};
DISABLED_OPS.contains(&op) || kids_disabled
}
struct Blaster {
aig: Aig,
vars: HashMap<String, Word>,
var_order: Vec<(String, u32)>,
}
impl Blaster {
fn new() -> Self {
Blaster {
aig: Aig::new(),
vars: HashMap::new(),
var_order: Vec::new(),
}
}
fn var_word(&mut self, name: &str, width: u32) -> Word {
if let Some(w) = self.vars.get(name) {
return w.clone();
}
let w = word_input(&mut self.aig, width);
self.vars.insert(name.to_string(), w.clone());
self.var_order.push((name.to_string(), width));
w
}
fn blast_bv(&mut self, term: &BvTerm) -> Result<Word, EvalError> {
let width = eval::bv_sort(term)?.width;
Ok(match term {
BvTerm::Const { value, .. } => crate::aig::word_const(*value, width),
BvTerm::Var { name, .. } => self.var_word(name, width),
BvTerm::Add(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_add(&mut self.aig, &wa, &wb)
}
BvTerm::Sub(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_sub(&mut self.aig, &wa, &wb)
}
BvTerm::Mul(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
muldiv::blast_mul(&mut self.aig, &wa, &wb)
}
BvTerm::Udiv(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
muldiv::blast_udiv(&mut self.aig, &wa, &wb)
}
BvTerm::And(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
bitwise::blast_and(&mut self.aig, &wa, &wb)
}
BvTerm::Or(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
bitwise::blast_or(&mut self.aig, &wa, &wb)
}
BvTerm::Xor(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
bitwise::blast_xor(&mut self.aig, &wa, &wb)
}
BvTerm::Shl(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
shift::blast_shl(&mut self.aig, &wa, &wb)
}
BvTerm::Lshr(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
shift::blast_lshr(&mut self.aig, &wa, &wb)
}
BvTerm::Ashr(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
shift::blast_ashr(&mut self.aig, &wa, &wb)
}
BvTerm::Rotr(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
shift::blast_rotr(&mut self.aig, &wa, &wb)
}
BvTerm::Extract { hi, lo, arg } => {
let w = self.blast_bv(arg)?;
structural::blast_extract(&w, *hi, *lo)
}
BvTerm::Concat(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
structural::blast_concat(&wa, &wb)
}
BvTerm::ZeroExt { by, arg } => {
let w = self.blast_bv(arg)?;
structural::blast_zero_ext(&w, *by)
}
BvTerm::SignExt { by, arg } => {
let w = self.blast_bv(arg)?;
structural::blast_sign_ext(&w, *by)
}
BvTerm::Ite { cond, then_, else_ } => {
let c = self.blast_bool(cond)?;
let (wt, we) = (self.blast_bv(then_)?, self.blast_bv(else_)?);
bitwise::blast_ite(&mut self.aig, c, &wt, &we)
}
})
}
fn blast_bool(&mut self, term: &BoolTerm) -> Result<Lit, EvalError> {
Ok(match term {
BoolTerm::Eq(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
bitwise::blast_eq(&mut self.aig, &wa, &wb)
}
BoolTerm::Ne(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
bitwise::blast_ne(&mut self.aig, &wa, &wb)
}
BoolTerm::Ult(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_ult(&mut self.aig, &wa, &wb)
}
BoolTerm::Ule(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_ule(&mut self.aig, &wa, &wb)
}
BoolTerm::Ugt(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_ugt(&mut self.aig, &wa, &wb)
}
BoolTerm::Uge(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_uge(&mut self.aig, &wa, &wb)
}
BoolTerm::Slt(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_slt(&mut self.aig, &wa, &wb)
}
BoolTerm::Sle(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_sle(&mut self.aig, &wa, &wb)
}
BoolTerm::Sgt(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_sgt(&mut self.aig, &wa, &wb)
}
BoolTerm::Sge(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
arith::blast_sge(&mut self.aig, &wa, &wb)
}
BoolTerm::Not(t) => self.blast_bool(t)?.not(),
BoolTerm::And(a, b) => {
let (la, lb) = (self.blast_bool(a)?, self.blast_bool(b)?);
self.aig.and(la, lb)
}
BoolTerm::Or(a, b) => {
let (la, lb) = (self.blast_bool(a)?, self.blast_bool(b)?);
self.aig.or(la, lb)
}
})
}
}
#[derive(Clone, Debug, Default)]
pub struct Solver {
assertions: Vec<BoolTerm>,
}
impl Solver {
pub fn new() -> Self {
Self {
assertions: Vec::new(),
}
}
pub fn assert(&mut self, term: BoolTerm) {
self.assertions.push(term);
}
pub fn num_assertions(&self) -> usize {
self.assertions.len()
}
pub fn check_sliver(assertions: &[crate::sliver::ExtBoolTerm]) -> CheckResult {
match crate::sliver::lower(assertions) {
Ok(core) => {
let mut solver = Solver::new();
for a in core {
solver.assert(a);
}
solver.check()
}
Err(_) => CheckResult::Unknown,
}
}
pub fn validate(&self) -> Result<(), EvalError> {
for a in &self.assertions {
validate_bool(a)?;
}
Ok(())
}
pub fn check(&self) -> CheckResult {
Self::verdict(self.solve_pipeline(None))
}
pub fn check_with_limit(&self, max_conflicts: u64) -> CheckResult {
Self::verdict(self.solve_pipeline(Some(max_conflicts)))
}
pub fn prove_equiv(a: BvTerm, b: BvTerm) -> CheckResult {
let mut solver = Solver::new();
solver.assert(BoolTerm::Ne(Box::new(a), Box::new(b)));
solver.check()
}
fn verdict(outcome: Pipeline) -> CheckResult {
match outcome {
Pipeline::Sat(env) => CheckResult::Sat(Model {
assignments: {
let mut a: Vec<(String, u128)> = env.into_iter().collect();
a.sort();
a
},
}),
Pipeline::Unsat {
certificate: Some(lrat),
cnf,
} => CheckResult::Unsat(Certificate { lrat, cnf }),
Pipeline::Unsat {
certificate: None, ..
}
| Pipeline::Unknown => CheckResult::Unknown,
}
}
#[cfg(any(test, feature = "oracle"))]
pub(crate) fn check_raw(&self) -> RawVerdict {
match self.solve_pipeline(None) {
Pipeline::Sat(env) => RawVerdict::Sat(env),
Pipeline::Unsat { .. } => RawVerdict::Unsat,
Pipeline::Unknown => RawVerdict::Unknown,
}
}
fn solve_pipeline(&self, budget: Option<u64>) -> Pipeline {
if self.assertions.is_empty() {
return Pipeline::Sat(Env::new());
}
if self.assertions.iter().any(bool_uses_disabled) {
return Pipeline::Unknown;
}
if self.validate().is_err() {
return Pipeline::Unknown;
}
let mut blaster = Blaster::new();
let mut roots = Vec::with_capacity(self.assertions.len());
for a in &self.assertions {
let a = crate::canon::canonicalize_bool(a);
match blaster.blast_bool(&a) {
Ok(lit) => roots.push(lit),
Err(_) => return Pipeline::Unknown,
}
}
let (cnf, map) = tseitin(&blaster.aig, &roots);
let mut sat_solver = SatSolver::new();
let verdict = match budget {
Some(max) => match sat_solver.solve_with_budget(&cnf, max) {
Some(v) => v,
None => return Pipeline::Unknown,
},
None => sat_solver.solve(&cnf),
};
match verdict {
SatResult::Unsat => {
let cert = crate::lrat::emit_lrat(cnf.clauses.len(), sat_solver.proof_trace());
match ordeal_lrat::check(&cnf.clauses, &cert) {
Ok(()) => Pipeline::Unsat {
certificate: Some(cert.into_bytes()),
cnf: cnf.clauses,
},
Err(_) => {
debug_assert!(false, "checker rejected our certificate — ordeal bug");
Pipeline::Unsat {
certificate: None,
cnf: Vec::new(),
}
}
}
}
SatResult::Sat(assignment) => {
let mut env = Env::new();
for (name, _width) in &blaster.var_order {
let word = &blaster.vars[name];
let mut value = 0u128;
for (i, lit) in word.iter().enumerate() {
let cnf_lit = map.cnf_lit(*lit);
let v = assignment[(cnf_lit.unsigned_abs() - 1) as usize];
let bit = if cnf_lit > 0 { v } else { !v };
value |= (bit as u128) << i;
}
env.insert(name.clone(), value);
}
let ok = self
.assertions
.iter()
.all(|a| eval::eval_bool(a, &env) == Ok(true));
if ok {
Pipeline::Sat(env)
} else {
debug_assert!(false, "SAT model failed self-check — ordeal bug");
Pipeline::Unknown
}
}
}
}
}
enum Pipeline {
Sat(Env),
Unsat {
certificate: Option<Vec<u8>>,
cnf: Vec<Vec<i32>>,
},
Unknown,
}
fn validate_bool(term: &BoolTerm) -> Result<(), EvalError> {
let pair = |a: &BvTerm, b: &BvTerm| -> Result<(), EvalError> {
let (wa, wb) = (eval::bv_sort(a)?.width, eval::bv_sort(b)?.width);
if wa == wb {
Ok(())
} else {
Err(EvalError::WidthMismatch {
left: wa,
right: wb,
})
}
};
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) => pair(a, b),
BoolTerm::Not(t) => validate_bool(t),
BoolTerm::And(a, b) | BoolTerm::Or(a, b) => {
validate_bool(a)?;
validate_bool(b)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::term::{BvTerm, Sort};
fn var(name: &str, w: u32) -> BvTerm {
BvTerm::Var {
name: name.into(),
sort: Sort::new(w),
}
}
fn c(value: u128, w: u32) -> BvTerm {
BvTerm::Const {
value,
sort: Sort::new(w),
}
}
fn b(t: BvTerm) -> Box<BvTerm> {
Box::new(t)
}
#[test]
fn empty_solver_is_trivially_sat() {
match Solver::new().check() {
CheckResult::Sat(m) => assert!(m.assignments.is_empty()),
other => panic!("empty conjunction must be Sat, got {other:?}"),
}
}
#[test]
fn unsat_carries_a_checker_validated_certificate() {
let x = var("x", 32);
let x1 = BvTerm::Add(b(x.clone()), b(c(1, 32)));
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(x), b(x1)));
match s.check() {
CheckResult::Unsat(cert) => {
assert!(!cert.lrat.is_empty(), "certificate must be present");
let text = String::from_utf8(cert.lrat).expect("LRAT is text");
assert!(text.lines().last().is_some_and(|l| l.contains(" 0")));
}
other => panic!("expected certificate-checked Unsat, got {other:?}"),
}
assert_eq!(s.check_raw(), RawVerdict::Unsat);
}
#[test]
fn certificate_is_independently_recheckable() {
let x = || var("x", 32);
match Solver::prove_equiv(
BvTerm::Mul(b(x()), b(c(2, 32))),
BvTerm::Shl(b(x()), b(c(1, 32))),
) {
CheckResult::Unsat(cert) => {
assert!(!cert.cnf.is_empty(), "certificate must carry the CNF");
cert.recheck()
.expect("consumer re-check must confirm UNSAT");
let mut tampered = cert.clone();
tampered.cnf.clear();
assert!(
tampered.recheck().is_err(),
"re-check must fail when the refuted CNF is removed"
);
}
other => panic!("x*2 ≡ x<<1 must be certificate-checked Unsat, got {other:?}"),
}
}
#[test]
fn sat_returns_self_checked_model() {
let x = var("x", 8);
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(BvTerm::Add(b(x), b(c(1, 8)))), b(c(5, 8))));
match s.check() {
CheckResult::Sat(m) => assert_eq!(m.assignments, vec![("x".into(), 4u128)]),
other => panic!("expected Sat with x=4, got {other:?}"),
}
}
#[test]
fn prove_equiv_layout_field_extract_roundtrips() {
let flags = var("flags", 8);
let hi = var("hi", 32);
let packed = BvTerm::Concat(b(hi), b(flags.clone()));
let low8 = BvTerm::Extract {
hi: 7,
lo: 0,
arg: b(packed),
};
match Solver::prove_equiv(low8, flags) {
CheckResult::Unsat(cert) => assert!(!cert.lrat.is_empty()),
other => panic!("layouts must be proven equivalent, got {other:?}"),
}
}
#[test]
fn prove_equiv_distinct_layouts_give_counterexample() {
match Solver::prove_equiv(var("a", 32), var("b", 32)) {
CheckResult::Sat(_) => {}
other => panic!("distinct terms are not equivalent, got {other:?}"),
}
}
#[test]
fn prove_equiv_width_mismatch_is_conservative_unknown() {
match Solver::prove_equiv(var("a", 32), var("b", 8)) {
CheckResult::Unknown => {}
other => panic!("width mismatch must be Unknown, got {other:?}"),
}
}
#[test]
fn sat_with_multiple_vars_and_assertions() {
let (x, y) = (var("x", 8), var("y", 8));
let mut s = Solver::new();
s.assert(BoolTerm::Ult(b(x.clone()), b(y.clone())));
s.assert(BoolTerm::Ult(b(y), b(c(3, 8))));
match s.check() {
CheckResult::Sat(m) => {
let get = |n: &str| m.assignments.iter().find(|(k, _)| k == n).unwrap().1;
assert!(get("x") < get("y") && get("y") < 3);
}
other => panic!("expected Sat, got {other:?}"),
}
}
#[test]
fn ill_sorted_query_is_unknown_and_validate_diagnoses() {
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(c(1, 8)), b(c(1, 32))));
assert!(matches!(s.check(), CheckResult::Unknown));
assert_eq!(
s.validate(),
Err(EvalError::WidthMismatch { left: 8, right: 32 })
);
}
#[test]
fn full_pipeline_on_every_op_family() {
let (x, y) = (var("x", 8), var("y", 8));
let mut s = Solver::new();
let lhs = BvTerm::Xor(
b(BvTerm::Lshr(
b(BvTerm::Mul(b(x.clone()), b(c(3, 8)))),
b(c(1, 8)),
)),
b(BvTerm::Udiv(b(y.clone()), b(c(2, 8)))),
);
s.assert(BoolTerm::Eq(b(lhs), b(c(7, 8))));
s.assert(BoolTerm::Uge(
b(BvTerm::Rotr(b(x.clone()), b(c(1, 8)))),
b(y.clone()),
));
s.assert(BoolTerm::Eq(
b(BvTerm::Extract {
hi: 11,
lo: 4,
arg: b(BvTerm::Concat(
b(BvTerm::SignExt { by: 8, arg: b(x) }),
b(y),
)),
}),
b(c(0xFF, 8)),
));
match s.check() {
CheckResult::Sat(m) => assert_eq!(m.assignments.len(), 2),
CheckResult::Unknown => {}
CheckResult::Unsat(cert) => {
assert!(!cert.lrat.is_empty(), "Unsat must carry the certificate")
}
}
}
#[test]
fn bounded_check_matches_check_within_budget() {
let x = var("x", 8);
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(BvTerm::Add(b(x), b(c(1, 8)))), b(c(5, 8))));
match s.check_with_limit(1_000_000) {
CheckResult::Sat(m) => assert_eq!(m.assignments, vec![("x".into(), 4u128)]),
other => panic!("expected Sat with x=4 within budget, got {other:?}"),
}
assert!(matches!(s.check(), CheckResult::Sat(_)));
let y = var("y", 32);
let y1 = BvTerm::Add(b(y.clone()), b(c(1, 32)));
let mut u = Solver::new();
u.assert(BoolTerm::Eq(b(y), b(y1)));
match u.check_with_limit(1_000_000) {
CheckResult::Unsat(cert) => assert!(!cert.lrat.is_empty()),
other => panic!("expected certificate-checked Unsat, got {other:?}"),
}
}
#[test]
fn zero_budget_forces_unknown() {
let s = hard_mul_equivalence();
assert!(matches!(s.check_with_limit(0), CheckResult::Unknown));
}
fn a5_mul_commutativity() -> Solver {
let (a, b_) = (var("a", 32), var("b", 32));
let mut s = Solver::new();
s.assert(BoolTerm::Ne(
b(BvTerm::Mul(b(a.clone()), b(b_.clone()))),
b(BvTerm::Mul(b(b_), b(a))),
));
s
}
fn hard_mul_equivalence() -> Solver {
let (a, bb, cc) = (var("a", 32), var("b", 32), var("c", 32));
let lhs = BvTerm::Mul(b(a.clone()), b(BvTerm::Add(b(bb.clone()), b(cc.clone()))));
let rhs = BvTerm::Add(
b(BvTerm::Mul(b(a.clone()), b(bb))),
b(BvTerm::Mul(b(a), b(cc))),
);
let mut s = Solver::new();
s.assert(BoolTerm::Ne(b(lhs), b(rhs)));
s
}
#[test]
fn a5_mul_commutativity_now_root_decided_at_any_budget() {
match a5_mul_commutativity().check_with_limit(0) {
CheckResult::Unsat(cert) => {
cert.recheck()
.expect("root-decided A5 certificate must re-check");
}
other => panic!("canonicalized A5 must be root-decided UNSAT, got {other:?}"),
}
}
#[test]
fn a5_mul_commutativity_decides_unsat_after_canonicalization() {
let s = a5_mul_commutativity();
let start = std::time::Instant::now();
let verdict = s.check();
let elapsed = start.elapsed();
match verdict {
CheckResult::Unsat(cert) => {
cert.recheck()
.expect("A5 certificate must independently re-check");
}
other => panic!("A5 must now decide UNSAT unbounded, got {other:?}"),
}
assert!(
elapsed < std::time::Duration::from_secs(5),
"canonicalized A5 must decide fast, took {elapsed:?}"
);
}
#[test]
fn bounded_check_is_deterministic() {
let a = hard_mul_equivalence().check_with_limit(100);
let b_ = hard_mul_equivalence().check_with_limit(100);
assert!(matches!(a, CheckResult::Unknown));
assert!(matches!(b_, CheckResult::Unknown));
let x = var("x", 8);
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(BvTerm::Add(b(x), b(c(1, 8)))), b(c(5, 8))));
let (m1, m2) = (s.check_with_limit(10_000), s.check_with_limit(10_000));
match (m1, m2) {
(CheckResult::Sat(a), CheckResult::Sat(b)) => {
assert_eq!(a.assignments, b.assignments);
}
other => panic!("expected stable Sat, got {other:?}"),
}
}
#[test]
fn check_sliver_entry_lowers_and_decides() {
use crate::sliver::{ArrayTerm, ExtBoolTerm, ExtBvTerm};
let bv32 = Sort::new(32);
let bv8 = Sort::new(8);
let idx = ExtBvTerm::Core(BvTerm::Const {
value: 5,
sort: bv32,
});
let stored = ExtBvTerm::Core(BvTerm::Var {
name: "v".into(),
sort: bv8,
});
let arr = ArrayTerm::Store {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(idx.clone()),
value: Box::new(stored.clone()),
};
let read = ExtBvTerm::Select {
array: Box::new(arr),
index: Box::new(idx),
};
let q = ExtBoolTerm::Eq(read, stored);
match Solver::check_sliver(&[q]) {
CheckResult::Sat(_) | CheckResult::Unknown => {}
CheckResult::Unsat(_) => panic!("read-over-write of a stored value is not UNSAT"),
}
}
#[test]
fn check_sliver_out_of_sliver_is_unknown() {
use crate::sliver::{ArrayTerm, ExtBoolTerm, ExtBvTerm};
let bv32 = Sort::new(32);
let bv8 = Sort::new(8);
let sym = ExtBvTerm::Core(BvTerm::Var {
name: "i".into(),
sort: bv32,
});
let read = ExtBvTerm::Select {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(sym),
};
let q = ExtBoolTerm::Eq(
read,
ExtBvTerm::Core(BvTerm::Const {
value: 0,
sort: bv8,
}),
);
assert!(matches!(Solver::check_sliver(&[q]), CheckResult::Unknown));
}
}