pub mod smtlib;
use crate::kernel::expr::PredicateKind;
use crate::kernel::{ExprData, ExprId, ExprPool};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogicError {
UnsupportedExpr(&'static str),
}
impl fmt::Display for LogicError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogicError::UnsupportedExpr(s) => write!(f, "{s}"),
}
}
}
impl std::error::Error for LogicError {}
impl crate::errors::AlkahestError for LogicError {
fn code(&self) -> &'static str {
"E-LOGIC-001"
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Formula {
Atom {
kind: PredicateKind,
args: Vec<ExprId>,
},
And(Box<Formula>, Box<Formula>),
Or(Box<Formula>, Box<Formula>),
Not(Box<Formula>),
True,
False,
Forall {
var: ExprId,
body: Box<Formula>,
},
Exists {
var: ExprId,
body: Box<Formula>,
},
}
impl Formula {
pub fn and(a: Formula, b: Formula) -> Self {
Formula::And(Box::new(a), Box::new(b))
}
pub fn or(a: Formula, b: Formula) -> Self {
Formula::Or(Box::new(a), Box::new(b))
}
#[allow(clippy::should_implement_trait)] pub fn not(a: Formula) -> Self {
Formula::Not(Box::new(a))
}
pub fn to_expr(&self, pool: &ExprPool) -> ExprId {
match self {
Formula::True => pool.pred_true(),
Formula::False => pool.pred_false(),
Formula::Atom { kind, args } => pool.predicate(kind.clone(), args.clone()),
Formula::And(l, r) => pool.pred_and(vec![l.to_expr(pool), r.to_expr(pool)]),
Formula::Or(l, r) => pool.pred_or(vec![l.to_expr(pool), r.to_expr(pool)]),
Formula::Not(x) => pool.pred_not(x.to_expr(pool)),
Formula::Forall { var, body } => pool.forall(*var, body.to_expr(pool)),
Formula::Exists { var, body } => pool.exists(*var, body.to_expr(pool)),
}
}
}
pub fn formula_from_expr(expr: ExprId, pool: &ExprPool) -> Result<Formula, LogicError> {
match pool.get(expr) {
ExprData::Predicate { kind, args } => match kind {
PredicateKind::True => Ok(Formula::True),
PredicateKind::False => Ok(Formula::False),
PredicateKind::And => {
if args.is_empty() {
Ok(Formula::True)
} else {
let mut it = args.into_iter();
let first = formula_from_expr(it.next().unwrap(), pool)?;
it.try_fold(first, |acc, e| {
Ok(Formula::and(acc, formula_from_expr(e, pool)?))
})
}
}
PredicateKind::Or => {
if args.is_empty() {
Ok(Formula::False)
} else {
let mut it = args.into_iter();
let first = formula_from_expr(it.next().unwrap(), pool)?;
it.try_fold(first, |acc, e| {
Ok(Formula::or(acc, formula_from_expr(e, pool)?))
})
}
}
PredicateKind::Not => {
if args.len() != 1 {
return Err(LogicError::UnsupportedExpr("Not predicate arity must be 1"));
}
Ok(Formula::not(formula_from_expr(args[0], pool)?))
}
PredicateKind::Lt
| PredicateKind::Le
| PredicateKind::Gt
| PredicateKind::Ge
| PredicateKind::Eq
| PredicateKind::Ne => {
if args.len() != 2 {
return Err(LogicError::UnsupportedExpr("relation arity must be 2"));
}
Ok(Formula::Atom { kind, args })
}
},
ExprData::Forall { var, body } => Ok(Formula::Forall {
var,
body: Box::new(formula_from_expr(body, pool)?),
}),
ExprData::Exists { var, body } => Ok(Formula::Exists {
var,
body: Box::new(formula_from_expr(body, pool)?),
}),
_ => Err(LogicError::UnsupportedExpr(
"expression is not a predicate or quantified formula",
)),
}
}
#[derive(Clone, Debug)]
enum Bound {
Lower { val: rug::Rational, strict: bool },
Upper { val: rug::Rational, strict: bool },
}
#[derive(Clone, Debug, Default)]
struct VarInterval {
lower: Option<Bound>,
upper: Option<Bound>,
}
impl VarInterval {
fn is_empty(&self) -> bool {
match (&self.lower, &self.upper) {
(
Some(Bound::Lower {
val: lo,
strict: ls,
}),
Some(Bound::Upper {
val: hi,
strict: us,
}),
) => {
if lo > hi {
return true;
}
if lo < hi {
return false;
}
*ls || *us
}
_ => false,
}
}
fn intersect(&self, other: &VarInterval) -> Option<VarInterval> {
let lower = match (&self.lower, &other.lower) {
(None, b) => b.clone(),
(a, None) => a.clone(),
(
Some(Bound::Lower { val: a, strict: sa }),
Some(Bound::Lower { val: b, strict: sb }),
) => {
if a > b {
Some(Bound::Lower {
val: a.clone(),
strict: *sa,
})
} else if b > a {
Some(Bound::Lower {
val: b.clone(),
strict: *sb,
})
} else {
Some(Bound::Lower {
val: a.clone(),
strict: *sa || *sb,
})
}
}
_ => return None,
};
let upper = match (&self.upper, &other.upper) {
(None, b) => b.clone(),
(a, None) => a.clone(),
(
Some(Bound::Upper { val: a, strict: sa }),
Some(Bound::Upper { val: b, strict: sb }),
) => {
if a < b {
Some(Bound::Upper {
val: a.clone(),
strict: *sa,
})
} else if b < a {
Some(Bound::Upper {
val: b.clone(),
strict: *sb,
})
} else {
Some(Bound::Upper {
val: a.clone(),
strict: *sa || *sb,
})
}
}
_ => return None,
};
let r = VarInterval { lower, upper };
if r.is_empty() {
None
} else {
Some(r)
}
}
}
fn rat_atom(pool: &ExprPool, id: ExprId) -> Option<rug::Rational> {
match pool.get(id) {
ExprData::Integer(n) => Some(rug::Rational::from(n.0)),
ExprData::Rational(r) => Some(r.0.clone()),
_ => None,
}
}
fn symbol_key(pool: &ExprPool, id: ExprId) -> Option<String> {
pool.with(id, |d| match d {
ExprData::Symbol { name, .. } => Some(name.clone()),
_ => None,
})
}
fn atom_to_interval(
pool: &ExprPool,
kind: PredicateKind,
args: &[ExprId],
) -> Option<(ExprId, VarInterval)> {
if args.len() != 2 {
return None;
}
let (a, b) = (args[0], args[1]);
let (var, c_id, swapped) = if symbol_key(pool, a).is_some() && rat_atom(pool, b).is_some() {
(a, b, false)
} else if rat_atom(pool, a).is_some() && symbol_key(pool, b).is_some() {
(b, a, true)
} else {
return None;
};
let c = rat_atom(pool, c_id)?;
let iv = match (kind, swapped) {
(PredicateKind::Lt, false) => VarInterval {
lower: None,
upper: Some(Bound::Upper {
val: c,
strict: true,
}),
},
(PredicateKind::Le, false) => VarInterval {
lower: None,
upper: Some(Bound::Upper {
val: c,
strict: false,
}),
},
(PredicateKind::Gt, false) => VarInterval {
lower: Some(Bound::Lower {
val: c,
strict: true,
}),
upper: None,
},
(PredicateKind::Ge, false) => VarInterval {
lower: Some(Bound::Lower {
val: c,
strict: false,
}),
upper: None,
},
(PredicateKind::Eq, false) => VarInterval {
lower: Some(Bound::Lower {
val: c.clone(),
strict: false,
}),
upper: Some(Bound::Upper {
val: c,
strict: false,
}),
},
(PredicateKind::Lt, true) => VarInterval {
lower: Some(Bound::Lower {
val: c,
strict: true,
}),
upper: None,
},
(PredicateKind::Le, true) => VarInterval {
lower: Some(Bound::Lower {
val: c,
strict: false,
}),
upper: None,
},
(PredicateKind::Gt, true) => VarInterval {
lower: None,
upper: Some(Bound::Upper {
val: c,
strict: true,
}),
},
(PredicateKind::Ge, true) => VarInterval {
lower: None,
upper: Some(Bound::Upper {
val: c,
strict: false,
}),
},
_ => return None,
};
Some((var, iv))
}
fn is_rel(k: &PredicateKind) -> bool {
matches!(
k,
PredicateKind::Lt
| PredicateKind::Le
| PredicateKind::Gt
| PredicateKind::Ge
| PredicateKind::Eq
| PredicateKind::Ne
)
}
fn dual_kind(kind: PredicateKind) -> PredicateKind {
use PredicateKind::*;
match kind {
Lt => Ge,
Le => Gt,
Gt => Le,
Ge => Lt,
Eq => Ne,
Ne => Eq,
other => other,
}
}
fn nnf(f: Formula) -> Formula {
match f {
Formula::Not(inner) => match *inner {
Formula::True => Formula::False,
Formula::False => Formula::True,
Formula::Not(g) => nnf(*g),
Formula::And(a, b) => nnf(Formula::or(Formula::not(*a), Formula::not(*b))),
Formula::Or(a, b) => nnf(Formula::and(Formula::not(*a), Formula::not(*b))),
Formula::Forall { var, body } => nnf(Formula::Exists {
var,
body: Box::new(Formula::not(*body)),
}),
Formula::Exists { var, body } => nnf(Formula::Forall {
var,
body: Box::new(Formula::not(*body)),
}),
Formula::Atom {
kind: PredicateKind::True,
..
} => Formula::False,
Formula::Atom {
kind: PredicateKind::False,
..
} => Formula::True,
Formula::Atom { kind, args } if is_rel(&kind) => Formula::Atom {
kind: dual_kind(kind),
args,
},
inner => Formula::Not(Box::new(inner)),
},
Formula::And(a, b) => Formula::and(nnf(*a), nnf(*b)),
Formula::Or(a, b) => Formula::or(nnf(*a), nnf(*b)),
Formula::Forall { var, body } => Formula::Forall {
var,
body: Box::new(nnf(*body)),
},
Formula::Exists { var, body } => Formula::Exists {
var,
body: Box::new(nnf(*body)),
},
other => other,
}
}
fn witness_rational(iv: &VarInterval) -> Option<rug::Rational> {
let eps = || rug::Rational::from((1, 10_000));
match (&iv.lower, &iv.upper) {
(None, None) => Some(rug::Rational::from(0)),
(Some(Bound::Lower { val: lo, strict: s }), None) => {
let e = eps();
Some(if *s { lo.clone() + &e } else { lo.clone() })
}
(None, Some(Bound::Upper { val: hi, strict: s })) => {
let e = eps();
Some(if *s { hi.clone() - &e } else { hi.clone() })
}
(
Some(Bound::Lower {
val: lo,
strict: sl,
}),
Some(Bound::Upper {
val: hi,
strict: su,
}),
) => {
if lo > hi {
return None;
}
if lo < hi {
return Some((lo.clone() + hi.clone()) / rug::Rational::from(2));
}
if *sl || *su {
None
} else {
Some(lo.clone())
}
}
_ => None,
}
}
fn map_to_witness(
m: &HashMap<ExprId, VarInterval>,
pool: &ExprPool,
) -> Result<HashMap<String, String>, SatFail> {
let mut out = HashMap::new();
for (&id, iv) in m {
let name = symbol_key(pool, id).ok_or(SatFail::Unknown)?;
let w = witness_rational(iv).ok_or(SatFail::Unknown)?;
out.insert(name, w.to_string());
}
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Satisfiability {
Sat(HashMap<String, String>),
Unsat,
Unknown,
}
enum SatFail {
Unsat,
Unknown,
}
fn merge_maps(
mut a: HashMap<ExprId, VarInterval>,
b: HashMap<ExprId, VarInterval>,
) -> Result<HashMap<ExprId, VarInterval>, SatFail> {
for (k, vb) in b {
match a.remove(&k) {
None => {
a.insert(k, vb);
}
Some(va) => {
let m = va.intersect(&vb).ok_or(SatFail::Unsat)?;
a.insert(k, m);
}
}
}
Ok(a)
}
fn sat_intervals(f: &Formula, pool: &ExprPool) -> Result<HashMap<ExprId, VarInterval>, SatFail> {
match f {
Formula::True => Ok(HashMap::new()),
Formula::False => Err(SatFail::Unsat),
Formula::Forall { .. } => Err(SatFail::Unknown),
Formula::Exists { body, .. } => sat_intervals(body, pool),
Formula::And(a, b) => {
let ma = sat_intervals(a, pool)?;
let mb = sat_intervals(b, pool)?;
merge_maps(ma, mb)
}
Formula::Or(a, b) => match sat_intervals(a, pool) {
Ok(m) => Ok(m),
Err(SatFail::Unsat) => sat_intervals(b, pool),
Err(SatFail::Unknown) => match sat_intervals(b, pool) {
Ok(m) => Ok(m),
Err(SatFail::Unsat) => Err(SatFail::Unknown),
Err(SatFail::Unknown) => Err(SatFail::Unknown),
},
},
Formula::Not(inner) => {
if let Formula::Atom { kind, args } = inner.as_ref() {
if is_rel(kind) {
let dual = Formula::Atom {
kind: dual_kind(kind.clone()),
args: args.clone(),
};
return sat_intervals(&dual, pool);
}
}
Err(SatFail::Unknown)
}
Formula::Atom { kind, args } => {
if matches!(
kind,
PredicateKind::And | PredicateKind::Or | PredicateKind::Not
) {
return Err(SatFail::Unknown);
}
if matches!(kind, PredicateKind::True) {
return Ok(HashMap::new());
}
if matches!(kind, PredicateKind::False) {
return Err(SatFail::Unsat);
}
let (v, iv) = atom_to_interval(pool, kind.clone(), args).ok_or(SatFail::Unknown)?;
if iv.is_empty() {
return Err(SatFail::Unsat);
}
let mut m = HashMap::new();
m.insert(v, iv);
Ok(m)
}
}
}
fn simplify_formula_constants(f: Formula) -> Formula {
match f {
Formula::And(a, b) => {
let la = simplify_formula_constants(*a);
let lb = simplify_formula_constants(*b);
match (&la, &lb) {
(Formula::False, _) | (_, Formula::False) => Formula::False,
(Formula::True, x) => x.clone(),
(x, Formula::True) => x.clone(),
_ => Formula::and(la, lb),
}
}
Formula::Or(a, b) => {
let la = simplify_formula_constants(*a);
let lb = simplify_formula_constants(*b);
match (&la, &lb) {
(Formula::True, _) | (_, Formula::True) => Formula::True,
(Formula::False, x) => x.clone(),
(x, Formula::False) => x.clone(),
_ => Formula::or(la, lb),
}
}
Formula::Not(x) => Formula::not(simplify_formula_constants(*x)),
Formula::Forall { var, body } => Formula::Forall {
var,
body: Box::new(simplify_formula_constants(*body)),
},
Formula::Exists { var, body } => Formula::Exists {
var,
body: Box::new(simplify_formula_constants(*body)),
},
other => other,
}
}
const MAX_PROP_VARS: usize = 64;
const MAX_PROP_NODES: usize = 4096;
const PROP_DECISION_BUDGET: u64 = 2_000_000;
pub(crate) const BOOL_TRUE: &str = "true";
pub(crate) const BOOL_FALSE: &str = "false";
#[derive(Debug, Clone, PartialEq, Eq)]
enum Prop {
Var(u32),
Const(bool),
And(Vec<Prop>),
Or(Vec<Prop>),
Not(Box<Prop>),
}
impl Prop {
fn eval(&self, model: &[bool]) -> bool {
match self {
Prop::Const(b) => *b,
Prop::Var(i) => model[*i as usize - 1],
Prop::Not(x) => !x.eval(model),
Prop::And(xs) => xs.iter().all(|x| x.eval(model)),
Prop::Or(xs) => xs.iter().any(|x| x.eval(model)),
}
}
}
struct PropVars {
index: HashMap<ExprId, u32>,
names: Vec<String>,
nodes: usize,
}
fn prop_from_expr(expr: ExprId, pool: &ExprPool, vars: &mut PropVars) -> Option<Prop> {
vars.nodes += 1;
if vars.nodes > MAX_PROP_NODES {
return None;
}
match pool.get(expr) {
ExprData::Symbol { name, .. } => {
if let Some(&i) = vars.index.get(&expr) {
return Some(Prop::Var(i));
}
if vars.names.contains(&name) {
return None;
}
if vars.names.len() >= MAX_PROP_VARS {
return None;
}
let i = vars.names.len() as u32 + 1;
vars.names.push(name);
vars.index.insert(expr, i);
Some(Prop::Var(i))
}
ExprData::Predicate { kind, args } => match kind {
PredicateKind::True => Some(Prop::Const(true)),
PredicateKind::False => Some(Prop::Const(false)),
PredicateKind::And => args
.into_iter()
.map(|a| prop_from_expr(a, pool, vars))
.collect::<Option<Vec<_>>>()
.map(Prop::And),
PredicateKind::Or => args
.into_iter()
.map(|a| prop_from_expr(a, pool, vars))
.collect::<Option<Vec<_>>>()
.map(Prop::Or),
PredicateKind::Not => {
if args.len() != 1 {
return None;
}
prop_from_expr(args[0], pool, vars).map(|p| Prop::Not(Box::new(p)))
}
_ => None,
},
_ => None,
}
}
fn prop_fold(p: Prop) -> Prop {
match p {
Prop::And(xs) => {
let mut out = Vec::with_capacity(xs.len());
for x in xs {
match prop_fold(x) {
Prop::Const(true) => {}
Prop::Const(false) => return Prop::Const(false),
other => out.push(other),
}
}
match out.len() {
0 => Prop::Const(true),
1 => out.pop().expect("len == 1"),
_ => Prop::And(out),
}
}
Prop::Or(xs) => {
let mut out = Vec::with_capacity(xs.len());
for x in xs {
match prop_fold(x) {
Prop::Const(false) => {}
Prop::Const(true) => return Prop::Const(true),
other => out.push(other),
}
}
match out.len() {
0 => Prop::Const(false),
1 => out.pop().expect("len == 1"),
_ => Prop::Or(out),
}
}
Prop::Not(x) => match prop_fold(*x) {
Prop::Const(b) => Prop::Const(!b),
other => Prop::Not(Box::new(other)),
},
other => other,
}
}
struct Tseitin {
clauses: Vec<BoolClause>,
next: u32,
}
impl Tseitin {
fn fresh(&mut self) -> Option<BoolLit> {
self.next = self.next.checked_add(1)?;
if self.next > (MAX_PROP_VARS + MAX_PROP_NODES) as u32 {
return None;
}
Some(self.next as BoolLit)
}
fn encode(&mut self, p: &Prop) -> Option<BoolLit> {
match p {
Prop::Const(_) => None,
Prop::Var(i) => Some(*i as BoolLit),
Prop::Not(x) => Some(-self.encode(x)?),
Prop::And(xs) => {
let lits = xs
.iter()
.map(|x| self.encode(x))
.collect::<Option<Vec<_>>>()?;
let g = self.fresh()?;
let mut long = Vec::with_capacity(lits.len() + 1);
long.push(g);
for l in lits {
self.clauses.push(vec![-g, l]);
long.push(-l);
}
self.clauses.push(long);
Some(g)
}
Prop::Or(xs) => {
let lits = xs
.iter()
.map(|x| self.encode(x))
.collect::<Option<Vec<_>>>()?;
let g = self.fresh()?;
let mut long = Vec::with_capacity(lits.len() + 1);
long.push(-g);
for l in lits {
self.clauses.push(vec![g, -l]);
long.push(l);
}
self.clauses.push(long);
Some(g)
}
}
}
}
#[cfg(debug_assertions)]
fn prop_brute_force_sat(p: &Prop, n_vars: usize) -> bool {
let mut model = vec![false; n_vars];
for bits in 0u32..(1u32 << n_vars) {
for (i, m) in model.iter_mut().enumerate() {
*m = bits & (1 << i) != 0;
}
if p.eval(&model) {
return true;
}
}
false
}
fn satisfiable_propositional(expr: ExprId, pool: &ExprPool) -> Option<Satisfiability> {
let mut vars = PropVars {
index: HashMap::new(),
names: Vec::new(),
nodes: 0,
};
let raw = prop_from_expr(expr, pool, &mut vars)?;
let folded = prop_fold(raw);
let n_vars = vars.names.len();
if let Prop::Const(b) = folded {
return Some(if b {
Satisfiability::Sat(HashMap::new())
} else {
Satisfiability::Unsat
});
}
let mut enc = Tseitin {
clauses: Vec::new(),
next: n_vars as u32,
};
let root = enc.encode(&folded)?;
enc.clauses.push(vec![root]);
let n_total = enc.next;
let verdict = dpll_sat_bounded(enc.clauses, n_total, PROP_DECISION_BUDGET);
#[cfg(debug_assertions)]
if n_vars <= 10 && !matches!(verdict, DpllOutcome::Exhausted) {
assert_eq!(
matches!(verdict, DpllOutcome::Sat(_)),
prop_brute_force_sat(&folded, n_vars),
"DPLL disagreed with exhaustive enumeration on {folded:?}"
);
}
match verdict {
DpllOutcome::Unsat => Some(Satisfiability::Unsat),
DpllOutcome::Exhausted => Some(Satisfiability::Unknown),
DpllOutcome::Sat(assign) => {
let model = &assign[..n_vars];
if !folded.eval(model) {
return Some(Satisfiability::Unknown);
}
let witness = vars
.names
.iter()
.zip(model)
.map(|(name, &v)| {
(
name.clone(),
if v { BOOL_TRUE } else { BOOL_FALSE }.to_string(),
)
})
.collect();
Some(Satisfiability::Sat(witness))
}
}
}
pub fn satisfiable(expr: ExprId, pool: &ExprPool) -> Satisfiability {
if let Some(verdict) = satisfiable_propositional(expr, pool) {
return verdict;
}
let f = match formula_from_expr(expr, pool) {
Ok(f) => f,
Err(_) => return Satisfiability::Unknown,
};
let f = nnf(simplify_formula_constants(f));
match sat_intervals(&f, pool).and_then(|m| map_to_witness(&m, pool)) {
Ok(w) => Satisfiability::Sat(w),
Err(SatFail::Unsat) => Satisfiability::Unsat,
Err(SatFail::Unknown) => Satisfiability::Unknown,
}
}
pub type BoolLit = i32;
pub type BoolClause = Vec<BoolLit>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DpllOutcome {
Sat(Vec<bool>),
Unsat,
Exhausted,
}
enum DpllStop {
Conflict,
Budget,
}
fn dpll_is_conflict(c: &BoolClause, a: &[Option<bool>]) -> bool {
c.iter().all(|&lit| {
let v = lit.unsigned_abs() as usize - 1;
let sign = lit > 0;
match a[v] {
Some(t) => t != sign,
None => false,
}
})
}
fn dpll_unit_prop(clauses: &[BoolClause], a: &mut [Option<bool>]) -> Result<(), ()> {
loop {
let mut progressed = false;
for cl in clauses {
let mut unassigned: Vec<(usize, bool)> = vec![];
let mut satisfied = false;
for &lit in cl {
let v = lit.unsigned_abs() as usize - 1;
let sign = lit > 0;
match a[v] {
None => unassigned.push((v, sign)),
Some(t) if t == sign => satisfied = true,
_ => {}
}
}
if satisfied {
continue;
}
if unassigned.is_empty() {
return Err(());
}
if unassigned.len() == 1 {
let (v, s) = unassigned[0];
if a[v].is_none() {
a[v] = Some(s);
progressed = true;
}
}
}
if !progressed {
break;
}
}
Ok(())
}
fn dpll_dfs(
clauses: &[BoolClause],
a: &mut [Option<bool>],
budget: &mut u64,
) -> Result<(), DpllStop> {
dpll_unit_prop(clauses, a).map_err(|()| DpllStop::Conflict)?;
for cl in clauses {
if dpll_is_conflict(cl, a) {
return Err(DpllStop::Conflict);
}
}
let Some((i, _)) = a.iter().enumerate().find(|(_, x)| x.is_none()) else {
return Ok(());
};
if *budget == 0 {
return Err(DpllStop::Budget);
}
*budget -= 1;
let snapshot = a.to_vec();
for value in [false, true] {
a[i] = Some(value);
match dpll_dfs(clauses, a, budget) {
Ok(()) => return Ok(()),
Err(DpllStop::Budget) => return Err(DpllStop::Budget),
Err(DpllStop::Conflict) => a.copy_from_slice(&snapshot),
}
}
Err(DpllStop::Conflict)
}
pub(crate) fn dpll_sat_bounded(
clauses: Vec<BoolClause>,
n_vars: u32,
max_decisions: u64,
) -> DpllOutcome {
let mut assign = vec![None; n_vars as usize];
let mut budget = max_decisions;
match dpll_dfs(&clauses, &mut assign, &mut budget) {
Ok(()) => {
let model: Vec<bool> = assign.into_iter().map(|x| x.unwrap_or(false)).collect();
debug_assert!(
clauses.iter().all(|cl| cl
.iter()
.any(|&lit| { model[lit.unsigned_abs() as usize - 1] == (lit > 0) })),
"dpll_sat_bounded returned a model that falsifies a clause"
);
DpllOutcome::Sat(model)
}
Err(DpllStop::Conflict) => DpllOutcome::Unsat,
Err(DpllStop::Budget) => DpllOutcome::Exhausted,
}
}
pub fn dpll_sat(clauses: Vec<BoolClause>, n_vars: u32) -> Option<Vec<bool>> {
match dpll_sat_bounded(clauses, n_vars, u64::MAX) {
DpllOutcome::Sat(model) => Some(model),
DpllOutcome::Unsat => None,
DpllOutcome::Exhausted => {
unreachable!("dpll_sat: the u64::MAX decision budget was exhausted")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
#[test]
fn and_contradiction_unsat() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let z = p.integer(0_i32);
let f = p.pred_and(vec![p.pred_gt(x, z), p.pred_lt(x, z)]);
assert_eq!(satisfiable(f, &p), Satisfiability::Unsat);
}
#[test]
fn or_cover_sat() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let z = p.integer(0_i32);
let f = p.pred_or(vec![p.pred_gt(x, z), p.pred_le(x, z)]);
match satisfiable(f, &p) {
Satisfiability::Sat(m) => assert!(m.contains_key("x")),
other => panic!("expected Sat, got {other:?}"),
}
}
#[test]
fn forall_unknown() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let f = p.forall(x, p.pred_gt(x, p.integer(0_i32)));
assert_eq!(satisfiable(f, &p), Satisfiability::Unknown);
}
#[test]
fn formula_quant_round_trip() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let body = p.pred_gt(x, p.integer(0_i32));
let q = Formula::Exists {
var: x,
body: Box::new(formula_from_expr(body, &p).unwrap()),
};
let e = q.to_expr(&p);
let back = formula_from_expr(e, &p).unwrap();
assert_eq!(back, q);
}
#[test]
fn dpll_tiny_sat() {
let r = dpll_sat(vec![vec![1, 2], vec![-1, 2]], 2);
assert!(r.is_some());
}
#[test]
fn dpll_restores_the_trail_on_backtrack() {
let clauses = vec![vec![1, 2], vec![1, -2], vec![-1, -2]];
let model = dpll_sat(clauses.clone(), 2).expect("instance is satisfiable");
for cl in &clauses {
assert!(
cl.iter()
.any(|&lit| model[lit.unsigned_abs() as usize - 1] == (lit > 0)),
"clause {cl:?} falsified by {model:?}"
);
}
}
#[test]
fn dpll_genuine_unsat_is_still_unsat() {
assert!(dpll_sat(vec![vec![1], vec![-1]], 1).is_none());
let all_excluded = vec![vec![1, 2], vec![1, -2], vec![-1, 2], vec![-1, -2]];
assert!(dpll_sat(all_excluded, 2).is_none());
}
#[test]
fn dpll_bounded_reports_exhausted_rather_than_guessing() {
let out = dpll_sat_bounded(vec![vec![1, 2], vec![-1, -2]], 2, 0);
assert_eq!(out, DpllOutcome::Exhausted);
assert!(matches!(
dpll_sat_bounded(vec![vec![1, 2], vec![-1, -2]], 2, 100),
DpllOutcome::Sat(_)
));
}
fn bools(p: &ExprPool, names: &[&str]) -> Vec<ExprId> {
names.iter().map(|n| p.symbol(*n, Domain::Real)).collect()
}
fn implies(p: &ExprPool, a: ExprId, b: ExprId) -> ExprId {
p.pred_or(vec![p.pred_not(a), b])
}
fn iff(p: &ExprPool, a: ExprId, b: ExprId) -> ExprId {
p.pred_and(vec![implies(p, a, b), implies(p, b, a)])
}
fn assert_witness_is(m: &HashMap<String, String>, expect: &[(&str, bool)]) {
for (name, want) in expect {
let got = m
.get(*name)
.unwrap_or_else(|| panic!("witness has no entry for {name}: {m:?}"));
assert_eq!(
got,
if *want { BOOL_TRUE } else { BOOL_FALSE },
"witness for {name}"
);
}
}
#[test]
fn propositional_unsat() {
let p = ExprPool::new();
let v = bools(&p, &["A", "B"]);
let (a, b) = (v[0], v[1]);
let f = p.pred_and(vec![p.pred_or(vec![a, b]), p.pred_not(a), p.pred_not(b)]);
assert_eq!(satisfiable(f, &p), Satisfiability::Unsat);
}
#[test]
fn propositional_sat_gives_a_checked_witness() {
let p = ExprPool::new();
let v = bools(&p, &["A", "B"]);
let (a, b) = (v[0], v[1]);
let f = p.pred_and(vec![p.pred_or(vec![a, b]), p.pred_not(a)]);
match satisfiable(f, &p) {
Satisfiability::Sat(m) => {
assert_eq!(m.len(), 2);
assert_witness_is(&m, &[("A", false), ("B", true)]);
}
other => panic!("expected Sat, got {other:?}"),
}
}
#[test]
fn propositional_contradiction_and_tautology() {
let p = ExprPool::new();
let a = p.symbol("A", Domain::Real);
let contradiction = p.pred_and(vec![a, p.pred_not(a)]);
assert_eq!(satisfiable(contradiction, &p), Satisfiability::Unsat);
let tautology = p.pred_or(vec![a, p.pred_not(a)]);
assert!(matches!(satisfiable(tautology, &p), Satisfiability::Sat(_)));
assert_eq!(
satisfiable(p.pred_not(tautology), &p),
Satisfiability::Unsat
);
}
#[test]
fn propositional_bare_symbol_is_a_boolean_variable() {
let p = ExprPool::new();
let a = p.symbol("A", Domain::Real);
match satisfiable(a, &p) {
Satisfiability::Sat(m) => assert_eq!(m.len(), 1),
other => panic!("expected Sat, got {other:?}"),
}
assert_eq!(
satisfiable(p.pred_not(a), &p),
Satisfiability::Sat(HashMap::from([("A".to_string(), BOOL_FALSE.to_string())]))
);
}
#[test]
fn propositional_implication_and_biconditional() {
let p = ExprPool::new();
let v = bools(&p, &["A", "B"]);
let (a, b) = (v[0], v[1]);
let mp = p.pred_and(vec![implies(&p, a, b), a, p.pred_not(b)]);
assert_eq!(satisfiable(mp, &p), Satisfiability::Unsat);
match satisfiable(p.pred_and(vec![implies(&p, a, b), a]), &p) {
Satisfiability::Sat(m) => assert_witness_is(&m, &[("A", true), ("B", true)]),
other => panic!("expected Sat, got {other:?}"),
}
let biconditional = iff(&p, a, b);
assert_eq!(
satisfiable(p.pred_and(vec![biconditional, a, p.pred_not(b)]), &p),
Satisfiability::Unsat
);
match satisfiable(p.pred_and(vec![biconditional, p.pred_not(a)]), &p) {
Satisfiability::Sat(m) => assert_witness_is(&m, &[("A", false), ("B", false)]),
other => panic!("expected Sat, got {other:?}"),
}
}
#[test]
fn propositional_constants_fold() {
let p = ExprPool::new();
let a = p.symbol("A", Domain::Real);
assert_eq!(
satisfiable(p.pred_true(), &p),
Satisfiability::Sat(HashMap::new())
);
assert_eq!(satisfiable(p.pred_false(), &p), Satisfiability::Unsat);
assert_eq!(
satisfiable(p.pred_and(vec![a, p.pred_false()]), &p),
Satisfiability::Unsat
);
assert_eq!(
satisfiable(p.pred_or(vec![a, p.pred_true()]), &p),
Satisfiability::Sat(HashMap::new())
);
}
#[test]
fn propositional_pigeonhole_three_into_two_is_unsat() {
let p = ExprPool::new();
let cell: Vec<Vec<ExprId>> = (0..3)
.map(|i| {
(0..2)
.map(|j| p.symbol(format!("p{i}{j}"), Domain::Real))
.collect()
})
.collect();
let mut clauses = Vec::new();
for row in &cell {
clauses.push(p.pred_or(row.clone()));
}
for (i, row_i) in cell.iter().enumerate() {
for row_k in &cell[i + 1..] {
for (a, b) in row_i.iter().zip(row_k) {
clauses.push(p.pred_or(vec![p.pred_not(*a), p.pred_not(*b)]));
}
}
}
assert_eq!(satisfiable(p.pred_and(clauses), &p), Satisfiability::Unsat);
}
#[test]
fn mixed_boolean_and_arithmetic_stays_unknown() {
let p = ExprPool::new();
let a = p.symbol("A", Domain::Real);
let x = p.symbol("x", Domain::Real);
let f = p.pred_and(vec![a, p.pred_gt(x, p.integer(0_i32))]);
assert_eq!(satisfiable(f, &p), Satisfiability::Unknown);
let g = p.pred_and(vec![
a,
p.pred_gt(x, p.integer(0_i32)),
p.pred_lt(x, p.integer(0_i32)),
]);
assert_eq!(satisfiable(g, &p), Satisfiability::Unknown);
}
#[test]
fn non_predicate_term_stays_unknown() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let t = p.add(vec![x, p.integer(1_i32)]);
assert_eq!(satisfiable(t, &p), Satisfiability::Unknown);
}
#[test]
fn quantified_propositional_stays_unknown() {
let p = ExprPool::new();
let a = p.symbol("A", Domain::Real);
assert_eq!(satisfiable(p.forall(a, a), &p), Satisfiability::Unknown);
}
#[test]
fn same_name_different_domain_refuses_rather_than_conflating() {
let p = ExprPool::new();
let a_real = p.symbol("A", Domain::Real);
let a_int = p.symbol("A", Domain::Integer);
assert_ne!(a_real, a_int);
let f = p.pred_and(vec![a_real, p.pred_not(a_int)]);
assert_eq!(satisfiable(f, &p), Satisfiability::Unknown);
}
#[test]
fn arithmetic_path_is_untouched() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let z = p.integer(0_i32);
assert_eq!(
satisfiable(p.pred_and(vec![p.pred_gt(x, z), p.pred_lt(x, z)]), &p),
Satisfiability::Unsat
);
match satisfiable(p.pred_gt(x, z), &p) {
Satisfiability::Sat(m) => {
let v = m.get("x").expect("witness for x");
assert_ne!(v, BOOL_TRUE);
assert_ne!(v, BOOL_FALSE);
assert!(
v.parse::<rug::Rational>().is_ok(),
"witness is not a rational: {v}"
);
}
other => panic!("expected Sat, got {other:?}"),
}
}
fn xorshift(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
fn random_prop(p: &ExprPool, vars: &[ExprId], depth: u32, rng: &mut u64) -> ExprId {
if depth == 0 {
return vars[(xorshift(rng) as usize) % vars.len()];
}
match xorshift(rng) % 5 {
0 => vars[(xorshift(rng) as usize) % vars.len()],
1 => p.pred_not(random_prop(p, vars, depth - 1, rng)),
2 => {
let k = 2 + (xorshift(rng) % 2) as usize;
p.pred_and(
(0..k)
.map(|_| random_prop(p, vars, depth - 1, rng))
.collect(),
)
}
3 => {
let k = 2 + (xorshift(rng) % 2) as usize;
p.pred_or(
(0..k)
.map(|_| random_prop(p, vars, depth - 1, rng))
.collect(),
)
}
_ => {
let a = random_prop(p, vars, depth - 1, rng);
let b = random_prop(p, vars, depth - 1, rng);
p.pred_or(vec![p.pred_not(a), b])
}
}
}
fn eval_expr_prop(p: &ExprPool, e: ExprId, env: &HashMap<ExprId, bool>) -> bool {
match p.get(e) {
ExprData::Symbol { .. } => env[&e],
ExprData::Predicate { kind, args } => match kind {
PredicateKind::True => true,
PredicateKind::False => false,
PredicateKind::Not => !eval_expr_prop(p, args[0], env),
PredicateKind::And => args.iter().all(|&a| eval_expr_prop(p, a, env)),
PredicateKind::Or => args.iter().any(|&a| eval_expr_prop(p, a, env)),
other => panic!("not propositional: {other:?}"),
},
other => panic!("not propositional: {other:?}"),
}
}
#[test]
fn propositional_agrees_with_exhaustive_enumeration() {
let mut rng = 0x2545_F491_4F6C_DD1D_u64;
for case in 0..400_u32 {
let p = ExprPool::new();
let n = 2 + (case % 4) as usize; let names: Vec<String> = (0..n).map(|i| format!("v{i}")).collect();
let vars: Vec<ExprId> = names.iter().map(|s| p.symbol(s, Domain::Real)).collect();
let f = random_prop(&p, &vars, 3, &mut rng);
let mut truth = false;
for bits in 0_u32..(1_u32 << n) {
let env: HashMap<ExprId, bool> = vars
.iter()
.enumerate()
.map(|(i, &v)| (v, bits & (1 << i) != 0))
.collect();
if eval_expr_prop(&p, f, &env) {
truth = true;
break;
}
}
match satisfiable(f, &p) {
Satisfiability::Unsat => {
assert!(!truth, "case {case}: reported Unsat but a model exists");
}
Satisfiability::Sat(m) => {
assert!(
truth,
"case {case}: reported Sat but the formula is unsatisfiable"
);
let env: HashMap<ExprId, bool> = vars
.iter()
.zip(&names)
.map(|(&v, name)| (v, m.get(name).map(|s| s == BOOL_TRUE) == Some(true)))
.collect();
assert!(
eval_expr_prop(&p, f, &env),
"case {case}: witness {m:?} does not satisfy the formula"
);
}
Satisfiability::Unknown => {
panic!("case {case}: purely propositional formula answered Unknown")
}
}
}
}
}