Skip to main content

sat_solvers/solvers/
minisat.rs

1//! MiniSat SAT solver executor.
2//!
3//! This module provides the [`MiniSatExecutor`] for running the MiniSat SAT solver.
4//! MiniSat is a minimalistic, open-source SAT solver that serves as a reference
5//! implementation for CDCL solvers and is widely used in education and research.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use sat_solvers::solvers::minisat::MiniSatExecutor;
11//! use sat_solvers::solvers::SolverExecutor;
12//! use std::time::Duration;
13//!
14//! let executor = MiniSatExecutor;
15//! let dimacs = "p cnf 3 2\n1 -2 0\n2 3 0\n";
16//!
17//! let result = executor.execute(dimacs, Some(Duration::from_secs(30))).unwrap();
18//! println!("Result: {}", result);
19//! ```
20
21use log::warn;
22use tempfile::NamedTempFile;
23
24use std::{
25    io::{Read, Write},
26    path::{Path, PathBuf},
27    time::Duration,
28};
29
30use crate::minisat_path;
31
32use super::{SATResult, SATSolverError, SolverExecutor, execute_with_timeout};
33
34/// Executor for the MiniSat SAT solver.
35///
36/// MiniSat is one of the most widely used and studied SAT solvers. It provides
37/// a clean, efficient implementation of the CDCL algorithm and serves as the
38/// foundation for many other solvers including Glucose.
39///
40/// # Output Format
41///
42/// Unlike other solvers, MiniSat outputs results to a file rather than stdout.
43/// The executor handles this automatically by creating a temporary output file.
44///
45/// # Example
46///
47/// ```no_run
48/// use sat_solvers::solvers::minisat::MiniSatExecutor;
49/// use sat_solvers::solvers::SolverExecutor;
50/// use sat_solvers::SATResult;
51/// use std::time::Duration;
52///
53/// let executor = MiniSatExecutor;
54///
55/// // Simple satisfiable formula: (x1 OR NOT x2) AND (x2 OR x3)
56/// let dimacs = "p cnf 3 2\n1 -2 0\n2 3 0\n";
57///
58/// match executor.execute(dimacs, Some(Duration::from_secs(60))) {
59///     Ok(SATResult::Satisfiable(assignment)) => {
60///         println!("SAT! Assignment: {:?}", assignment);
61///     }
62///     Ok(SATResult::Unsatisfiable) => println!("UNSAT"),
63///     Ok(SATResult::Unknown) => println!("Unknown"),
64///     Err(e) => eprintln!("Error: {}", e),
65/// }
66/// ```
67pub struct MiniSatExecutor;
68
69impl SolverExecutor for MiniSatExecutor {
70    fn get_solver_path(&self) -> PathBuf {
71        minisat_path()
72    }
73
74    fn execute(
75        &self,
76        dimacs: &str,
77        timeout: Option<Duration>,
78    ) -> Result<SATResult, SATSolverError> {
79        let mut input_file = NamedTempFile::new()?;
80        input_file.write_all(dimacs.as_bytes())?;
81        input_file.flush()?;
82
83        let output_file = NamedTempFile::new()?;
84
85        let mut args = self.get_args(input_file.path());
86        args.push(output_file.path().to_string_lossy().to_string());
87
88        let (_, stderr) = execute_with_timeout(&self.get_solver_path(), &args, timeout)?;
89        let mut stdout = String::new();
90        output_file
91            .into_file()
92            .read_to_string(&mut stdout)
93            .map_err(|e| {
94                SATSolverError::OutputParseError(format!(
95                    "Error reading minisat output file: {:?}",
96                    e
97                ))
98            })?;
99        self.parse_output(&stdout, &stderr)
100    }
101
102    fn get_args(&self, input_file: &Path) -> Vec<String> {
103        vec![
104            "-verb=0".to_string(), // Minimal verbosity for cleaner output
105            input_file.to_string_lossy().to_string(), // Note: MiniSat outputs to stdout by default if no result file specified
106        ]
107    }
108
109    fn parse_output(&self, stdout: &str, stderr: &str) -> Result<SATResult, SATSolverError> {
110        let mut is_sat = None;
111        let mut assignment = Vec::new();
112        let mut found_assignment_line = false;
113
114        for line in stdout.lines() {
115            let line = line.trim();
116
117            match line {
118                "SAT" | "UNSAT" => {
119                    if is_sat.is_some() {
120                        return Err(SATSolverError::OutputParseError(
121                            "Multiple satisfiability results found".to_string(),
122                        ));
123                    }
124                    is_sat = Some(line == "SAT");
125                }
126                _ if !line.is_empty()
127                    && !line.starts_with('c')
128                    && is_sat == Some(true)
129                    && !found_assignment_line =>
130                {
131                    // MiniSat outputs assignment as space-separated literals on next line after "SAT"
132                    // No "v " prefix like other solvers
133                    found_assignment_line = true;
134
135                    let vars: Vec<i32> = line
136                        .split_whitespace()
137                        .filter_map(|s| s.parse().ok())
138                        .filter(|&x| x != 0) // Remove terminating 0 if present
139                        .collect();
140
141                    for var in vars {
142                        assignment.push(var);
143                    }
144                }
145                _ => continue,
146            }
147        }
148
149        // Process stderr for any warnings or errors
150        for line in stderr.lines() {
151            if line.trim().starts_with("c ") {
152                continue;
153            }
154
155            warn!("{}", line.trim());
156        }
157
158        match is_sat {
159            Some(true) => Ok(SATResult::Satisfiable(assignment)),
160            Some(false) => Ok(SATResult::Unsatisfiable),
161            None => Err(SATSolverError::OutputParseError(
162                "No satisfiability result found".to_string(),
163            )),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_parse_output() {
174        let executor = MiniSatExecutor;
175        let stdout = "SAT\n1 -2 3 0\n";
176        let stderr = "";
177
178        let result = executor.parse_output(stdout, stderr).unwrap();
179        match result {
180            SATResult::Satisfiable(assignment) => {
181                assert_eq!(assignment.len(), 3);
182            }
183            _ => panic!("Expected satisfiable result"),
184        }
185    }
186
187    #[test]
188    fn test_parse_unsat_output() {
189        let executor = MiniSatExecutor;
190        let stdout = "UNSAT\n";
191        let stderr = "";
192
193        let result = executor.parse_output(stdout, stderr).unwrap();
194        assert!(matches!(result, SATResult::Unsatisfiable));
195    }
196
197    #[test]
198    fn test_parse_invalid_output() {
199        let executor = MiniSatExecutor;
200
201        // Test multiple satisfiability results
202        let stdout = "UNSAT\nSAT\n";
203        let stderr = "";
204        assert!(executor.parse_output(stdout, stderr).is_err());
205
206        // Test assignment without satisfiability result
207        let stdout = "v 1 -2 3 0\n";
208        let stderr = "";
209        assert!(executor.parse_output(stdout, stderr).is_err());
210
211        // Test no result
212        let stdout = "c This is just a comment\n";
213        let stderr = "";
214        assert!(executor.parse_output(stdout, stderr).is_err());
215    }
216}