use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use crate::base::arena::Arena;
use crate::base::assumptions::{AssumptionCache, Props};
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
const LT: u8 = 0b001;
const EQ: u8 = 0b010;
const GT: u8 = 0b100;
const ALL: u8 = 0b111;
const MAX_SAT_VARS: usize = 24;
const SAT_BUDGET: usize = 200_000;
const MAX_CLAUSES: usize = 4096;
const MAX_LITERALS: usize = 50_000;
const MAX_TRUTH_TABLE_VARS: usize = 8;
const MAX_SIMPLIFY_PASSES: usize = 6;
const MAX_CONSENSUS_TERMS: usize = 32;
const MAX_CONSENSUS_STEPS: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Rel {
a: ExprId,
b: ExprId,
mask: u8,
}
fn mirror(mask: u8) -> u8 {
let mut m = mask & EQ;
if mask & LT != 0 {
m |= GT;
}
if mask & GT != 0 {
m |= LT;
}
m
}
fn should_swap(arena: &Arena, p: ExprId, q: ExprId) -> bool {
if p == q {
return false;
}
let pn = matches!(arena.node(p), ExprNode::Num(_));
let qn = matches!(arena.node(q), ExprNode::Num(_));
match (pn, qn) {
(true, false) => true,
(false, true) => false,
_ => arena.sort_key(p) > arena.sort_key(q),
}
}
fn rel_of(arena: &Arena, id: ExprId) -> Option<Rel> {
let (p, q, mask) = match arena.node(id) {
ExprNode::Gt(p, q) => (*p, *q, GT),
ExprNode::Ge(p, q) => (*p, *q, GT | EQ),
ExprNode::Eq_(p, q) => (*p, *q, EQ),
ExprNode::Ne(p, q) => (*p, *q, LT | GT),
_ => return None,
};
Some(if should_swap(arena, p, q) {
Rel {
a: q,
b: p,
mask: mirror(mask),
}
} else {
Rel { a: p, b: q, mask }
})
}
fn fold_rel(arena: &mut Arena, rel: Rel) -> ExprId {
let Rel { a, b, mask } = rel;
if mask == 0 {
return arena.bool_false;
}
if mask == ALL {
return arena.bool_true;
}
if let Some(o) = crate::transforms::sets::cmp_exprs(arena, a, b) {
let bit = match o {
std::cmp::Ordering::Less => LT,
std::cmp::Ordering::Equal => EQ,
std::cmp::Ordering::Greater => GT,
};
return if mask & bit != 0 {
arena.bool_true
} else {
arena.bool_false
};
}
match mask {
GT => arena.gt(a, b),
LT => arena.gt(b, a),
EQ => arena.eq_(a, b),
m if m == GT | EQ => arena.ge(a, b),
m if m == LT | EQ => arena.ge(b, a),
_ => arena.ne_(a, b),
}
}
fn neg_lit(arena: &mut Arena, lit: ExprId) -> ExprId {
if lit == arena.bool_true {
return arena.bool_false;
}
if lit == arena.bool_false {
return arena.bool_true;
}
if let Some(r) = rel_of(arena, lit) {
return fold_rel(
arena,
Rel {
mask: ALL & !r.mask,
..r
},
);
}
arena.not(lit)
}
fn is_connective(arena: &Arena, id: ExprId) -> bool {
matches!(
arena.node(id),
ExprNode::And(_) | ExprNode::Or(_) | ExprNode::Not(_)
)
}
fn bool_post_order(arena: &Arena, root: ExprId) -> Vec<ExprId> {
let mut order = Vec::new();
let mut visited: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: Vec<(ExprId, bool)> = vec![(root, false)];
while let Some((id, expanded)) = stack.pop() {
if visited.contains(&id) {
continue;
}
if expanded {
visited.insert(id);
order.push(id);
continue;
}
stack.push((id, true));
match arena.node(id) {
ExprNode::And(ch) | ExprNode::Or(ch) => {
for &c in ch.iter().rev() {
if !visited.contains(&c) {
stack.push((c, false));
}
}
}
ExprNode::Not(x) if !visited.contains(x) => stack.push((*x, false)),
_ => {}
}
}
order
}
pub(crate) fn to_nnf(arena: &mut Arena, root: ExprId) -> ExprId {
let mut memo: FxHashMap<(ExprId, bool), ExprId> = FxHashMap::default();
let mut stack: Vec<(ExprId, bool, bool)> = vec![(root, true, false)];
while let Some((id, pol, expanded)) = stack.pop() {
if memo.contains_key(&(id, pol)) {
continue;
}
if !expanded {
stack.push((id, pol, true));
match arena.node(id) {
ExprNode::And(ch) | ExprNode::Or(ch) => {
for &c in ch.iter().rev() {
stack.push((c, pol, false));
}
}
ExprNode::Not(x) => stack.push((*x, !pol, false)),
_ => {}
}
continue;
}
let result = match arena.node(id).clone() {
ExprNode::BoolTrue => {
if pol {
arena.bool_true
} else {
arena.bool_false
}
}
ExprNode::BoolFalse => {
if pol {
arena.bool_false
} else {
arena.bool_true
}
}
ExprNode::Gt(..) | ExprNode::Ge(..) | ExprNode::Eq_(..) | ExprNode::Ne(..) => {
let r = rel_of(arena, id).unwrap_or(Rel {
a: id,
b: id,
mask: ALL,
});
let mask = if pol { r.mask } else { ALL & !r.mask };
fold_rel(arena, Rel { mask, ..r })
}
ExprNode::And(ch) => {
let kids: SmallVec<[ExprId; 6]> = ch
.iter()
.map(|c| memo.get(&(*c, pol)).copied().unwrap_or(*c))
.collect();
if pol {
arena.and(&kids)
} else {
arena.or(&kids)
}
}
ExprNode::Or(ch) => {
let kids: SmallVec<[ExprId; 6]> = ch
.iter()
.map(|c| memo.get(&(*c, pol)).copied().unwrap_or(*c))
.collect();
if pol {
arena.or(&kids)
} else {
arena.and(&kids)
}
}
ExprNode::Not(x) => memo.get(&(x, !pol)).copied().unwrap_or(id),
_ => {
if pol {
id
} else {
arena.not(id)
}
}
};
memo.insert((id, pol), result);
}
memo.get(&(root, true)).copied().unwrap_or(root)
}
#[derive(Default)]
struct Context {
pairs: FxHashMap<(ExprId, ExprId), u8>,
atoms: FxHashMap<ExprId, bool>,
}
fn simplify_connective(arena: &mut Arena, is_and: bool, children: &[ExprId]) -> ExprId {
let identity = if is_and {
arena.bool_true
} else {
arena.bool_false
};
let absorbing = if is_and {
arena.bool_false
} else {
arena.bool_true
};
let mut lits: Vec<ExprId> = Vec::new();
let mut seen: FxHashSet<ExprId> = FxHashSet::default();
let mut work: Vec<ExprId> = children.iter().rev().copied().collect();
while let Some(c) = work.pop() {
let same = match arena.node(c) {
ExprNode::And(ch) if is_and => Some(ch.clone()),
ExprNode::Or(ch) if !is_and => Some(ch.clone()),
_ => None,
};
if let Some(ch) = same {
for &k in ch.iter().rev() {
work.push(k);
}
continue;
}
if c == identity {
continue;
}
if c == absorbing {
return absorbing;
}
if seen.insert(c) {
lits.push(c);
}
}
let mut ctx = Context::default();
let mut pair_order: Vec<(ExprId, ExprId)> = Vec::new();
let mut others: Vec<ExprId> = Vec::new();
for &l in &lits {
if let Some(r) = rel_of(arena, l) {
let key = (r.a, r.b);
let entry = ctx.pairs.entry(key).or_insert_with(|| {
pair_order.push(key);
if is_and { ALL } else { 0 }
});
if is_and {
*entry &= r.mask;
} else {
*entry |= r.mask;
}
} else {
others.push(l);
}
}
let mut merged: Vec<ExprId> = Vec::new();
for key in &pair_order {
let mask = ctx.pairs[key];
let e = fold_rel(
arena,
Rel {
a: key.0,
b: key.1,
mask,
},
);
if e == absorbing {
return absorbing;
}
if e != identity {
merged.push(e);
}
}
if !is_and {
for m in ctx.pairs.values_mut() {
*m = ALL & !*m;
}
}
let mut opaque: Vec<ExprId> = Vec::new();
let mut duals: Vec<ExprId> = Vec::new();
for &l in &others {
let is_dual = match arena.node(l) {
ExprNode::Or(_) => is_and,
ExprNode::And(_) => !is_and,
_ => false,
};
if is_dual {
duals.push(l);
continue;
}
let (atom, val) = match arena.node(l) {
ExprNode::Not(x) => (*x, false),
_ => (l, true),
};
let known = if is_and { val } else { !val };
if let Some(&prev) = ctx.atoms.get(&atom)
&& prev != known
{
return absorbing;
}
ctx.atoms.insert(atom, known);
opaque.push(l);
}
let mut reduced_duals: Vec<ExprId> = Vec::new();
for &d in &duals {
let kids: SmallVec<[ExprId; 6]> = match arena.node(d) {
ExprNode::Or(ch) | ExprNode::And(ch) => ch.clone(),
_ => SmallVec::new(),
};
let mut kept: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut drop_dual = false;
for &k in &kids {
let status: Option<bool> = if let Some(r) = rel_of(arena, k) {
match ctx.pairs.get(&(r.a, r.b)) {
Some(&known) => {
if known & !r.mask == 0 {
Some(true) } else if known & r.mask == 0 {
Some(false)
} else {
None
}
}
None => None,
}
} else {
let (atom, val) = match arena.node(k) {
ExprNode::Not(x) => (*x, false),
_ => (k, true),
};
ctx.atoms.get(&atom).map(|&kn| kn == val)
};
match status {
Some(true) => {
if is_and {
drop_dual = true; break;
} else {
continue;
}
}
Some(false) => {
if is_and {
continue; } else {
drop_dual = true; break;
}
}
None => kept.push(k),
}
}
if drop_dual {
continue;
}
let rebuilt = if kept.len() == kids.len() {
d
} else if is_and {
arena.or(&kept) } else {
arena.and(&kept) };
if rebuilt == absorbing {
return absorbing;
}
if rebuilt != identity {
reduced_duals.push(rebuilt);
}
}
let reduced_duals = consensus(arena, is_and, reduced_duals);
if reduced_duals.contains(&absorbing) {
return absorbing;
}
let mut out: Vec<ExprId> =
Vec::with_capacity(merged.len() + opaque.len() + reduced_duals.len());
out.extend(merged);
out.extend(opaque);
out.extend(reduced_duals);
let mut seen2: FxHashSet<ExprId> = FxHashSet::default();
out.retain(|c| seen2.insert(*c));
out.sort_by(|&x, &y| arena.sort_key(x).cmp(arena.sort_key(y)));
if out.is_empty() {
identity
} else if out.len() == 1 {
out[0]
} else if is_and {
arena.and(&out)
} else {
arena.or(&out)
}
}
enum Resolution {
Merge(Vec<ExprId>),
Redundant(Vec<ExprId>),
}
fn resolve(arena: &mut Arena, is_and: bool, t1: &[ExprId], t2: &[ExprId]) -> Option<Resolution> {
let common: Vec<ExprId> = t1.iter().copied().filter(|l| t2.contains(l)).collect();
let d1: Vec<ExprId> = t1.iter().copied().filter(|l| !t2.contains(l)).collect();
let d2: Vec<ExprId> = t2.iter().copied().filter(|l| !t1.contains(l)).collect();
if d1.is_empty() || d2.is_empty() {
return None; }
let combine = |arena: &mut Arena, l1: ExprId, l2: ExprId| -> Option<Option<ExprId>> {
if let (Some(r1), Some(r2)) = (rel_of(arena, l1), rel_of(arena, l2))
&& (r1.a, r1.b) == (r2.a, r2.b)
{
let mask = if is_and {
r1.mask & r2.mask
} else {
r1.mask | r2.mask
};
let folded = fold_rel(arena, Rel { mask, ..r1 });
let identity_inside = if is_and {
arena.bool_false
} else {
arena.bool_true
};
return Some(if folded == identity_inside {
None
} else {
Some(folded)
});
}
if neg_lit(arena, l1) == l2 {
return Some(None);
}
None
};
if d1.len() == 1 && d2.len() == 1 {
if let Some(combined) = combine(arena, d1[0], d2[0]) {
let mut term = common;
if let Some(l) = combined {
term.push(l);
term.sort_by(|&x, &y| arena.sort_key(x).cmp(arena.sort_key(y)));
}
return Some(Resolution::Merge(term));
}
return None;
}
let mut pair: Option<(ExprId, ExprId)> = None;
for &l1 in &d1 {
for &l2 in &d2 {
if combine(arena, l1, l2) == Some(None) {
if pair.is_some() {
return None; }
pair = Some((l1, l2));
}
}
}
let (l1, l2) = pair?;
let mut resolvent: Vec<ExprId> = t1.iter().copied().filter(|&l| l != l1).collect();
for &l in t2 {
if l != l2 && !resolvent.contains(&l) {
resolvent.push(l);
}
}
Some(Resolution::Redundant(resolvent))
}
fn consensus(arena: &mut Arena, is_and: bool, duals: Vec<ExprId>) -> Vec<ExprId> {
if duals.len() < 2 || duals.len() > MAX_CONSENSUS_TERMS {
return duals;
}
let mut terms: Vec<Vec<ExprId>> = duals
.iter()
.map(|&d| match arena.node(d) {
ExprNode::Or(ch) | ExprNode::And(ch) => ch.to_vec(),
_ => vec![d],
})
.collect();
let mut budget = MAX_CONSENSUS_STEPS;
let mut changed = true;
let mut any_change = false;
'restart: while changed && budget > 0 {
changed = false;
for i in 0..terms.len() {
for j in (i + 1)..terms.len() {
budget -= 1;
if budget == 0 {
break 'restart;
}
match resolve(arena, is_and, &terms[i], &terms[j]) {
Some(Resolution::Merge(term)) => {
tracing::trace!("consensus: merged two terms differing in one literal");
terms[i] = term;
terms.remove(j);
changed = true;
any_change = true;
continue 'restart;
}
Some(Resolution::Redundant(resolvent)) => {
let before = terms.len();
let (ti, tj) = (terms[i].clone(), terms[j].clone());
terms.retain(|t| {
*t == ti || *t == tj || !resolvent.iter().all(|l| t.contains(l))
});
if terms.len() != before {
tracing::trace!("consensus: dropped a term implied by a resolvent");
changed = true;
any_change = true;
continue 'restart;
}
}
None => {}
}
}
}
}
if !any_change {
return duals;
}
if terms.iter().any(Vec::is_empty) {
return vec![if is_and {
arena.bool_false
} else {
arena.bool_true
}];
}
terms
.into_iter()
.map(|t| {
if t.len() == 1 {
t[0]
} else if is_and {
arena.or(&t)
} else {
arena.and(&t)
}
})
.collect()
}
fn simplify_pass(arena: &mut Arena, root: ExprId) -> ExprId {
let order = bool_post_order(arena, root);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
for &id in &order {
let new = match arena.node(id).clone() {
ExprNode::And(ch) => {
let kids: SmallVec<[ExprId; 6]> = ch
.iter()
.map(|c| cache.get(c).copied().unwrap_or(*c))
.collect();
simplify_connective(arena, true, &kids)
}
ExprNode::Or(ch) => {
let kids: SmallVec<[ExprId; 6]> = ch
.iter()
.map(|c| cache.get(c).copied().unwrap_or(*c))
.collect();
simplify_connective(arena, false, &kids)
}
ExprNode::Not(x) => {
let nx = cache.get(&x).copied().unwrap_or(x);
neg_lit(arena, nx)
}
ExprNode::Gt(..) | ExprNode::Ge(..) | ExprNode::Eq_(..) | ExprNode::Ne(..) => {
match rel_of(arena, id) {
Some(r) => fold_rel(arena, r),
None => id,
}
}
_ => id,
};
cache.insert(id, new);
}
cache.get(&root).copied().unwrap_or(root)
}
pub(crate) fn simplify_bool(arena: &mut Arena, root: ExprId) -> ExprId {
let mut cur = root;
for _ in 0..MAX_SIMPLIFY_PASSES {
let n = to_nnf(arena, cur);
let s = simplify_pass(arena, n);
if s == cur {
break;
}
cur = s;
}
cur
}
pub(crate) fn simplify_bool_full(arena: &mut Arena, root: ExprId) -> ExprId {
use crate::simplify::simplify_engine::{SimplifyOpts, unified_simplify};
let atoms_list = atoms(arena, root);
let opts = SimplifyOpts::default();
let mut operand_memo: FxHashMap<ExprId, ExprId> = FxHashMap::default();
let mut pairs: Vec<(ExprId, ExprId)> = Vec::new();
for atom in atoms_list {
let (a, b) = match arena.node(atom) {
ExprNode::Gt(a, b) | ExprNode::Ge(a, b) | ExprNode::Eq_(a, b) | ExprNode::Ne(a, b) => {
(*a, *b)
}
_ => continue,
};
let mut simp = |arena: &mut Arena, e: ExprId| -> ExprId {
if let Some(&s) = operand_memo.get(&e) {
return s;
}
let s = unified_simplify(arena, e, &opts).expr;
operand_memo.insert(e, s);
s
};
let na = simp(arena, a);
let nb = simp(arena, b);
if na == a && nb == b {
continue;
}
let new_atom = match arena.node(atom).clone() {
ExprNode::Gt(..) => arena.gt(na, nb),
ExprNode::Ge(..) => arena.ge(na, nb),
ExprNode::Eq_(..) => arena.eq_(na, nb),
_ => arena.ne_(na, nb),
};
pairs.push((atom, new_atom));
}
let rebuilt = if pairs.is_empty() {
root
} else {
arena.subs_map_structural(root, &pairs)
};
simplify_bool(arena, rebuilt)
}
type Clauses = Vec<Vec<ExprId>>;
fn cross(a: &Clauses, b: &Clauses) -> Option<Clauses> {
if a.len().saturating_mul(b.len()) > MAX_CLAUSES {
return None;
}
let la: usize = a.iter().map(Vec::len).sum();
let lb: usize = b.iter().map(Vec::len).sum();
if la
.saturating_mul(b.len())
.saturating_add(lb.saturating_mul(a.len()))
> MAX_LITERALS
{
return None;
}
let mut out = Vec::with_capacity(a.len() * b.len());
for x in a {
for y in b {
let mut c = x.clone();
for &l in y {
if !c.contains(&l) {
c.push(l);
}
}
out.push(c);
}
}
Some(out)
}
fn clean_clause(arena: &mut Arena, clause: &[ExprId], cnf: bool) -> Option<Vec<ExprId>> {
let e = simplify_connective(arena, !cnf, clause);
let absorbing = if cnf {
arena.bool_true
} else {
arena.bool_false
};
let identity = if cnf {
arena.bool_false
} else {
arena.bool_true
};
if e == absorbing {
return None;
}
if e == identity {
return Some(Vec::new());
}
match arena.node(e) {
ExprNode::Or(ch) if cnf => Some(ch.iter().copied().collect()),
ExprNode::And(ch) if !cnf => Some(ch.iter().copied().collect()),
_ => Some(vec![e]),
}
}
fn distribute(arena: &mut Arena, root: ExprId, cnf: bool) -> ExprId {
let nnf = simplify_bool(arena, root);
if nnf == arena.bool_true || nnf == arena.bool_false {
return nnf;
}
let order = bool_post_order(arena, nnf);
let mut memo: FxHashMap<ExprId, Clauses> = FxHashMap::default();
for &id in &order {
let clauses: Clauses = match arena.node(id).clone() {
ExprNode::And(ch) | ExprNode::Or(ch) => {
let is_and = matches!(arena.node(id), ExprNode::And(_));
let concat = is_and == cnf;
let mut acc: Clauses = if concat { Vec::new() } else { vec![Vec::new()] };
for c in &ch {
let sub = memo.get(c).cloned().unwrap_or_else(|| vec![vec![*c]]);
if concat {
acc.extend(sub);
} else {
match cross(&acc, &sub) {
Some(x) => acc = x,
None => return nnf, }
}
if acc.len() > MAX_CLAUSES
|| acc.iter().map(Vec::len).sum::<usize>() > MAX_LITERALS
{
return nnf;
}
}
acc
}
ExprNode::BoolTrue => {
if cnf {
Vec::new()
} else {
vec![Vec::new()]
}
}
ExprNode::BoolFalse => {
if cnf {
vec![Vec::new()]
} else {
Vec::new()
}
}
_ => vec![vec![id]],
};
memo.insert(id, clauses);
}
let raw = memo.remove(&nnf).unwrap_or_default();
let mut cleaned: Vec<Vec<ExprId>> = Vec::new();
for c in &raw {
if let Some(cc) = clean_clause(arena, c, cnf) {
if cc.is_empty() {
return if cnf {
arena.bool_false
} else {
arena.bool_true
};
}
cleaned.push(cc);
}
}
cleaned.sort_by_key(Vec::len);
let check_subsumption = cleaned.len() <= 512;
let mut kept: Vec<Vec<ExprId>> = Vec::new();
let mut seen: FxHashSet<Vec<ExprId>> = FxHashSet::default();
for c in cleaned {
if !seen.insert(c.clone()) {
continue;
}
let subsumed = check_subsumption && kept.iter().any(|k| k.iter().all(|l| c.contains(l)));
if !subsumed {
kept.push(c);
}
}
let inner: Vec<ExprId> = kept
.iter()
.map(|c| if cnf { arena.or(c) } else { arena.and(c) })
.collect();
let mut inner = inner;
inner.sort_by(|&x, &y| arena.sort_key(x).cmp(arena.sort_key(y)));
if cnf {
arena.and(&inner)
} else {
arena.or(&inner)
}
}
pub(crate) fn to_cnf(arena: &mut Arena, root: ExprId) -> ExprId {
distribute(arena, root, true)
}
pub(crate) fn to_dnf(arena: &mut Arena, root: ExprId) -> ExprId {
distribute(arena, root, false)
}
pub(crate) fn atoms(arena: &Arena, root: ExprId) -> Vec<ExprId> {
bool_post_order(arena, root)
.into_iter()
.filter(|&id| {
!is_connective(arena, id)
&& !matches!(arena.node(id), ExprNode::BoolTrue | ExprNode::BoolFalse)
})
.collect()
}
enum FNode {
Const(bool),
Rel(usize, u8),
Atom(usize, bool),
And(Vec<usize>),
Or(Vec<usize>),
}
struct Formula {
nodes: Vec<FNode>,
root: usize,
pair_keys: Vec<(ExprId, ExprId)>,
atom_keys: Vec<ExprId>,
}
impl Formula {
fn build(arena: &Arena, root: ExprId) -> Formula {
let order = bool_post_order(arena, root);
let mut index: FxHashMap<ExprId, usize> = FxHashMap::default();
let mut pair_index: FxHashMap<(ExprId, ExprId), usize> = FxHashMap::default();
let mut atom_index: FxHashMap<ExprId, usize> = FxHashMap::default();
let mut nodes: Vec<FNode> = Vec::with_capacity(order.len());
let mut pair_keys = Vec::new();
let mut atom_keys = Vec::new();
for &id in &order {
let node = match arena.node(id) {
ExprNode::BoolTrue => FNode::Const(true),
ExprNode::BoolFalse => FNode::Const(false),
ExprNode::And(ch) => {
FNode::And(ch.iter().filter_map(|c| index.get(c).copied()).collect())
}
ExprNode::Or(ch) => {
FNode::Or(ch.iter().filter_map(|c| index.get(c).copied()).collect())
}
ExprNode::Not(x) => {
match rel_of(arena, *x) {
Some(r) => {
let p = *pair_index.entry((r.a, r.b)).or_insert_with(|| {
pair_keys.push((r.a, r.b));
pair_keys.len() - 1
});
FNode::Rel(p, ALL & !r.mask)
}
None => {
let a = *atom_index.entry(*x).or_insert_with(|| {
atom_keys.push(*x);
atom_keys.len() - 1
});
FNode::Atom(a, false)
}
}
}
_ => match rel_of(arena, id) {
Some(r) => {
let p = *pair_index.entry((r.a, r.b)).or_insert_with(|| {
pair_keys.push((r.a, r.b));
pair_keys.len() - 1
});
FNode::Rel(p, r.mask)
}
None => {
let a = *atom_index.entry(id).or_insert_with(|| {
atom_keys.push(id);
atom_keys.len() - 1
});
FNode::Atom(a, true)
}
},
};
index.insert(id, nodes.len());
nodes.push(node);
}
let root = index.get(&root).copied().unwrap_or(0);
Formula {
nodes,
root,
pair_keys,
atom_keys,
}
}
fn var_count(&self) -> usize {
self.pair_keys.len() + self.atom_keys.len()
}
}
enum Prop {
Determined(bool),
Conflict,
Changed,
Stuck,
}
impl Formula {
fn values(&self, pairs: &[u8], atoms: &[Option<bool>]) -> Vec<Option<bool>> {
let mut vals: Vec<Option<bool>> = Vec::with_capacity(self.nodes.len());
for n in &self.nodes {
let v = match n {
FNode::Const(b) => Some(*b),
FNode::Rel(p, mask) => {
let s = pairs[*p];
if s & !mask == 0 {
Some(true)
} else if s & mask == 0 {
Some(false)
} else {
None
}
}
FNode::Atom(a, pos) => atoms[*a].map(|v| v == *pos),
FNode::And(ch) => {
let mut acc = Some(true);
for &c in ch {
match vals[c] {
Some(false) => {
acc = Some(false);
break;
}
None => acc = None,
Some(true) => {}
}
}
acc
}
FNode::Or(ch) => {
let mut acc = Some(false);
for &c in ch {
match vals[c] {
Some(true) => {
acc = Some(true);
break;
}
None => acc = None,
Some(false) => {}
}
}
acc
}
};
vals.push(v);
}
vals
}
fn eval(&self, pairs: &[u8], atoms: &[Option<bool>]) -> Option<bool> {
self.values(pairs, atoms)[self.root]
}
fn propagate(&self, target: bool, pairs: &mut [u8], atoms: &mut [Option<bool>]) -> Prop {
let vals = self.values(pairs, atoms);
if let Some(v) = vals[self.root] {
return Prop::Determined(v);
}
let mut want: Vec<Option<bool>> = vec![None; self.nodes.len()];
want[self.root] = Some(target);
let mut changed = false;
for i in (0..self.nodes.len()).rev() {
let Some(w) = want[i] else { continue };
if vals[i].is_some() {
continue;
}
match &self.nodes[i] {
FNode::Const(_) => {}
FNode::Rel(p, mask) => {
let m = if w { *mask } else { ALL & !mask };
let new = pairs[*p] & m;
if new == 0 {
return Prop::Conflict;
}
if new != pairs[*p] {
pairs[*p] = new;
changed = true;
}
}
FNode::Atom(a, pos) => {
let v = w == *pos;
match atoms[*a] {
Some(cur) if cur != v => return Prop::Conflict,
Some(_) => {}
None => {
atoms[*a] = Some(v);
changed = true;
}
}
}
FNode::And(ch) | FNode::Or(ch) => {
let is_and = matches!(self.nodes[i], FNode::And(_));
let all = is_and == w;
let undetermined: Vec<usize> =
ch.iter().copied().filter(|&c| vals[c].is_none()).collect();
let forced: &[usize] = if all || undetermined.len() == 1 {
&undetermined
} else {
&[]
};
for &c in forced {
match want[c] {
Some(prev) if prev != w => return Prop::Conflict,
_ => want[c] = Some(w),
}
}
}
}
}
if changed { Prop::Changed } else { Prop::Stuck }
}
}
fn search_for(f: &Formula, target: bool) -> Option<bool> {
if f.var_count() > MAX_SAT_VARS {
return None;
}
let pairs = vec![ALL; f.pair_keys.len()];
let atoms = vec![None; f.atom_keys.len()];
let mut budget = SAT_BUDGET;
search_rec(f, target, pairs, atoms, &mut budget)
}
fn search_rec(
f: &Formula,
target: bool,
mut pairs: Vec<u8>,
mut atoms: Vec<Option<bool>>,
budget: &mut usize,
) -> Option<bool> {
loop {
if *budget == 0 {
return None;
}
*budget -= 1;
match f.propagate(target, &mut pairs, &mut atoms) {
Prop::Determined(v) => return Some(v == target),
Prop::Conflict => return Some(false),
Prop::Changed => continue,
Prop::Stuck => break,
}
}
let pick = pairs
.iter()
.enumerate()
.filter(|(_, s)| s.count_ones() > 1)
.min_by_key(|(_, s)| s.count_ones())
.map(|(i, _)| i);
if let Some(i) = pick {
let allowed = pairs[i];
for state in [LT, EQ, GT] {
if allowed & state == 0 {
continue;
}
let mut p2 = pairs.clone();
p2[i] = state;
match search_rec(f, target, p2, atoms.clone(), budget) {
Some(true) => return Some(true),
Some(false) => {}
None => return None,
}
}
return Some(false);
}
if let Some(i) = atoms.iter().position(Option::is_none) {
for v in [true, false] {
let mut a2 = atoms.clone();
a2[i] = Some(v);
match search_rec(f, target, pairs.clone(), a2, budget) {
Some(true) => return Some(true),
Some(false) => {}
None => return None,
}
}
return Some(false);
}
None
}
fn symbol_is_free(arena: &Arena, sym: ExprId) -> bool {
const RESTRICTING: Props = Props::POSITIVE
.union(Props::NEGATIVE)
.union(Props::NONNEGATIVE)
.union(Props::NONPOSITIVE)
.union(Props::ZERO)
.union(Props::NONZERO)
.union(Props::INTEGER)
.union(Props::RATIONAL)
.union(Props::IRRATIONAL)
.union(Props::ALGEBRAIC)
.union(Props::TRANSCENDENTAL)
.union(Props::EVEN)
.union(Props::ODD)
.union(Props::PRIME)
.union(Props::COMPOSITE)
.union(Props::IMAGINARY)
.union(Props::INFINITE);
match arena.node(sym) {
ExprNode::Symbol(sid) => {
let a = arena.symbol_assumptions(*sid);
!a.known_true.intersects(RESTRICTING)
&& !a.known_false.intersects(RESTRICTING)
&& a.query(Props::REAL) != Some(false)
}
_ => false,
}
}
fn pairs_independent(arena: &mut Arena, f: &Formula) -> bool {
let mut used: FxHashSet<ExprId> = FxHashSet::default();
for &(a, b) in &f.pair_keys {
let d = arena.sub(a, b);
let d = crate::transforms::eval::eval(arena, d);
let syms = crate::base::walk::free_symbols(arena, d);
if syms.len() != 1 || !symbol_is_free(arena, syms[0]) || !used.insert(syms[0]) {
return false;
}
match crate::poly::polybridge::expr_to_poly(arena, d, syms[0]) {
Some(p) if p.degree() == Some(1) => {}
_ => return false,
}
}
true
}
fn single_variable(arena: &Arena, root: ExprId) -> Option<ExprId> {
let syms = crate::base::walk::free_symbols(arena, root);
if syms.len() != 1 || !symbol_is_free(arena, syms[0]) {
return None;
}
Some(syms[0])
}
fn univariate_status(arena: &mut Arena, root: ExprId) -> Option<(bool, bool)> {
let var = single_variable(arena, root)?;
let set = crate::transforms::sets::reduce_inequalities(arena, &[root], var).ok()?;
let empty = crate::transforms::sets::is_empty(arena, set)?;
let full = crate::transforms::sets::is_full(arena, set)?;
crate::transforms::sets::as_intervals(arena, set)?;
Some((empty, full))
}
pub(crate) fn is_tautology(arena: &mut Arena, root: ExprId) -> Option<bool> {
let s = simplify_bool(arena, root);
if s == arena.bool_true {
return Some(true);
}
if s == arena.bool_false {
return Some(false);
}
let f = Formula::build(arena, s);
match search_for(&f, false) {
Some(false) => return Some(true), Some(true) if pairs_independent(arena, &f) => return Some(false),
_ => {}
}
univariate_status(arena, s).map(|(_, full)| full)
}
pub(crate) fn is_contradiction(arena: &mut Arena, root: ExprId) -> Option<bool> {
let s = simplify_bool(arena, root);
if s == arena.bool_false {
return Some(true);
}
if s == arena.bool_true {
return Some(false);
}
let f = Formula::build(arena, s);
match search_for(&f, true) {
Some(false) => return Some(true), Some(true) if pairs_independent(arena, &f) => return Some(false),
_ => {}
}
univariate_status(arena, s).map(|(empty, _)| empty)
}
pub(crate) fn satisfiable(arena: &mut Arena, root: ExprId) -> Option<bool> {
is_contradiction(arena, root).map(|c| !c)
}
pub(crate) fn truth_table(
arena: &mut Arena,
root: ExprId,
vars: &[ExprId],
) -> Result<Vec<(Vec<bool>, bool)>, SymplexError> {
if vars.len() > MAX_TRUTH_TABLE_VARS {
return Err(SymplexError::InvalidArgument {
operation: "truth_table",
reason: format!(
"at most {MAX_TRUTH_TABLE_VARS} variables supported, got {}",
vars.len()
),
});
}
let s = simplify_bool(arena, root);
let f = Formula::build(arena, s);
enum VarRef {
Pair(usize, u8),
Atom(usize),
Absent,
}
let mut refs: Vec<VarRef> = Vec::with_capacity(vars.len());
for &v in vars {
if v == arena.bool_true || v == arena.bool_false {
return Err(SymplexError::InvalidArgument {
operation: "truth_table",
reason: "constants are not variables".into(),
});
}
let r = if let Some(rel) = rel_of(arena, v) {
match f.pair_keys.iter().position(|k| *k == (rel.a, rel.b)) {
Some(p) => VarRef::Pair(p, rel.mask),
None => VarRef::Absent,
}
} else {
let (atom, pos) = match arena.node(v) {
ExprNode::Not(x) => (*x, false),
_ => (v, true),
};
match f.atom_keys.iter().position(|k| *k == atom) {
Some(a) if pos => VarRef::Atom(a),
Some(_) => {
return Err(SymplexError::InvalidArgument {
operation: "truth_table",
reason: "negated atoms are not variables".into(),
});
}
None => VarRef::Absent,
}
};
refs.push(r);
}
let n = vars.len();
let mut rows = Vec::with_capacity(1 << n);
for bits in 0..(1usize << n) {
let mut pairs = vec![ALL; f.pair_keys.len()];
let mut atoms = vec![None; f.atom_keys.len()];
let mut values = Vec::with_capacity(n);
let mut consistent = true;
for (i, r) in refs.iter().enumerate() {
let val = (bits >> (n - 1 - i)) & 1 == 1;
values.push(val);
match r {
VarRef::Pair(p, mask) => {
let m = if val { *mask } else { ALL & !mask };
pairs[*p] &= m;
if pairs[*p] == 0 {
consistent = false;
}
}
VarRef::Atom(a) => atoms[*a] = Some(val),
VarRef::Absent => {}
}
}
if !consistent {
continue;
}
match f.eval(&pairs, &atoms) {
Some(v) => rows.push((values, v)),
None => {
return Err(SymplexError::ComputationFailed {
operation: "truth_table",
reason: "the formula is not determined by the given variables".into(),
});
}
}
}
Ok(rows)
}
pub(crate) fn eval_bool(
arena: &mut Arena,
assumptions: &mut AssumptionCache,
root: ExprId,
) -> ExprId {
let e = crate::transforms::eval::eval(arena, root);
let order = bool_post_order(arena, e);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
let t = arena.bool_true;
let f = arena.bool_false;
for &id in &order {
let new = match arena.node(id).clone() {
ExprNode::Gt(a, b) | ExprNode::Ge(a, b) | ExprNode::Eq_(a, b) | ExprNode::Ne(a, b) => {
let d = arena.sub(a, b);
let d = crate::transforms::eval::eval(arena, d);
let (yes, no) = match arena.node(id) {
ExprNode::Gt(..) => (Props::POSITIVE, Props::NONPOSITIVE),
ExprNode::Ge(..) => (Props::NONNEGATIVE, Props::NEGATIVE),
ExprNode::Eq_(..) => (Props::ZERO, Props::NONZERO),
_ => (Props::NONZERO, Props::ZERO),
};
if assumptions.query(&*arena, d, yes) == Some(true) {
t
} else if assumptions.query(&*arena, d, no) == Some(true) {
f
} else {
id
}
}
ExprNode::And(ch) => {
let mut kids: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut any_false = false;
for c in &ch {
let k = cache.get(c).copied().unwrap_or(*c);
if k == f {
any_false = true;
break;
}
if k != t {
kids.push(k);
}
}
if any_false {
f
} else if kids.len() == ch.len() && kids.iter().zip(ch.iter()).all(|(a, b)| a == b)
{
id
} else {
arena.and(&kids)
}
}
ExprNode::Or(ch) => {
let mut kids: SmallVec<[ExprId; 6]> = SmallVec::new();
let mut any_true = false;
for c in &ch {
let k = cache.get(c).copied().unwrap_or(*c);
if k == t {
any_true = true;
break;
}
if k != f {
kids.push(k);
}
}
if any_true {
t
} else if kids.len() == ch.len() && kids.iter().zip(ch.iter()).all(|(a, b)| a == b)
{
id
} else {
arena.or(&kids)
}
}
ExprNode::Not(x) => {
let nx = cache.get(&x).copied().unwrap_or(x);
if nx == x { id } else { arena.not(nx) }
}
_ => id,
};
cache.insert(id, new);
}
cache.get(&e).copied().unwrap_or(e)
}
pub(crate) fn piecewise_simplify(arena: &mut Arena, root: ExprId) -> ExprId {
let order = crate::base::walk::post_order_ids(arena, root);
let mut cache: FxHashMap<ExprId, ExprId> = FxHashMap::default();
let mut assumptions = AssumptionCache::new();
for &id in &order {
let new = match arena.node(id).clone() {
ExprNode::Piecewise(pairs) => {
let t = arena.bool_true;
let f = arena.bool_false;
let mut out: Vec<(ExprId, ExprId)> = Vec::new();
let mut seen_conds: FxHashSet<ExprId> = FxHashSet::default();
for &(v, c) in &pairs {
let nv = cache.get(&v).copied().unwrap_or(v);
let nc = cache.get(&c).copied().unwrap_or(c);
let nc = eval_bool(arena, &mut assumptions, nc);
let nc = simplify_bool(arena, nc);
if nc == f || !seen_conds.insert(nc) {
continue;
}
if let Some(last) = out.last_mut()
&& last.0 == nv
{
let merged = arena.or(&[last.1, nc]);
last.1 = simplify_bool(arena, merged);
if last.1 == t {
break;
}
continue;
}
out.push((nv, nc));
if nc == t {
break;
}
}
if out.is_empty() {
arena.nan
} else if out.len() == 1 && out[0].1 == t {
out[0].0
} else if out.len() == pairs.len()
&& out.iter().zip(pairs.iter()).all(|(a, b)| a == b)
{
id
} else {
arena.piecewise(&out)
}
}
_ => {
if arena.node(id).is_atom() {
id
} else {
crate::base::walk::rebuild_with_cache(arena, id, &cache)
}
}
};
cache.insert(id, new);
}
cache.get(&root).copied().unwrap_or(root)
}
#[cfg(test)]
mod tests {
use super::*;
fn display(arena: &Arena, id: ExprId) -> String {
arena.display(id).to_string()
}
struct Fx {
arena: Arena,
x: ExprId,
y: ExprId,
zero: ExprId,
one: ExprId,
}
fn fx() -> Fx {
let mut arena = Arena::new();
let x = arena.symbol("x");
let y = arena.symbol("y");
let zero = arena.zero;
let one = arena.one;
Fx {
arena,
x,
y,
zero,
one,
}
}
#[test]
fn relational_negation() {
let mut f = fx();
let gt = f.arena.gt(f.x, f.zero);
let n = f.arena.not(gt);
let s = simplify_bool(&mut f.arena, n);
assert_eq!(display(&f.arena, s), "0 >= x");
let ge = f.arena.ge(f.x, f.zero);
let n = f.arena.not(ge);
let s = simplify_bool(&mut f.arena, n);
assert_eq!(display(&f.arena, s), "0 > x");
let eq = f.arena.eq_(f.x, f.zero);
let n = f.arena.not(eq);
let s = simplify_bool(&mut f.arena, n);
assert_eq!(display(&f.arena, s), "x != 0");
}
#[test]
fn numeric_folding() {
let mut f = fx();
let two = f.arena.int(2);
let g = f.arena.gt(two, f.one);
assert_eq!(simplify_bool(&mut f.arena, g), f.arena.bool_true);
let pi = f.arena.pi;
let three = f.arena.int(3);
let g = f.arena.gt(pi, three);
assert_eq!(simplify_bool(&mut f.arena, g), f.arena.bool_true);
let xp1 = f.arena.add(&[f.x, f.one]);
let g = f.arena.gt(xp1, f.x);
assert_eq!(simplify_bool(&mut f.arena, g), f.arena.bool_true);
let g = f.arena.gt(f.x, f.x);
assert_eq!(simplify_bool(&mut f.arena, g), f.arena.bool_false);
let g = f.arena.ge(f.x, f.x);
assert_eq!(simplify_bool(&mut f.arena, g), f.arena.bool_true);
}
#[test]
fn same_pair_merging() {
let mut f = fx();
let gt = f.arena.gt(f.x, f.zero);
let ge = f.arena.ge(f.x, f.zero);
let eq = f.arena.eq_(f.x, f.zero);
let lt = f.arena.gt(f.zero, f.x);
let le = f.arena.ge(f.zero, f.x);
let a = f.arena.and(&[gt, ge]);
assert_eq!(simplify_bool(&mut f.arena, a), gt);
let o = f.arena.or(&[gt, eq]);
assert_eq!(simplify_bool(&mut f.arena, o), ge);
let a = f.arena.and(&[gt, lt]);
assert_eq!(simplify_bool(&mut f.arena, a), f.arena.bool_false);
let o = f.arena.or(&[gt, le]);
assert_eq!(simplify_bool(&mut f.arena, o), f.arena.bool_true);
let a = f.arena.and(&[ge, le]);
assert_eq!(simplify_bool(&mut f.arena, a), eq);
}
#[test]
fn flatten_dedupe_constants() {
let mut f = fx();
let p = f.arena.gt(f.x, f.zero);
let q = f.arena.gt(f.y, f.zero);
let inner = f.arena.and(&[p, q]);
let t = f.arena.bool_true;
let outer = f
.arena
.intern(ExprNode::And(smallvec::smallvec![inner, p, t]));
let s = simplify_bool(&mut f.arena, outer);
match f.arena.node(s) {
ExprNode::And(ch) => assert_eq!(ch.len(), 2, "{}", display(&f.arena, s)),
_ => panic!("expected And: {}", display(&f.arena, s)),
}
let fl = f.arena.bool_false;
let dead = f.arena.intern(ExprNode::And(smallvec::smallvec![p, fl]));
assert_eq!(simplify_bool(&mut f.arena, dead), fl);
let o = f.arena.intern(ExprNode::Or(smallvec::smallvec![p, t]));
assert_eq!(simplify_bool(&mut f.arena, o), t);
}
#[test]
fn absorption_and_complement() {
let mut f = fx();
let p = f.arena.gt(f.x, f.zero);
let q = f.arena.gt(f.y, f.zero);
let pq = f.arena.or(&[p, q]);
let a = f.arena.and(&[p, pq]);
assert_eq!(simplify_bool(&mut f.arena, a), p, "A ∧ (A ∨ B) = A");
let pq = f.arena.and(&[p, q]);
let o = f.arena.or(&[p, pq]);
assert_eq!(simplify_bool(&mut f.arena, o), p, "A ∨ (A ∧ B) = A");
let np = f.arena.not(p);
let a = f.arena.and(&[p, np]);
assert_eq!(simplify_bool(&mut f.arena, a), f.arena.bool_false);
let o = f.arena.or(&[p, np]);
assert_eq!(simplify_bool(&mut f.arena, o), f.arena.bool_true);
let npq = f.arena.or(&[np, q]);
let a = f.arena.and(&[p, npq]);
let s = simplify_bool(&mut f.arena, a);
let expected = simplify_bool(&mut f.arena, pq);
assert_eq!(s, expected, "{}", display(&f.arena, s));
let s1 = f.arena.symbol("p");
let ns1 = f.arena.not(s1);
let a = f.arena.and(&[s1, ns1]);
assert_eq!(simplify_bool(&mut f.arena, a), f.arena.bool_false);
}
#[test]
fn de_morgan_and_double_negation() {
let mut f = fx();
let p = f.arena.gt(f.x, f.zero);
let q = f.arena.gt(f.y, f.zero);
let a = f.arena.and(&[p, q]);
let n = f.arena.not(a);
let s = simplify_bool(&mut f.arena, n);
assert!(
matches!(f.arena.node(s), ExprNode::Or(_)),
"{}",
display(&f.arena, s)
);
let d = display(&f.arena, s);
assert!(d.contains("0 >= x") && d.contains("0 >= y"), "{d}");
let n1 = f.arena.not(s);
let nn = f.arena.not(n1);
assert_eq!(simplify_bool(&mut f.arena, nn), s);
}
#[test]
fn nnf_cnf_dnf() {
let mut f = fx();
let p = f.arena.symbol("p");
let q = f.arena.symbol("q");
let r = f.arena.symbol("r");
let qr = f.arena.and(&[q, r]);
let e = f.arena.or(&[p, qr]);
let c = to_cnf(&mut f.arena, e);
match f.arena.node(c).clone() {
ExprNode::And(ch) => {
assert_eq!(ch.len(), 2);
for k in ch {
assert!(matches!(f.arena.node(k), ExprNode::Or(_)));
}
}
_ => panic!("expected CNF And: {}", display(&f.arena, c)),
}
assert_eq!(to_dnf(&mut f.arena, e), simplify_bool(&mut f.arena, e));
let pq = f.arena.and(&[p, q]);
let pr = f.arena.and(&[p, r]);
let e2 = f.arena.or(&[pq, pr]);
let c2 = to_cnf(&mut f.arena, e2);
let qr_or = f.arena.or(&[q, r]);
let expected = f.arena.and(&[p, qr_or]);
let expected = simplify_bool(&mut f.arena, expected);
assert_eq!(c2, expected, "{}", display(&f.arena, c2));
let n = f.arena.not(pq);
let nnf = to_nnf(&mut f.arena, n);
assert!(matches!(f.arena.node(nnf), ExprNode::Or(_)));
}
#[test]
fn tautology_and_satisfiability() {
let mut f = fx();
let p = f.arena.symbol("p");
let q = f.arena.symbol("q");
let np = f.arena.not(p);
let nq = f.arena.not(q);
let pq = f.arena.or(&[np, q]);
let ante = f.arena.and(&[pq, p]);
let nante = f.arena.not(ante);
let mp = f.arena.or(&[nante, q]);
assert_eq!(is_tautology(&mut f.arena, mp), Some(true));
assert_eq!(is_contradiction(&mut f.arena, mp), Some(false));
let a = f.arena.and(&[p, np]);
let b = f.arena.and(&[q, nq]);
let c = f.arena.or(&[a, b]);
assert_eq!(is_contradiction(&mut f.arena, c), Some(true));
assert_eq!(satisfiable(&mut f.arena, c), Some(false));
let o = f.arena.or(&[p, q]);
assert_eq!(satisfiable(&mut f.arena, o), Some(true));
assert_eq!(is_tautology(&mut f.arena, o), Some(false));
let gt1 = f.arena.gt(f.x, f.one);
let gt0 = f.arena.gt(f.x, f.zero);
let ngt1 = f.arena.not(gt1);
let imp = f.arena.or(&[ngt1, gt0]);
assert_eq!(is_tautology(&mut f.arena, imp), Some(true));
let lt0 = f.arena.gt(f.zero, f.x);
let both = f.arena.and(&[gt1, lt0]);
assert_eq!(is_contradiction(&mut f.arena, both), Some(true));
assert_eq!(satisfiable(&mut f.arena, both), Some(false));
assert_eq!(satisfiable(&mut f.arena, gt0), Some(true));
assert_eq!(is_tautology(&mut f.arena, gt0), Some(false));
let gy = f.arena.gt(f.y, f.zero);
let both = f.arena.and(&[gt0, gy]);
assert_eq!(satisfiable(&mut f.arena, both), Some(true));
assert_eq!(is_tautology(&mut f.arena, both), Some(false));
let gxy = f.arena.gt(f.x, f.y);
let dep = f.arena.and(&[gxy, gt0]);
assert_eq!(satisfiable(&mut f.arena, dep), None);
assert_eq!(is_tautology(&mut f.arena, gxy), None);
let two = f.arena.int(2);
let x2 = f.arena.pow(f.x, two);
let ge0 = f.arena.ge(x2, f.zero);
assert_eq!(is_tautology(&mut f.arena, ge0), Some(true));
let lt0 = f.arena.gt(f.zero, x2);
assert_eq!(satisfiable(&mut f.arena, lt0), Some(false));
}
#[test]
fn unit_propagation_solves_pigeonhole() {
let mut f = fx();
let p = |arena: &mut Arena, i: usize, j: usize| {
let s = arena.symbol(&format!("p{i}{j}"));
let zero = arena.zero;
arena.gt(s, zero)
};
let mut clauses: Vec<ExprId> = Vec::new();
for i in 0..4 {
let a = p(&mut f.arena, i, 0);
let b = p(&mut f.arena, i, 1);
let c = p(&mut f.arena, i, 2);
clauses.push(f.arena.or(&[a, b, c]));
}
for j in 0..3 {
for i in 0..4 {
for k in (i + 1)..4 {
let a = p(&mut f.arena, i, j);
let b = p(&mut f.arena, k, j);
let na = f.arena.not(a);
let nb = f.arena.not(b);
clauses.push(f.arena.or(&[na, nb]));
}
}
}
let php = f.arena.and(&clauses);
assert_eq!(satisfiable(&mut f.arena, php), Some(false));
assert_eq!(is_contradiction(&mut f.arena, php), Some(true));
}
#[test]
fn truth_table_basic() {
let mut f = fx();
let p = f.arena.symbol("p");
let q = f.arena.symbol("q");
let e = f.arena.and(&[p, q]);
let rows = truth_table(&mut f.arena, e, &[p, q]).unwrap();
assert_eq!(
rows,
vec![
(vec![false, false], false),
(vec![false, true], false),
(vec![true, false], false),
(vec![true, true], true),
]
);
let gt = f.arena.gt(f.x, f.zero);
let le = f.arena.ge(f.zero, f.x);
let e = f.arena.or(&[gt, le]);
let rows = truth_table(&mut f.arena, e, &[gt]).unwrap();
assert!(rows.iter().all(|(_, v)| *v));
let e = f.arena.and(&[p, q]);
assert!(truth_table(&mut f.arena, e, &[p]).is_err());
let many: Vec<ExprId> = (0..9).map(|i| f.arena.symbol(&format!("v{i}"))).collect();
assert!(truth_table(&mut f.arena, e, &many).is_err());
}
#[test]
fn atoms_list() {
let mut f = fx();
let p = f.arena.gt(f.x, f.zero);
let q = f.arena.symbol("q");
let nq = f.arena.not(q);
let t = f.arena.bool_true;
let e = f.arena.and(&[p, nq, t]);
let a = atoms(&f.arena, e);
assert_eq!(a.len(), 2);
assert!(a.contains(&p) && a.contains(&q));
}
#[test]
fn eval_with_assumptions() {
let mut f = fx();
let mut cache = AssumptionCache::new();
let sid = match f.arena.node(f.x) {
ExprNode::Symbol(s) => *s,
_ => unreachable!(),
};
let mut a = f.arena.symbol_assumptions(sid);
a.assert_true(Props::POSITIVE);
f.arena.set_symbol_assumptions(sid, a);
let gt = f.arena.gt(f.x, f.zero);
assert_eq!(eval_bool(&mut f.arena, &mut cache, gt), f.arena.bool_true);
let le = f.arena.ge(f.zero, f.x);
assert_eq!(eval_bool(&mut f.arena, &mut cache, le), f.arena.bool_false);
let ne = f.arena.ne_(f.x, f.zero);
assert_eq!(eval_bool(&mut f.arena, &mut cache, ne), f.arena.bool_true);
let gy = f.arena.gt(f.y, f.zero);
assert_eq!(eval_bool(&mut f.arena, &mut cache, gy), gy);
let both = f.arena.and(&[gt, gy]);
assert_eq!(eval_bool(&mut f.arena, &mut cache, both), gy);
}
#[test]
fn piecewise_simplification() {
let mut f = fx();
let t = f.arena.bool_true;
let fl = f.arena.bool_false;
let gt = f.arena.gt(f.x, f.zero);
let le = f.arena.ge(f.zero, f.x);
let negx = f.arena.neg(f.x);
let pw = f.arena.piecewise(&[(f.x, fl), (f.x, gt), (f.x, le)]);
assert_eq!(piecewise_simplify(&mut f.arena, pw), f.x);
let pw = f.arena.piecewise(&[(f.x, gt), (negx, t), (f.one, gt)]);
let s = piecewise_simplify(&mut f.arena, pw);
match f.arena.node(s) {
ExprNode::Piecewise(p) => assert_eq!(p.len(), 2),
_ => panic!("expected Piecewise: {}", display(&f.arena, s)),
}
let inner = f.arena.piecewise(&[(f.one, t)]);
let sum = f.arena.add(&[inner, f.x]);
let s = piecewise_simplify(&mut f.arena, sum);
let expected = f.arena.add(&[f.one, f.x]);
assert_eq!(s, expected);
let pw = f.arena.piecewise(&[(f.x, fl)]);
assert_eq!(piecewise_simplify(&mut f.arena, pw), f.arena.nan);
}
#[test]
fn simplify_is_idempotent_and_sorted() {
let mut f = fx();
let p = f.arena.gt(f.x, f.zero);
let q = f.arena.gt(f.y, f.zero);
let a = f.arena.and(&[q, p]);
let b = f.arena.and(&[p, q]);
let sa = simplify_bool(&mut f.arena, a);
let sb = simplify_bool(&mut f.arena, b);
assert_eq!(sa, sb, "order-independent normal form");
assert_eq!(simplify_bool(&mut f.arena, sa), sa);
}
}