use elenchus_compiler::{AtomId, AtomKey, Clause, Compiled, Fact, Lit, Origin, Value};
use elenchus_solver::sat::{self, Cnf, SatLit, Var};
use elenchus_solver::{Status, solve};
use proptest::prelude::*;
fn clause_sat(mask: u64, clause: &[SatLit]) -> bool {
clause
.iter()
.any(|&l| ((mask >> l.var()) & 1 == 1) != l.is_negative())
}
fn brute_sat(n: usize, clauses: &[Vec<SatLit>]) -> bool {
(0u64..(1u64 << n)).any(|mask| clauses.iter().all(|c| clause_sat(mask, c)))
}
fn brute_full_model_count(n: usize, clauses: &[Vec<SatLit>]) -> usize {
(0u64..(1u64 << n))
.filter(|&mask| clauses.iter().all(|c| clause_sat(mask, c)))
.count()
}
type RawCnf = Vec<Vec<(u32, bool)>>;
type EngineCase = (usize, Vec<u8>, RawCnf);
fn instance() -> impl Strategy<Value = (usize, RawCnf)> {
(1usize..=6).prop_flat_map(|n| {
let lit = (0u32..(n as u32), any::<bool>());
let clause = prop::collection::vec(lit, 1..=4);
(Just(n), prop::collection::vec(clause, 0..=14))
})
}
fn to_clauses(raw: &[Vec<(u32, bool)>]) -> Vec<Vec<SatLit>> {
raw.iter()
.map(|c| c.iter().map(|&(v, p)| SatLit::new(v, p)).collect())
.collect()
}
fn to_cnf(n: usize, raw: &[Vec<(u32, bool)>]) -> Cnf {
let mut cnf = Cnf::new(n);
for c in to_clauses(raw) {
cnf.add_clause(c);
}
cnf
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(800))]
#[test]
fn sat_matches_bruteforce((n, raw) in instance()) {
let cnf = to_cnf(n, &raw);
let clauses = to_clauses(&raw);
prop_assert_eq!(sat::solve(&cnf).is_some(), brute_sat(n, &clauses));
}
#[test]
fn returned_model_is_valid((n, raw) in instance()) {
let cnf = to_cnf(n, &raw);
if let Some(model) = sat::solve(&cnf) {
for clause in &to_clauses(&raw) {
prop_assert!(clause.iter().any(|&l| model[l.var() as usize] != l.is_negative()));
}
}
}
#[test]
fn model_count_is_exact((n, raw) in instance()) {
let cnf = to_cnf(n, &raw);
let clauses = to_clauses(&raw);
let all_vars: Vec<Var> = (0..n as Var).collect();
let counted = sat::models_upto(&cnf, &all_vars, 1usize << n);
prop_assert_eq!(counted, brute_full_model_count(n, &clauses));
}
}
fn origin() -> Origin {
Origin {
source: "<prop>".into(),
line: 0,
premise: None,
kind: "EXCLUSIVE",
}
}
fn engine_instance() -> impl Strategy<Value = EngineCase> {
(2usize..=6).prop_flat_map(|n| {
let facts = prop::collection::vec(0u8..3, n);
let lit = (0u32..(n as u32), any::<bool>());
let clause = prop::collection::vec(lit, 1..=4);
(Just(n), facts, prop::collection::vec(clause, 0..=10))
})
}
fn build_compiled(n: usize, fact_choice: &[u8], raw: &[Vec<(u32, bool)>]) -> Compiled {
let atoms: Vec<AtomKey> = (0..n)
.map(|i| AtomKey {
subject: "s".into(),
predicate: alloc_p(i),
object: None,
})
.collect();
let facts: Vec<Fact> = fact_choice
.iter()
.enumerate()
.filter_map(|(i, &c)| match c {
1 => Some(Fact {
atom: i as AtomId,
value: Value::True,
origin: origin(),
}),
2 => Some(Fact {
atom: i as AtomId,
value: Value::False,
origin: origin(),
}),
_ => None,
})
.collect();
let clauses: Vec<Clause> = raw
.iter()
.map(|c| Clause {
lits: c
.iter()
.map(|&(v, neg)| Lit {
atom: v,
negated: neg,
})
.collect(),
origin: origin(),
})
.collect();
Compiled {
atoms,
facts,
clauses,
rules: Vec::new(),
checks: Vec::new(),
pending_imports: Vec::new(),
}
}
fn alloc_p(i: usize) -> String {
format!("p{i}")
}
fn encode(compiled: &Compiled) -> Cnf {
let mut cnf = Cnf::new(compiled.atoms.len());
for clause in &compiled.clauses {
cnf.add_clause(
clause
.lits
.iter()
.map(|l| SatLit::new(l.atom, l.negated))
.collect(),
);
}
for f in &compiled.facts {
cnf.add_clause(vec![match f.value {
Value::True => SatLit::positive(f.atom),
Value::False => SatLit::negative(f.atom),
}]);
}
cnf
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(500))]
#[test]
fn forward_conflict_implies_unsat((n, facts, raw) in engine_instance()) {
let compiled = build_compiled(n, &facts, &raw);
let report = solve(&compiled);
if report.status == Status::Conflict {
prop_assert!(sat::solve(&encode(&compiled)).is_none());
}
}
#[test]
fn solve_is_deterministic((n, facts, raw) in engine_instance()) {
let compiled = build_compiled(n, &facts, &raw);
prop_assert_eq!(solve(&compiled), solve(&compiled));
}
#[test]
fn to_json_is_always_valid((n, facts, raw) in engine_instance()) {
let json = solve(&build_compiled(n, &facts, &raw)).to_json();
prop_assert!(serde_json::from_str::<serde_json::Value>(&json).is_ok(), "{}", json);
}
}