Skip to main content

logicaffeine_proof/
rup.rs

1//! The **RUP** (Reverse Unit Propagation) linear checker — the fast default trust tier.
2//!
3//! Modern certified SAT separates an untrusted, fast solver from a tiny, auditable checker
4//! that replays the solver's resolution proof by unit propagation alone (Goldberg & Novikov,
5//! 2003 — RUP; Wetzler/Heule/Hunt, 2014 — DRAT; Cruz-Filipe et al., 2017 — LRAT; Tan/Heule/
6//! Myreen, 2021 — the formally-verified `cake_lpr`). A learned clause `C` is valid iff
7//! assuming `¬C` and unit-propagating the current clause database hits a conflict; the
8//! refutation is verified iff, after adding every learned clause in order, unit propagation
9//! over the whole database derives the empty clause.
10//!
11//! This module re-derives NOTHING symbolic and builds no proof tree — so it sidesteps the
12//! emit-cost wall that sank eager probing. The solve produces the learned-clause trace for
13//! free ([`crate::cdcl::Solver::learned`]); here we *check* it, independently of the solver.
14//! The checker is deliberately small and naive (linear, fixpoint unit propagation) — its
15//! simplicity IS the trust. If it cannot confirm the solver's UNSAT, we fail closed.
16
17use crate::cdcl::{Lit, SolveResult, Solver};
18use crate::cnf::Cnf;
19use crate::ProofExpr;
20
21/// A certified propositional entailment verdict.
22#[derive(Clone, Copy, PartialEq, Eq, Debug)]
23pub enum Verdict {
24    /// `premises ⊨ goal`, and an independent RUP replay confirmed the refutation.
25    Entailed,
26    /// `premises ⊭ goal` (the solver found a model of `premises ∧ ¬goal`).
27    NotEntailed,
28}
29
30/// Decide `premises ⊨ goal` with the CDCL solver and **independently certify** an
31/// `Entailed` verdict by RUP replay. `None` if the problem is not purely propositional over
32/// recognisable atoms, OR if the solver claims UNSAT but the checker cannot confirm it (a
33/// solver bug — fail closed, never report a false `Entailed`).
34pub fn entails_certified(premises: &[ProofExpr], goal: &ProofExpr) -> Option<Verdict> {
35    let mut cnf = Cnf::new();
36    for p in premises {
37        cnf.assert(p)?;
38    }
39    certified_from_cnf(cnf, goal)
40}
41
42/// Certify `prepared ⊨ goal` against a premise CNF clausified ONCE by
43/// [`Cnf::from_premises`]. Solving a whole puzzle this way pays the of-pair Tseitin cost a
44/// single time, then each cell only adds its `¬goal` unit — the incremental win.
45pub fn entails_certified_prepared(prepared: &Cnf, goal: &ProofExpr) -> Option<Verdict> {
46    certified_from_cnf(prepared.clone(), goal)
47}
48
49/// Shared core: add `¬goal` to `cnf`, solve, and RUP-certify an UNSAT (entailed) verdict.
50fn certified_from_cnf(mut cnf: Cnf, goal: &ProofExpr) -> Option<Verdict> {
51    cnf.assert_neg(goal)?;
52    let num_vars = cnf.num_vars();
53    // Hand the clauses straight to the solver (one copy, inside the solver) — the RUP check
54    // reads them back via `original_clauses()`, so the clause set is never copied again.
55    let mut solver = cnf.into_solver();
56    match solver.solve() {
57        SolveResult::Sat(_) => Some(Verdict::NotEntailed),
58        SolveResult::Unsat => {
59            let learned: Vec<Vec<Lit>> =
60                solver.learned().iter().map(|c| c.lits.clone()).collect();
61            if check_refutation(num_vars, solver.original_clauses(), &learned) {
62                Some(Verdict::Entailed)
63            } else {
64                None // solver said UNSAT, the trusted checker can't confirm → fail closed
65            }
66        }
67    }
68}
69
70/// Verify a DRAT/LRAT-style refutation: every learned clause is RUP w.r.t. the database
71/// built so far, and the full database is then refuted by unit propagation (the empty
72/// clause is RUP). Returns `false` if any step does not check — so a corrupted or bogus
73/// trace is rejected.
74pub fn check_refutation(num_vars: usize, original: &[Vec<Lit>], learned: &[Vec<Lit>]) -> bool {
75    let mut db: Vec<Vec<Lit>> = original.to_vec();
76    for c in learned {
77        if !is_rup(num_vars, &db, c) {
78            return false;
79        }
80        db.push(c.clone());
81    }
82    // The empty clause is RUP w.r.t. the final DB iff unit propagation alone conflicts.
83    is_rup(num_vars, &db, &[])
84}
85
86/// Is clause `c` derivable by reverse unit propagation from `db`? Assume `¬c` (set each of
87/// `c`'s literals false) and unit-propagate `db`; `c` is RUP iff that reaches a conflict.
88pub(crate) fn is_rup(num_vars: usize, db: &[Vec<Lit>], c: &[Lit]) -> bool {
89    let mut assign: Vec<Option<bool>> = vec![None; num_vars];
90    for &l in c {
91        // Assume ¬l. If that already clashes, ¬c is unsatisfiable ⇒ c is (trivially) RUP.
92        if !set_true(&mut assign, l.negated()) {
93            return true;
94        }
95    }
96    propagate(db, &mut assign)
97}
98
99/// The value of literal `l` under `assign`.
100#[inline]
101pub(crate) fn lit_val(assign: &[Option<bool>], l: Lit) -> Option<bool> {
102    assign[l.var() as usize].map(|b| if l.is_positive() { b } else { !b })
103}
104
105/// Set `l` true. Returns `false` if `l` is already false (a conflict).
106#[inline]
107pub(crate) fn set_true(assign: &mut [Option<bool>], l: Lit) -> bool {
108    match lit_val(assign, l) {
109        Some(true) => true,
110        Some(false) => false,
111        None => {
112            assign[l.var() as usize] = Some(l.is_positive());
113            true
114        }
115    }
116}
117
118/// Fixpoint unit propagation over `db`. Returns `true` on conflict (some clause all-false).
119/// Naive on purpose: a checker you can read in one sitting is a checker you can trust.
120pub(crate) fn propagate(db: &[Vec<Lit>], assign: &mut [Option<bool>]) -> bool {
121    loop {
122        let mut changed = false;
123        for clause in db {
124            // Evaluate the clause robustly to DUPLICATE literals (`x ∨ x` is the unit `x`) and
125            // TAUTOLOGIES (`x ∨ ¬x` is always satisfied) — both are legal CNF, and a trusted checker
126            // must count *distinct* unset literals or it will miss real unit propagations.
127            let mut satisfied = false;
128            let mut unset: Vec<Lit> = Vec::new();
129            for &l in clause {
130                match lit_val(assign, l) {
131                    Some(true) => {
132                        satisfied = true;
133                        break;
134                    }
135                    Some(false) => {}
136                    None => {
137                        if unset.contains(&l.negated()) {
138                            satisfied = true; // tautology — never propagates, never conflicts
139                            break;
140                        }
141                        if !unset.contains(&l) {
142                            unset.push(l);
143                        }
144                    }
145                }
146            }
147            if satisfied {
148                continue;
149            }
150            if unset.is_empty() {
151                return true; // every literal false ⇒ conflict
152            }
153            if unset.len() == 1 {
154                set_true(assign, unset[0]);
155                changed = true;
156            }
157        }
158        if !changed {
159            return false;
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::cdcl::Lit;
168    use crate::{ProofExpr, ProofTerm};
169
170    fn c(s: &str) -> ProofTerm {
171        ProofTerm::Constant(s.to_string())
172    }
173    fn pred(name: &str, args: Vec<ProofTerm>) -> ProofExpr {
174        ProofExpr::Predicate { name: name.to_string(), args, world: None }
175    }
176    fn not(e: ProofExpr) -> ProofExpr {
177        ProofExpr::Not(Box::new(e))
178    }
179    fn or(a: ProofExpr, b: ProofExpr) -> ProofExpr {
180        ProofExpr::Or(Box::new(a), Box::new(b))
181    }
182    fn imp(a: ProofExpr, b: ProofExpr) -> ProofExpr {
183        ProofExpr::Implies(Box::new(a), Box::new(b))
184    }
185
186    #[test]
187    fn certified_disjunctive_syllogism() {
188        let p = pred("P", vec![c("a")]);
189        let q = pred("Q", vec![c("a")]);
190        let prem = vec![or(p.clone(), q.clone()), not(p.clone())];
191        assert_eq!(entails_certified(&prem, &q), Some(Verdict::Entailed));
192        assert_eq!(entails_certified(&[or(p, q.clone())], &q), Some(Verdict::NotEntailed));
193    }
194
195    #[test]
196    fn certified_two_value_grid() {
197        let in_ = |t: &str, s: &str| pred("In", vec![c(t), c(s)]);
198        let (fl, me) = ("Florida", "Maine");
199        let prem = vec![
200            or(in_("Alpha", fl), in_("Alpha", me)),
201            or(in_("Beta", fl), in_("Beta", me)),
202            imp(in_("Alpha", fl), not(in_("Beta", fl))),
203            imp(in_("Alpha", me), not(in_("Beta", me))),
204            or(in_("Alpha", fl), in_("Beta", fl)),
205            or(in_("Alpha", me), in_("Beta", me)),
206            in_("Alpha", fl),
207        ];
208        assert_eq!(entails_certified(&prem, &in_("Beta", me)), Some(Verdict::Entailed));
209        assert_eq!(entails_certified(&prem, &in_("Beta", fl)), Some(Verdict::NotEntailed));
210    }
211
212    #[test]
213    fn rup_rejects_a_bogus_proof() {
214        // Original `(p ∨ q)` is SATISFIABLE. A "proof" that bolts on the un-entailed units
215        // `¬p`, `¬q` must NOT verify — the checker rejects clauses that are not RUP.
216        let p = Lit::pos(0);
217        let q = Lit::pos(1);
218        let original = vec![vec![p, q]];
219        let bogus_learned = vec![vec![p.negated()], vec![q.negated()]];
220        assert!(
221            !check_refutation(2, &original, &bogus_learned),
222            "a non-RUP learned clause must be rejected"
223        );
224    }
225
226    #[test]
227    fn rup_needs_the_proof_not_just_the_clauses() {
228        // A genuinely UNSAT formula whose unsatisfiability is NOT visible to unit
229        // propagation alone (all four 2-literal clauses over p,q) must fail to verify
230        // WITHOUT the solver's learned clauses — proving the checker really replays the
231        // proof rather than re-searching.
232        let p = Lit::pos(0);
233        let q = Lit::pos(1);
234        let original = vec![
235            vec![p, q],
236            vec![p, q.negated()],
237            vec![p.negated(), q],
238            vec![p.negated(), q.negated()],
239        ];
240        assert!(!check_refutation(2, &original, &[]), "UP alone cannot refute this; needs the proof");
241        // With the resolvents the solver would learn (`p`, then `¬p` ⇒ empty), it checks.
242        let learned = vec![vec![p], vec![p.negated()]];
243        assert!(check_refutation(2, &original, &learned), "the learned resolvents make it RUP");
244    }
245}