1#![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#[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>;
100type OptTermCallbackStore<'a> = Option<Box<TermCallbackPtr<'a>>>;
102
103pub 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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug)]
453pub struct Interrupter {
454 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum Config {
482 Default,
484 Basic,
486 Plain,
488 Sat,
490 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#[derive(Debug, Clone, Copy)]
526pub enum Limit {
527 Conflicts(c_uint),
529 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
546pub 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
553pub 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 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}