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,
Urem,
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::Urem(..) => OpKind::Urem,
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::Urem(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::Urem(a, b) => {
let (wa, wb) = (self.blast_bv(a)?, self.blast_bv(b)?);
muldiv::blast_urem(&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 prove_equiv_sliver(
a: crate::sliver::ExtBvTerm,
b: crate::sliver::ExtBvTerm,
) -> CheckResult {
Self::check_sliver(&[crate::sliver::ExtBoolTerm::Ne(a, b)])
}
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(Bound::None))
}
pub fn check_with_limit(&self, max_conflicts: u64) -> CheckResult {
Self::verdict(self.solve_pipeline(Bound::Conflicts(max_conflicts)))
}
pub fn check_with_deadline(&self, timeout_ms: u64) -> CheckResult {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
Self::verdict(self.solve_pipeline(Bound::Deadline(deadline)))
}
pub fn prove_valid(goal: BoolTerm) -> CheckResult {
let mut solver = Solver::new();
solver.assert(BoolTerm::Not(Box::new(goal)));
solver.check()
}
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()
}
pub fn check_cnf(formula: &crate::cnf::CnfFormula) -> CheckResult {
let mut sat_solver = SatSolver::new();
match sat_solver.solve(formula) {
SatResult::Unsat => {
let cert =
crate::lrat::emit_lrat_trimmed(formula.clauses.len(), sat_solver.proof_trace());
match ordeal_lrat::check(&formula.clauses, &cert) {
Ok(()) => CheckResult::Unsat(Certificate {
lrat: cert.into_bytes(),
cnf: formula.clauses.clone(),
}),
Err(_) => {
debug_assert!(false, "checker rejected our certificate — ordeal bug");
CheckResult::Unknown
}
}
}
SatResult::Sat(assignment) => {
debug_assert!(
formula.eval(&assignment),
"SAT model must satisfy the formula — ordeal bug"
);
let assignments = assignment
.iter()
.enumerate()
.map(|(i, &b)| (format!("v{}", i + 1), b as u128))
.collect();
CheckResult::Sat(Model { assignments })
}
}
}
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(Bound::None) {
Pipeline::Sat(env) => RawVerdict::Sat(env),
Pipeline::Unsat { .. } => RawVerdict::Unsat,
Pipeline::Unknown => RawVerdict::Unknown,
}
}
fn solve_pipeline(&self, bound: Bound) -> 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 bound {
Bound::Conflicts(max) => match sat_solver.solve_with_budget(&cnf, max) {
Some(v) => v,
None => return Pipeline::Unknown,
},
Bound::Deadline(d) => match sat_solver.solve_with_deadline(&cnf, d) {
Some(v) => v,
None => return Pipeline::Unknown,
},
Bound::None => sat_solver.solve(&cnf),
};
match verdict {
SatResult::Unsat => {
let cert =
crate::lrat::emit_lrat_trimmed(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 Bound {
None,
Conflicts(u64),
Deadline(std::time::Instant),
}
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_valid_tautology_is_unsat_and_rechecks() {
let x = var("x", 32);
let goal = BoolTerm::Eq(
b(BvTerm::Or(
b(x.clone()),
b(BvTerm::Const {
value: 0,
sort: crate::term::Sort::new(32),
}),
)),
b(x),
);
match Solver::prove_valid(goal) {
CheckResult::Unsat(cert) => cert.recheck().expect("valid goal's cert must re-check"),
other => panic!("x | 0 == x must be valid, got {other:?}"),
}
}
#[test]
fn prove_valid_falsifiable_goal_yields_a_counterexample() {
let x = var("x", 32);
let goal = BoolTerm::Eq(
b(BvTerm::Add(
b(x.clone()),
b(BvTerm::Const {
value: 1,
sort: crate::term::Sort::new(32),
}),
)),
b(x),
);
match Solver::prove_valid(goal) {
CheckResult::Sat(_) => {}
other => panic!("x + 1 == x must be falsifiable, got {other:?}"),
}
}
#[test]
fn prove_valid_is_the_dual_of_prove_equiv() {
let mk = || {
let hi = var("hi", 32);
let flags = var("flags", 8);
let packed = BvTerm::Concat(b(hi), b(flags.clone()));
let low8 = BvTerm::Extract {
hi: 7,
lo: 0,
arg: b(packed),
};
(low8, flags)
};
let (a, bt) = mk();
let via_equiv = Solver::prove_equiv(a, bt);
let (a2, b2) = mk();
let via_valid = Solver::prove_valid(BoolTerm::Eq(b(a2), b(b2)));
assert!(
matches!(via_equiv, CheckResult::Unsat(_))
&& matches!(via_valid, CheckResult::Unsat(_)),
"prove_equiv and prove_valid(Eq) must agree: {via_equiv:?} vs {via_valid:?}"
);
}
#[test]
fn meld_offset_fold_regression_guard() {
let base = var("base", 32);
let n = BvTerm::Const {
value: 16,
sort: crate::term::Sort::new(32),
};
let folded = BoolTerm::Eq(b(BvTerm::Add(b(base.clone()), b(n))), b(base));
match Solver::prove_valid(folded) {
CheckResult::Sat(model) => {
assert!(
model.assignments.iter().any(|(name, _)| name == "base"),
"counterexample must bind `base`"
);
}
other => panic!("base + 16 == base must be falsifiable, got {other:?}"),
}
}
#[test]
fn check_cnf_inconsistent_model_is_unsat_and_rechecks() {
let f = crate::cnf::CnfFormula {
num_vars: 1,
clauses: vec![vec![1], vec![-1]],
};
match Solver::check_cnf(&f) {
CheckResult::Unsat(cert) => cert
.recheck()
.expect("propositional UNSAT must carry a re-checkable certificate"),
other => panic!("a ∧ ¬a must be UNSAT, got {other:?}"),
}
}
#[test]
fn check_cnf_consistent_model_yields_a_configuration() {
let f = crate::cnf::CnfFormula {
num_vars: 2,
clauses: vec![vec![-1, 2], vec![1]],
};
match Solver::check_cnf(&f) {
CheckResult::Sat(model) => {
let get = |n: &str| {
model
.assignments
.iter()
.find(|(k, _)| k == n)
.map(|(_, v)| *v)
};
assert_eq!(get("v1"), Some(1), "a is required");
assert_eq!(get("v2"), Some(1), "a → b forces b");
}
other => panic!("consistent model must be SAT, got {other:?}"),
}
}
#[test]
fn check_cnf_certificate_matches_the_submitted_clauses() {
let f = crate::cnf::CnfFormula {
num_vars: 2,
clauses: vec![vec![1, 2], vec![-1], vec![-2]],
};
match Solver::check_cnf(&f) {
CheckResult::Unsat(cert) => {
assert_eq!(
cert.cnf, f.clauses,
"certificate must carry the submitted CNF"
);
cert.recheck().expect("must re-check against those clauses");
}
other => panic!("expected UNSAT, got {other:?}"),
}
}
#[test]
fn check_cnf_agrees_with_the_bitblaster_on_a_shared_query() {
let via_cnf = Solver::check_cnf(&crate::cnf::CnfFormula {
num_vars: 1,
clauses: vec![vec![1], vec![-1]],
});
let p = var("p", 1);
let one = BvTerm::Const {
value: 1,
sort: crate::term::Sort::new(1),
};
let zero = BvTerm::Const {
value: 0,
sort: crate::term::Sort::new(1),
};
let mut s = Solver::new();
s.assert(BoolTerm::Eq(b(p.clone()), b(one)));
s.assert(BoolTerm::Eq(b(p), b(zero)));
let via_bv = s.check();
assert!(
matches!(via_cnf, CheckResult::Unsat(_)) && matches!(via_bv, CheckResult::Unsat(_)),
"propositional and bit-blaster paths must agree: {via_cnf:?} vs {via_bv:?}"
);
}
#[test]
fn check_cnf_feature_model_conflict_is_certified_unsat() {
let f = crate::cnf::CnfFormula {
num_vars: 3,
clauses: vec![
vec![1], vec![-2, -3], vec![2], vec![3], ],
};
match Solver::check_cnf(&f) {
CheckResult::Unsat(cert) => cert.recheck().expect("conflict cert must re-check"),
other => panic!("tls ∧ nomalloc must be inconsistent, got {other:?}"),
}
let ok = crate::cnf::CnfFormula {
num_vars: 3,
clauses: vec![vec![1], vec![-2, -3], vec![2]],
};
assert!(
matches!(Solver::check_cnf(&ok), CheckResult::Sat(_)),
"core+tls without nomalloc must be a valid configuration"
);
}
#[test]
fn deadline_exhaustion_is_conservative_unknown() {
let s = hard_mul_equivalence();
match s.check_with_deadline(0) {
CheckResult::Unknown => {}
CheckResult::Unsat(cert) => cert.recheck().expect("decided-in-time cert must check"),
CheckResult::Sat(m) => panic!("deadline must never yield a wrong verdict: {m:?}"),
}
}
#[test]
fn deadline_generous_decides_normally() {
let mut s = Solver::new();
let x = var("x", 32);
s.assert(BoolTerm::Ne(b(x.clone()), b(x)));
match s.check_with_deadline(60_000) {
CheckResult::Unsat(cert) => cert.recheck().expect("cert must re-check"),
other => panic!("x != x must be UNSAT, got {other:?}"),
}
}
#[test]
fn deadline_zero_still_root_decides() {
match a5_mul_commutativity().check_with_deadline(0) {
CheckResult::Unsat(cert) => cert.recheck().expect("root-decided cert must check"),
other => panic!("canonicalized A5 must root-decide under any deadline, got {other:?}"),
}
}
#[test]
fn prove_equiv_sliver_read_over_write() {
let (i, v) = (sym_bv32("i"), sym_bv8("v"));
let read = sel(st(base("m"), i.clone(), v.clone()), i);
match Solver::prove_equiv_sliver(read, v) {
CheckResult::Unsat(cert) => cert.recheck().expect("cert must re-check"),
other => panic!("select(store(m,i,v),i) ≡ v must prove, got {other:?}"),
}
}
#[test]
fn prove_equiv_sliver_distinct_reads_differ() {
let r1 = sel(base("m"), sym_bv32("i"));
let r2 = sel(base("m"), sym_bv32("j"));
match Solver::prove_equiv_sliver(r1, r2) {
CheckResult::Sat(_) => {}
other => panic!("m[i] ≢ m[j] in general, 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 bv8 = Sort::new(8);
let bad_index = ExtBvTerm::Core(BvTerm::Var {
name: "i".into(),
sort: bv8,
});
let read = ExtBvTerm::Select {
array: Box::new(ArrayTerm::Var { name: "a".into() }),
index: Box::new(bad_index),
};
let q = ExtBoolTerm::Eq(
read,
ExtBvTerm::Core(BvTerm::Const {
value: 0,
sort: bv8,
}),
);
assert!(matches!(Solver::check_sliver(&[q]), CheckResult::Unknown));
}
fn sym_bv32(name: &str) -> crate::sliver::ExtBvTerm {
crate::sliver::ExtBvTerm::Core(BvTerm::Var {
name: name.into(),
sort: Sort::new(32),
})
}
fn sym_bv8(name: &str) -> crate::sliver::ExtBvTerm {
crate::sliver::ExtBvTerm::Core(BvTerm::Var {
name: name.into(),
sort: Sort::new(8),
})
}
fn sel(a: crate::sliver::ArrayTerm, i: crate::sliver::ExtBvTerm) -> crate::sliver::ExtBvTerm {
crate::sliver::ExtBvTerm::Select {
array: Box::new(a),
index: Box::new(i),
}
}
fn st(
a: crate::sliver::ArrayTerm,
i: crate::sliver::ExtBvTerm,
v: crate::sliver::ExtBvTerm,
) -> crate::sliver::ArrayTerm {
crate::sliver::ArrayTerm::Store {
array: Box::new(a),
index: Box::new(i),
value: Box::new(v),
}
}
fn base(n: &str) -> crate::sliver::ArrayTerm {
crate::sliver::ArrayTerm::Var { name: n.into() }
}
#[test]
fn symbolic_read_over_write_same_index_is_valid() {
use crate::sliver::ExtBoolTerm;
let (i, v) = (sym_bv32("i"), sym_bv8("v"));
let read = sel(st(base("a"), i.clone(), v.clone()), i);
match Solver::check_sliver(&[ExtBoolTerm::Not(Box::new(ExtBoolTerm::Eq(read, v)))]) {
CheckResult::Unsat(_) => {}
other => panic!("select(store(a,i,v),i) must equal v; got {other:?}"),
}
}
#[test]
fn symbolic_read_over_write_distinct_index_sees_through() {
use crate::sliver::ExtBoolTerm;
let (i, j, v) = (sym_bv32("i"), sym_bv32("j"), sym_bv8("v"));
let read = sel(st(base("a"), i.clone(), v), j.clone());
let underlying = sel(base("a"), j.clone());
let q = vec![
ExtBoolTerm::Not(Box::new(ExtBoolTerm::Eq(i, j))),
ExtBoolTerm::Not(Box::new(ExtBoolTerm::Eq(read, underlying))),
];
match Solver::check_sliver(&q) {
CheckResult::Unsat(_) => {}
other => panic!("with i != j the store must be transparent; got {other:?}"),
}
}
#[test]
fn symbolic_read_over_write_does_not_assume_non_aliasing() {
use crate::sliver::ExtBoolTerm;
let (i, j, v) = (sym_bv32("i"), sym_bv32("j"), sym_bv8("v"));
let read = sel(st(base("a"), i, v), j.clone());
let underlying = sel(base("a"), j);
match Solver::check_sliver(&[ExtBoolTerm::Not(Box::new(ExtBoolTerm::Eq(
read, underlying,
)))]) {
CheckResult::Sat(_) => {}
other => panic!("aliasing must stay possible without i != j; got {other:?}"),
}
}
#[test]
fn symbolic_and_concrete_reads_are_congruent() {
use crate::sliver::ExtBoolTerm;
let i = sym_bv32("i");
let five = crate::sliver::ExtBvTerm::Core(BvTerm::Const {
value: 5,
sort: Sort::new(32),
});
let q = vec![
ExtBoolTerm::Eq(i.clone(), five.clone()),
ExtBoolTerm::Not(Box::new(ExtBoolTerm::Eq(
sel(base("a"), i),
sel(base("a"), five),
))),
];
match Solver::check_sliver(&q) {
CheckResult::Unsat(_) => {}
other => panic!("i = 5 must force a[i] = a[5] (missing congruence?); got {other:?}"),
}
}
#[test]
fn loom_multibyte_le_chain_over_symbolic_base_round_trips() {
use crate::sliver::{ExtBoolTerm, ExtOp};
let addr = sym_bv32("addr");
let (b0, b1) = (sym_bv8("b0"), sym_bv8("b1"));
let addr1 = crate::sliver::ExtBvTerm::Op(Box::new(ExtOp::Add(
addr.clone(),
crate::sliver::ExtBvTerm::Core(BvTerm::Const {
value: 1,
sort: Sort::new(32),
}),
)));
let mem = st(
st(base("mem"), addr.clone(), b0.clone()),
addr1.clone(),
b1.clone(),
);
let q = vec![ExtBoolTerm::Not(Box::new(ExtBoolTerm::And(
Box::new(ExtBoolTerm::Eq(sel(mem.clone(), addr1), b1)),
Box::new(ExtBoolTerm::Eq(sel(mem, addr), b0)),
)))];
match Solver::check_sliver(&q) {
CheckResult::Unsat(_) => {}
other => panic!("loom's LE chain over a symbolic base must round-trip; got {other:?}"),
}
}
}