use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::sat::literal::Lit;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Step {
Add(Vec<Lit>),
Delete(Vec<Lit>),
}
fn lit_of(n: i64) -> Lit {
Lit::new((n.unsigned_abs() - 1) as u32, n < 0)
}
pub fn parse_cnf(input: &str) -> Result<Vec<Vec<Lit>>, String> {
let mut clauses = Vec::new();
let mut clause: Vec<Lit> = Vec::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('c') || line.starts_with('p') {
continue;
}
for tok in line.split_whitespace() {
let n: i64 = tok.parse().map_err(|_| format!("invalid token {tok:?}"))?;
if n == 0 {
clauses.push(core::mem::take(&mut clause));
} else {
clause.push(lit_of(n));
}
}
}
if !clause.is_empty() {
clauses.push(clause);
}
Ok(clauses)
}
pub fn parse_proof(input: &str) -> Result<Vec<Step>, String> {
let mut steps = Vec::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('c') {
continue;
}
let (is_del, rest) = match line.strip_prefix("d ") {
Some(r) => (true, r),
None => (false, line),
};
let mut clause = Vec::new();
for tok in rest.split_whitespace() {
let n: i64 = tok.parse().map_err(|_| format!("invalid token {tok:?}"))?;
if n == 0 {
break;
}
clause.push(lit_of(n));
}
steps.push(if is_del {
Step::Delete(clause)
} else {
Step::Add(clause)
});
}
Ok(steps)
}
fn propagate_conflict(clauses: &[Vec<Lit>], assign: &mut [Option<bool>]) -> bool {
let mut changed = true;
while changed {
changed = false;
for c in clauses {
let mut unassigned: Option<Lit> = None;
let mut satisfied = false;
let mut count_unassigned = 0;
for &l in c {
match assign[l.var() as usize] {
Some(v) if v != l.sign() => {
satisfied = true;
break;
}
Some(_) => {} None => {
count_unassigned += 1;
unassigned = Some(l);
}
}
}
if satisfied {
continue;
}
if count_unassigned == 0 {
return true; }
if count_unassigned == 1 {
let l = unassigned.unwrap();
assign[l.var() as usize] = Some(!l.sign());
changed = true;
}
}
}
false
}
fn is_rup(clauses: &[Vec<Lit>], clause: &[Lit], num_vars: usize) -> bool {
let mut assign = alloc::vec![None; num_vars];
for &l in clause {
let want_false = l.sign(); match assign[l.var() as usize] {
Some(v) if v != want_false => return true, _ => assign[l.var() as usize] = Some(want_false),
}
}
propagate_conflict(clauses, &mut assign)
}
fn is_rat(clauses: &[Vec<Lit>], clause: &[Lit], num_vars: usize) -> bool {
if is_rup(clauses, clause, num_vars) {
return true;
}
let Some(&p) = clause.first() else {
return false;
};
let np = !p;
for d in clauses {
if !d.contains(&np) {
continue;
}
let mut resolvent: Vec<Lit> = clause.to_vec();
for &l in d {
if l != np && !resolvent.contains(&l) {
resolvent.push(l);
}
}
if !is_rup(clauses, &resolvent, num_vars) {
return false;
}
}
true
}
pub fn check(cnf: &[Vec<Lit>], proof: &[Step]) -> Result<(), String> {
let mut num_vars = 0usize;
let mut scan = |c: &[Lit]| {
for &l in c {
num_vars = num_vars.max(l.var() as usize + 1);
}
};
for c in cnf {
scan(c);
}
for s in proof {
match s {
Step::Add(c) | Step::Delete(c) => scan(c),
}
}
let mut db: Vec<Vec<Lit>> = cnf.to_vec();
let mut derived_empty = false;
for step in proof {
match step {
Step::Add(c) => {
if !is_rat(&db, c, num_vars) {
return Err(format!(
"clause {:?} is not RAT-redundant",
c.iter().map(|l| l.to_string()).collect::<Vec<_>>()
));
}
if c.is_empty() {
derived_empty = true;
}
db.push(c.clone());
}
Step::Delete(c) => {
if let Some(pos) = db.iter().position(|d| same_clause(d, c)) {
db.swap_remove(pos);
}
}
}
}
if derived_empty {
Ok(())
} else {
Err("proof does not derive the empty clause".into())
}
}
fn same_clause(a: &[Lit], b: &[Lit]) -> bool {
a.len() == b.len() && a.iter().all(|l| b.contains(l))
}
pub fn check_text(cnf: &str, proof: &str) -> Result<(), String> {
let cnf = parse_cnf(cnf)?;
let proof = parse_proof(proof)?;
check(&cnf, &proof)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checks_trivial_refutation() {
let cnf = "p cnf 1 2\n1 0\n-1 0\n";
let proof = "0\n"; assert!(check_text(cnf, proof).is_ok());
}
#[test]
fn checks_rup_refutation() {
let cnf = "p cnf 2 4\n1 2 0\n-1 2 0\n1 -2 0\n-1 -2 0\n";
let proof = "2 0\n-2 0\n0\n";
assert!(check_text(cnf, proof).is_ok(), "valid DRUP proof rejected");
}
#[test]
fn rejects_invalid_addition() {
let cnf = "p cnf 2 1\n1 2 0\n"; let proof = "1 0\n0\n";
assert!(check_text(cnf, proof).is_err());
}
#[test]
fn rejects_incomplete_proof() {
let cnf = "p cnf 2 4\n1 2 0\n-1 2 0\n1 -2 0\n-1 -2 0\n";
let proof = "2 0\n"; assert!(check_text(cnf, proof).is_err());
}
#[test]
fn handles_deletions() {
let cnf = "p cnf 2 4\n1 2 0\n-1 2 0\n1 -2 0\n-1 -2 0\n";
let proof = "2 0\nd 1 2 0\n-2 0\n0\n";
assert!(check_text(cnf, proof).is_ok());
}
}