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};
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>> = solver.learned().iter().map(|c| c.lits.clone()).collect();
60            if check_refutation(num_vars, solver.original_clauses(), &learned) {
61                Some(Verdict::Entailed)
62            } else {
63                None // solver said UNSAT, the trusted checker can't confirm → fail closed
64            }
65        }
66    }
67}
68
69/// Verify a DRAT/LRAT-style refutation: every learned clause is RUP w.r.t. the database
70/// built so far, and the full database is then refuted by unit propagation (the empty
71/// clause is RUP). Returns `false` if any step does not check — so a corrupted or bogus
72/// trace is rejected.
73pub fn check_refutation(num_vars: usize, original: &[Vec<Lit>], learned: &[Vec<Lit>]) -> bool {
74    let mut db: Vec<Vec<Lit>> = original.to_vec();
75    for c in learned {
76        if !is_rup(num_vars, &db, c) {
77            return false;
78        }
79        db.push(c.clone());
80    }
81    // The empty clause is RUP w.r.t. the final DB iff unit propagation alone conflicts.
82    is_rup(num_vars, &db, &[])
83}
84
85/// Is clause `c` derivable by reverse unit propagation from `db`? Assume `¬c` (set each of
86/// `c`'s literals false) and unit-propagate `db`; `c` is RUP iff that reaches a conflict.
87pub(crate) fn is_rup(num_vars: usize, db: &[Vec<Lit>], c: &[Lit]) -> bool {
88    let mut assign: Vec<Option<bool>> = vec![None; num_vars];
89    for &l in c {
90        // Assume ¬l. If that already clashes, ¬c is unsatisfiable ⇒ c is (trivially) RUP.
91        if !set_true(&mut assign, l.negated()) {
92            return true;
93        }
94    }
95    propagate(db, &mut assign)
96}
97
98/// The value of literal `l` under `assign`.
99#[inline]
100pub(crate) fn lit_val(assign: &[Option<bool>], l: Lit) -> Option<bool> {
101    assign[l.var() as usize].map(|b| if l.is_positive() { b } else { !b })
102}
103
104/// Set `l` true. Returns `false` if `l` is already false (a conflict).
105#[inline]
106pub(crate) fn set_true(assign: &mut [Option<bool>], l: Lit) -> bool {
107    match lit_val(assign, l) {
108        Some(true) => true,
109        Some(false) => false,
110        None => {
111            assign[l.var() as usize] = Some(l.is_positive());
112            true
113        }
114    }
115}
116
117/// What a clause says under an assignment.
118#[derive(Clone, Copy, PartialEq, Eq, Debug)]
119pub(crate) enum ClauseState {
120    /// Every literal is false.
121    Conflict,
122    /// No literal is true and exactly one distinct literal is unset: it is forced.
123    Unit(Lit),
124    /// A literal is true, or two distinct literals are unset (a tautology's `x` and `¬x` among them): no literal
125    /// after them can make the clause a conflict or a unit.
126    Quiet,
127}
128
129/// The state of `clause` under `assign`. A repeated literal counts once (`x ∨ x` is the unit `x`); a tautology
130/// (`x ∨ ¬x`) has two distinct unset literals and never propagates.
131#[inline]
132pub(crate) fn clause_state(clause: &[Lit], assign: &[Option<bool>]) -> ClauseState {
133    let mut unset: Option<Lit> = None;
134    for &l in clause {
135        match lit_val(assign, l) {
136            Some(true) => return ClauseState::Quiet,
137            Some(false) => {}
138            None => match unset {
139                None => unset = Some(l),
140                Some(u) if u == l => {}
141                Some(_) => return ClauseState::Quiet,
142            },
143        }
144    }
145    unset.map_or(ClauseState::Conflict, ClauseState::Unit)
146}
147
148/// Fixpoint unit propagation over `db`. Returns `true` on conflict (some clause all-false).
149/// Naive on purpose: a checker you can read in one sitting is a checker you can trust.
150pub(crate) fn propagate(db: &[Vec<Lit>], assign: &mut [Option<bool>]) -> bool {
151    loop {
152        let mut changed = false;
153        for clause in db {
154            match clause_state(clause, assign) {
155                ClauseState::Conflict => return true,
156                ClauseState::Unit(l) => {
157                    set_true(assign, l);
158                    changed = true;
159                }
160                ClauseState::Quiet => {}
161            }
162        }
163        if !changed {
164            return false;
165        }
166    }
167}
168
169#[cfg(test)]
170#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/rup/tests.rs"]
171mod tests;