Skip to main content

sat_solvers/
lib.rs

1//! # SAT Solvers
2//!
3//! A unified Rust interface to multiple SAT solvers with automatic source compilation.
4//!
5//! This crate provides a simple way to use popular SAT solvers (CaDiCaL, MiniSat, Glucose,
6//! Lingeling, Kissat) without requiring pre-installed binaries. The solvers are compiled
7//! from source during `cargo build`, ensuring reproducible builds across different systems.
8//!
9//! ## Features
10//!
11//! - **Self-contained**: Solvers are built from bundled source code during compilation
12//! - **Unified interface**: All solvers implement the [`SolverExecutor`] trait
13//! - **Feature-gated**: Only compile the solvers you need
14//! - **Timeout support**: Built-in timeout handling for long-running problems
15//! - **DIMACS format**: Standard CNF input format supported by all solvers
16//!
17//! ## Quick Start
18//!
19//! Add `sat-solvers` to your `Cargo.toml`:
20//!
21//! ```toml
22//! [dependencies]
23//! sat-solvers = "0.1"
24//! ```
25//!
26//! By default, CaDiCaL and MiniSat are enabled. To use other solvers, enable their features:
27//!
28//! ```toml
29//! [dependencies]
30//! sat-solvers = { version = "0.1", features = ["cadical", "glucose", "kissat"] }
31//! ```
32//!
33//! ## Example
34//!
35//! Solve a simple SAT problem using CaDiCaL:
36//!
37//! ```no_run
38//! use sat_solvers::solvers::SolverExecutor;
39//! use sat_solvers::SATResult;
40//! use std::time::Duration;
41//!
42//! # #[cfg(feature = "cadical")]
43//! # fn main() {
44//! use sat_solvers::solvers::cadical::CaDiCaLExecutor;
45//!
46//! // Define a simple CNF formula in DIMACS format:
47//! // (x1 OR NOT x2) AND (x2 OR x3)
48//! let dimacs = "p cnf 3 2\n1 -2 0\n2 3 0\n";
49//!
50//! let executor = CaDiCaLExecutor;
51//! let result = executor.execute(dimacs, Some(Duration::from_secs(30))).unwrap();
52//!
53//! match result {
54//!     SATResult::Satisfiable(assignment) => {
55//!         println!("Satisfiable! Assignment: {:?}", assignment);
56//!     }
57//!     SATResult::Unsatisfiable => {
58//!         println!("Unsatisfiable");
59//!     }
60//!     SATResult::Unknown => {
61//!         println!("Unknown (timeout or resource limit)");
62//!     }
63//! }
64//! # }
65//! # #[cfg(not(feature = "cadical"))]
66//! # fn main() {}
67//! ```
68//!
69//! ## Available Solvers
70//!
71//! | Feature     | Solver    | Description                                      |
72//! |-------------|-----------|--------------------------------------------------|
73//! | `cadical`   | CaDiCaL   | State-of-the-art CDCL solver, SAT Competition winner |
74//! | `minisat`   | MiniSat   | Classic, widely-used SAT solver                  |
75//! | `glucose`   | Glucose   | MiniSat derivative with improved learning        |
76//! | `lingeling` | Lingeling | High-performance solver by Armin Biere           |
77//! | `kissat`    | Kissat    | Successor to CaDiCaL, SAT Competition 2020+ winner |
78//!
79//! ## Build Requirements
80//!
81//! Since solvers are compiled from source, you need:
82//!
83//! - A C++ compiler (g++ or clang++)
84//! - `make`
85//! - `cmake` (for Glucose)
86//!
87//! ## DIMACS CNF Format
88//!
89//! The solvers accept input in DIMACS CNF format:
90//!
91//! ```text
92//! p cnf <num_variables> <num_clauses>
93//! <literal1> <literal2> ... 0
94//! <literal1> <literal2> ... 0
95//! ...
96//! ```
97//!
98//! - Variables are positive integers (1, 2, 3, ...)
99//! - Negative literals represent negation (-1 means NOT x1)
100//! - Each clause ends with 0
101
102#![warn(missing_docs)]
103#![deny(rustdoc::broken_intra_doc_links)]
104
105use thiserror::Error;
106
107use std::fmt;
108use std::path::PathBuf;
109#[cfg(any(
110    feature = "minisat",
111    feature = "glucose",
112    feature = "cadical",
113    feature = "kissat",
114    feature = "lingeling"
115))]
116use std::process::Command;
117use std::process::Output;
118
119pub mod solvers;
120pub use solvers::SolverExecutor;
121
122/// Identifies which SAT solver to use.
123///
124/// Each variant corresponds to a different SAT solver implementation.
125/// The availability of each solver depends on the enabled feature flags.
126///
127/// # Example
128///
129/// ```
130/// use sat_solvers::SATSolverKind;
131///
132/// let solver = SATSolverKind::CaDiCaL;
133/// println!("Using solver: {}", solver);
134/// ```
135#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
136pub enum SATSolverKind {
137    /// CaDiCaL - State-of-the-art CDCL SAT solver by Armin Biere.
138    /// Winner of multiple SAT Competitions.
139    CaDiCaL,
140    /// MiniSat - A minimalistic, open-source SAT solver.
141    /// One of the most widely used and studied SAT solvers.
142    MiniSat,
143    /// Glucose - A CDCL SAT solver based on MiniSat with improved clause learning.
144    Glucose,
145    /// Lingeling - A high-performance SAT solver by Armin Biere.
146    Lingeling,
147    /// Kissat - Successor to CaDiCaL, winner of SAT Competition 2020 and later.
148    Kissat,
149}
150
151impl fmt::Display for SATSolverKind {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            SATSolverKind::CaDiCaL => write!(f, "CaDiCaL"),
155            SATSolverKind::MiniSat => write!(f, "MiniSat"),
156            SATSolverKind::Glucose => write!(f, "Glucose"),
157            SATSolverKind::Lingeling => write!(f, "Lingeling"),
158            SATSolverKind::Kissat => write!(f, "Kissat"),
159        }
160    }
161}
162
163/// The result of a SAT solver execution.
164///
165/// A SAT problem can have three possible outcomes:
166/// - [`Satisfiable`](SATResult::Satisfiable): A satisfying assignment exists
167/// - [`Unsatisfiable`](SATResult::Unsatisfiable): No satisfying assignment exists
168/// - [`Unknown`](SATResult::Unknown): The solver couldn't determine satisfiability
169///   (e.g., due to timeout or resource limits)
170///
171/// # Example
172///
173/// ```
174/// use sat_solvers::SATResult;
175///
176/// let result = SATResult::Satisfiable(vec![1, -2, 3]);
177///
178/// if result.is_satisfiable() {
179///     if let SATResult::Satisfiable(assignment) = result {
180///         // assignment contains the satisfying variable assignments
181///         // Positive values mean the variable is true, negative means false
182///         assert!(assignment.contains(&1));  // x1 = true
183///         assert!(assignment.contains(&-2)); // x2 = false
184///     }
185/// }
186/// ```
187#[derive(Debug, PartialEq, Eq, Clone)]
188pub enum SATResult {
189    /// The formula is satisfiable. Contains the satisfying assignment as a
190    /// vector of literals. Positive values indicate the variable is true,
191    /// negative values indicate the variable is false.
192    Satisfiable(Vec<i32>),
193    /// The formula is unsatisfiable (no satisfying assignment exists).
194    Unsatisfiable,
195    /// The solver could not determine satisfiability (e.g., timeout).
196    Unknown,
197}
198
199impl SATResult {
200    /// Returns `true` if the result is [`Satisfiable`](SATResult::Satisfiable).
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// use sat_solvers::SATResult;
206    ///
207    /// let sat = SATResult::Satisfiable(vec![1, -2]);
208    /// let unsat = SATResult::Unsatisfiable;
209    ///
210    /// assert!(sat.is_satisfiable());
211    /// assert!(!unsat.is_satisfiable());
212    /// ```
213    pub fn is_satisfiable(&self) -> bool {
214        matches!(self, SATResult::Satisfiable(_))
215    }
216
217    /// Returns `true` if the result is [`Unsatisfiable`](SATResult::Unsatisfiable).
218    ///
219    /// # Example
220    ///
221    /// ```
222    /// use sat_solvers::SATResult;
223    ///
224    /// let unsat = SATResult::Unsatisfiable;
225    /// assert!(unsat.is_unsatisfiable());
226    /// ```
227    pub fn is_unsatisfiable(&self) -> bool {
228        matches!(self, SATResult::Unsatisfiable)
229    }
230
231    /// Returns `true` if the result is [`Unknown`](SATResult::Unknown).
232    ///
233    /// # Example
234    ///
235    /// ```
236    /// use sat_solvers::SATResult;
237    ///
238    /// let unknown = SATResult::Unknown;
239    /// assert!(unknown.is_unknown());
240    /// ```
241    pub fn is_unknown(&self) -> bool {
242        matches!(self, SATResult::Unknown)
243    }
244}
245
246impl fmt::Display for SATResult {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            SATResult::Satisfiable(model) => write!(f, "SATISFIABLE with model: {:?}", model),
250            SATResult::Unsatisfiable => write!(f, "UNSATISFIABLE"),
251            SATResult::Unknown => write!(f, "UNKNOWN"),
252        }
253    }
254}
255
256/// Errors that can occur when working with SAT solvers.
257///
258/// This enum represents all possible errors that can be returned by
259/// the crate's public API.
260#[derive(Debug, Error)]
261pub enum SATSolversError {
262    /// The requested solver is not available in this build.
263    ///
264    /// This occurs when trying to use a solver whose feature flag
265    /// was not enabled at compile time.
266    #[error("Solver '{0}' is not available in this build.")]
267    SolverNotAvailable(String),
268
269    /// The solver binary was not found at the expected path.
270    ///
271    /// This typically indicates a build issue where the solver
272    /// failed to compile or the binary was moved/deleted.
273    #[error("Solver binary not found at path: {0}")]
274    SolverBinaryNotFound(PathBuf),
275
276    /// An I/O error occurred while executing the solver.
277    #[error("I/O Error: {0}")]
278    IoError(#[from] std::io::Error),
279}
280
281type Result<T> = std::result::Result<T, SATSolversError>;
282
283/// Returns the path to the MiniSat binary.
284///
285/// The path can be overridden by setting the `MINISAT_BINARY_PATH` environment variable.
286/// Otherwise, the path determined at build time is used.
287///
288/// # Panics
289///
290/// Panics if the `minisat` feature is not enabled.
291#[cfg(feature = "minisat")]
292pub fn minisat_path() -> PathBuf {
293    std::env::var("MINISAT_BINARY_PATH")
294        .unwrap_or_else(|_| env!("MINISAT_BINARY_PATH").to_string())
295        .into()
296}
297
298/// Returns the path to the Glucose binary.
299///
300/// The path can be overridden by setting the `GLUCOSE_BINARY_PATH` environment variable.
301/// Otherwise, the path determined at build time is used.
302///
303/// # Panics
304///
305/// Panics if the `glucose` feature is not enabled.
306#[cfg(feature = "glucose")]
307pub fn glucose_path() -> PathBuf {
308    std::env::var("GLUCOSE_BINARY_PATH")
309        .unwrap_or_else(|_| env!("GLUCOSE_BINARY_PATH").to_string())
310        .into()
311}
312
313/// Returns the path to the CaDiCaL binary.
314///
315/// The path can be overridden by setting the `CADICAL_BINARY_PATH` environment variable.
316/// Otherwise, the path determined at build time is used.
317///
318/// # Panics
319///
320/// Panics if the `cadical` feature is not enabled.
321#[cfg(feature = "cadical")]
322pub fn cadical_path() -> PathBuf {
323    std::env::var("CADICAL_BINARY_PATH")
324        .unwrap_or_else(|_| env!("CADICAL_BINARY_PATH").to_string())
325        .into()
326}
327
328/// Returns the path to the Kissat binary.
329///
330/// The path can be overridden by setting the `KISSAT_BINARY_PATH` environment variable.
331/// Otherwise, the path determined at build time is used.
332///
333/// # Panics
334///
335/// Panics if the `kissat` feature is not enabled.
336#[cfg(feature = "kissat")]
337pub fn kissat_path() -> PathBuf {
338    std::env::var("KISSAT_BINARY_PATH")
339        .unwrap_or_else(|_| env!("KISSAT_BINARY_PATH").to_string())
340        .into()
341}
342
343/// Returns the path to the Lingeling binary.
344///
345/// The path can be overridden by setting the `LINGELING_BINARY_PATH` environment variable.
346/// Otherwise, the path determined at build time is used.
347///
348/// # Panics
349///
350/// Panics if the `lingeling` feature is not enabled.
351#[cfg(feature = "lingeling")]
352pub fn lingeling_path() -> PathBuf {
353    std::env::var("LINGELING_BINARY_PATH")
354        .unwrap_or_else(|_| env!("LINGELING_BINARY_PATH").to_string())
355        .into()
356}
357
358/// Returns the path to the specified solver's binary.
359///
360/// # Arguments
361///
362/// * `solver` - The name of the solver (case-insensitive). Valid values are:
363///   `"minisat"`, `"glucose"`, `"cadical"`, `"kissat"`, `"lingeling"`
364///
365/// # Errors
366///
367/// Returns [`SATSolversError::SolverNotAvailable`] if the solver is not
368/// enabled via feature flags or if the solver name is not recognized.
369///
370/// # Example
371///
372/// ```no_run
373/// use sat_solvers::solver_path;
374///
375/// # #[cfg(feature = "cadical")]
376/// let path = solver_path("cadical").unwrap();
377/// ```
378pub fn solver_path(solver: &str) -> Result<PathBuf> {
379    match solver.to_ascii_lowercase() {
380        #[cfg(feature = "minisat")]
381        ref s if s == "minisat" => Ok(minisat_path()),
382        #[cfg(feature = "glucose")]
383        ref s if s == "glucose" => Ok(glucose_path()),
384        #[cfg(feature = "cadical")]
385        ref s if s == "cadical" => Ok(cadical_path()),
386        #[cfg(feature = "kissat")]
387        ref s if s == "kissat" => Ok(kissat_path()),
388        #[cfg(feature = "lingeling")]
389        ref s if s == "lingeling" => Ok(lingeling_path()),
390        _ => Err(SATSolversError::SolverNotAvailable(solver.to_string())),
391    }
392}
393
394/// Runs the specified solver with the given command-line arguments.
395///
396/// This is a low-level function that executes the solver binary directly.
397/// For most use cases, prefer using the [`SolverExecutor`] trait instead.
398///
399/// # Arguments
400///
401/// * `solver` - The name of the solver (case-insensitive)
402/// * `args` - Command-line arguments to pass to the solver
403///
404/// # Errors
405///
406/// Returns [`SATSolversError::SolverNotAvailable`] if the solver is not enabled,
407/// or [`SATSolversError::IoError`] if execution fails.
408///
409/// # Example
410///
411/// ```no_run
412/// use sat_solvers::run_solver;
413///
414/// # #[cfg(feature = "cadical")]
415/// let output = run_solver("cadical", &["--help"]).unwrap();
416/// ```
417#[allow(unused_variables)]
418pub fn run_solver(solver: &str, args: &[&str]) -> Result<Output> {
419    match solver.to_ascii_lowercase() {
420        #[cfg(feature = "minisat")]
421        ref s if s == "minisat" => Ok(Command::new(minisat_path()).args(args).output()?),
422        #[cfg(feature = "glucose")]
423        ref s if s == "glucose" => Ok(Command::new(glucose_path()).args(args).output()?),
424        #[cfg(feature = "cadical")]
425        ref s if s == "cadical" => Ok(Command::new(cadical_path()).args(args).output()?),
426        #[cfg(feature = "kissat")]
427        ref s if s == "kissat" => Ok(Command::new(kissat_path()).args(args).output()?),
428        #[cfg(feature = "lingeling")]
429        ref s if s == "lingeling" => Ok(Command::new(lingeling_path()).args(args).output()?),
430        _ => Err(SATSolversError::SolverNotAvailable(solver.to_string())),
431    }
432}
433
434/// Ensures that the specified solver binary exists at its expected path.
435///
436/// # Arguments
437///
438/// * `solver` - The name of the solver (case-insensitive)
439///
440/// # Errors
441///
442/// Returns [`SATSolversError::SolverNotAvailable`] if the solver is not enabled,
443/// or [`SATSolversError::SolverBinaryNotFound`] if the binary doesn't exist.
444///
445/// # Example
446///
447/// ```no_run
448/// use sat_solvers::ensure_solver_exists;
449///
450/// # #[cfg(feature = "cadical")]
451/// match ensure_solver_exists("cadical") {
452///     Ok(path) => println!("CaDiCaL found at: {}", path.display()),
453///     Err(e) => eprintln!("Error: {}", e),
454/// }
455/// ```
456pub fn ensure_solver_exists(solver: &str) -> Result<PathBuf> {
457    let p = solver_path(solver);
458    match p {
459        Ok(path) => {
460            if path.exists() {
461                Ok(path)
462            } else {
463                Err(SATSolversError::SolverBinaryNotFound(path))
464            }
465        }
466        Err(e) => Err(e),
467    }
468}