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}