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}