Skip to main content

elenchus_solver/
sat.rs

1//! A compact, single-threaded CDCL SAT solver in `no_std`, replicating the core
2//! algorithm of varisat (jix/varisat) in a readable, lazy style.
3//!
4//! `Solver::run` drives the CDCL loop to a terminal state: it propagates, and on a
5//! conflict analyzes/backjumps/learns, otherwise it decides (`Solver::decide`).
6//! Model enumeration is a lazy [`Models`] iterator that solves **incrementally** —
7//! each `next()` adds a blocking clause and continues from the existing state
8//! rather than re-solving from scratch.
9//!
10//! **Assumptions** ([`solve_assuming`]): literals forced true before VSIDS
11//! branching. They are decided first; a contradicted assumption yields an unsat
12//! **core** (a sufficient subset of the assumptions) via MiniSat's `analyzeFinal`.
13//! This is the primitive behind incremental cores and what-if queries.
14//!
15//! Pieces mirror varisat's modules: the trail + decision levels
16//! (`prop/assignment.rs`), two-watched-literal propagation (`prop/long.rs`),
17//! 1-UIP conflict analysis with clause learning (`analyze_conflict.rs`),
18//! non-chronological backjumping, VSIDS decisions with phase saving, and
19//! assumption-based solving. Remaining infrastructure (proof/DRAT logging,
20//! clause-DB GC, restarts, the `partial_ref` context, multithreading) is
21//! intentionally omitted.
22
23extern crate alloc;
24
25use alloc::vec;
26use alloc::vec::Vec;
27
28// --- literals & formulas ---------------------------------------------------
29
30/// A boolean variable, identified by a dense index.
31pub type Var = u32;
32
33/// A literal: a variable plus a sign, packed as `var << 1 | negative`.
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub struct SatLit(u32);
36
37impl SatLit {
38    /// A literal for `var`, positive (true) or negative (`NOT var`).
39    pub fn new(var: Var, positive: bool) -> Self {
40        SatLit((var << 1) | (!positive as u32))
41    }
42    /// The positive literal `var`.
43    pub fn positive(var: Var) -> Self {
44        Self::new(var, true)
45    }
46    /// The negative literal `NOT var`.
47    pub fn negative(var: Var) -> Self {
48        Self::new(var, false)
49    }
50    /// The underlying variable.
51    pub fn var(self) -> Var {
52        self.0 >> 1
53    }
54    /// Whether this is the negative polarity.
55    pub fn is_negative(self) -> bool {
56        self.0 & 1 == 1
57    }
58    /// The same variable with the opposite sign.
59    pub fn negate(self) -> SatLit {
60        SatLit(self.0 ^ 1)
61    }
62    /// The packed code, used directly as an index into the watch lists.
63    fn code(self) -> usize {
64        self.0 as usize
65    }
66}
67
68/// A CNF formula over `num_vars` variables.
69#[derive(Clone, Debug, Default)]
70pub struct Cnf {
71    /// Number of variables; every [`Var`] used must be `< num_vars`.
72    pub num_vars: usize,
73    /// The clauses, each a disjunction of literals (the formula is their AND).
74    pub clauses: Vec<Vec<SatLit>>,
75}
76
77impl Cnf {
78    /// An empty formula over `num_vars` variables.
79    pub fn new(num_vars: usize) -> Self {
80        Cnf {
81            num_vars,
82            clauses: Vec::new(),
83        }
84    }
85    /// Append one clause (a disjunction of literals).
86    pub fn add_clause(&mut self, lits: Vec<SatLit>) {
87        self.clauses.push(lits);
88    }
89}
90
91// --- internal state --------------------------------------------------------
92
93/// Why a variable was assigned — needed for conflict analysis and backtracking.
94#[derive(Clone, Copy)]
95enum Reason {
96    Decision,
97    Unit,
98    Long(usize),
99}
100
101/// One watched-literal entry: a clause plus a cached "other" literal so a true
102/// blocking literal lets us skip the clause entirely.
103#[derive(Clone, Copy)]
104struct Watch {
105    cref: usize,
106    blocking: SatLit,
107}
108
109/// What the decision phase produced. The search loop reacts to each.
110enum Decision {
111    /// A literal (an assumption or a VSIDS branch) was enqueued; propagate next.
112    Propagated,
113    /// Every variable is assigned under the assumptions — satisfiable.
114    Sat,
115    /// An assumption is contradicted; carries a sufficient core (a subset of the
116    /// assumptions). An empty core means UNSAT independent of the assumptions.
117    UnsatCore(Vec<SatLit>),
118}
119
120/// The full CDCL search state: the assignment trail with decision levels, the
121/// clause database with two-watched-literal indices, VSIDS activities with phase
122/// saving, and a reusable `seen` scratch buffer for conflict analysis.
123struct Solver {
124    num_vars: usize,
125    clauses: Vec<Vec<SatLit>>, // originals + learned + blocking
126    watches: Vec<Vec<Watch>>, // indexed by literal code; a clause watching `w` lives in watches[!w]
127    assign: Vec<Option<bool>>, // per var
128    level: Vec<u32>,          // per var (valid when assigned)
129    reason: Vec<Reason>,      // per var (valid when assigned)
130    trail: Vec<SatLit>,
131    decisions: Vec<usize>, // trail index where each decision level starts
132    qhead: usize,
133    activity: Vec<f64>,
134    var_inc: f64,
135    polarity: Vec<bool>, // phase saving
136    seen: Vec<bool>,     // reusable scratch for analyze (invariant: all-false between calls)
137    ok: bool,            // false once the formula is known UNSAT
138    // Literals forced true before VSIDS branching. Decision levels 1..=len map
139    // one-to-one to assumptions[0..]; an already-true assumption still consumes a
140    // (dummy) level so that mapping holds. Empty for a plain solve.
141    assumptions: Vec<SatLit>,
142}
143
144impl Solver {
145    /// Build a solver and load every clause of `cnf` under the empty assignment.
146    fn new(cnf: &Cnf) -> Self {
147        let n = cnf.num_vars;
148        let mut s = Solver {
149            num_vars: n,
150            clauses: Vec::new(),
151            watches: vec![Vec::new(); 2 * n],
152            assign: vec![None; n],
153            level: vec![0; n],
154            reason: vec![Reason::Decision; n],
155            trail: Vec::new(),
156            decisions: Vec::new(),
157            qhead: 0,
158            activity: vec![0.0; n],
159            var_inc: 1.0,
160            polarity: vec![false; n],
161            seen: vec![false; n],
162            ok: true,
163            assumptions: Vec::new(),
164        };
165        for clause in &cnf.clauses {
166            s.add_clause(clause);
167        }
168        s
169    }
170
171    // -- assignment queries --
172
173    /// Is `l` currently assigned true? (Unassigned counts as neither true nor false.)
174    fn lit_is_true(&self, l: SatLit) -> bool {
175        self.assign[l.var() as usize] == Some(!l.is_negative())
176    }
177    /// Is `l` currently assigned false?
178    fn lit_is_false(&self, l: SatLit) -> bool {
179        self.assign[l.var() as usize] == Some(l.is_negative())
180    }
181    /// The current decision level (= number of open decisions).
182    fn current_level(&self) -> u32 {
183        self.decisions.len() as u32
184    }
185
186    // -- clause loading --
187
188    /// Register clause `cref` to be watched by literals `a` and `b`. A clause
189    /// watching a literal is stored under that literal's *negation's* code, so
190    /// it is revisited exactly when the watched literal becomes false.
191    fn watch(&mut self, cref: usize, a: SatLit, b: SatLit) {
192        self.watches[a.negate().code()].push(Watch { cref, blocking: b });
193        self.watches[b.negate().code()].push(Watch { cref, blocking: a });
194    }
195
196    /// Attach a clause under the *current* assignment. Both watched literals must
197    /// be non-false, or the clause is unit/conflicting and is handled directly.
198    /// This is what makes incremental clause addition (blocking clauses added
199    /// mid-search, at level 0) correct — naively watching `lits[0..2]` would break
200    /// the invariant when one is already false.
201    fn add_clause(&mut self, lits: &[SatLit]) {
202        if !self.ok {
203            return;
204        }
205        if lits.is_empty() {
206            self.ok = false;
207            return;
208        }
209        if lits.len() == 1 {
210            let l = lits[0];
211            if self.lit_is_false(l) {
212                self.ok = false;
213            } else if !self.lit_is_true(l) {
214                self.enqueue(l, Reason::Unit);
215            }
216            return;
217        }
218
219        // Find up to two non-false literals to watch.
220        let mut clause = lits.to_vec();
221        let mut first = None;
222        let mut second = None;
223        for (i, &l) in clause.iter().enumerate() {
224            if !self.lit_is_false(l) {
225                if first.is_none() {
226                    first = Some(i);
227                } else {
228                    second = Some(i);
229                    break;
230                }
231            }
232        }
233        let cref = self.clauses.len();
234        match (first, second) {
235            // Every literal is false under the current assignment → conflict.
236            (None, _) => self.ok = false,
237            // Exactly one non-false literal → the clause is unit; assert it.
238            (Some(a), None) => {
239                clause.swap(0, a);
240                self.watch(cref, clause[0], clause[1]);
241                let unit = clause[0];
242                self.clauses.push(clause);
243                if !self.lit_is_true(unit) {
244                    self.enqueue(unit, Reason::Long(cref));
245                }
246            }
247            // Two non-false literals → watch them (moved to positions 0 and 1).
248            (Some(a), Some(b)) => {
249                clause.swap(0, a);
250                clause.swap(1, b);
251                self.watch(cref, clause[0], clause[1]);
252                self.clauses.push(clause);
253            }
254        }
255    }
256
257    /// Assign `l` true at the current level with the given `reason`, and push it
258    /// onto the trail for propagation.
259    fn enqueue(&mut self, l: SatLit, reason: Reason) {
260        let v = l.var() as usize;
261        self.assign[v] = Some(!l.is_negative());
262        self.level[v] = self.current_level();
263        self.reason[v] = reason;
264        self.trail.push(l);
265    }
266
267    // -- propagation (two-watched-literal) --
268
269    /// Unit-propagate to a fixpoint. Returns the conflicting clause, if any.
270    fn propagate(&mut self) -> Option<usize> {
271        while self.qhead < self.trail.len() {
272            let p = self.trail[self.qhead];
273            self.qhead += 1;
274            if let Some(cref) = self.propagate_lit(p) {
275                return Some(cref);
276            }
277        }
278        None
279    }
280
281    /// Process the clauses watching `!p` after `p` became true.
282    fn propagate_lit(&mut self, p: SatLit) -> Option<usize> {
283        let fl = p.negate(); // the watched literal that just became false
284        let mut ws = core::mem::take(&mut self.watches[p.code()]);
285        let mut read = 0;
286        let mut write = 0;
287        let mut conflict = None;
288
289        while read < ws.len() {
290            let w = ws[read];
291            read += 1;
292
293            // A satisfied clause (true blocking literal) needs no inspection.
294            if self.lit_is_true(w.blocking) {
295                ws[write] = w;
296                write += 1;
297                continue;
298            }
299
300            let cref = w.cref;
301            if self.clauses[cref][0] == fl {
302                self.clauses[cref].swap(0, 1);
303            }
304            let other = self.clauses[cref][0];
305            let kept = Watch {
306                cref,
307                blocking: other,
308            };
309
310            if other != w.blocking && self.lit_is_true(other) {
311                ws[write] = kept;
312                write += 1;
313                continue;
314            }
315
316            // Try to move the watch to a non-false unwatched literal.
317            if let Some(repl) = self.find_replacement(cref, fl) {
318                self.watches[repl.negate().code()].push(kept);
319                continue; // watch left this list
320            }
321
322            // No replacement: keep watching `fl`; the clause is unit or conflicting.
323            ws[write] = kept;
324            write += 1;
325            if self.lit_is_false(other) {
326                while read < ws.len() {
327                    ws[write] = ws[read];
328                    write += 1;
329                    read += 1;
330                }
331                conflict = Some(cref);
332                break;
333            }
334            self.enqueue(other, Reason::Long(cref));
335        }
336
337        ws.truncate(write);
338        self.watches[p.code()] = ws;
339        conflict
340    }
341
342    /// Find a non-false literal in `clause[2..]`, swap it into the watched slot.
343    fn find_replacement(&mut self, cref: usize, fl: SatLit) -> Option<SatLit> {
344        let len = self.clauses[cref].len();
345        for k in 2..len {
346            let ck = self.clauses[cref][k];
347            if !self.lit_is_false(ck) {
348                self.clauses[cref][1] = ck;
349                self.clauses[cref][k] = fl;
350                return Some(ck);
351            }
352        }
353        None
354    }
355
356    // -- conflict analysis (1-UIP) --
357
358    /// VSIDS: raise variable `v`'s activity, rescaling all activities if it would
359    /// overflow `f64`'s comfortable range.
360    fn bump(&mut self, v: usize) {
361        self.activity[v] += self.var_inc;
362        if self.activity[v] > 1e100 {
363            for a in &mut self.activity {
364                *a *= 1e-100;
365            }
366            self.var_inc *= 1e-100;
367        }
368    }
369
370    /// Learn an asserting clause from `conflict` and return (clause, backjump level).
371    /// Uses the reusable `seen` buffer and restores it to all-false on exit.
372    fn analyze(&mut self, conflict: usize) -> (Vec<SatLit>, u32) {
373        let cur_level = self.current_level();
374        let mut learned: Vec<SatLit> = vec![SatLit(0)]; // slot 0 = asserting literal
375        let mut touched: Vec<Var> = Vec::new();
376        let mut counter = 0usize;
377        let mut idx = self.trail.len();
378        let mut p: Option<SatLit> = None;
379        let mut confl = conflict;
380
381        loop {
382            let start = if p.is_some() { 1 } else { 0 }; // a reason clause has p at index 0
383            for j in start..self.clauses[confl].len() {
384                let q = self.clauses[confl][j];
385                let v = q.var() as usize;
386                if !self.seen[v] && self.level[v] > 0 {
387                    self.seen[v] = true;
388                    touched.push(v as Var);
389                    self.bump(v);
390                    if self.level[v] == cur_level {
391                        counter += 1;
392                    } else {
393                        learned.push(q);
394                    }
395                }
396            }
397            // The most recently assigned `seen` literal on the trail.
398            loop {
399                idx -= 1;
400                if self.seen[self.trail[idx].var() as usize] {
401                    break;
402                }
403            }
404            let lit = self.trail[idx];
405            self.seen[lit.var() as usize] = false;
406            counter -= 1;
407            p = Some(lit);
408            if counter == 0 {
409                break;
410            }
411            confl = match self.reason[lit.var() as usize] {
412                Reason::Long(c) => c,
413                _ => unreachable!("a resolved current-level literal must have a clause reason"),
414            };
415        }
416        learned[0] = p.unwrap().negate();
417
418        let backjump = self.assertion_level(&mut learned);
419        self.var_inc *= 1.0 / 0.95; // VSIDS decay
420
421        for v in touched {
422            self.seen[v as usize] = false; // restore the scratch buffer
423        }
424        (learned, backjump)
425    }
426
427    /// Move the highest-level non-asserting literal to index 1 and return its
428    /// level (the level to backjump to), or 0 for a unit clause.
429    fn assertion_level(&self, learned: &mut [SatLit]) -> u32 {
430        if learned.len() == 1 {
431            return 0;
432        }
433        let mut max_i = 1;
434        let mut max_l = self.level[learned[1].var() as usize];
435        for (i, &lit) in learned.iter().enumerate().skip(2) {
436            let l = self.level[lit.var() as usize];
437            if l > max_l {
438                max_l = l;
439                max_i = i;
440            }
441        }
442        learned.swap(1, max_i);
443        max_l
444    }
445
446    /// MiniSat's `analyzeFinal`. `true_lit` is currently TRUE on the trail and is
447    /// the negation of a contradicted assumption; walk its implication graph and
448    /// collect the assumptions that entail it. Returns a *sufficient* core — a
449    /// subset of [`Solver::assumptions`] (including the contradicted assumption
450    /// itself) such that `cnf ∧ core` is unsatisfiable. Restores `seen` on exit.
451    fn analyze_final(&mut self, true_lit: SatLit) -> Vec<SatLit> {
452        let mut core = vec![true_lit.negate()]; // the contradicted assumption
453        if self.current_level() == 0 {
454            // `cnf` entails `~assumption` outright; the assumption alone suffices.
455            return core;
456        }
457        let assn = self.assumptions.len() as u32;
458        let start = self.decisions[0]; // trail index where level 1 begins
459        self.seen[true_lit.var() as usize] = true;
460        let mut touched = vec![true_lit.var()];
461        let mut i = self.trail.len();
462        while i > start {
463            i -= 1;
464            let x = self.trail[i].var() as usize;
465            if !self.seen[x] {
466                continue;
467            }
468            self.seen[x] = false;
469            match self.reason[x] {
470                // A decision sitting at an assumption level *is* an assumption.
471                Reason::Decision => {
472                    if self.level[x] > 0 && self.level[x] <= assn {
473                        core.push(self.trail[i]);
474                    }
475                }
476                Reason::Unit => {}
477                // Pull in the antecedents (clause[1..] are the false literals).
478                Reason::Long(cr) => {
479                    for j in 1..self.clauses[cr].len() {
480                        let v = self.clauses[cr][j].var();
481                        if self.level[v as usize] > 0 && !self.seen[v as usize] {
482                            self.seen[v as usize] = true;
483                            touched.push(v);
484                        }
485                    }
486                }
487            }
488        }
489        for v in touched {
490            self.seen[v as usize] = false;
491        }
492        core
493    }
494
495    /// Undo assignments above `level`, saving each unset variable's phase for
496    /// later reuse, and rewind the propagation queue to that level.
497    fn backtrack(&mut self, level: u32) {
498        if self.current_level() <= level {
499            return;
500        }
501        let new_len = self.decisions[level as usize];
502        for i in new_len..self.trail.len() {
503            let v = self.trail[i].var() as usize;
504            self.polarity[v] = self.assign[v] == Some(true);
505            self.assign[v] = None;
506        }
507        self.trail.truncate(new_len);
508        self.decisions.truncate(level as usize);
509        self.qhead = new_len;
510    }
511
512    /// Install a freshly learned clause and enqueue its asserting literal.
513    fn learn(&mut self, learned: Vec<SatLit>) {
514        if learned.len() == 1 {
515            self.enqueue(learned[0], Reason::Unit);
516        } else {
517            let cref = self.clauses.len();
518            self.watch(cref, learned[0], learned[1]);
519            let assert_lit = learned[0];
520            self.clauses.push(learned);
521            self.enqueue(assert_lit, Reason::Long(cref));
522        }
523    }
524
525    // -- decisions --
526
527    /// Choose the next decision: the unassigned variable with the highest VSIDS
528    /// activity, using its saved phase. `None` means all variables are assigned.
529    fn pick_branch(&self) -> Option<SatLit> {
530        let mut best: Option<usize> = None;
531        let mut best_act = -1.0;
532        for v in 0..self.num_vars {
533            if self.assign[v].is_none() && self.activity[v] > best_act {
534                best_act = self.activity[v];
535                best = Some(v);
536            }
537        }
538        best.map(|v| SatLit::new(v as Var, self.polarity[v]))
539    }
540
541    // -- the state machine --
542
543    /// The decision phase: place the next not-yet-satisfied assumption (or detect a
544    /// contradicted one and return its core), otherwise branch by VSIDS. Each
545    /// assumption — even an already-true one (a dummy level) — consumes exactly one
546    /// decision level, so level `i+1` always corresponds to `assumptions[i]`.
547    fn decide(&mut self) -> Decision {
548        while (self.current_level() as usize) < self.assumptions.len() {
549            let p = self.assumptions[self.current_level() as usize];
550            if self.lit_is_true(p) {
551                self.decisions.push(self.trail.len()); // dummy level, nothing enqueued
552            } else if self.lit_is_false(p) {
553                return Decision::UnsatCore(self.analyze_final(p.negate()));
554            } else {
555                self.decisions.push(self.trail.len());
556                self.enqueue(p, Reason::Decision);
557                return Decision::Propagated;
558            }
559        }
560        match self.pick_branch() {
561            None => Decision::Sat,
562            Some(lit) => {
563                self.decisions.push(self.trail.len());
564                self.enqueue(lit, Reason::Decision);
565                Decision::Propagated
566            }
567        }
568    }
569
570    /// Drive the search to a terminal state under the current assumptions.
571    /// `Ok(())` = SAT; `Err(core)` = UNSAT with a sufficient subset of the
572    /// assumptions (empty when unsat regardless of them). Re-entrant: after
573    /// [`Solver::block`] resets to level 0, calling it again continues the search.
574    fn run(&mut self) -> Result<(), Vec<SatLit>> {
575        if !self.ok {
576            return Err(Vec::new());
577        }
578        loop {
579            if let Some(cref) = self.propagate() {
580                if self.current_level() == 0 {
581                    self.ok = false;
582                    return Err(Vec::new());
583                }
584                let (learned, backjump) = self.analyze(cref);
585                self.backtrack(backjump);
586                self.learn(learned);
587            } else {
588                match self.decide() {
589                    Decision::Propagated => {}
590                    Decision::Sat => return Ok(()),
591                    Decision::UnsatCore(core) => return Err(core),
592                }
593            }
594        }
595    }
596
597    /// Plain satisfiability (no assumptions): `true` if a model exists. Re-entrant
598    /// for [`Models`] enumeration.
599    fn search(&mut self) -> bool {
600        self.run().is_ok()
601    }
602
603    /// Snapshot the assignment as `var -> bool` (any still-unassigned variable,
604    /// possible when it is unconstrained, defaults to false).
605    fn model(&self) -> Vec<bool> {
606        self.assign.iter().map(|a| a.unwrap_or(false)).collect()
607    }
608
609    /// Forbid the current `model`'s projection, then reset to level 0 so the next
610    /// [`Solver::search`] finds a different model. Returns `false` if the
611    /// projection is empty (there is only one model to report).
612    fn block(&mut self, project: &[Var], model: &[bool]) -> bool {
613        if project.is_empty() {
614            return false;
615        }
616        let block: Vec<SatLit> = project
617            .iter()
618            .map(|&v| {
619                if model[v as usize] {
620                    SatLit::negative(v)
621                } else {
622                    SatLit::positive(v)
623                }
624            })
625            .collect();
626        self.backtrack(0);
627        self.add_clause(&block);
628        true
629    }
630}
631
632// --- public API ------------------------------------------------------------
633
634/// The outcome of [`solve_assuming`].
635#[derive(Clone, Debug, PartialEq, Eq)]
636pub enum Solved {
637    /// Satisfiable: a full model (`var -> bool`).
638    Sat(Vec<bool>),
639    /// Unsatisfiable under the assumptions: a *sufficient* subset of them (a core)
640    /// such that `cnf ∧ core` is unsatisfiable. Empty means the formula is
641    /// unsatisfiable regardless of the assumptions. Not guaranteed minimal.
642    Unsat(Vec<SatLit>),
643}
644
645/// Solve `cnf` with every literal in `assumptions` forced true. Returns a model,
646/// or an unsat core — a sufficient (not necessarily minimal) subset of
647/// `assumptions`. Minimize the core separately if you need 1-minimality.
648pub fn solve_assuming(cnf: &Cnf, assumptions: &[SatLit]) -> Solved {
649    let mut s = Solver::new(cnf);
650    s.assumptions = assumptions.to_vec();
651    match s.run() {
652        Ok(()) => Solved::Sat(s.model()),
653        Err(core) => Solved::Unsat(core),
654    }
655}
656
657/// Solve a CNF. Returns a full model (`var -> bool`) or `None` if unsatisfiable.
658pub fn solve(cnf: &Cnf) -> Option<Vec<bool>> {
659    match solve_assuming(cnf, &[]) {
660        Solved::Sat(model) => Some(model),
661        Solved::Unsat(_) => None,
662    }
663}
664
665/// A lazy iterator over the models of a CNF, distinct on the `project` variables.
666/// Solving is **incremental**: each step adds a blocking clause and continues
667/// from the existing solver state instead of restarting from scratch.
668pub struct Models {
669    solver: Solver,
670    project: Vec<Var>,
671    done: bool,
672}
673
674impl Iterator for Models {
675    type Item = Vec<bool>;
676
677    fn next(&mut self) -> Option<Vec<bool>> {
678        if self.done {
679            return None;
680        }
681        if !self.solver.search() {
682            self.done = true;
683            return None;
684        }
685        let model = self.solver.model();
686        if !self.solver.block(&self.project, &model) {
687            self.done = true;
688        }
689        Some(model)
690    }
691}
692
693/// Lazily enumerate all models of `cnf`, distinct over `project`.
694pub fn all_models(cnf: &Cnf, project: Vec<Var>) -> Models {
695    Models {
696        solver: Solver::new(cnf),
697        project,
698        done: false,
699    }
700}
701
702/// Up to `limit` models, distinct over `project` (eagerly collected).
703pub fn models(cnf: &Cnf, project: &[Var], limit: usize) -> Vec<Vec<bool>> {
704    all_models(cnf, project.to_vec()).take(limit).collect()
705}
706
707/// Count distinct models projected onto `project`, up to `limit`.
708pub fn models_upto(cnf: &Cnf, project: &[Var], limit: usize) -> usize {
709    all_models(cnf, project.to_vec()).take(limit).count()
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn trivial_sat() {
718        let mut c = Cnf::new(2);
719        c.add_clause(vec![SatLit::positive(0), SatLit::positive(1)]);
720        assert!(solve(&c).is_some());
721    }
722
723    #[test]
724    fn unit_contradiction_unsat() {
725        let mut c = Cnf::new(1);
726        c.add_clause(vec![SatLit::positive(0)]);
727        c.add_clause(vec![SatLit::negative(0)]);
728        assert!(solve(&c).is_none());
729    }
730
731    #[test]
732    fn all_four_combos_excluded_is_unsat() {
733        let mut c = Cnf::new(2);
734        let (a, b) = (0u32, 1u32);
735        c.add_clause(vec![SatLit::positive(a), SatLit::positive(b)]);
736        c.add_clause(vec![SatLit::negative(a), SatLit::positive(b)]);
737        c.add_clause(vec![SatLit::positive(a), SatLit::negative(b)]);
738        c.add_clause(vec![SatLit::negative(a), SatLit::negative(b)]);
739        assert!(solve(&c).is_none());
740    }
741
742    #[test]
743    fn forced_chain_has_unique_model() {
744        let mut c = Cnf::new(2);
745        c.add_clause(vec![SatLit::negative(0), SatLit::positive(1)]);
746        c.add_clause(vec![SatLit::positive(0)]);
747        let m = solve(&c).unwrap();
748        assert!(m[0] && m[1]);
749        assert_eq!(models_upto(&c, &[0, 1], 5), 1);
750    }
751
752    #[test]
753    fn or_clause_has_three_models() {
754        let mut c = Cnf::new(2);
755        c.add_clause(vec![SatLit::positive(0), SatLit::positive(1)]);
756        assert_eq!(models_upto(&c, &[0, 1], 10), 3);
757    }
758
759    #[test]
760    fn lazy_models_iterator_is_incremental() {
761        // (a∨b) has 3 models; the iterator yields them lazily one at a time.
762        let mut c = Cnf::new(2);
763        c.add_clause(vec![SatLit::positive(0), SatLit::positive(1)]);
764        let first_two: Vec<_> = all_models(&c, vec![0, 1]).take(2).collect();
765        assert_eq!(first_two.len(), 2);
766        assert_ne!(first_two[0], first_two[1]);
767        assert_eq!(all_models(&c, vec![0, 1]).count(), 3);
768    }
769
770    #[test]
771    fn assumption_forces_a_model() {
772        // (a ∨ b); assume ¬a ⇒ b must be true.
773        let mut c = Cnf::new(2);
774        c.add_clause(vec![SatLit::positive(0), SatLit::positive(1)]);
775        match solve_assuming(&c, &[SatLit::negative(0)]) {
776            Solved::Sat(m) => {
777                assert!(!m[0] && m[1]);
778            }
779            Solved::Unsat(_) => panic!("should be SAT under ¬a"),
780        }
781    }
782
783    #[test]
784    fn contradicted_assumptions_yield_a_sufficient_core() {
785        // (¬a ∨ ¬b); assume a and b ⇒ UNSAT, core ⊆ {a, b} and cnf ∧ core UNSAT.
786        let mut c = Cnf::new(2);
787        c.add_clause(vec![SatLit::negative(0), SatLit::negative(1)]);
788        let assumptions = [SatLit::positive(0), SatLit::positive(1)];
789        match solve_assuming(&c, &assumptions) {
790            Solved::Unsat(core) => {
791                assert!(!core.is_empty());
792                assert!(core.iter().all(|l| assumptions.contains(l)));
793                // cnf ∧ core is unsatisfiable.
794                let mut cc = c.clone();
795                for l in &core {
796                    cc.add_clause(vec![*l]);
797                }
798                assert!(solve(&cc).is_none());
799            }
800            Solved::Sat(_) => panic!("a ∧ b violates (¬a ∨ ¬b)"),
801        }
802    }
803
804    #[test]
805    fn satisfiable_assumptions_round_trip() {
806        // Independent vars; assuming a few is fine and the model honors them.
807        let mut c = Cnf::new(3);
808        c.add_clause(vec![
809            SatLit::positive(0),
810            SatLit::positive(1),
811            SatLit::positive(2),
812        ]);
813        let assumptions = [SatLit::positive(0), SatLit::negative(2)];
814        match solve_assuming(&c, &assumptions) {
815            Solved::Sat(m) => {
816                assert!(m[0] && !m[2]);
817            }
818            Solved::Unsat(_) => panic!("should be SAT"),
819        }
820    }
821
822    #[test]
823    fn larger_random_like_sat_is_solved() {
824        let mut c = Cnf::new(5);
825        let l = |v: u32, p: bool| SatLit::new(v, p);
826        c.add_clause(vec![l(0, true), l(1, true), l(2, false)]);
827        c.add_clause(vec![l(0, false), l(2, true), l(3, true)]);
828        c.add_clause(vec![l(1, false), l(3, false), l(4, true)]);
829        c.add_clause(vec![l(2, false), l(4, false)]);
830        c.add_clause(vec![l(0, true), l(4, true)]);
831        let m = solve(&c).expect("sat");
832        for clause in &c.clauses {
833            assert!(
834                clause
835                    .iter()
836                    .any(|&lit| m[lit.var() as usize] != lit.is_negative())
837            );
838        }
839    }
840}