use crate::cnf::CnfFormula;
use cadical_sys::{CaDiCal, Status};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Clone, Debug)]
pub enum CadicalVerdict {
Sat(Vec<bool>),
Unsat {
lrat: String,
},
}
#[derive(Clone, Debug)]
pub enum CadicalError {
Inconclusive,
Configuration(&'static str),
ProofIo(String),
ProofRejected(ordeal_lrat::CheckError),
ModelSelfCheckFailed,
}
pub fn solve(formula: &CnfFormula) -> Result<CadicalVerdict, CadicalError> {
solve_inner(formula, None)
}
pub fn solve_with_conflict_limit(
formula: &CnfFormula,
max_conflicts: i32,
) -> Result<CadicalVerdict, CadicalError> {
solve_inner(formula, Some(max_conflicts))
}
fn solve_inner(
formula: &CnfFormula,
max_conflicts: Option<i32>,
) -> Result<CadicalVerdict, CadicalError> {
static CALL: AtomicU64 = AtomicU64::new(0);
let stamp = format!(
"{}-{}",
std::process::id(),
CALL.fetch_add(1, Ordering::Relaxed)
);
let proof_path = std::env::temp_dir().join(format!("ordeal-cadical-{stamp}.lrat"));
let dimacs_path = std::env::temp_dir().join(format!("ordeal-cadical-{stamp}.cnf"));
let cleanup = || {
let _ = std::fs::remove_file(&proof_path);
let _ = std::fs::remove_file(&dimacs_path);
};
let proof_path_str = proof_path
.to_str()
.ok_or(CadicalError::Configuration("non-UTF-8 temp path"))?
.to_string();
let dimacs_path_str = dimacs_path
.to_str()
.ok_or(CadicalError::Configuration("non-UTF-8 temp path"))?
.to_string();
let mut dimacs = format!("p cnf {} {}\n", formula.num_vars, formula.clauses.len());
for clause in &formula.clauses {
for &lit in clause {
dimacs.push_str(&lit.to_string());
dimacs.push(' ');
}
dimacs.push_str("0\n");
}
std::fs::write(&dimacs_path, dimacs).map_err(|e| {
cleanup();
CadicalError::ProofIo(e.to_string())
})?;
let mut solver = CaDiCal::new();
for (option, value, err) in [
("lrat", 1, "lrat option rejected"),
("binary", 0, "binary option rejected"),
] {
if !solver.set(option.to_string(), value) {
cleanup();
return Err(CadicalError::Configuration(err));
}
}
if !solver.trace_proof2(proof_path_str) {
cleanup();
return Err(CadicalError::Configuration("trace_proof rejected"));
}
let mut vars: i32 = 0;
let parse_err = solver.read_dimacs2(dimacs_path_str, &mut vars, 1);
if parse_err != "Null" {
cleanup();
return Err(CadicalError::ProofIo(format!("DIMACS parse: {parse_err}")));
}
if let Some(conflicts) = max_conflicts
&& !solver.limit("conflicts".to_string(), conflicts)
{
cleanup();
return Err(CadicalError::Configuration("conflicts limit rejected"));
}
let status = solver.solve();
match status {
Status::UNSATISFIABLE => {
solver.conclude();
drop(solver);
let lrat = std::fs::read_to_string(&proof_path).map_err(|e| {
cleanup();
CadicalError::ProofIo(e.to_string())
})?;
cleanup();
match ordeal_lrat::check(&formula.clauses, &lrat) {
Ok(()) => Ok(CadicalVerdict::Unsat { lrat }),
Err(e) => Err(CadicalError::ProofRejected(e)),
}
}
Status::SATISFIABLE => {
let assignment: Vec<bool> = (1..=formula.num_vars)
.map(|v| solver.val(v as i32) > 0)
.collect();
drop(solver);
cleanup();
if formula.eval(&assignment) {
Ok(CadicalVerdict::Sat(assignment))
} else {
Err(CadicalError::ModelSelfCheckFailed)
}
}
_ => {
drop(solver);
cleanup();
Err(CadicalError::Inconclusive)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sat::{SatResult, SatSolver};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
}
fn random_cnf(rng: &mut Rng, num_vars: u32, num_clauses: usize) -> CnfFormula {
let mut clauses = Vec::with_capacity(num_clauses);
for _ in 0..num_clauses {
let len = 1 + (rng.next() % 3) as usize;
let mut clause = Vec::with_capacity(len);
for _ in 0..len {
let var = 1 + (rng.next() % u64::from(num_vars)) as i32;
let lit = if rng.next() & 1 == 0 { var } else { -var };
clause.push(lit);
}
clauses.push(clause);
}
CnfFormula { num_vars, clauses }
}
#[test]
fn cadical_agrees_with_own_core_on_random_cnfs() {
let mut rng = Rng(0xDEA1_5EED);
let (mut sats, mut unsats) = (0u32, 0u32);
for round in 0..200 {
let num_vars = 4 + (round % 12) as u32;
let num_clauses = (f64::from(num_vars) * 4.3) as usize;
let formula = random_cnf(&mut rng, num_vars, num_clauses);
let own = SatSolver::new().solve(&formula);
let cadical = solve(&formula).expect("CaDiCaL produced no verdict");
match (own, cadical) {
(SatResult::Sat(_), CadicalVerdict::Sat(_)) => sats += 1,
(SatResult::Unsat, CadicalVerdict::Unsat { .. }) => unsats += 1,
(own, cadical) => {
panic!("backend disagreement on round {round}: own={own:?} cadical={cadical:?}")
}
}
}
assert!(
sats > 0 && unsats > 0,
"one-sided corpus: {sats} sat / {unsats} unsat"
);
}
#[test]
fn cadical_unsat_carries_checker_validated_lrat() {
let formula = CnfFormula {
num_vars: 2,
clauses: vec![vec![1, 2], vec![1, -2], vec![-1]],
};
match solve(&formula).expect("verdict") {
CadicalVerdict::Unsat { lrat } => {
assert!(!lrat.is_empty());
ordeal_lrat::check(&formula.clauses, &lrat)
.expect("checker must re-accept the carried proof");
}
v => panic!("expected Unsat, got {v:?}"),
}
}
}