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;