rustsat_kissat/
lib.rs

1//! # rustsat-kissat - Interface to the Kissat SAT Solver for RustSAT
2//!
3//! Armin Biere's SAT solver [Kissat](https://github.com/arminbiere/kissat) to be used with the [RustSAT](https://github.com/chrjabs/rustsat) library.
4//!
5//! **Note**: at the moment this crate is known to not work on Windows since Kissat is non-trivial to get to work on Windows.
6//!
7//! ## Features
8//!
9//! - `debug`: if this feature is enables, the C library will be built with debug functionality if the Rust project is built in debug mode
10//! - `safe`: disable writing through `popen` for more safe usage of the library in applications
11//! - `quiet`: exclude message and profiling code (logging too)
12//!
13//! ## Kissat Versions
14//!
15//! Kissat versions can be selected via cargo crate features.
16//! The following Kissat versions are available:
17//! - `v4-0-4`: [Version 4.0.3](https://github.com/arminbiere/kissat/releases/tag/rel-4.0.4)
18//! - `v4-0-3`: [Version 4.0.3](https://github.com/arminbiere/kissat/releases/tag/rel-4.0.3)
19//! - `v4-0-2`: [Version 4.0.2](https://github.com/arminbiere/kissat/releases/tag/rel-4.0.2)
20//! - `v4-0-1`: [Version 4.0.1](https://github.com/arminbiere/kissat/releases/tag/rel-4.0.1)
21//! - `v4-0-0`: [Version 4.0.0](https://github.com/arminbiere/kissat/releases/tag/rel-4.0.0)
22//! - `v3-1-0`: [Version 3.1.0](https://github.com/arminbiere/kissat/releases/tag/rel-3.1.0)
23//! - `v3-0-0`: [Version 3.0.0](https://github.com/arminbiere/kissat/releases/tag/rel-3.0.0)
24//! - `sc2022-light`: [SAT Competition 2022 Light](https://github.com/arminbiere/kissat/releases/tag/sc2022-light)
25//! - `sc2022-hyper`: [SAT Competition 2022 Hyper](https://github.com/arminbiere/kissat/releases/tag/sc2022-hyper)
26//! - `sc2022-bulky`: [SAT Competition 2022 Bulky](https://github.com/arminbiere/kissat/releases/tag/sc2022-bulky)
27//!
28//! Without any features selected, the newest version will be used.
29//! If conflicting Kissat versions are requested, the newest requested version will be selected.
30//!
31//! If the determined version is _not_ the newest available, and no custom source directory is
32//! specified (see customization below), the Kissat source code is downloaded at compile time,
33//! which requires network access.
34//!
35//! ## Customization
36//!
37//! In order to build a custom version of Kissat, this crate supports the `KISSAT_SRC_DIR`
38//! environment variable.
39//! If this is set, Kissat will be built from the path specified there.
40//!
41//! ## Minimum Supported Rust Version (MSRV)
42//!
43//! Currently, the MSRV is 1.77.0, the plan is to always support an MSRV that is at least a year
44//! old.
45//!
46//! Bumps in the MSRV will _not_ be considered breaking changes. If you need a specific MSRV, make
47//! sure to pin a precise version of RustSAT.
48//!
49//! Note that the specified minimum-supported Rust version only applies if the _newest_ version of
50//! Kissat is build.
51//! Older versions are pulled down via the [`git2`](https://crates.io/crates/git2) crate, which has
52//! transitive dependencies that have a higher MSRV.
53
54#![warn(clippy::pedantic)]
55#![warn(missing_docs)]
56#![warn(missing_debug_implementations)]
57
58use core::ffi::{c_int, c_uint, c_void, CStr};
59use std::{ffi::CString, fmt};
60
61use rustsat::{
62    solvers::{
63        ControlSignal, Interrupt, InterruptSolver, Solve, SolveStats, SolverResult, SolverState,
64        SolverStats, StateError, Terminate,
65    },
66    types::{Cl, Clause, Lit, TernaryVal, Var},
67    utils::Timer,
68};
69use thiserror::Error;
70
71/// Fatal error returned if the Kissat API returns an invalid value
72#[derive(Error, Clone, Copy, PartialEq, Eq, Debug)]
73#[error("kissat c-api returned an invalid value: {api_call} -> {value}")]
74pub struct InvalidApiReturn {
75    api_call: &'static str,
76    value: c_int,
77}
78
79#[derive(Debug, PartialEq, Eq, Default)]
80enum InternalSolverState {
81    #[default]
82    Configuring,
83    Input,
84    Sat,
85    Unsat(Vec<Lit>),
86}
87
88impl InternalSolverState {
89    fn to_external(&self) -> SolverState {
90        match self {
91            InternalSolverState::Configuring => SolverState::Configuring,
92            InternalSolverState::Input => SolverState::Input,
93            InternalSolverState::Sat => SolverState::Sat,
94            InternalSolverState::Unsat(_) => SolverState::Unsat,
95        }
96    }
97}
98
99type TermCallbackPtr<'a> = Box<dyn FnMut() -> ControlSignal + 'a>;
100/// Double boxing is necessary to get thin pointers for casting
101type OptTermCallbackStore<'a> = Option<Box<TermCallbackPtr<'a>>>;
102
103/// The Kissat solver type
104pub struct Kissat<'term> {
105    handle: *mut ffi::kissat,
106    state: InternalSolverState,
107    terminate_cb: OptTermCallbackStore<'term>,
108    stats: SolverStats,
109}
110
111impl fmt::Debug for Kissat<'_> {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.debug_struct("Kissat")
114            .field("handle", &self.handle)
115            .field("state", &self.state)
116            .field(
117                "terminate_cb",
118                if self.terminate_cb.is_some() {
119                    &"some callback"
120                } else {
121                    &"no callback"
122                },
123            )
124            .field("stats", &self.stats)
125            .finish()
126    }
127}
128
129unsafe impl Send for Kissat<'_> {}
130
131impl Default for Kissat<'_> {
132    fn default() -> Self {
133        let solver = Self {
134            handle: unsafe { ffi::kissat_init() },
135            state: InternalSolverState::default(),
136            terminate_cb: None,
137            stats: SolverStats::default(),
138        };
139        let quiet = CString::new("quiet").unwrap();
140        unsafe { ffi::kissat_set_option(solver.handle, quiet.as_ptr(), 1) };
141        solver
142    }
143}
144
145impl Kissat<'_> {
146    #[allow(clippy::cast_precision_loss)]
147    #[inline]
148    fn update_avg_clause_len(&mut self, clause: &Cl) {
149        self.stats.avg_clause_len =
150            (self.stats.avg_clause_len * ((self.stats.n_clauses - 1) as f32) + clause.len() as f32)
151                / self.stats.n_clauses as f32;
152    }
153
154    /// Gets the commit ID that Kissat was built from
155    ///
156    /// # Panics
157    ///
158    /// If the Kissat library returns an id that is invalid UTF-8.
159    #[must_use]
160    pub fn commit_id() -> &'static str {
161        let c_chars = unsafe { ffi::kissat_id() };
162        let c_str = unsafe { CStr::from_ptr(c_chars) };
163        c_str.to_str().expect("Kissat id returned invalid UTF-8.")
164    }
165
166    /// Gets the Kissat version
167    ///
168    /// # Panics
169    ///
170    /// If the Kissat library returns a version that is invalid UTF-8.
171    #[must_use]
172    pub fn version() -> &'static str {
173        let c_chars = unsafe { ffi::kissat_version() };
174        let c_str = unsafe { CStr::from_ptr(c_chars) };
175        c_str
176            .to_str()
177            .expect("Kissat version returned invalid UTF-8.")
178    }
179
180    /// Gets the compiler Kissat was built with
181    ///
182    /// # Panics
183    ///
184    /// If the Kissat library returns a compiler that is invalid UTF-8.
185    #[must_use]
186    pub fn compiler() -> &'static str {
187        let c_chars = unsafe { ffi::kissat_compiler() };
188        let c_str = unsafe { CStr::from_ptr(c_chars) };
189        c_str
190            .to_str()
191            .expect("Kissat compiler returned invalid UTF-8.")
192    }
193
194    /// Sets a pre-defined configuration for Kissat's internal options
195    ///
196    /// # Errors
197    ///
198    /// - Returns [`StateError`] if the solver is not in [`SolverState::Configuring`]
199    /// - Returns [`InvalidApiReturn`] if the C-API does not return `true`. This case can be
200    ///   considered a bug in either the Kissat library or this crate.
201    pub fn set_configuration(&mut self, config: Config) -> anyhow::Result<()> {
202        if self.state != InternalSolverState::Configuring {
203            return Err(StateError {
204                required_state: SolverState::Configuring,
205                actual_state: self.state.to_external(),
206            }
207            .into());
208        }
209        let config_name: &CStr = config.into();
210        let ret = unsafe { ffi::kissat_set_configuration(self.handle, config_name.as_ptr()) };
211        if ret != 0 {
212            Ok(())
213        } else {
214            Err(InvalidApiReturn {
215                api_call: "kissat_configure",
216                value: 0,
217            }
218            .into())
219        }
220    }
221
222    /// Sets the value of a Kissat option. For possible options, check Kissat with `kissat --help`.
223    /// Requires state [`SolverState::Configuring`].
224    ///
225    /// # Errors
226    ///
227    /// Returns [`InvalidApiReturn`] if the C-API does not return `true`. This is most likely due
228    /// to a wrongly specified `name` or an invalid `value`.
229    pub fn set_option(&mut self, name: &str, value: c_int) -> anyhow::Result<()> {
230        let c_name = CString::new(name)?;
231        if unsafe { ffi::kissat_set_option(self.handle, c_name.as_ptr(), value) } != 0 {
232            Ok(())
233        } else {
234            Err(InvalidApiReturn {
235                api_call: "kissat_set_option",
236                value: 0,
237            }
238            .into())
239        }
240    }
241
242    /// Gets the value of a Kissat option. For possible options, check Kissat with `kissat --help`.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`InvalidApiReturn`] if the C-API does not return `true`. This is most likely due
247    /// to a wrongly specified `name`.
248    pub fn get_option(&self, name: &str) -> anyhow::Result<c_int> {
249        let c_name = CString::new(name)?;
250        Ok(unsafe { ffi::kissat_get_option(self.handle, c_name.as_ptr()) })
251    }
252
253    /// Sets an internal limit for Kissat
254    pub fn set_limit(&mut self, limit: Limit) {
255        match limit {
256            Limit::Conflicts(val) => unsafe { ffi::kissat_set_conflict_limit(self.handle, val) },
257            Limit::Decisions(val) => unsafe { ffi::kissat_set_decision_limit(self.handle, val) },
258        }
259    }
260
261    /// Prints the solver statistics from the Kissat backend
262    pub fn print_stats(&self) {
263        unsafe { ffi::kissat_print_statistics(self.handle) }
264    }
265}
266
267impl Extend<Clause> for Kissat<'_> {
268    fn extend<T: IntoIterator<Item = Clause>>(&mut self, iter: T) {
269        iter.into_iter()
270            .for_each(|cl| self.add_clause(cl).expect("Error adding clause in extend"));
271    }
272}
273
274impl<'a, C> Extend<&'a C> for Kissat<'_>
275where
276    C: AsRef<Cl> + ?Sized,
277{
278    fn extend<T: IntoIterator<Item = &'a C>>(&mut self, iter: T) {
279        iter.into_iter().for_each(|cl| {
280            self.add_clause_ref(cl)
281                .expect("Error adding clause in extend");
282        });
283    }
284}
285
286impl Solve for Kissat<'_> {
287    fn signature(&self) -> &'static str {
288        let c_chars = unsafe { ffi::kissat_signature() };
289        let c_str = unsafe { CStr::from_ptr(c_chars) };
290        c_str
291            .to_str()
292            .expect("Kissat signature returned invalid UTF-8.")
293    }
294
295    fn reserve(&mut self, max_var: Var) -> anyhow::Result<()> {
296        self.state = InternalSolverState::Input;
297        unsafe { ffi::kissat_reserve(self.handle, max_var.to_ipasir()) };
298        Ok(())
299    }
300
301    fn solve(&mut self) -> anyhow::Result<SolverResult> {
302        // If already solved, return state
303        if let InternalSolverState::Sat = self.state {
304            return Ok(SolverResult::Sat);
305        }
306        if let InternalSolverState::Unsat(_) = self.state {
307            return Ok(SolverResult::Unsat);
308        }
309        let start = Timer::now();
310        // Solve with Kissat backend
311        let res = unsafe { ffi::kissat_solve(self.handle) };
312        self.stats.cpu_solve_time += start.elapsed();
313        match res {
314            0 => {
315                self.stats.n_terminated += 1;
316                self.state = InternalSolverState::Input;
317                Ok(SolverResult::Interrupted)
318            }
319            10 => {
320                self.stats.n_sat += 1;
321                self.state = InternalSolverState::Sat;
322                Ok(SolverResult::Sat)
323            }
324            20 => {
325                self.stats.n_unsat += 1;
326                self.state = InternalSolverState::Unsat(vec![]);
327                Ok(SolverResult::Unsat)
328            }
329            value => Err(InvalidApiReturn {
330                api_call: "kissat_solve",
331                value,
332            }
333            .into()),
334        }
335    }
336
337    fn lit_val(&self, lit: Lit) -> anyhow::Result<TernaryVal> {
338        if self.state != InternalSolverState::Sat {
339            return Err(StateError {
340                required_state: SolverState::Sat,
341                actual_state: self.state.to_external(),
342            }
343            .into());
344        }
345        let lit = lit.to_ipasir();
346        match unsafe { ffi::kissat_value(self.handle, lit) } {
347            0 => Ok(TernaryVal::DontCare),
348            p if p == lit => Ok(TernaryVal::True),
349            n if n == -lit => Ok(TernaryVal::False),
350            value => Err(InvalidApiReturn {
351                api_call: "kissat_value",
352                value,
353            }
354            .into()),
355        }
356    }
357
358    fn add_clause_ref<C>(&mut self, clause: &C) -> anyhow::Result<()>
359    where
360        C: AsRef<Cl> + ?Sized,
361    {
362        // Kissat is non-incremental, so only add if in input or configuring state
363        if !matches!(
364            self.state,
365            InternalSolverState::Input | InternalSolverState::Configuring
366        ) {
367            return Err(StateError {
368                required_state: SolverState::Input,
369                actual_state: self.state.to_external(),
370            }
371            .into());
372        }
373        let clause = clause.as_ref();
374        // Update wrapper-internal state
375        self.stats.n_clauses += 1;
376        self.update_avg_clause_len(clause);
377        clause.iter().for_each(|l| match self.stats.max_var {
378            None => self.stats.max_var = Some(l.var()),
379            Some(var) => {
380                if l.var() > var {
381                    self.stats.max_var = Some(l.var());
382                }
383            }
384        });
385        self.state = InternalSolverState::Input;
386        // Call Kissat backend
387        clause
388            .iter()
389            .for_each(|l| unsafe { ffi::kissat_add(self.handle, l.to_ipasir()) });
390        unsafe { ffi::kissat_add(self.handle, 0) };
391        Ok(())
392    }
393}
394
395impl<'term> Terminate<'term> for Kissat<'term> {
396    /// Sets a terminator callback that is regularly called during solving.
397    ///
398    /// # Examples
399    ///
400    /// Terminate solver after 10 callback calls.
401    ///
402    /// ```
403    /// use rustsat::solvers::{ControlSignal, Solve, SolverResult, Terminate};
404    /// use rustsat_kissat::Kissat;
405    ///
406    /// let mut solver = Kissat::default();
407    ///
408    /// // Load instance
409    ///
410    /// let mut cnt = 1;
411    /// solver.attach_terminator(move || {
412    ///     if cnt > 10 {
413    ///         ControlSignal::Terminate
414    ///     } else {
415    ///         cnt += 1;
416    ///         ControlSignal::Continue
417    ///     }
418    /// });
419    ///
420    /// let ret = solver.solve().unwrap();
421    ///
422    /// // Assuming an instance is actually loaded and runs long enough
423    /// // assert_eq!(ret, SolverResult::Interrupted);
424    /// ```
425    fn attach_terminator<CB>(&mut self, cb: CB)
426    where
427        CB: FnMut() -> ControlSignal + 'term,
428    {
429        self.terminate_cb = Some(Box::new(Box::new(cb)));
430        let cb_ptr =
431            std::ptr::from_mut(self.terminate_cb.as_mut().unwrap().as_mut()).cast::<c_void>();
432        unsafe { ffi::kissat_set_terminate(self.handle, cb_ptr, Some(ffi::kissat_terminate_cb)) }
433    }
434
435    fn detach_terminator(&mut self) {
436        self.terminate_cb = None;
437        unsafe { ffi::kissat_set_terminate(self.handle, std::ptr::null_mut(), None) }
438    }
439}
440
441impl Interrupt for Kissat<'_> {
442    type Interrupter = Interrupter;
443
444    fn interrupter(&mut self) -> Self::Interrupter {
445        Interrupter {
446            handle: self.handle,
447        }
448    }
449}
450
451/// An Interrupter for the Kissat solver
452#[derive(Debug)]
453pub struct Interrupter {
454    /// The C API handle
455    handle: *mut ffi::kissat,
456}
457
458unsafe impl Send for Interrupter {}
459unsafe impl Sync for Interrupter {}
460
461impl InterruptSolver for Interrupter {
462    fn interrupt(&self) {
463        unsafe { ffi::kissat_terminate(self.handle) }
464    }
465}
466
467impl SolveStats for Kissat<'_> {
468    fn stats(&self) -> SolverStats {
469        self.stats.clone()
470    }
471}
472
473impl Drop for Kissat<'_> {
474    fn drop(&mut self) {
475        unsafe { ffi::kissat_release(self.handle) }
476    }
477}
478
479/// Possible Kissat configurations
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum Config {
482    /// Default configuration
483    Default,
484    /// Basic CDCL solving ([`Config::Plain`] but no restarts, minimize, reduce)
485    Basic,
486    /// Plain CDCL solving without advanced techniques
487    Plain,
488    /// Target satisfiable instances
489    Sat,
490    /// Target unsatisfiable instances
491    Unsat,
492}
493
494impl From<&Config> for &'static CStr {
495    fn from(value: &Config) -> Self {
496        match value {
497            Config::Default => c"default",
498            Config::Basic => c"basic",
499            Config::Plain => c"plain",
500            Config::Sat => c"sat",
501            Config::Unsat => c"unsat",
502        }
503    }
504}
505
506impl From<Config> for &'static CStr {
507    fn from(value: Config) -> Self {
508        (&value).into()
509    }
510}
511
512impl fmt::Display for Config {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        match self {
515            Config::Default => write!(f, "default"),
516            Config::Basic => write!(f, "basic"),
517            Config::Plain => write!(f, "plain"),
518            Config::Sat => write!(f, "sat"),
519            Config::Unsat => write!(f, "unsat"),
520        }
521    }
522}
523
524/// Possible Kissat limits
525#[derive(Debug, Clone, Copy)]
526pub enum Limit {
527    /// A limit on the number of conflicts
528    Conflicts(c_uint),
529    /// A limit on the number of decisions
530    Decisions(c_uint),
531}
532
533impl fmt::Display for Limit {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        match self {
536            Limit::Conflicts(val) => write!(f, "conflicts ({val})"),
537            Limit::Decisions(val) => write!(f, "decisions ({val})"),
538        }
539    }
540}
541
542extern "C" fn rustsat_kissat_panic_instead_of_abort() {
543    panic!("kissat called kissat_abort");
544}
545
546/// Changes Kissat's abort behavior to cause a Rust panic instead
547pub fn panic_instead_of_abort() {
548    unsafe {
549        ffi::kissat_call_function_instead_of_abort(Some(rustsat_kissat_panic_instead_of_abort));
550    };
551}
552
553/// Changes Kissat's abort behavior to call the given function instead
554pub fn call_instead_of_abort(abort: Option<unsafe extern "C" fn()>) {
555    unsafe { ffi::kissat_call_function_instead_of_abort(abort) };
556}
557
558#[cfg(test)]
559mod test {
560    use super::{Config, Kissat, Limit};
561    use rustsat::{
562        lit,
563        solvers::{Solve, SolverState, StateError},
564    };
565
566    rustsat_solvertests::basic_unittests!(
567        Kissat,
568        "kissat-(sc2022-(light|hyper|bulky)|[major].[minor].[patch])"
569    );
570
571    #[test]
572    fn configure() {
573        let mut solver = Kissat::default();
574        solver.set_configuration(Config::Default).unwrap();
575        solver.set_configuration(Config::Basic).unwrap();
576        solver.set_configuration(Config::Plain).unwrap();
577        solver.set_configuration(Config::Sat).unwrap();
578        solver.set_configuration(Config::Unsat).unwrap();
579        solver.add_unit(lit![0]).unwrap();
580        assert!(solver.set_configuration(Config::Default).is_err_and(|e| e
581            .downcast::<StateError>()
582            .unwrap()
583            == StateError {
584                required_state: SolverState::Configuring,
585                actual_state: SolverState::Input
586            }));
587    }
588
589    #[test]
590    fn options() {
591        let mut solver = Kissat::default();
592        assert_eq!(solver.get_option("warmup").unwrap(), 1);
593        solver.set_option("warmup", 0).unwrap();
594        assert_eq!(solver.get_option("warmup").unwrap(), 0);
595    }
596
597    #[test]
598    fn limit() {
599        let mut solver = Kissat::default();
600        solver.set_limit(Limit::Conflicts(100));
601    }
602}
603
604mod ffi {
605    #![allow(non_upper_case_globals)]
606    #![allow(non_camel_case_types)]
607    #![allow(non_snake_case)]
608
609    use core::ffi::{c_int, c_void};
610
611    use rustsat::solvers::ControlSignal;
612
613    use super::TermCallbackPtr;
614
615    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
616
617    // Raw callbacks forwarding to user callbacks
618    pub extern "C" fn kissat_terminate_cb(ptr: *mut c_void) -> c_int {
619        let cb = unsafe { &mut *ptr.cast::<TermCallbackPtr>() };
620        match cb() {
621            ControlSignal::Continue => 0,
622            ControlSignal::Terminate => 1,
623        }
624    }
625}