sat_solver/sat/dpll.rs
1//! Defines the DPLL (Davis-Putnam-Logemann-Loveland) SAT solver.
2//!
3//! This module provides the `Dpll` struct, which implements a classical DPLL
4//! algorithm for solving Boolean Satisfiability (SAT) problems. The solver
5//! takes a Conjunctive Normal Form (CNF) formula as input and determines
6//! whether it's satisfiable. If satisfiable, it can also provide a model
7//! (an assignment of truth values to variables that satisfies the formula).
8//!
9//! The `Dpll` solver is generic over a `SolverConfig` trait, allowing
10//! different underlying data structures and strategies (e.g. for variable
11//! selection, literal representation) to be plugged in. A `DefaultConfig`
12//! is provided for common use cases.
13//!
14//! The core logic involves:
15//! 1. **Unit Propagation:** If a clause is a unit clause (contains only one
16//! unassigned literal), that literal must be assigned a value to make the
17//! clause true. This assignment may, in turn, create new unit clauses.
18//! 2. **Decision:** If no more unit propagations can be made and the formula is
19//! neither clearly satisfied nor falsified, an unassigned variable is chosen,
20//! and the algorithm recursively tries assigning it true and then false.
21//! 3. **Backtracking:** Implicitly handled by the recursive calls and cloning. If a branch
22//! leads to a conflict (a clause becomes unsatisfiable), that branch is abandoned.
23//!
24//! Was a near direct translation of the original Haskell implementation
25
26use crate::sat::assignment::Assignment;
27use crate::sat::clause_storage::LiteralStorage;
28use crate::sat::cnf;
29use crate::sat::cnf::Cnf;
30use crate::sat::literal::Literal;
31use crate::sat::propagation::Propagator;
32use crate::sat::solver::{DefaultConfig, SolutionStats, Solutions, Solver, SolverConfig};
33use crate::sat::trail::Trail;
34use crate::sat::variable_selection::VariableSelection;
35
36/// Represents a DPLL SAT solver.
37///
38/// This struct encapsulates the state of the solver, including the CNF formula,
39/// current assignment, decision level, and components for propagation and
40/// variable selection. It is generic over `Config`, which defines the specific
41/// types and strategies used by the solver.
42#[derive(Debug, Clone)]
43pub struct Dpll<Config: SolverConfig + Clone = DefaultConfig> {
44 /// The trail of assignments and decisions made by the solver.
45 /// It's used by the propagator to manage assignments and detect conflicts.
46 pub trail: Trail<Config::Literal, Config::LiteralStorage>,
47 /// The current assignment of truth values to variables.
48 pub assignment: Config::Assignment,
49 /// The current decision level in the DPLL search tree.
50 /// Incremented each time a new variable is chosen for branching.
51 pub decision_level: cnf::DecisionLevel,
52 /// The Conjunctive Normal Form (CNF) formula being solved.
53 pub cnf: Cnf<Config::Literal, Config::LiteralStorage>,
54 /// The variable selection heuristic.
55 /// Used to pick the next unassigned variable for a decision.
56 pub selector: Config::VariableSelector,
57 /// The unit propagator.
58 /// Responsible for performing unit propagation based on the current assignment.
59 pub propagator: Config::Propagator,
60}
61
62impl<Config: SolverConfig + Clone> Solver<Config> for Dpll<Config> {
63 /// Creates a new DPLL solver instance for the given CNF formula.
64 ///
65 /// # Arguments
66 ///
67 /// * `cnf`: The CNF formula to be solved.
68 ///
69 /// # Returns
70 ///
71 /// A new `Dpll` instance initialised with the provided formula and
72 /// default components (propagator, assignment tracker, trail, variable selector)
73 /// based on the `Config`.
74 fn new<L: Literal, S: LiteralStorage<L>>(cnf: Cnf<L, S>) -> Self {
75 let cnf = cnf.convert();
76
77 let lits = &cnf.lits;
78 let propagator = Propagator::new(&cnf);
79 let assignment = Assignment::new(cnf.num_vars);
80 let trail = Trail::new(&cnf.clauses, cnf.num_vars);
81 let selector = Config::VariableSelector::new(cnf.num_vars, lits, &cnf.clauses);
82
83 Self {
84 trail,
85 assignment,
86 decision_level: 0,
87 cnf,
88 selector,
89 propagator,
90 }
91 }
92
93 fn from_parts<L: Literal, S: LiteralStorage<L>>(
94 cnf: Cnf<L, S>,
95 assignment: Config::Assignment,
96 _manager: Config::ClauseManager,
97 propagator: Config::Propagator,
98 _restarter: Config::Restarter,
99 selector: Config::VariableSelector,
100 ) -> Self {
101 let cnf = cnf.convert();
102 let trail = Trail::new(&cnf.clauses, cnf.num_vars);
103
104 Self {
105 assignment,
106 propagator,
107 trail,
108 cnf,
109 selector,
110 decision_level: 0,
111 }
112 }
113
114 /// Attempts to solve the SAT formula.
115 ///
116 /// This method implements the core DPLL algorithm:
117 /// 1. It first performs unit propagation (`self.propagate()`).
118 /// 2. Then, it checks if the formula is already satisfied (`self.is_sat()`). If so,
119 /// it returns the current assignment.
120 /// 3. It checks if the formula is unsatisfiable (`self.is_unsat()`) due to a conflict.
121 /// If so, it returns `None`.
122 /// 4. If the formula is neither satisfied nor unsatisfiable, it selects an unassigned
123 /// variable using `self.selector.pick()`.
124 /// 5. It increments the decision level.
125 /// 6. It creates two branches: one where the chosen variable is assigned `true`,
126 /// and one where it's assigned `false`. These branches are explored recursively
127 /// by calling `solve()` on cloned solver states.
128 /// 7. If the 'true' branch finds a solution, it's returned.
129 /// 8. Otherwise, if the 'false' branch finds a solution, it's returned.
130 /// 9. If neither branch finds a solution, the formula is unsatisfiable from the
131 /// current state, and `None` is returned.
132 ///
133 /// # Returns
134 ///
135 /// * `Some(Solutions)`: If the formula is satisfiable, containing the satisfying assignment.
136 /// * `None`: If the formula is unsatisfiable.
137 fn solve(&mut self) -> Option<Solutions> {
138 self.propagate();
139
140 if self.is_sat() {
141 return Some(self.solutions());
142 }
143
144 if self.is_unsat() {
145 return None;
146 }
147
148 let var = self.selector.pick(&self.assignment)?;
149
150 self.decision_level = self.decision_level.wrapping_add(1);
151
152 let mut true_branch = self.clone();
153 let mut false_branch = self.clone();
154
155 true_branch
156 .assignment
157 .assign(Config::Literal::new(var.variable(), true));
158 false_branch
159 .assignment
160 .assign(Config::Literal::new(var.variable(), false));
161
162 if let Some(solutions) = true_branch.solve() {
163 return Some(solutions);
164 }
165
166 false_branch.solve()
167 }
168
169 /// Returns the current satisfying assignment if the formula is SAT.
170 ///
171 /// This method should typically be called after `solve()` returns `Some(...)`.
172 /// It retrieves the model (variable assignments) from the `assignment` component.
173 /// Variables not assigned a value in the model might be "don't care" variables.
174 fn solutions(&self) -> Solutions {
175 self.assignment.get_solutions()
176 }
177
178 /// Returns statistics about the solving process.
179 ///
180 /// Note: Some statistics, like `propagations`, might be estimations or
181 /// specific to this simple DPLL implementation rather than a general count
182 /// of propagation steps.
183 /// `conflicts`, `restarts`, `learnt_clauses`, `removed_clauses` are currently placeholders.
184 fn stats(&self) -> SolutionStats {
185 SolutionStats {
186 conflicts: 0,
187 decisions: self.decision_level,
188 propagations: self.cnf.num_vars.saturating_sub(self.decision_level),
189 restarts: 0,
190 learnt_clauses: 0,
191 removed_clauses: 0,
192 }
193 }
194
195 /// Placeholder for a debugging function.
196 ///
197 /// Currently, this function is not implemented and will panic if called.
198 fn debug(&mut self) {
199 todo!()
200 }
201}
202
203impl<Config: SolverConfig + Clone> Dpll<Config> {
204 /// Performs unit propagation.
205 ///
206 /// This method delegates to the configured `propagator` to find and
207 /// apply unit clauses, updating the `trail` and `assignment` accordingly.
208 /// If propagation leads to a conflict, the `trail` will be marked,
209 /// and subsequent `is_unsat()` calls should detect this.
210 fn propagate(&mut self) {
211 self.propagator
212 .propagate(&mut self.trail, &mut self.assignment, &mut self.cnf);
213 }
214
215 /// Checks if the CNF formula is satisfied under the current assignment.
216 ///
217 /// A formula is satisfied if all its clauses are satisfied. A clause is
218 /// satisfied if at least one of its literals is true under the current
219 /// assignment.
220 ///
221 /// # Returns
222 ///
223 /// `true` if all clauses are satisfied, `false` otherwise.
224 /// Returns `true` for an empty CNF (a CNF with no clauses).
225 fn is_sat(&self) -> bool {
226 self.cnf.iter().all(|clause| {
227 clause
228 .iter()
229 .any(|lit| self.assignment.literal_value(*lit) == Some(true))
230 })
231 }
232
233 /// Checks if the CNF formula is unsatisfiable under the current assignment.
234 ///
235 /// A formula is unsatisfiable if at least one of its clauses is falsified.
236 /// A clause is falsified if all of its literals are false under the current
237 /// assignment. This state indicates a conflict.
238 ///
239 /// # Returns
240 ///
241 /// `true` if any clause is falsified, `false` otherwise.
242 /// Returns `true` if the CNF contains an empty clause.
243 fn is_unsat(&self) -> bool {
244 self.cnf.iter().any(|clause| {
245 clause
246 .iter()
247 .all(|lit| self.assignment.literal_value(*lit) == Some(false))
248 })
249 }
250}