Skip to main content

oxiz_sat/
solver.rs

1//! CDCL SAT Solver
2
3use crate::chb::CHB;
4use crate::chrono::ChronoBacktrack;
5use crate::clause::{ClauseDatabase, ClauseId};
6use crate::literal::{LBool, Lit, Var};
7use crate::lrb::LRB;
8use crate::trail::{Reason, Trail};
9use crate::vsids::VSIDS;
10use crate::watched::{WatchLists, Watcher};
11use smallvec::SmallVec;
12
13/// Binary implication graph for efficient binary clause propagation
14/// For each literal L, stores the list of literals that are implied when L is false
15/// (i.e., for binary clause (~L v M), when L is assigned false, M must be true)
16#[derive(Debug, Clone)]
17struct BinaryImplicationGraph {
18    /// implications[lit] = list of (implied_lit, clause_id) pairs
19    implications: Vec<Vec<(Lit, ClauseId)>>,
20}
21
22impl BinaryImplicationGraph {
23    fn new(num_vars: usize) -> Self {
24        Self {
25            implications: vec![Vec::new(); num_vars * 2],
26        }
27    }
28
29    fn resize(&mut self, num_vars: usize) {
30        self.implications.resize(num_vars * 2, Vec::new());
31    }
32
33    fn add(&mut self, lit: Lit, implied: Lit, clause_id: ClauseId) {
34        self.implications[lit.code() as usize].push((implied, clause_id));
35    }
36
37    fn get(&self, lit: Lit) -> &[(Lit, ClauseId)] {
38        &self.implications[lit.code() as usize]
39    }
40
41    fn clear(&mut self) {
42        for implications in &mut self.implications {
43            implications.clear();
44        }
45    }
46}
47
48/// Result from a theory check
49#[derive(Debug, Clone)]
50pub enum TheoryCheckResult {
51    /// Theory is satisfied under current assignment
52    Sat,
53    /// Theory detected a conflict, returns conflict clause literals
54    Conflict(SmallVec<[Lit; 8]>),
55    /// Theory propagated new literals (lit, reason clause)
56    Propagated(Vec<(Lit, SmallVec<[Lit; 8]>)>),
57}
58
59/// Callback trait for theory solvers
60/// The CDCL(T) solver implements this to receive theory callbacks
61pub trait TheoryCallback {
62    /// Called when a literal is assigned
63    /// Returns a theory check result
64    fn on_assignment(&mut self, lit: Lit) -> TheoryCheckResult;
65
66    /// Called after propagation is complete to do a full theory check
67    fn final_check(&mut self) -> TheoryCheckResult;
68
69    /// Called when the decision level increases
70    fn on_new_level(&mut self, _level: u32) {}
71
72    /// Called when backtracking
73    fn on_backtrack(&mut self, level: u32);
74}
75
76/// Result of SAT solving
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum SolverResult {
79    /// Satisfiable
80    Sat,
81    /// Unsatisfiable
82    Unsat,
83    /// Unknown (e.g., timeout, resource limit)
84    Unknown,
85}
86
87/// Solver configuration
88#[derive(Debug, Clone)]
89pub struct SolverConfig {
90    /// Restart interval (number of conflicts)
91    pub restart_interval: u64,
92    /// Restart multiplier for geometric restarts
93    pub restart_multiplier: f64,
94    /// Clause deletion threshold
95    pub clause_deletion_threshold: usize,
96    /// Variable decay factor
97    pub var_decay: f64,
98    /// Clause decay factor
99    pub clause_decay: f64,
100    /// Random polarity probability (0.0 to 1.0)
101    pub random_polarity_prob: f64,
102    /// Restart strategy: "luby" or "geometric"
103    pub restart_strategy: RestartStrategy,
104    /// Enable lazy hyper-binary resolution
105    pub enable_lazy_hyper_binary: bool,
106    /// Use CHB instead of VSIDS for branching
107    pub use_chb_branching: bool,
108    /// Use LRB (Learning Rate Branching) for branching
109    pub use_lrb_branching: bool,
110    /// Enable inprocessing (periodic preprocessing during search)
111    pub enable_inprocessing: bool,
112    /// Inprocessing interval (number of conflicts between inprocessing)
113    pub inprocessing_interval: u64,
114    /// Enable chronological backtracking
115    pub enable_chronological_backtrack: bool,
116    /// Chronological backtracking threshold (max distance from assertion level)
117    pub chrono_backtrack_threshold: u32,
118}
119
120/// Restart strategy
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum RestartStrategy {
123    /// Luby sequence restarts
124    Luby,
125    /// Geometric restarts
126    Geometric,
127    /// Glucose-style dynamic restarts based on LBD
128    Glucose,
129    /// Local restarts based on LBD trail
130    LocalLbd,
131}
132
133impl Default for SolverConfig {
134    fn default() -> Self {
135        Self {
136            restart_interval: 100,
137            restart_multiplier: 1.5,
138            clause_deletion_threshold: 10000,
139            var_decay: 0.95,
140            clause_decay: 0.999,
141            random_polarity_prob: 0.02,
142            restart_strategy: RestartStrategy::Luby,
143            enable_lazy_hyper_binary: true,
144            use_chb_branching: false,
145            use_lrb_branching: false,
146            enable_inprocessing: false,
147            inprocessing_interval: 5000,
148            enable_chronological_backtrack: true,
149            chrono_backtrack_threshold: 100,
150        }
151    }
152}
153
154/// Statistics for the solver
155#[derive(Debug, Default, Clone)]
156pub struct SolverStats {
157    /// Number of decisions made
158    pub decisions: u64,
159    /// Number of propagations
160    pub propagations: u64,
161    /// Number of conflicts
162    pub conflicts: u64,
163    /// Number of restarts
164    pub restarts: u64,
165    /// Number of learned clauses
166    pub learned_clauses: u64,
167    /// Number of deleted clauses
168    pub deleted_clauses: u64,
169    /// Number of binary clauses learned
170    pub binary_clauses: u64,
171    /// Number of unit clauses learned
172    pub unit_clauses: u64,
173    /// Total LBD of learned clauses
174    pub total_lbd: u64,
175    /// Number of clause minimizations
176    pub minimizations: u64,
177    /// Literals removed by minimization
178    pub literals_removed: u64,
179    /// Number of chronological backtracks
180    pub chrono_backtracks: u64,
181    /// Number of non-chronological backtracks
182    pub non_chrono_backtracks: u64,
183}
184
185impl SolverStats {
186    /// Get average LBD of learned clauses
187    #[must_use]
188    pub fn avg_lbd(&self) -> f64 {
189        if self.learned_clauses == 0 {
190            0.0
191        } else {
192            self.total_lbd as f64 / self.learned_clauses as f64
193        }
194    }
195
196    /// Get average decisions per conflict
197    #[must_use]
198    pub fn avg_decisions_per_conflict(&self) -> f64 {
199        if self.conflicts == 0 {
200            0.0
201        } else {
202            self.decisions as f64 / self.conflicts as f64
203        }
204    }
205
206    /// Get propagations per conflict
207    #[must_use]
208    pub fn propagations_per_conflict(&self) -> f64 {
209        if self.conflicts == 0 {
210            0.0
211        } else {
212            self.propagations as f64 / self.conflicts as f64
213        }
214    }
215
216    /// Get clause deletion ratio
217    #[must_use]
218    pub fn deletion_ratio(&self) -> f64 {
219        if self.learned_clauses == 0 {
220            0.0
221        } else {
222            self.deleted_clauses as f64 / self.learned_clauses as f64
223        }
224    }
225
226    /// Get chronological backtrack ratio
227    #[must_use]
228    pub fn chrono_backtrack_ratio(&self) -> f64 {
229        let total = self.chrono_backtracks + self.non_chrono_backtracks;
230        if total == 0 {
231            0.0
232        } else {
233            self.chrono_backtracks as f64 / total as f64
234        }
235    }
236
237    /// Display formatted statistics
238    pub fn display(&self) {
239        println!("========== Solver Statistics ==========");
240        println!("Decisions:              {:>12}", self.decisions);
241        println!("Propagations:           {:>12}", self.propagations);
242        println!("Conflicts:              {:>12}", self.conflicts);
243        println!("Restarts:               {:>12}", self.restarts);
244        println!("Learned clauses:        {:>12}", self.learned_clauses);
245        println!("  - Unit clauses:       {:>12}", self.unit_clauses);
246        println!("  - Binary clauses:     {:>12}", self.binary_clauses);
247        println!("Deleted clauses:        {:>12}", self.deleted_clauses);
248        println!("Minimizations:          {:>12}", self.minimizations);
249        println!("Literals removed:       {:>12}", self.literals_removed);
250        println!("Chrono backtracks:      {:>12}", self.chrono_backtracks);
251        println!("Non-chrono backtracks:  {:>12}", self.non_chrono_backtracks);
252        println!("---------------------------------------");
253        println!("Avg LBD:                {:>12.2}", self.avg_lbd());
254        println!(
255            "Avg decisions/conflict: {:>12.2}",
256            self.avg_decisions_per_conflict()
257        );
258        println!(
259            "Propagations/conflict:  {:>12.2}",
260            self.propagations_per_conflict()
261        );
262        println!(
263            "Deletion ratio:         {:>12.2}%",
264            self.deletion_ratio() * 100.0
265        );
266        println!(
267            "Chrono backtrack ratio: {:>12.2}%",
268            self.chrono_backtrack_ratio() * 100.0
269        );
270        println!("=======================================");
271    }
272}
273
274/// CDCL SAT Solver
275#[derive(Debug)]
276pub struct Solver {
277    /// Configuration
278    config: SolverConfig,
279    /// Number of variables
280    num_vars: usize,
281    /// Clause database
282    clauses: ClauseDatabase,
283    /// Assignment trail
284    trail: Trail,
285    /// Watch lists
286    watches: WatchLists,
287    /// VSIDS branching heuristic
288    vsids: VSIDS,
289    /// CHB branching heuristic
290    chb: CHB,
291    /// LRB branching heuristic
292    lrb: LRB,
293    /// Statistics
294    stats: SolverStats,
295    /// Learnt clause for conflict analysis
296    learnt: SmallVec<[Lit; 16]>,
297    /// Seen flags for conflict analysis
298    seen: Vec<bool>,
299    /// Analyze stack
300    analyze_stack: Vec<Lit>,
301    /// Current restart threshold
302    restart_threshold: u64,
303    /// Assertions stack for incremental solving (number of original clauses)
304    assertion_levels: Vec<usize>,
305    /// Trail sizes at each assertion level (for proper pop backtracking)
306    assertion_trail_sizes: Vec<usize>,
307    /// Clause IDs added at each assertion level (for proper pop)
308    assertion_clause_ids: Vec<Vec<ClauseId>>,
309    /// Model (if sat)
310    model: Vec<LBool>,
311    /// Whether formula is trivially unsatisfiable
312    trivially_unsat: bool,
313    /// Phase saving: last polarity assigned to each variable
314    phase: Vec<bool>,
315    /// Luby sequence index for restarts
316    luby_index: u64,
317    /// Level marks for LBD computation
318    level_marks: Vec<u32>,
319    /// Current mark counter for LBD computation
320    lbd_mark: u32,
321    /// Learned clause IDs for deletion
322    learned_clause_ids: Vec<ClauseId>,
323    /// Number of conflicts since last clause deletion
324    conflicts_since_deletion: u64,
325    /// PRNG state (xorshift64)
326    rng_state: u64,
327    /// For Glucose-style restarts: average LBD of recent conflicts
328    recent_lbd_sum: u64,
329    /// Number of conflicts contributing to recent_lbd_sum
330    recent_lbd_count: u64,
331    /// Binary implication graph for fast binary clause propagation
332    binary_graph: BinaryImplicationGraph,
333    /// Global average LBD for local restarts
334    global_lbd_sum: u64,
335    /// Number of conflicts contributing to global LBD
336    global_lbd_count: u64,
337    /// Conflicts since last local restart
338    conflicts_since_local_restart: u64,
339    /// Conflicts since last inprocessing
340    conflicts_since_inprocessing: u64,
341    /// Chronological backtracking helper
342    chrono_backtrack: ChronoBacktrack,
343    /// Clause activity bump increment (for MapleSAT-style clause bumping)
344    clause_bump_increment: f64,
345}
346
347impl Default for Solver {
348    fn default() -> Self {
349        Self::new()
350    }
351}
352
353impl Solver {
354    /// Create a new solver
355    #[must_use]
356    pub fn new() -> Self {
357        Self::with_config(SolverConfig::default())
358    }
359
360    /// Create a new solver with configuration
361    #[must_use]
362    pub fn with_config(config: SolverConfig) -> Self {
363        let chrono_enabled = config.enable_chronological_backtrack;
364        let chrono_threshold = config.chrono_backtrack_threshold;
365
366        Self {
367            restart_threshold: config.restart_interval,
368            config,
369            num_vars: 0,
370            clauses: ClauseDatabase::new(),
371            trail: Trail::new(0),
372            watches: WatchLists::new(0),
373            vsids: VSIDS::new(0),
374            chb: CHB::new(0),
375            lrb: LRB::new(0),
376            stats: SolverStats::default(),
377            learnt: SmallVec::new(),
378            seen: Vec::new(),
379            analyze_stack: Vec::new(),
380            assertion_levels: vec![0],
381            assertion_trail_sizes: vec![0],
382            assertion_clause_ids: vec![Vec::new()],
383            model: Vec::new(),
384            trivially_unsat: false,
385            phase: Vec::new(),
386            luby_index: 0,
387            level_marks: Vec::new(),
388            lbd_mark: 0,
389            learned_clause_ids: Vec::new(),
390            conflicts_since_deletion: 0,
391            rng_state: 0x853c_49e6_748f_ea9b, // Random seed
392            recent_lbd_sum: 0,
393            recent_lbd_count: 0,
394            binary_graph: BinaryImplicationGraph::new(0),
395            global_lbd_sum: 0,
396            global_lbd_count: 0,
397            conflicts_since_local_restart: 0,
398            conflicts_since_inprocessing: 0,
399            chrono_backtrack: ChronoBacktrack::new(chrono_enabled, chrono_threshold),
400            clause_bump_increment: 1.0,
401        }
402    }
403
404    /// Create a new variable
405    pub fn new_var(&mut self) -> Var {
406        let var = Var::new(self.num_vars as u32);
407        self.num_vars += 1;
408        self.trail.resize(self.num_vars);
409        self.watches.resize(self.num_vars);
410        self.binary_graph.resize(self.num_vars);
411        self.vsids.insert(var);
412        self.chb.insert(var);
413        self.lrb.resize(self.num_vars);
414        self.seen.resize(self.num_vars, false);
415        self.model.resize(self.num_vars, LBool::Undef);
416        self.phase.resize(self.num_vars, false); // Default phase: negative
417        // Resize level_marks to at least num_vars (enough for decision levels)
418        if self.level_marks.len() < self.num_vars {
419            self.level_marks.resize(self.num_vars, 0);
420        }
421        var
422    }
423
424    /// Ensure we have at least n variables
425    pub fn ensure_vars(&mut self, n: usize) {
426        while self.num_vars < n {
427            self.new_var();
428        }
429    }
430
431    /// Add a clause
432    pub fn add_clause(&mut self, lits: impl IntoIterator<Item = Lit>) -> bool {
433        let mut clause_lits: SmallVec<[Lit; 8]> = lits.into_iter().collect();
434
435        // Ensure we have all variables
436        for lit in &clause_lits {
437            let var_idx = lit.var().index();
438            if var_idx >= self.num_vars {
439                self.ensure_vars(var_idx + 1);
440            }
441        }
442
443        // Remove duplicates and check for tautology
444        clause_lits.sort_by_key(|l| l.code());
445        clause_lits.dedup();
446
447        // Check for tautology (x and ~x in same clause)
448        for i in 0..clause_lits.len() {
449            for j in (i + 1)..clause_lits.len() {
450                if clause_lits[i] == clause_lits[j].negate() {
451                    return true; // Tautology - always satisfied
452                }
453            }
454        }
455
456        // Handle special cases
457        match clause_lits.len() {
458            0 => {
459                self.trivially_unsat = true;
460                return false; // Empty clause - unsat
461            }
462            1 => {
463                // Unit clause - enqueue at decision level 0
464                // Unit clauses must be assigned at level 0 to survive backtracking.
465                // After solve(), current_level may be > 0, so we must backtrack first.
466                let lit = clause_lits[0];
467
468                if self.trail.lit_value(lit).is_false() {
469                    // The literal conflicts with the current trail.
470                    // Check if the conflict is at decision level 0 (permanent constraint)
471                    // or from a previous solve (can be retried after backtrack).
472                    let var = lit.var();
473                    let level = self.trail.level(var);
474                    if level == 0 {
475                        // Conflict with a level-0 assignment - truly UNSAT
476                        self.trivially_unsat = true;
477                        return false;
478                    } else {
479                        // Conflict with higher-level assignment from previous solve.
480                        // Backtrack to root and assign the new unit literal at level 0.
481                        self.backtrack_to_root();
482                        self.trail.assign_decision(lit);
483                        return true;
484                    }
485                }
486
487                if self.trail.lit_value(lit).is_true() {
488                    // Already satisfied - check if at level 0
489                    let var = lit.var();
490                    let level = self.trail.level(var);
491                    if level == 0 {
492                        // Already assigned at level 0, nothing to do
493                        return true;
494                    }
495                    // Assigned at higher level - backtrack and reassign at level 0
496                    self.backtrack_to_root();
497                    self.trail.assign_decision(lit);
498                    return true;
499                }
500
501                // Variable is unassigned - backtrack to level 0 first to ensure
502                // the assignment is at level 0 (survives future backtracks)
503                if self.trail.decision_level() > 0 {
504                    self.backtrack_to_root();
505                }
506                self.trail.assign_decision(lit);
507                return true;
508            }
509            2 => {
510                // Binary clause - check if it conflicts with current assignment
511                let lit0 = clause_lits[0];
512                let lit1 = clause_lits[1];
513                let val0 = self.trail.lit_value(lit0);
514                let val1 = self.trail.lit_value(lit1);
515
516                // If clause is satisfied, just add it
517                if val0.is_true() || val1.is_true() {
518                    // Clause already satisfied by current assignment
519                    let clause_id = self.clauses.add_original(clause_lits.iter().copied());
520                    if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
521                        current_level_clauses.push(clause_id);
522                    }
523                    self.binary_graph.add(lit0.negate(), lit1, clause_id);
524                    self.binary_graph.add(lit1.negate(), lit0, clause_id);
525                    self.watches
526                        .add(lit0.negate(), Watcher::new(clause_id, lit1));
527                    self.watches
528                        .add(lit1.negate(), Watcher::new(clause_id, lit0));
529                    return true;
530                }
531
532                // If both literals are false, we have a conflict
533                if val0.is_false() && val1.is_false() {
534                    // Check if both are at level 0
535                    let level0 = self.trail.level(lit0.var());
536                    let level1 = self.trail.level(lit1.var());
537
538                    if level0 == 0 && level1 == 0 {
539                        // Conflict at level 0 - UNSAT
540                        self.trivially_unsat = true;
541                        return false;
542                    }
543
544                    // Backtrack to level 0 and add clause
545                    // The clause will be propagated on next solve()
546                    self.backtrack_to_root();
547                }
548
549                // If one literal is false and one undefined, propagate
550                // after adding the clause (via next solve())
551
552                let clause_id = self.clauses.add_original(clause_lits.iter().copied());
553                if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
554                    current_level_clauses.push(clause_id);
555                }
556                self.binary_graph.add(lit0.negate(), lit1, clause_id);
557                self.binary_graph.add(lit1.negate(), lit0, clause_id);
558                self.watches
559                    .add(lit0.negate(), Watcher::new(clause_id, lit1));
560                self.watches
561                    .add(lit1.negate(), Watcher::new(clause_id, lit0));
562                return true;
563            }
564            _ => {}
565        }
566
567        // Add clause (3+ literals)
568        // Check if clause is satisfied or conflicts with current assignment
569        let num_false = clause_lits
570            .iter()
571            .filter(|&l| self.trail.lit_value(*l).is_false())
572            .count();
573        let has_true = clause_lits
574            .iter()
575            .any(|l| self.trail.lit_value(*l).is_true());
576
577        if !has_true && num_false == clause_lits.len() {
578            // All literals are false - conflict
579            // Check if all at level 0
580            let all_at_zero = clause_lits.iter().all(|l| self.trail.level(l.var()) == 0);
581            if all_at_zero {
582                self.trivially_unsat = true;
583                return false;
584            }
585            // Backtrack to level 0
586            self.backtrack_to_root();
587        }
588
589        let clause_id = self.clauses.add_original(clause_lits.iter().copied());
590
591        // Track clause for incremental solving
592        if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
593            current_level_clauses.push(clause_id);
594        }
595
596        // Set up watches - prefer non-false literals for watching
597        let lit0 = clause_lits[0];
598        let lit1 = clause_lits[1];
599
600        self.watches
601            .add(lit0.negate(), Watcher::new(clause_id, lit1));
602        self.watches
603            .add(lit1.negate(), Watcher::new(clause_id, lit0));
604
605        true
606    }
607
608    /// Add a clause from DIMACS literals
609    pub fn add_clause_dimacs(&mut self, lits: &[i32]) -> bool {
610        self.add_clause(lits.iter().map(|&l| Lit::from_dimacs(l)))
611    }
612
613    /// Solve the SAT problem
614    pub fn solve(&mut self) -> SolverResult {
615        // Check if trivially unsatisfiable
616        if self.trivially_unsat {
617            return SolverResult::Unsat;
618        }
619
620        // Initial propagation
621        if self.propagate().is_some() {
622            return SolverResult::Unsat;
623        }
624
625        loop {
626            // Propagate
627            if let Some(conflict) = self.propagate() {
628                self.stats.conflicts += 1;
629                self.conflicts_since_inprocessing += 1;
630
631                if self.trail.decision_level() == 0 {
632                    return SolverResult::Unsat;
633                }
634
635                // Analyze conflict
636                let (backtrack_level, learnt_clause) = self.analyze(conflict);
637
638                // Backtrack with phase saving
639                self.backtrack_with_phase_saving(backtrack_level);
640
641                // Learn clause
642                if learnt_clause.len() == 1 {
643                    // Store unit learned clause in database for persistence
644                    let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
645                    self.stats.learned_clauses += 1;
646                    self.stats.unit_clauses += 1;
647                    self.learned_clause_ids.push(clause_id);
648
649                    // Track for incremental solving
650                    if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
651                        current_level_clauses.push(clause_id);
652                    }
653
654                    self.trail.assign_decision(learnt_clause[0]);
655                } else {
656                    // Compute LBD for the learned clause
657                    let lbd = self.compute_lbd(&learnt_clause);
658
659                    // Track recent LBD for Glucose-style and local restarts
660                    self.recent_lbd_sum += u64::from(lbd);
661                    self.recent_lbd_count += 1;
662                    self.global_lbd_sum += u64::from(lbd);
663                    self.global_lbd_count += 1;
664
665                    // Reset recent LBD tracking periodically
666                    if self.recent_lbd_count >= 5000 {
667                        self.recent_lbd_sum /= 2;
668                        self.recent_lbd_count /= 2;
669                    }
670
671                    let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
672                    self.stats.learned_clauses += 1;
673
674                    // Set LBD score for the clause
675                    if let Some(clause) = self.clauses.get_mut(clause_id) {
676                        clause.lbd = lbd;
677                    }
678
679                    // Track learned clause for potential deletion
680                    self.learned_clause_ids.push(clause_id);
681
682                    // Track clause for incremental solving
683                    if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
684                        current_level_clauses.push(clause_id);
685                    }
686
687                    // Watch first two literals
688                    let lit0 = learnt_clause[0];
689                    let lit1 = learnt_clause[1];
690                    self.watches
691                        .add(lit0.negate(), Watcher::new(clause_id, lit1));
692                    self.watches
693                        .add(lit1.negate(), Watcher::new(clause_id, lit0));
694
695                    // Propagate the asserting literal
696                    self.trail.assign_propagation(learnt_clause[0], clause_id);
697                }
698
699                // Decay activities
700                self.vsids.decay();
701                self.chb.decay();
702                self.lrb.decay();
703                self.lrb.on_conflict();
704                self.clauses.decay_activity(self.config.clause_decay);
705                // Increase clause bump increment (inverse of decay)
706                self.clause_bump_increment /= self.config.clause_decay;
707
708                // Track conflicts for clause deletion
709                self.conflicts_since_deletion += 1;
710
711                // Periodic clause database reduction
712                if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
713                    self.reduce_clause_database();
714                    self.conflicts_since_deletion = 0;
715
716                    // Vivification after clause database reduction (at level 0 after restart)
717                    if self.stats.restarts.is_multiple_of(10) {
718                        let saved_level = self.trail.decision_level();
719                        if saved_level == 0 {
720                            self.vivify_clauses();
721                        }
722                    }
723                }
724
725                // Check for restart
726                if self.stats.conflicts >= self.restart_threshold {
727                    self.restart();
728                }
729
730                // Periodic inprocessing
731                if self.config.enable_inprocessing
732                    && self.conflicts_since_inprocessing >= self.config.inprocessing_interval
733                {
734                    self.inprocess();
735                    self.conflicts_since_inprocessing = 0;
736                }
737            } else {
738                // No conflict - try to decide
739                if let Some(var) = self.pick_branch_var() {
740                    self.stats.decisions += 1;
741                    self.trail.new_decision_level();
742
743                    // Use phase saving with random polarity
744                    let polarity = if self.rand_bool(self.config.random_polarity_prob) {
745                        // Random polarity
746                        self.rand_bool(0.5)
747                    } else {
748                        // Saved phase
749                        self.phase[var.index()]
750                    };
751                    let lit = if polarity {
752                        Lit::pos(var)
753                    } else {
754                        Lit::neg(var)
755                    };
756                    self.trail.assign_decision(lit);
757                } else {
758                    // All variables assigned - SAT
759                    self.save_model();
760                    return SolverResult::Sat;
761                }
762            }
763        }
764    }
765
766    /// Solve with assumptions and return unsat core if UNSAT
767    ///
768    /// This is the key method for MaxSAT: it solves under assumptions and
769    /// if the result is UNSAT, returns the subset of assumptions in the core.
770    ///
771    /// # Arguments
772    /// * `assumptions` - Literals that must be true
773    ///
774    /// # Returns
775    /// * `(SolverResult, Option<Vec<Lit>>)` - Result and unsat core (if UNSAT)
776    pub fn solve_with_assumptions(
777        &mut self,
778        assumptions: &[Lit],
779    ) -> (SolverResult, Option<Vec<Lit>>) {
780        if self.trivially_unsat {
781            return (SolverResult::Unsat, Some(Vec::new()));
782        }
783
784        // Ensure all assumption variables exist
785        for &lit in assumptions {
786            while self.num_vars <= lit.var().index() {
787                self.new_var();
788            }
789        }
790
791        // Initial propagation at level 0
792        if self.propagate().is_some() {
793            return (SolverResult::Unsat, Some(Vec::new()));
794        }
795
796        // Create a new decision level for assumptions
797        let assumption_level_start = self.trail.decision_level();
798
799        // Assign assumptions as decisions
800        for (i, &lit) in assumptions.iter().enumerate() {
801            // Check if already assigned
802            let value = self.trail.lit_value(lit);
803            if value.is_true() {
804                continue; // Already satisfied
805            }
806            if value.is_false() {
807                // Conflict with assumption - extract core from conflicting assumptions
808                let core = self.extract_assumption_core(assumptions, i);
809                self.backtrack(assumption_level_start);
810                return (SolverResult::Unsat, Some(core));
811            }
812
813            // Make decision for assumption
814            self.trail.new_decision_level();
815            self.trail.assign_decision(lit);
816
817            // Propagate after each assumption
818            if let Some(_conflict) = self.propagate() {
819                // Conflict during assumption propagation
820                let core = self.analyze_assumption_conflict(assumptions);
821                self.backtrack(assumption_level_start);
822                return (SolverResult::Unsat, Some(core));
823            }
824        }
825
826        // Now solve normally
827        loop {
828            if let Some(conflict) = self.propagate() {
829                self.stats.conflicts += 1;
830
831                // Check if conflict involves assumptions
832                let backtrack_level = self.analyze_conflict_level(conflict);
833
834                if backtrack_level <= assumption_level_start {
835                    // Conflict forces backtracking past assumptions - UNSAT
836                    let core = self.analyze_assumption_conflict(assumptions);
837                    self.backtrack(assumption_level_start);
838                    return (SolverResult::Unsat, Some(core));
839                }
840
841                let (bt_level, learnt_clause) = self.analyze(conflict);
842                self.backtrack_with_phase_saving(bt_level.max(assumption_level_start + 1));
843                self.learn_clause(learnt_clause);
844
845                self.vsids.decay();
846                self.clauses.decay_activity(self.config.clause_decay);
847                self.handle_clause_deletion_and_restart_limited(assumption_level_start);
848            } else {
849                // No conflict - try to decide
850                if let Some(var) = self.pick_branch_var() {
851                    self.stats.decisions += 1;
852                    self.trail.new_decision_level();
853
854                    let polarity = if self.rand_bool(self.config.random_polarity_prob) {
855                        self.rand_bool(0.5)
856                    } else {
857                        self.phase.get(var.index()).copied().unwrap_or(false)
858                    };
859                    let lit = if polarity {
860                        Lit::pos(var)
861                    } else {
862                        Lit::neg(var)
863                    };
864                    self.trail.assign_decision(lit);
865                } else {
866                    // All variables assigned - SAT
867                    self.save_model();
868                    self.backtrack(assumption_level_start);
869                    return (SolverResult::Sat, None);
870                }
871            }
872        }
873    }
874
875    /// Extract a core of assumptions that caused a conflict
876    fn extract_assumption_core(&self, assumptions: &[Lit], conflict_idx: usize) -> Vec<Lit> {
877        // The conflicting assumption and any assumptions it depends on
878        let mut core = Vec::new();
879        let conflict_lit = assumptions[conflict_idx];
880
881        // Find assumptions that led to this conflict
882        for &lit in &assumptions[..=conflict_idx] {
883            if self.seen.get(lit.var().index()).copied().unwrap_or(false) || lit == conflict_lit {
884                core.push(lit);
885            }
886        }
887
888        // If core is empty, just return the conflicting assumption
889        if core.is_empty() {
890            core.push(conflict_lit);
891        }
892
893        core
894    }
895
896    /// Analyze conflict to find assumptions in the unsat core
897    fn analyze_assumption_conflict(&mut self, assumptions: &[Lit]) -> Vec<Lit> {
898        // Use seen flags to mark which assumptions are in the conflict
899        let mut core = Vec::new();
900
901        // Walk back through the trail to find conflicting assumptions
902        for &lit in assumptions {
903            let var = lit.var();
904            if var.index() < self.trail.assignments().len() {
905                let value = self.trail.lit_value(lit);
906                // If the negation of an assumption is implied, it's in the core
907                if value.is_false() || self.seen.get(var.index()).copied().unwrap_or(false) {
908                    core.push(lit);
909                }
910            }
911        }
912
913        // If no specific core found, return all assumptions
914        if core.is_empty() {
915            core.extend(assumptions.iter().copied());
916        }
917
918        core
919    }
920
921    /// Get the minimum backtrack level for a conflict
922    fn analyze_conflict_level(&self, conflict: ClauseId) -> u32 {
923        let clause = match self.clauses.get(conflict) {
924            Some(c) => c,
925            None => return 0,
926        };
927
928        let mut min_level = u32::MAX;
929        for lit in clause.lits.iter().copied() {
930            let level = self.trail.level(lit.var());
931            if level > 0 && level < min_level {
932                min_level = level;
933            }
934        }
935
936        if min_level == u32::MAX { 0 } else { min_level }
937    }
938
939    /// Handle clause deletion and restart, but don't backtrack past assumptions
940    fn handle_clause_deletion_and_restart_limited(&mut self, min_level: u32) {
941        self.conflicts_since_deletion += 1;
942
943        if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
944            self.reduce_clause_database();
945            self.conflicts_since_deletion = 0;
946        }
947
948        if self.stats.conflicts >= self.restart_threshold {
949            // Limited restart - don't backtrack past assumptions
950            self.backtrack(min_level);
951            self.stats.restarts += 1;
952            self.luby_index += 1;
953            self.restart_threshold =
954                self.stats.conflicts + self.config.restart_interval * Self::luby(self.luby_index);
955        }
956    }
957
958    /// Unit propagation using two-watched literals
959    fn propagate(&mut self) -> Option<ClauseId> {
960        while let Some(lit) = self.trail.next_to_propagate() {
961            self.stats.propagations += 1;
962
963            // First, propagate binary implications (faster)
964            let binary_implications = self.binary_graph.get(lit).to_vec();
965            for &(implied_lit, clause_id) in &binary_implications {
966                let value = self.trail.lit_value(implied_lit);
967                if value.is_false() {
968                    // Conflict in binary clause
969                    return Some(clause_id);
970                } else if !value.is_defined() {
971                    // Propagate
972                    self.trail.assign_propagation(implied_lit, clause_id);
973
974                    // Lazy hyper-binary resolution: check if we can learn a binary clause
975                    if self.config.enable_lazy_hyper_binary {
976                        self.check_hyper_binary_resolution(lit, implied_lit, clause_id);
977                    }
978                }
979            }
980
981            // Get watches for the negation of the propagated literal
982            let watches = std::mem::take(self.watches.get_mut(lit));
983
984            let mut i = 0;
985            while i < watches.len() {
986                let watcher = watches[i];
987
988                // Check blocker
989                if self.trail.lit_value(watcher.blocker).is_true() {
990                    i += 1;
991                    continue;
992                }
993
994                let clause = match self.clauses.get_mut(watcher.clause) {
995                    Some(c) if !c.deleted => c,
996                    _ => {
997                        i += 1;
998                        continue;
999                    }
1000                };
1001
1002                // Make sure the false literal is at position 1
1003                if clause.lits[0] == lit.negate() {
1004                    clause.swap(0, 1);
1005                }
1006
1007                // If first watch is true, clause is satisfied
1008                let first = clause.lits[0];
1009                if self.trail.lit_value(first).is_true() {
1010                    // Update blocker
1011                    self.watches
1012                        .get_mut(lit)
1013                        .push(Watcher::new(watcher.clause, first));
1014                    i += 1;
1015                    continue;
1016                }
1017
1018                // Look for a new watch
1019                let mut found = false;
1020                for j in 2..clause.lits.len() {
1021                    let l = clause.lits[j];
1022                    if !self.trail.lit_value(l).is_false() {
1023                        clause.swap(1, j);
1024                        self.watches
1025                            .add(clause.lits[1].negate(), Watcher::new(watcher.clause, first));
1026                        found = true;
1027                        break;
1028                    }
1029                }
1030
1031                if found {
1032                    i += 1;
1033                    continue;
1034                }
1035
1036                // No new watch found - clause is unit or conflicting
1037                self.watches
1038                    .get_mut(lit)
1039                    .push(Watcher::new(watcher.clause, first));
1040
1041                if self.trail.lit_value(first).is_false() {
1042                    // Conflict
1043                    // Put remaining watches back
1044                    for j in (i + 1)..watches.len() {
1045                        self.watches.get_mut(lit).push(watches[j]);
1046                    }
1047                    return Some(watcher.clause);
1048                } else {
1049                    // Unit propagation
1050                    self.trail.assign_propagation(first, watcher.clause);
1051
1052                    // Lazy hyper-binary resolution
1053                    if self.config.enable_lazy_hyper_binary {
1054                        self.check_hyper_binary_resolution(lit, first, watcher.clause);
1055                    }
1056                }
1057
1058                i += 1;
1059            }
1060        }
1061
1062        None
1063    }
1064
1065    /// Analyze conflict and learn clause
1066    fn analyze(&mut self, conflict: ClauseId) -> (u32, SmallVec<[Lit; 16]>) {
1067        // Debug: print conflict info
1068        if cfg!(debug_assertions) && self.num_vars <= 5 {
1069            eprintln!("[ANALYZE] Conflict clause id={:?}", conflict);
1070            if let Some(c) = self.clauses.get(conflict) {
1071                let lits_str: Vec<String> = c
1072                    .lits
1073                    .iter()
1074                    .map(|lit| {
1075                        let val = self.trail.lit_value(*lit);
1076                        let level = self.trail.level(lit.var());
1077                        let sign = if lit.is_pos() { "" } else { "~" };
1078                        format!("{}v{}@{}={:?}", sign, lit.var().index(), level, val)
1079                    })
1080                    .collect();
1081                eprintln!("[ANALYZE] Conflict clause: ({})", lits_str.join(" | "));
1082            }
1083            eprintln!("[ANALYZE] Trail:");
1084            for &lit in self.trail.assignments() {
1085                let var = lit.var();
1086                let level = self.trail.level(var);
1087                let reason = self.trail.reason(var);
1088                let sign = if lit.is_pos() { "" } else { "~" };
1089                eprintln!("  {}v{}@{} reason={:?}", sign, var.index(), level, reason);
1090            }
1091        }
1092
1093        self.learnt.clear();
1094        self.learnt.push(Lit::from_code(0)); // Placeholder for asserting literal
1095
1096        let mut counter = 0;
1097        let mut p = None;
1098        let mut index = self.trail.assignments().len();
1099        let current_level = self.trail.decision_level();
1100
1101        // Reset seen flags
1102        for s in &mut self.seen {
1103            *s = false;
1104        }
1105
1106        let mut reason_clause = conflict;
1107
1108        loop {
1109            // Process reason clause (must exist, as it's either conflict or a propagation reason)
1110            let Some(clause) = self.clauses.get(reason_clause) else {
1111                break; // Should not happen in valid state
1112            };
1113            let start = if p.is_some() { 1 } else { 0 };
1114            let is_learned = clause.learned;
1115
1116            // Record clause usage for tier promotion and bump activity (if it's a learned clause)
1117            if is_learned && let Some(clause_mut) = self.clauses.get_mut(reason_clause) {
1118                clause_mut.record_usage();
1119                // Promote to Core if LBD ≤ 2 (GLUE clause)
1120                if clause_mut.lbd <= 2 {
1121                    clause_mut.promote_to_core();
1122                }
1123                // Bump clause activity (MapleSAT-style)
1124                clause_mut.activity += self.clause_bump_increment;
1125            }
1126
1127            let Some(clause) = self.clauses.get(reason_clause) else {
1128                break;
1129            };
1130            for &lit in &clause.lits[start..] {
1131                let var = lit.var();
1132                let level = self.trail.level(var);
1133
1134                if !self.seen[var.index()] && level > 0 {
1135                    self.seen[var.index()] = true;
1136                    self.vsids.bump(var);
1137                    self.chb.bump(var);
1138                    self.lrb.on_reason(var);
1139
1140                    if level == current_level {
1141                        counter += 1;
1142                    } else {
1143                        // Add the literal itself (not negated) to the learned clause.
1144                        // The conflict clause has all literals FALSE. To prevent this
1145                        // conflict, we need at least one of these literals to become TRUE.
1146                        self.learnt.push(lit);
1147                    }
1148                }
1149            }
1150
1151            // Find next literal to analyze
1152            let mut current_lit;
1153            loop {
1154                index -= 1;
1155                current_lit = self.trail.assignments()[index];
1156                p = Some(current_lit);
1157                if self.seen[current_lit.var().index()] {
1158                    break;
1159                }
1160            }
1161
1162            counter -= 1;
1163            if counter == 0 {
1164                break;
1165            }
1166
1167            let var = current_lit.var();
1168            match self.trail.reason(var) {
1169                Reason::Propagation(c) => reason_clause = c,
1170                _ => break,
1171            }
1172        }
1173
1174        // Set asserting literal (p is guaranteed to be Some at this point)
1175        if let Some(lit) = p {
1176            self.learnt[0] = lit.negate();
1177        }
1178
1179        // Minimize learnt clause using recursive resolution
1180        self.minimize_learnt_clause();
1181
1182        // Calculate assertion level (traditional backtrack level)
1183        let assertion_level = if self.learnt.len() == 1 {
1184            0
1185        } else {
1186            // Find second highest level
1187            let mut max_level = 0;
1188            let mut max_idx = 1;
1189            for (i, &lit) in self.learnt.iter().enumerate().skip(1) {
1190                let level = self.trail.level(lit.var());
1191                if level > max_level {
1192                    max_level = level;
1193                    max_idx = i;
1194                }
1195            }
1196            // Move second watch to position 1
1197            self.learnt.swap(1, max_idx);
1198            max_level
1199        };
1200
1201        // Apply chronological backtracking if enabled
1202        let backtrack_level = self.chrono_backtrack.compute_backtrack_level(
1203            &self.trail,
1204            &self.learnt,
1205            assertion_level,
1206        );
1207
1208        // Track chronological vs non-chronological backtracks
1209        if backtrack_level != assertion_level {
1210            self.stats.chrono_backtracks += 1;
1211        } else {
1212            self.stats.non_chrono_backtracks += 1;
1213        }
1214
1215        // Debug: print learned clause
1216        if cfg!(debug_assertions) && self.num_vars <= 5 {
1217            let lits_str: Vec<String> = self
1218                .learnt
1219                .iter()
1220                .map(|lit| {
1221                    let sign = if lit.is_pos() { "" } else { "~" };
1222                    format!("{}v{}", sign, lit.var().index())
1223                })
1224                .collect();
1225            eprintln!(
1226                "[ANALYZE] Learned clause: ({}), backtrack_level={}",
1227                lits_str.join(" | "),
1228                backtrack_level
1229            );
1230        }
1231
1232        (backtrack_level, self.learnt.clone())
1233    }
1234
1235    /// Pick next variable to branch on
1236    fn pick_branch_var(&mut self) -> Option<Var> {
1237        if self.config.use_lrb_branching {
1238            // Use LRB branching
1239            while let Some(var) = self.lrb.select() {
1240                if !self.trail.is_assigned(var) {
1241                    self.lrb.on_assign(var);
1242                    return Some(var);
1243                }
1244            }
1245        } else if self.config.use_chb_branching {
1246            // Use CHB branching
1247            // Rebuild heap periodically to reflect score changes
1248            if self.stats.decisions.is_multiple_of(100) {
1249                self.chb.rebuild_heap();
1250            }
1251
1252            while let Some(var) = self.chb.pop_max() {
1253                if !self.trail.is_assigned(var) {
1254                    return Some(var);
1255                }
1256            }
1257        } else {
1258            // Use VSIDS branching
1259            while let Some(var) = self.vsids.pop_max() {
1260                if !self.trail.is_assigned(var) {
1261                    return Some(var);
1262                }
1263            }
1264        }
1265        None
1266    }
1267
1268    /// Minimize the learned clause by removing redundant literals
1269    ///
1270    /// A literal can be removed if it is implied by the remaining literals.
1271    /// We use a recursive check: a literal l is redundant if its reason clause
1272    /// contains only literals that are either:
1273    /// - Already in the learnt clause (marked as seen)
1274    /// - At decision level 0 (always true in the learned clause context)
1275    /// - Themselves redundant (recursive check)
1276    ///
1277    /// This also performs clause strengthening by checking for stronger implications
1278    fn minimize_learnt_clause(&mut self) {
1279        if self.learnt.len() <= 2 {
1280            // Don't minimize very small clauses
1281            return;
1282        }
1283
1284        let original_len = self.learnt.len();
1285
1286        // Mark all literals in the learned clause as "in clause"
1287        // We use analyze_stack to track literals to check
1288        self.analyze_stack.clear();
1289
1290        // Phase 1: Basic minimization - remove redundant literals
1291        let mut j = 1; // Write position
1292        for i in 1..self.learnt.len() {
1293            let lit = self.learnt[i];
1294            if self.lit_is_redundant(lit) {
1295                // Skip this literal (it's redundant)
1296            } else {
1297                // Keep this literal
1298                self.learnt[j] = lit;
1299                j += 1;
1300            }
1301        }
1302        self.learnt.truncate(j);
1303
1304        // Phase 2: Clause strengthening - check for self-subsuming resolution
1305        // If the clause contains both l and ~l' where l' is in a reason clause,
1306        // we might be able to strengthen the clause
1307        self.strengthen_learnt_clause();
1308
1309        // Track minimization statistics
1310        let final_len = self.learnt.len();
1311        if final_len < original_len {
1312            self.stats.minimizations += 1;
1313            self.stats.literals_removed += (original_len - final_len) as u64;
1314        }
1315    }
1316
1317    /// Strengthen the learned clause using on-the-fly self-subsuming resolution
1318    fn strengthen_learnt_clause(&mut self) {
1319        if self.learnt.len() <= 2 {
1320            return;
1321        }
1322
1323        // Check each literal to see if we can strengthen by resolution
1324        let mut j = 1;
1325        for i in 1..self.learnt.len() {
1326            let lit = self.learnt[i];
1327            let var = lit.var();
1328
1329            // Check if this literal can be strengthened
1330            if let Reason::Propagation(reason_id) = self.trail.reason(var)
1331                && let Some(reason_clause) = self.clauses.get(reason_id)
1332                && reason_clause.lits.len() == 2
1333            {
1334                // Binary reason: one literal is lit, the other is the implied literal
1335                let other_lit = if reason_clause.lits[0] == lit.negate() {
1336                    reason_clause.lits[1]
1337                } else if reason_clause.lits[1] == lit.negate() {
1338                    reason_clause.lits[0]
1339                } else {
1340                    // Keep the literal
1341                    self.learnt[j] = lit;
1342                    j += 1;
1343                    continue;
1344                };
1345
1346                // If other_lit is already in the learned clause at level 0,
1347                // we can remove lit
1348                if self.trail.level(other_lit.var()) == 0 && self.seen[other_lit.var().index()] {
1349                    // Skip this literal (strengthened)
1350                    continue;
1351                }
1352            }
1353
1354            // Keep this literal
1355            self.learnt[j] = lit;
1356            j += 1;
1357        }
1358        self.learnt.truncate(j);
1359    }
1360
1361    /// Check if a literal is redundant in the learned clause
1362    ///
1363    /// A literal is redundant if its reason clause only contains:
1364    /// - Literals marked as seen (in the learned clause)
1365    /// - Literals at decision level 0
1366    /// - Literals that are themselves redundant (recursive)
1367    fn lit_is_redundant(&mut self, lit: Lit) -> bool {
1368        let var = lit.var();
1369
1370        // Decision variables and theory propagations are not redundant
1371        let reason = match self.trail.reason(var) {
1372            Reason::Decision => return false,
1373            Reason::Theory => return false, // Theory propagations can't be minimized
1374            Reason::Propagation(c) => c,
1375        };
1376
1377        let reason_clause = match self.clauses.get(reason) {
1378            Some(c) => c,
1379            None => return false,
1380        };
1381
1382        // Check all literals in the reason clause
1383        for &reason_lit in &reason_clause.lits {
1384            if reason_lit == lit.negate() {
1385                // Skip the literal we're analyzing
1386                continue;
1387            }
1388
1389            let reason_var = reason_lit.var();
1390
1391            // Level 0 literals are always OK
1392            if self.trail.level(reason_var) == 0 {
1393                continue;
1394            }
1395
1396            // If the literal is in the learned clause (seen), it's OK
1397            if self.seen[reason_var.index()] {
1398                continue;
1399            }
1400
1401            // Otherwise, this literal prevents minimization
1402            // (A full recursive check would be more powerful but more expensive)
1403            return false;
1404        }
1405
1406        true
1407    }
1408
1409    /// Backtrack with phase saving
1410    fn backtrack_with_phase_saving(&mut self, level: u32) {
1411        // Collect variables that will be unassigned
1412        let mut unassigned_vars = Vec::new();
1413
1414        // Save phases before backtracking
1415        let phase = &mut self.phase;
1416        let lrb = &mut self.lrb;
1417        self.trail.backtrack_to_with_callback(level, |lit| {
1418            let var = lit.var();
1419            if var.index() < phase.len() {
1420                phase[var.index()] = lit.is_pos();
1421            }
1422            // Re-insert variable into LRB heap
1423            lrb.unassign(var);
1424            unassigned_vars.push(var);
1425        });
1426
1427        // Re-insert unassigned variables into VSIDS and CHB heaps
1428        for var in unassigned_vars {
1429            if !self.vsids.contains(var) {
1430                self.vsids.insert(var);
1431            }
1432            if !self.chb.contains(var) {
1433                self.chb.insert(var);
1434            }
1435        }
1436    }
1437
1438    /// Backtrack to a given level without saving phases
1439    fn backtrack(&mut self, level: u32) {
1440        self.trail.backtrack_to(level);
1441    }
1442
1443    /// Compute the Luby sequence value for index i (1-indexed: luby(1)=1, luby(2)=1, ...)
1444    /// Sequence: 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, ...
1445    /// For 0-indexed input, we add 1 internally.
1446    fn luby(i: u64) -> u64 {
1447        let i = i + 1; // Convert to 1-indexed
1448
1449        // Find k such that 2^k - 1 >= i
1450        let mut k = 1u32;
1451        while (1u64 << k) - 1 < i {
1452            k += 1;
1453        }
1454
1455        let seq_len = (1u64 << k) - 1;
1456
1457        if i == seq_len {
1458            // i is exactly 2^k - 1, return 2^(k-1)
1459            1u64 << (k - 1)
1460        } else {
1461            // Recurse: luby(i) = luby(i - (2^(k-1) - 1))
1462            // The sequence up to 2^k - 1 is: luby(1..2^(k-1)-1), luby(1..2^(k-1)-1), 2^(k-1)
1463            let half_len = (1u64 << (k - 1)) - 1;
1464            if i <= half_len {
1465                Self::luby(i - 1) // Already 0-indexed internally
1466            } else if i <= 2 * half_len {
1467                Self::luby(i - half_len - 1)
1468            } else {
1469                1u64 << (k - 1)
1470            }
1471        }
1472    }
1473
1474    /// Restart
1475    fn restart(&mut self) {
1476        self.stats.restarts += 1;
1477        self.backtrack_with_phase_saving(0);
1478
1479        // Calculate next restart threshold based on strategy
1480        match self.config.restart_strategy {
1481            RestartStrategy::Luby => {
1482                self.luby_index += 1;
1483                self.restart_threshold = self.stats.conflicts
1484                    + Self::luby(self.luby_index) * self.config.restart_interval;
1485            }
1486            RestartStrategy::Geometric => {
1487                let current_interval = if self.restart_threshold > self.stats.conflicts {
1488                    self.restart_threshold - self.stats.conflicts
1489                } else {
1490                    self.config.restart_interval
1491                };
1492                let next_interval =
1493                    (current_interval as f64 * self.config.restart_multiplier) as u64;
1494                self.restart_threshold = self.stats.conflicts + next_interval;
1495            }
1496            RestartStrategy::Glucose => {
1497                // Glucose-style dynamic restarts based on LBD
1498                // Restart when recent average LBD is higher than global average
1499                // For now, use geometric with dynamic adjustment
1500                let current_interval = if self.restart_threshold > self.stats.conflicts {
1501                    self.restart_threshold - self.stats.conflicts
1502                } else {
1503                    self.config.restart_interval
1504                };
1505
1506                // Adjust based on recent LBD trend
1507                let next_interval = if self.recent_lbd_count > 50 {
1508                    let recent_avg = self.recent_lbd_sum / self.recent_lbd_count.max(1);
1509                    // If recent LBD is low (good), increase interval; if high, decrease
1510                    if recent_avg < 5 {
1511                        // Good quality clauses - increase interval
1512                        ((current_interval as f64) * 1.1) as u64
1513                    } else {
1514                        // Poor quality clauses - decrease interval
1515                        ((current_interval as f64) * 0.9) as u64
1516                    }
1517                } else {
1518                    current_interval
1519                };
1520
1521                self.restart_threshold = self.stats.conflicts + next_interval.max(100);
1522            }
1523            RestartStrategy::LocalLbd => {
1524                // Local restarts based on LBD
1525                // Check if we should do a local restart
1526                self.conflicts_since_local_restart += 1;
1527
1528                if self.conflicts_since_local_restart >= 50 && self.should_local_restart() {
1529                    // Perform local restart - backtrack to a safe level, not to 0
1530                    let local_level = self.compute_local_restart_level();
1531                    self.backtrack_with_phase_saving(local_level);
1532                    self.conflicts_since_local_restart = 0;
1533                    // Reset recent LBD for next window
1534                    self.recent_lbd_sum = 0;
1535                    self.recent_lbd_count = 0;
1536                } else {
1537                    // Standard restart if too many conflicts
1538                    let current_interval = if self.restart_threshold > self.stats.conflicts {
1539                        self.restart_threshold - self.stats.conflicts
1540                    } else {
1541                        self.config.restart_interval
1542                    };
1543                    self.restart_threshold = self.stats.conflicts + current_interval;
1544                }
1545                return; // Don't do full backtrack to 0
1546            }
1547        }
1548
1549        // Re-add all unassigned variables to VSIDS heap
1550        for i in 0..self.num_vars {
1551            let var = Var::new(i as u32);
1552            if !self.trail.is_assigned(var) && !self.vsids.contains(var) {
1553                self.vsids.insert(var);
1554            }
1555        }
1556    }
1557
1558    /// Check if we should perform a local restart
1559    /// Returns true if recent average LBD is significantly higher than global average
1560    fn should_local_restart(&self) -> bool {
1561        if self.recent_lbd_count < 50 || self.global_lbd_count < 100 {
1562            return false;
1563        }
1564
1565        let recent_avg = self.recent_lbd_sum / self.recent_lbd_count.max(1);
1566        let global_avg = self.global_lbd_sum / self.global_lbd_count.max(1);
1567
1568        // Local restart if recent average is 1.25x higher than global average
1569        recent_avg * 4 > global_avg * 5
1570    }
1571
1572    /// Compute the level to backtrack to for local restart
1573    /// Use a level that preserves some of the search progress
1574    fn compute_local_restart_level(&self) -> u32 {
1575        let current_level = self.trail.decision_level();
1576
1577        // Backtrack to about 20% of current depth to preserve some work
1578        if current_level > 5 {
1579            current_level / 5
1580        } else {
1581            0
1582        }
1583    }
1584
1585    /// Compute LBD (Literal Block Distance) of a clause
1586    /// LBD is the number of distinct decision levels in the clause
1587    fn compute_lbd(&mut self, lits: &[Lit]) -> u32 {
1588        self.lbd_mark += 1;
1589        let mark = self.lbd_mark;
1590
1591        let mut count = 0u32;
1592        for &lit in lits {
1593            let level = self.trail.level(lit.var()) as usize;
1594            if level < self.level_marks.len() && self.level_marks[level] != mark {
1595                self.level_marks[level] = mark;
1596                count += 1;
1597            }
1598        }
1599
1600        count
1601    }
1602
1603    /// Reduce the learned clause database using tier-based deletion strategy
1604    /// - Core tier (Tier 1): Rarely deleted, only if very inactive
1605    /// - Mid tier (Tier 2): Delete ~30% based on activity
1606    /// - Local tier (Tier 3): Delete ~75% based on activity
1607    fn reduce_clause_database(&mut self) {
1608        use crate::clause::ClauseTier;
1609
1610        let mut core_candidates: Vec<(ClauseId, f64)> = Vec::new();
1611        let mut mid_candidates: Vec<(ClauseId, f64)> = Vec::new();
1612        let mut local_candidates: Vec<(ClauseId, f64)> = Vec::new();
1613
1614        for &cid in &self.learned_clause_ids {
1615            if let Some(clause) = self.clauses.get(cid) {
1616                if clause.deleted {
1617                    continue;
1618                }
1619
1620                // Don't delete binary clauses (very useful)
1621                if clause.lits.len() <= 2 {
1622                    continue;
1623                }
1624
1625                // Check if clause is currently a reason for any assignment
1626                // (We can't delete reason clauses)
1627                let is_reason = clause.lits.iter().any(|&lit| {
1628                    let var = lit.var();
1629                    if self.trail.is_assigned(var) {
1630                        matches!(self.trail.reason(var), Reason::Propagation(r) if r == cid)
1631                    } else {
1632                        false
1633                    }
1634                });
1635
1636                if !is_reason {
1637                    match clause.tier {
1638                        ClauseTier::Core => core_candidates.push((cid, clause.activity)),
1639                        ClauseTier::Mid => mid_candidates.push((cid, clause.activity)),
1640                        ClauseTier::Local => local_candidates.push((cid, clause.activity)),
1641                    }
1642                }
1643            }
1644        }
1645
1646        // Sort by activity (ascending) - delete low-activity clauses first
1647        core_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1648        mid_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1649        local_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1650
1651        // Delete different percentages from each tier
1652        // Core: Delete bottom 10% (very conservative)
1653        let num_core_delete = core_candidates.len() / 10;
1654        // Mid: Delete bottom 30%
1655        let num_mid_delete = (mid_candidates.len() * 3) / 10;
1656        // Local: Delete bottom 75% (very aggressive)
1657        let num_local_delete = (local_candidates.len() * 3) / 4;
1658
1659        for (cid, _) in core_candidates.iter().take(num_core_delete) {
1660            self.clauses.remove(*cid);
1661            self.stats.deleted_clauses += 1;
1662        }
1663
1664        for (cid, _) in mid_candidates.iter().take(num_mid_delete) {
1665            self.clauses.remove(*cid);
1666            self.stats.deleted_clauses += 1;
1667        }
1668
1669        for (cid, _) in local_candidates.iter().take(num_local_delete) {
1670            self.clauses.remove(*cid);
1671            self.stats.deleted_clauses += 1;
1672        }
1673
1674        // Clean up learned_clause_ids (remove deleted clauses)
1675        self.learned_clause_ids
1676            .retain(|&cid| self.clauses.get(cid).is_some_and(|c| !c.deleted));
1677    }
1678
1679    /// Save the model
1680    fn save_model(&mut self) {
1681        self.model.resize(self.num_vars, LBool::Undef);
1682        for i in 0..self.num_vars {
1683            self.model[i] = self.trail.value(Var::new(i as u32));
1684        }
1685    }
1686
1687    /// Get the model (if sat)
1688    #[must_use]
1689    pub fn model(&self) -> &[LBool] {
1690        &self.model
1691    }
1692
1693    /// Get the value of a variable in the model
1694    #[must_use]
1695    pub fn model_value(&self, var: Var) -> LBool {
1696        self.model.get(var.index()).copied().unwrap_or(LBool::Undef)
1697    }
1698
1699    /// Get statistics
1700    #[must_use]
1701    pub fn stats(&self) -> &SolverStats {
1702        &self.stats
1703    }
1704
1705    /// Get number of variables
1706    #[must_use]
1707    pub fn num_vars(&self) -> usize {
1708        self.num_vars
1709    }
1710
1711    /// Get number of clauses
1712    #[must_use]
1713    pub fn num_clauses(&self) -> usize {
1714        self.clauses.len()
1715    }
1716
1717    /// Push a new assertion level (for incremental solving)
1718    ///
1719    /// This saves the current state so that clauses added after this point
1720    /// can be removed with pop(). Automatically backtracks to decision level 0
1721    /// to ensure a clean state for adding new constraints.
1722    pub fn push(&mut self) {
1723        // Backtrack to level 0 to ensure clean state
1724        // This is necessary because solve() may leave assignments on the trail
1725        // Use phase-saving backtrack to properly re-insert variables into decision heaps
1726        self.backtrack_with_phase_saving(0);
1727
1728        self.assertion_levels.push(self.clauses.num_original());
1729        self.assertion_trail_sizes.push(self.trail.size());
1730        self.assertion_clause_ids.push(Vec::new());
1731    }
1732
1733    /// Pop to previous assertion level
1734    pub fn pop(&mut self) {
1735        if self.assertion_levels.len() > 1 {
1736            self.assertion_levels.pop();
1737
1738            // Get the trail size to backtrack to
1739            let trail_size = self.assertion_trail_sizes.pop().unwrap_or(0);
1740
1741            // Remove all clauses added at this assertion level
1742            if let Some(clause_ids_to_remove) = self.assertion_clause_ids.pop() {
1743                for clause_id in clause_ids_to_remove {
1744                    // Remove from clause database
1745                    self.clauses.remove(clause_id);
1746
1747                    // Remove from learned clause tracking if it's a learned clause
1748                    self.learned_clause_ids.retain(|&id| id != clause_id);
1749
1750                    // Note: Watch lists will be cleaned up naturally during propagation
1751                    // as they check if clauses are deleted before using them
1752                }
1753            }
1754
1755            // Backtrack trail to the exact size it was at push()
1756            // This properly handles unit clauses that were added after push
1757            // Note: backtrack_to_size clears values but doesn't re-insert into heaps,
1758            // so we need to manually re-insert unassigned variables.
1759            let current_size = self.trail.size();
1760            if current_size > trail_size {
1761                // Collect variables that will be unassigned
1762                let mut unassigned_vars = Vec::new();
1763                for i in trail_size..current_size {
1764                    let lit = self.trail.assignments()[i];
1765                    unassigned_vars.push(lit.var());
1766                }
1767
1768                self.trail.backtrack_to_size(trail_size);
1769
1770                // Re-insert unassigned variables into decision heaps
1771                for var in unassigned_vars {
1772                    if !self.vsids.contains(var) {
1773                        self.vsids.insert(var);
1774                    }
1775                    if !self.chb.contains(var) {
1776                        self.chb.insert(var);
1777                    }
1778                    self.lrb.unassign(var);
1779                }
1780            }
1781
1782            // Ensure we're at decision level 0 with proper heap re-insertion
1783            self.backtrack_with_phase_saving(0);
1784
1785            // Clear the trivially_unsat flag as we've removed problematic clauses
1786            self.trivially_unsat = false;
1787        }
1788    }
1789
1790    /// Backtrack to decision level 0 (for AllSAT enumeration)
1791    ///
1792    /// This is necessary after a SAT result before adding blocking clauses
1793    /// to ensure the new clauses can trigger propagation correctly.
1794    /// Uses phase-saving backtrack to properly re-insert unassigned variables
1795    /// into the decision heaps (VSIDS, CHB, LRB).
1796    pub fn backtrack_to_root(&mut self) {
1797        self.backtrack_with_phase_saving(0);
1798    }
1799
1800    /// Reset the solver
1801    pub fn reset(&mut self) {
1802        self.clauses = ClauseDatabase::new();
1803        self.trail.clear();
1804        self.watches.clear();
1805        self.vsids.clear();
1806        self.chb.clear();
1807        self.stats = SolverStats::default();
1808        self.learnt.clear();
1809        self.seen.clear();
1810        self.analyze_stack.clear();
1811        self.assertion_levels.clear();
1812        self.assertion_levels.push(0);
1813        self.assertion_trail_sizes.clear();
1814        self.assertion_trail_sizes.push(0);
1815        self.assertion_clause_ids.clear();
1816        self.assertion_clause_ids.push(Vec::new());
1817        self.model.clear();
1818        self.num_vars = 0;
1819        self.restart_threshold = self.config.restart_interval;
1820        self.trivially_unsat = false;
1821        self.phase.clear();
1822        self.luby_index = 0;
1823        self.level_marks.clear();
1824        self.lbd_mark = 0;
1825        self.learned_clause_ids.clear();
1826        self.conflicts_since_deletion = 0;
1827        self.rng_state = 0x853c_49e6_748f_ea9b;
1828        self.recent_lbd_sum = 0;
1829        self.recent_lbd_count = 0;
1830        self.binary_graph.clear();
1831        self.global_lbd_sum = 0;
1832        self.global_lbd_count = 0;
1833        self.conflicts_since_local_restart = 0;
1834    }
1835
1836    /// Solve with theory integration via callbacks
1837    ///
1838    /// This implements the CDCL(T) loop:
1839    /// 1. BCP (Boolean Constraint Propagation)
1840    /// 2. Theory propagation (via callback)
1841    /// 3. On conflict: analyze and learn
1842    /// 4. Decision
1843    /// 5. Final theory check when all vars assigned
1844    pub fn solve_with_theory<T: TheoryCallback>(&mut self, theory: &mut T) -> SolverResult {
1845        if self.trivially_unsat {
1846            return SolverResult::Unsat;
1847        }
1848
1849        // Initial propagation
1850        if self.propagate().is_some() {
1851            return SolverResult::Unsat;
1852        }
1853
1854        loop {
1855            // Boolean propagation
1856            if let Some(conflict) = self.propagate() {
1857                self.stats.conflicts += 1;
1858
1859                if self.trail.decision_level() == 0 {
1860                    return SolverResult::Unsat;
1861                }
1862
1863                let (backtrack_level, learnt_clause) = self.analyze(conflict);
1864                theory.on_backtrack(backtrack_level);
1865                self.backtrack_with_phase_saving(backtrack_level);
1866                self.learn_clause(learnt_clause);
1867
1868                self.vsids.decay();
1869                self.clauses.decay_activity(self.config.clause_decay);
1870                self.handle_clause_deletion_and_restart();
1871                continue;
1872            }
1873
1874            // Theory propagation check after each assignment
1875            loop {
1876                // Get newly assigned literals and notify theory
1877                let assignments = self.trail.assignments().to_vec();
1878                let mut theory_conflict = None;
1879                let mut theory_propagations = Vec::new();
1880
1881                // Check each new assignment with theory
1882                for &lit in &assignments {
1883                    match theory.on_assignment(lit) {
1884                        TheoryCheckResult::Sat => {}
1885                        TheoryCheckResult::Conflict(conflict_lits) => {
1886                            theory_conflict = Some(conflict_lits);
1887                            break;
1888                        }
1889                        TheoryCheckResult::Propagated(props) => {
1890                            theory_propagations.extend(props);
1891                        }
1892                    }
1893                }
1894
1895                // Handle theory conflict
1896                if let Some(conflict_lits) = theory_conflict {
1897                    self.stats.conflicts += 1;
1898
1899                    if self.trail.decision_level() == 0 {
1900                        return SolverResult::Unsat;
1901                    }
1902
1903                    let (backtrack_level, learnt_clause) =
1904                        self.analyze_theory_conflict(&conflict_lits);
1905                    theory.on_backtrack(backtrack_level);
1906                    self.backtrack_with_phase_saving(backtrack_level);
1907                    self.learn_clause(learnt_clause);
1908
1909                    self.vsids.decay();
1910                    self.clauses.decay_activity(self.config.clause_decay);
1911                    self.handle_clause_deletion_and_restart();
1912                    continue;
1913                }
1914
1915                // Handle theory propagations
1916                let mut made_propagation = false;
1917                for (lit, reason_lits) in theory_propagations {
1918                    if !self.trail.is_assigned(lit.var()) {
1919                        // Add reason clause and propagate
1920                        let clause_id = self.add_theory_reason_clause(&reason_lits, lit);
1921                        self.trail.assign_propagation(lit, clause_id);
1922                        made_propagation = true;
1923                    }
1924                }
1925
1926                if made_propagation {
1927                    // Re-run Boolean propagation
1928                    if let Some(conflict) = self.propagate() {
1929                        self.stats.conflicts += 1;
1930
1931                        if self.trail.decision_level() == 0 {
1932                            return SolverResult::Unsat;
1933                        }
1934
1935                        let (backtrack_level, learnt_clause) = self.analyze(conflict);
1936                        theory.on_backtrack(backtrack_level);
1937                        self.backtrack_with_phase_saving(backtrack_level);
1938                        self.learn_clause(learnt_clause);
1939
1940                        self.vsids.decay();
1941                        self.clauses.decay_activity(self.config.clause_decay);
1942                        self.handle_clause_deletion_and_restart();
1943                    }
1944                    continue;
1945                }
1946
1947                break;
1948            }
1949
1950            // Try to decide
1951            if let Some(var) = self.pick_branch_var() {
1952                self.stats.decisions += 1;
1953                self.trail.new_decision_level();
1954                let new_level = self.trail.decision_level();
1955                theory.on_new_level(new_level);
1956
1957                let polarity = if self.rand_bool(self.config.random_polarity_prob) {
1958                    self.rand_bool(0.5)
1959                } else {
1960                    self.phase[var.index()]
1961                };
1962                let lit = if polarity {
1963                    Lit::pos(var)
1964                } else {
1965                    Lit::neg(var)
1966                };
1967                self.trail.assign_decision(lit);
1968            } else {
1969                // All variables assigned - do final theory check
1970                match theory.final_check() {
1971                    TheoryCheckResult::Sat => {
1972                        self.save_model();
1973                        return SolverResult::Sat;
1974                    }
1975                    TheoryCheckResult::Conflict(conflict_lits) => {
1976                        self.stats.conflicts += 1;
1977
1978                        if self.trail.decision_level() == 0 {
1979                            return SolverResult::Unsat;
1980                        }
1981
1982                        let (backtrack_level, learnt_clause) =
1983                            self.analyze_theory_conflict(&conflict_lits);
1984                        theory.on_backtrack(backtrack_level);
1985                        self.backtrack_with_phase_saving(backtrack_level);
1986                        self.learn_clause(learnt_clause);
1987
1988                        self.vsids.decay();
1989                        self.clauses.decay_activity(self.config.clause_decay);
1990                        self.handle_clause_deletion_and_restart();
1991                    }
1992                    TheoryCheckResult::Propagated(props) => {
1993                        // Handle late propagations
1994                        for (lit, reason_lits) in props {
1995                            if !self.trail.is_assigned(lit.var()) {
1996                                let clause_id = self.add_theory_reason_clause(&reason_lits, lit);
1997                                self.trail.assign_propagation(lit, clause_id);
1998                            }
1999                        }
2000                    }
2001                }
2002            }
2003        }
2004    }
2005
2006    /// Analyze a theory conflict (given as a list of literals that are all false)
2007    fn analyze_theory_conflict(&mut self, conflict_lits: &[Lit]) -> (u32, SmallVec<[Lit; 16]>) {
2008        self.learnt.clear();
2009        self.learnt.push(Lit::from_code(0)); // Placeholder
2010
2011        let mut counter = 0;
2012        let current_level = self.trail.decision_level();
2013
2014        // Reset seen flags
2015        for s in &mut self.seen {
2016            *s = false;
2017        }
2018
2019        // Process conflict literals
2020        for &lit in conflict_lits {
2021            let var = lit.var();
2022            let level = self.trail.level(var);
2023
2024            if !self.seen[var.index()] && level > 0 {
2025                self.seen[var.index()] = true;
2026                self.vsids.bump(var);
2027                self.chb.bump(var);
2028
2029                if level == current_level {
2030                    counter += 1;
2031                } else {
2032                    // Add the literal itself (not negated) to the learned clause.
2033                    // The conflict clause has all literals FALSE. To prevent this
2034                    // conflict, we need at least one of these literals to become TRUE.
2035                    // So we add the literal directly to the learned clause.
2036                    self.learnt.push(lit);
2037                }
2038            }
2039        }
2040
2041        // Find UIP by walking back through trail
2042        let mut index = self.trail.assignments().len();
2043        let mut p = None;
2044
2045        while counter > 0 {
2046            index -= 1;
2047            let current_lit = self.trail.assignments()[index];
2048            p = Some(current_lit);
2049            let var = current_lit.var();
2050
2051            if self.seen[var.index()] {
2052                counter -= 1;
2053
2054                if counter > 0
2055                    && let Reason::Propagation(reason_clause) = self.trail.reason(var)
2056                    && let Some(clause) = self.clauses.get(reason_clause)
2057                {
2058                    // Get reason and process its literals
2059                    for &lit in &clause.lits[1..] {
2060                        let reason_var = lit.var();
2061                        let level = self.trail.level(reason_var);
2062
2063                        if !self.seen[reason_var.index()] && level > 0 {
2064                            self.seen[reason_var.index()] = true;
2065                            self.vsids.bump(reason_var);
2066                            self.chb.bump(reason_var);
2067
2068                            if level == current_level {
2069                                counter += 1;
2070                            } else {
2071                                // Add the literal itself to the learned clause
2072                                self.learnt.push(lit);
2073                            }
2074                        }
2075                    }
2076                }
2077            }
2078        }
2079
2080        // Set asserting literal
2081        if let Some(uip) = p {
2082            self.learnt[0] = uip.negate();
2083        }
2084
2085        // Minimize
2086        self.minimize_learnt_clause();
2087
2088        // Calculate backtrack level
2089        let backtrack_level = if self.learnt.len() == 1 {
2090            0
2091        } else {
2092            let mut max_level = 0;
2093            let mut max_idx = 1;
2094            for (i, &lit) in self.learnt.iter().enumerate().skip(1) {
2095                let level = self.trail.level(lit.var());
2096                if level > max_level {
2097                    max_level = level;
2098                    max_idx = i;
2099                }
2100            }
2101            self.learnt.swap(1, max_idx);
2102            max_level
2103        };
2104
2105        (backtrack_level, self.learnt.clone())
2106    }
2107
2108    /// Add a theory reason clause
2109    /// The clause is: reason_lits[0] OR reason_lits[1] OR ... OR propagated_lit
2110    fn add_theory_reason_clause(&mut self, reason_lits: &[Lit], propagated_lit: Lit) -> ClauseId {
2111        let mut clause_lits: SmallVec<[Lit; 8]> = SmallVec::new();
2112        clause_lits.push(propagated_lit);
2113        for &lit in reason_lits {
2114            clause_lits.push(lit.negate());
2115        }
2116
2117        let clause_id = self.clauses.add_learned(clause_lits.iter().copied());
2118
2119        // Set up watches
2120        if clause_lits.len() >= 2 {
2121            let lit0 = clause_lits[0];
2122            let lit1 = clause_lits[1];
2123            self.watches
2124                .add(lit0.negate(), Watcher::new(clause_id, lit1));
2125            self.watches
2126                .add(lit1.negate(), Watcher::new(clause_id, lit0));
2127        }
2128
2129        clause_id
2130    }
2131
2132    /// Learn a clause and set up watches
2133    /// Includes on-the-fly subsumption check
2134    fn learn_clause(&mut self, learnt_clause: SmallVec<[Lit; 16]>) {
2135        if learnt_clause.len() == 1 {
2136            // Store unit learned clause in database for persistence across backtracks
2137            let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
2138            self.stats.learned_clauses += 1;
2139            self.stats.unit_clauses += 1;
2140            self.learned_clause_ids.push(clause_id);
2141
2142            self.trail.assign_decision(learnt_clause[0]);
2143        } else if learnt_clause.len() == 2 {
2144            // Binary learned clause - add to binary implication graph
2145            let lbd = self.compute_lbd(&learnt_clause);
2146            let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
2147            self.stats.learned_clauses += 1;
2148            self.stats.binary_clauses += 1;
2149            self.stats.total_lbd += lbd as u64;
2150
2151            if let Some(clause) = self.clauses.get_mut(clause_id) {
2152                clause.lbd = lbd;
2153            }
2154
2155            self.learned_clause_ids.push(clause_id);
2156
2157            let lit0 = learnt_clause[0];
2158            let lit1 = learnt_clause[1];
2159
2160            // Add to binary graph
2161            self.binary_graph.add(lit0.negate(), lit1, clause_id);
2162            self.binary_graph.add(lit1.negate(), lit0, clause_id);
2163
2164            self.watches
2165                .add(lit0.negate(), Watcher::new(clause_id, lit1));
2166            self.watches
2167                .add(lit1.negate(), Watcher::new(clause_id, lit0));
2168
2169            self.trail.assign_propagation(learnt_clause[0], clause_id);
2170        } else {
2171            let lbd = self.compute_lbd(&learnt_clause);
2172            self.stats.total_lbd += lbd as u64;
2173            let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
2174            self.stats.learned_clauses += 1;
2175
2176            if let Some(clause) = self.clauses.get_mut(clause_id) {
2177                clause.lbd = lbd;
2178            }
2179
2180            self.learned_clause_ids.push(clause_id);
2181
2182            let lit0 = learnt_clause[0];
2183            let lit1 = learnt_clause[1];
2184            self.watches
2185                .add(lit0.negate(), Watcher::new(clause_id, lit1));
2186            self.watches
2187                .add(lit1.negate(), Watcher::new(clause_id, lit0));
2188
2189            self.trail.assign_propagation(learnt_clause[0], clause_id);
2190
2191            // On-the-fly subsumption: check if this new clause subsumes existing clauses
2192            if learnt_clause.len() <= 5 && lbd <= 3 {
2193                self.check_subsumption(clause_id);
2194            }
2195        }
2196    }
2197
2198    /// Check if the given clause subsumes any existing clauses
2199    /// A clause C subsumes C' if all literals of C are in C'
2200    fn check_subsumption(&mut self, new_clause_id: ClauseId) {
2201        let new_clause = match self.clauses.get(new_clause_id) {
2202            Some(c) => c.lits.clone(),
2203            None => return,
2204        };
2205
2206        if new_clause.len() > 10 {
2207            return; // Don't check subsumption for large clauses (too expensive)
2208        }
2209
2210        // Check against learned clauses only
2211        let mut to_remove = Vec::new();
2212        for &cid in &self.learned_clause_ids {
2213            if cid == new_clause_id {
2214                continue;
2215            }
2216
2217            if let Some(clause) = self.clauses.get(cid) {
2218                if clause.deleted || clause.lits.len() < new_clause.len() {
2219                    continue;
2220                }
2221
2222                // Check if new_clause subsumes clause
2223                if new_clause.iter().all(|&lit| clause.lits.contains(&lit)) {
2224                    to_remove.push(cid);
2225                }
2226            }
2227        }
2228
2229        // Remove subsumed clauses
2230        for cid in to_remove {
2231            self.clauses.remove(cid);
2232            self.stats.deleted_clauses += 1;
2233        }
2234    }
2235
2236    /// Handle clause deletion check and restart check
2237    fn handle_clause_deletion_and_restart(&mut self) {
2238        self.conflicts_since_deletion += 1;
2239
2240        if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
2241            self.reduce_clause_database();
2242            self.conflicts_since_deletion = 0;
2243        }
2244
2245        if self.stats.conflicts >= self.restart_threshold {
2246            self.restart();
2247        }
2248    }
2249
2250    /// Get the current trail (for theory solvers)
2251    #[must_use]
2252    pub fn trail(&self) -> &Trail {
2253        &self.trail
2254    }
2255
2256    /// Get the current decision level
2257    #[must_use]
2258    pub fn decision_level(&self) -> u32 {
2259        self.trail.decision_level()
2260    }
2261
2262    /// Generate a random u64 using xorshift64
2263    fn rand_u64(&mut self) -> u64 {
2264        let mut x = self.rng_state;
2265        x ^= x << 13;
2266        x ^= x >> 7;
2267        x ^= x << 17;
2268        self.rng_state = x;
2269        x
2270    }
2271
2272    /// Generate a random f64 in [0, 1)
2273    fn rand_f64(&mut self) -> f64 {
2274        const MAX: f64 = u64::MAX as f64;
2275        (self.rand_u64() as f64) / MAX
2276    }
2277
2278    /// Generate a random boolean with given probability of being true
2279    fn rand_bool(&mut self, probability: f64) -> bool {
2280        self.rand_f64() < probability
2281    }
2282
2283    /// Check for hyper-binary resolution opportunity
2284    /// When propagating `implied` due to `lit` being assigned, check if we can
2285    /// learn a binary clause by resolving the reason clauses
2286    fn check_hyper_binary_resolution(&mut self, _lit: Lit, implied: Lit, reason_id: ClauseId) {
2287        // Only check at higher decision levels to avoid overhead
2288        if self.trail.decision_level() < 2 {
2289            return;
2290        }
2291
2292        // Get the reason clause
2293        let reason_clause = match self.clauses.get(reason_id) {
2294            Some(c) if c.lits.len() >= 2 && c.lits.len() <= 4 => c.lits.clone(),
2295            _ => return,
2296        };
2297
2298        // Check if we can derive a binary clause
2299        // Look for literals in the reason clause that are assigned at the current level
2300        let current_level = self.trail.decision_level();
2301        let mut current_level_lits = SmallVec::<[Lit; 4]>::new();
2302        let mut has_non_zero_level_other = false;
2303
2304        for &reason_lit in &reason_clause {
2305            if reason_lit != implied {
2306                let var = reason_lit.var();
2307                let level = self.trail.level(var);
2308                if level == current_level {
2309                    current_level_lits.push(reason_lit);
2310                } else if level > 0 {
2311                    // There's a literal at a non-zero level other than current
2312                    // This means the learned clause would depend on that assignment
2313                    // which is not safe for incremental solving
2314                    has_non_zero_level_other = true;
2315                }
2316            }
2317        }
2318
2319        // If there's exactly one literal from the current level besides the implied one,
2320        // and all others are at level 0, we can safely learn a binary clause.
2321        // IMPORTANT: We must ensure ALL other literals are at level 0 for the learned
2322        // clause to be valid when new constraints are added incrementally.
2323        if current_level_lits.len() == 1 && !has_non_zero_level_other {
2324            let other_lit = current_level_lits[0];
2325
2326            // Check if we can create a useful binary clause
2327            // The reason clause had other_lit FALSE and implied it. So we learn:
2328            // other_lit | implied (if other_lit is false, implied must be true)
2329            let binary_clause_lits = [other_lit, implied];
2330
2331            // Check if this binary clause is new and useful
2332            // The binary clause is: other_lit | implied
2333            // This means: ~other_lit -> implied, and ~implied -> other_lit
2334            if !self.has_binary_implication(other_lit.negate(), implied) {
2335                // Learn this binary clause on-the-fly
2336                let clause_id = self.clauses.add_learned(binary_clause_lits.iter().copied());
2337                // Add correct implications: ~A -> B and ~B -> A for clause (A | B)
2338                self.binary_graph
2339                    .add(other_lit.negate(), implied, clause_id);
2340                self.binary_graph
2341                    .add(implied.negate(), other_lit, clause_id);
2342                self.stats.learned_clauses += 1;
2343            }
2344        }
2345    }
2346
2347    /// Check if a binary implication already exists
2348    fn has_binary_implication(&self, from_lit: Lit, to_lit: Lit) -> bool {
2349        self.binary_graph
2350            .get(from_lit)
2351            .iter()
2352            .any(|(lit, _)| *lit == to_lit)
2353    }
2354
2355    /// Vivification: try to strengthen clauses by checking if some literals are redundant
2356    /// This is an inprocessing technique that should be called periodically
2357    fn vivify_clauses(&mut self) {
2358        if self.trail.decision_level() != 0 {
2359            return; // Only vivify at decision level 0
2360        }
2361
2362        let mut vivified_count = 0;
2363        let max_vivifications = 100; // Limit to avoid too much overhead
2364
2365        // Try to vivify some learned clauses
2366        let clause_ids: Vec<ClauseId> = self
2367            .learned_clause_ids
2368            .iter()
2369            .copied()
2370            .take(max_vivifications)
2371            .collect();
2372
2373        for clause_id in clause_ids {
2374            if vivified_count >= max_vivifications {
2375                break;
2376            }
2377
2378            let clause_lits = match self.clauses.get(clause_id) {
2379                Some(c) if !c.deleted && c.lits.len() > 2 => c.lits.clone(),
2380                _ => continue,
2381            };
2382
2383            // Try to find redundant literals in the clause
2384            // Assign all literals except one to false and see if we can derive the last one
2385            for skip_idx in 0..clause_lits.len() {
2386                // Save current state
2387                let saved_level = self.trail.decision_level();
2388
2389                // Assign all literals except skip_idx to false
2390                self.trail.new_decision_level();
2391                let mut conflict = false;
2392
2393                for (i, &lit) in clause_lits.iter().enumerate() {
2394                    if i == skip_idx {
2395                        continue;
2396                    }
2397
2398                    let value = self.trail.lit_value(lit);
2399                    if value.is_true() {
2400                        // Clause is already satisfied
2401                        conflict = false;
2402                        break;
2403                    } else if value.is_false() {
2404                        // Already false
2405                        continue;
2406                    } else {
2407                        // Assign to false
2408                        self.trail.assign_decision(lit.negate());
2409
2410                        // Propagate
2411                        if self.propagate().is_some() {
2412                            conflict = true;
2413                            break;
2414                        }
2415                    }
2416                }
2417
2418                // Backtrack
2419                self.backtrack(saved_level);
2420
2421                if conflict
2422                    && let Some(clause) = self.clauses.get_mut(clause_id)
2423                    && clause.lits.len() > 2
2424                {
2425                    // The literal at skip_idx is implied by the rest
2426                    // We can remove it from the clause (vivification succeeded)
2427                    clause.lits.remove(skip_idx);
2428                    vivified_count += 1;
2429                    break; // Done with this clause
2430                }
2431            }
2432        }
2433    }
2434
2435    /// Perform inprocessing (apply preprocessing during search)
2436    fn inprocess(&mut self) {
2437        use crate::Preprocessor;
2438
2439        // Only inprocess at decision level 0
2440        if self.trail.decision_level() != 0 {
2441            return;
2442        }
2443
2444        // Create preprocessor with current number of variables
2445        let mut preprocessor = Preprocessor::new(self.num_vars);
2446
2447        // Apply lightweight preprocessing techniques
2448        let _pure_elim = preprocessor.pure_literal_elimination(&mut self.clauses);
2449        let _subsumption = preprocessor.subsumption_elimination(&mut self.clauses);
2450
2451        // On-the-fly clause strengthening
2452        self.strengthen_clauses_inprocessing();
2453
2454        // Rebuild watch lists for any modified clauses
2455        // This is a simplified approach - in a full implementation,
2456        // we would track which clauses were removed and update watches incrementally
2457    }
2458
2459    /// On-the-fly clause strengthening during inprocessing
2460    ///
2461    /// Try to remove literals from clauses by checking if they're redundant.
2462    /// A literal is redundant if the clause is satisfied when it's assigned to false.
2463    fn strengthen_clauses_inprocessing(&mut self) {
2464        if self.trail.decision_level() != 0 {
2465            return;
2466        }
2467
2468        let max_clauses_to_strengthen = 50; // Limit to avoid overhead
2469        let mut strengthened_count = 0;
2470
2471        // Collect candidate clauses (learned clauses with LBD > 2)
2472        let mut candidates: Vec<(ClauseId, u32)> = Vec::new();
2473
2474        for &clause_id in &self.learned_clause_ids {
2475            if let Some(clause) = self.clauses.get(clause_id)
2476                && !clause.deleted
2477                && clause.lits.len() > 3
2478                && clause.lbd > 2
2479            {
2480                candidates.push((clause_id, clause.lbd));
2481            }
2482        }
2483
2484        // Sort by LBD (prioritize higher LBD clauses for strengthening)
2485        candidates.sort_by_key(|(_, lbd)| std::cmp::Reverse(*lbd));
2486
2487        for (clause_id, _) in candidates.iter().take(max_clauses_to_strengthen) {
2488            if strengthened_count >= max_clauses_to_strengthen {
2489                break;
2490            }
2491
2492            let clause_lits = match self.clauses.get(*clause_id) {
2493                Some(c) if !c.deleted && c.lits.len() > 3 => c.lits.clone(),
2494                _ => continue,
2495            };
2496
2497            // Try to remove each literal by checking if the remaining clause is still valid
2498            let mut literals_to_remove = Vec::new();
2499
2500            for (i, &lit) in clause_lits.iter().enumerate() {
2501                // Save current trail state
2502                let saved_level = self.trail.decision_level();
2503
2504                // Try assigning this literal to false
2505                self.trail.new_decision_level();
2506                self.trail.assign_decision(lit.negate());
2507
2508                // Propagate
2509                let conflict = self.propagate();
2510
2511                // Backtrack
2512                self.backtrack(saved_level);
2513
2514                if conflict.is_some() {
2515                    // Assigning this literal to false causes a conflict
2516                    // This means the rest of the clause implies this literal
2517                    // So this literal can potentially be removed (strengthening)
2518
2519                    // But we need to be careful: only remove if the remaining clause
2520                    // is still non-tautological and non-empty
2521                    let mut remaining: Vec<Lit> = clause_lits
2522                        .iter()
2523                        .enumerate()
2524                        .filter(|(j, _)| *j != i)
2525                        .map(|(_, &l)| l)
2526                        .collect();
2527
2528                    // Check if remaining clause is still valid (at least 2 literals)
2529                    if remaining.len() >= 2 {
2530                        // Check it's not a tautology
2531                        remaining.sort_by_key(|l| l.code());
2532                        let mut is_tautology = false;
2533                        for k in 0..remaining.len() - 1 {
2534                            if remaining[k] == remaining[k + 1].negate() {
2535                                is_tautology = true;
2536                                break;
2537                            }
2538                        }
2539
2540                        if !is_tautology {
2541                            literals_to_remove.push(i);
2542                            break; // Only remove one literal at a time
2543                        }
2544                    }
2545                }
2546            }
2547
2548            // Apply strengthening if we found literals to remove
2549            if !literals_to_remove.is_empty() {
2550                // First, remove literals
2551                if let Some(clause) = self.clauses.get_mut(*clause_id) {
2552                    // Remove literals in reverse order to preserve indices
2553                    for &idx in literals_to_remove.iter().rev() {
2554                        if idx < clause.lits.len() {
2555                            clause.lits.remove(idx);
2556                        }
2557                    }
2558                }
2559
2560                // Then, recompute LBD (after the mutable borrow ends)
2561                if let Some(clause) = self.clauses.get(*clause_id) {
2562                    let lits_clone = clause.lits.clone();
2563                    let new_lbd = self.compute_lbd(&lits_clone);
2564
2565                    // Now update the LBD
2566                    if let Some(clause) = self.clauses.get_mut(*clause_id) {
2567                        clause.lbd = new_lbd;
2568                    }
2569
2570                    strengthened_count += 1;
2571                }
2572            }
2573        }
2574    }
2575
2576    /// Debug method: print all learned clauses
2577    pub fn debug_print_learned_clauses(&self) {
2578        println!(
2579            "=== Learned Clauses ({}) ===",
2580            self.learned_clause_ids.len()
2581        );
2582        for (i, &cid) in self.learned_clause_ids.iter().enumerate() {
2583            if let Some(clause) = self.clauses.get(cid)
2584                && !clause.deleted
2585            {
2586                let lits: Vec<String> = clause
2587                    .lits
2588                    .iter()
2589                    .map(|lit| {
2590                        let var = lit.var().index();
2591                        if lit.is_pos() {
2592                            format!("v{}", var)
2593                        } else {
2594                            format!("~v{}", var)
2595                        }
2596                    })
2597                    .collect();
2598                println!(
2599                    "  Learned {}: ({}), LBD={}",
2600                    i,
2601                    lits.join(" | "),
2602                    clause.lbd
2603                );
2604            }
2605        }
2606    }
2607
2608    /// Debug method: print binary implication graph entries
2609    pub fn debug_print_binary_graph(&self) {
2610        println!("=== Binary Implication Graph ===");
2611        for lit_code in 0..(self.num_vars * 2) {
2612            let lit = Lit::from_code(lit_code as u32);
2613            let implications = self.binary_graph.get(lit);
2614            if !implications.is_empty() {
2615                let lit_str = if lit.is_pos() {
2616                    format!("v{}", lit.var().index())
2617                } else {
2618                    format!("~v{}", lit.var().index())
2619                };
2620                for &(implied, _cid) in implications {
2621                    let impl_str = if implied.is_pos() {
2622                        format!("v{}", implied.var().index())
2623                    } else {
2624                        format!("~v{}", implied.var().index())
2625                    };
2626                    println!("  {} -> {}", lit_str, impl_str);
2627                }
2628            }
2629        }
2630    }
2631}
2632
2633#[cfg(test)]
2634mod tests {
2635    use super::*;
2636
2637    #[test]
2638    fn test_empty_sat() {
2639        let mut solver = Solver::new();
2640        assert_eq!(solver.solve(), SolverResult::Sat);
2641    }
2642
2643    #[test]
2644    fn test_simple_sat() {
2645        let mut solver = Solver::new();
2646        let _x = solver.new_var();
2647        let _y = solver.new_var();
2648
2649        // x or y
2650        solver.add_clause_dimacs(&[1, 2]);
2651        // not x or y
2652        solver.add_clause_dimacs(&[-1, 2]);
2653
2654        assert_eq!(solver.solve(), SolverResult::Sat);
2655        assert!(solver.model_value(Var::new(1)).is_true()); // y must be true
2656    }
2657
2658    #[test]
2659    fn test_simple_unsat() {
2660        let mut solver = Solver::new();
2661        let _x = solver.new_var();
2662
2663        // x
2664        solver.add_clause_dimacs(&[1]);
2665        // not x
2666        solver.add_clause_dimacs(&[-1]);
2667
2668        assert_eq!(solver.solve(), SolverResult::Unsat);
2669    }
2670
2671    #[test]
2672    fn test_pigeonhole_2_1() {
2673        // 2 pigeons, 1 hole - UNSAT
2674        let mut solver = Solver::new();
2675        let _p1h1 = solver.new_var(); // pigeon 1 in hole 1
2676        let _p2h1 = solver.new_var(); // pigeon 2 in hole 1
2677
2678        // Each pigeon must be in some hole
2679        solver.add_clause_dimacs(&[1]); // p1 in h1
2680        solver.add_clause_dimacs(&[2]); // p2 in h1
2681
2682        // No hole can have two pigeons
2683        solver.add_clause_dimacs(&[-1, -2]); // not (p1h1 and p2h1)
2684
2685        assert_eq!(solver.solve(), SolverResult::Unsat);
2686    }
2687
2688    #[test]
2689    fn test_3sat_random() {
2690        let mut solver = Solver::new();
2691        for _ in 0..10 {
2692            solver.new_var();
2693        }
2694
2695        // Random 3-SAT instance (likely SAT)
2696        solver.add_clause_dimacs(&[1, 2, 3]);
2697        solver.add_clause_dimacs(&[-1, 4, 5]);
2698        solver.add_clause_dimacs(&[2, -3, 6]);
2699        solver.add_clause_dimacs(&[-4, 7, 8]);
2700        solver.add_clause_dimacs(&[5, -6, 9]);
2701        solver.add_clause_dimacs(&[-7, 8, 10]);
2702        solver.add_clause_dimacs(&[1, -8, -9]);
2703        solver.add_clause_dimacs(&[-2, 3, -10]);
2704
2705        let result = solver.solve();
2706        assert_eq!(result, SolverResult::Sat);
2707    }
2708
2709    #[test]
2710    fn test_luby_sequence() {
2711        // Luby sequence: 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, ...
2712        assert_eq!(Solver::luby(0), 1);
2713        assert_eq!(Solver::luby(1), 1);
2714        assert_eq!(Solver::luby(2), 2);
2715        assert_eq!(Solver::luby(3), 1);
2716        assert_eq!(Solver::luby(4), 1);
2717        assert_eq!(Solver::luby(5), 2);
2718        assert_eq!(Solver::luby(6), 4);
2719        assert_eq!(Solver::luby(7), 1);
2720    }
2721
2722    #[test]
2723    fn test_phase_saving() {
2724        let mut solver = Solver::new();
2725        for _ in 0..5 {
2726            solver.new_var();
2727        }
2728
2729        // Set up a problem where phase saving helps
2730        solver.add_clause_dimacs(&[1, 2]);
2731        solver.add_clause_dimacs(&[-1, 3]);
2732        solver.add_clause_dimacs(&[-2, 4]);
2733        solver.add_clause_dimacs(&[-3, -4, 5]);
2734        solver.add_clause_dimacs(&[-5, 1]);
2735
2736        let result = solver.solve();
2737        assert_eq!(result, SolverResult::Sat);
2738    }
2739
2740    #[test]
2741    fn test_lbd_computation() {
2742        // Test that clause deletion can handle a problem that generates learned clauses
2743        let mut solver = Solver::with_config(SolverConfig {
2744            clause_deletion_threshold: 5, // Trigger deletion quickly
2745            ..SolverConfig::default()
2746        });
2747
2748        for _ in 0..20 {
2749            solver.new_var();
2750        }
2751
2752        // A harder problem to generate more conflicts and learned clauses
2753        // PHP(3,2): 3 pigeons, 2 holes - UNSAT
2754        // Variables: p_i_h (pigeon i in hole h)
2755        // p11=1, p12=2, p21=3, p22=4, p31=5, p32=6
2756
2757        // Each pigeon must be in some hole
2758        solver.add_clause_dimacs(&[1, 2]); // p1 in h1 or h2
2759        solver.add_clause_dimacs(&[3, 4]); // p2 in h1 or h2
2760        solver.add_clause_dimacs(&[5, 6]); // p3 in h1 or h2
2761
2762        // No hole can have two pigeons
2763        solver.add_clause_dimacs(&[-1, -3]); // not (p1h1 and p2h1)
2764        solver.add_clause_dimacs(&[-1, -5]); // not (p1h1 and p3h1)
2765        solver.add_clause_dimacs(&[-3, -5]); // not (p2h1 and p3h1)
2766        solver.add_clause_dimacs(&[-2, -4]); // not (p1h2 and p2h2)
2767        solver.add_clause_dimacs(&[-2, -6]); // not (p1h2 and p3h2)
2768        solver.add_clause_dimacs(&[-4, -6]); // not (p2h2 and p3h2)
2769
2770        let result = solver.solve();
2771        assert_eq!(result, SolverResult::Unsat);
2772        // Verify we had some conflicts (and thus learned clauses)
2773        assert!(solver.stats().conflicts > 0);
2774    }
2775
2776    #[test]
2777    fn test_clause_activity_decay() {
2778        let mut solver = Solver::new();
2779        for _ in 0..10 {
2780            solver.new_var();
2781        }
2782
2783        // Add some clauses
2784        solver.add_clause_dimacs(&[1, 2, 3]);
2785        solver.add_clause_dimacs(&[-1, 4, 5]);
2786        solver.add_clause_dimacs(&[-2, -3, 6]);
2787
2788        // Solve (should be SAT)
2789        let result = solver.solve();
2790        assert_eq!(result, SolverResult::Sat);
2791    }
2792
2793    #[test]
2794    fn test_clause_minimization() {
2795        // Test that clause minimization works correctly on a problem
2796        // that will generate learned clauses
2797        let mut solver = Solver::new();
2798
2799        for _ in 0..15 {
2800            solver.new_var();
2801        }
2802
2803        // A problem structure that generates conflicts and learned clauses
2804        // Graph coloring with 3 colors on 5 vertices
2805        // Vertices: 1-5, Colors: R(0-4), G(5-9), B(10-14)
2806
2807        // Each vertex has at least one color
2808        solver.add_clause_dimacs(&[1, 6, 11]); // v1: R or G or B
2809        solver.add_clause_dimacs(&[2, 7, 12]); // v2
2810        solver.add_clause_dimacs(&[3, 8, 13]); // v3
2811        solver.add_clause_dimacs(&[4, 9, 14]); // v4
2812        solver.add_clause_dimacs(&[5, 10, 15]); // v5
2813
2814        // At most one color per vertex (pairwise exclusion)
2815        solver.add_clause_dimacs(&[-1, -6]); // v1: not (R and G)
2816        solver.add_clause_dimacs(&[-1, -11]); // v1: not (R and B)
2817        solver.add_clause_dimacs(&[-6, -11]); // v1: not (G and B)
2818
2819        solver.add_clause_dimacs(&[-2, -7]);
2820        solver.add_clause_dimacs(&[-2, -12]);
2821        solver.add_clause_dimacs(&[-7, -12]);
2822
2823        solver.add_clause_dimacs(&[-3, -8]);
2824        solver.add_clause_dimacs(&[-3, -13]);
2825        solver.add_clause_dimacs(&[-8, -13]);
2826
2827        // Adjacent vertices have different colors (edges: 1-2, 2-3, 3-4, 4-5)
2828        solver.add_clause_dimacs(&[-1, -2]); // edge 1-2: not both R
2829        solver.add_clause_dimacs(&[-6, -7]); // edge 1-2: not both G
2830        solver.add_clause_dimacs(&[-11, -12]); // edge 1-2: not both B
2831
2832        solver.add_clause_dimacs(&[-2, -3]); // edge 2-3
2833        solver.add_clause_dimacs(&[-7, -8]);
2834        solver.add_clause_dimacs(&[-12, -13]);
2835
2836        let result = solver.solve();
2837        assert_eq!(result, SolverResult::Sat);
2838
2839        // The solver may or may not have conflicts/learned clauses depending on
2840        // the decision heuristic. The key is that the result is correct.
2841        // If there are learned clauses, minimization would have been applied.
2842    }
2843
2844    /// A simple theory callback that does nothing (pure SAT)
2845    struct NullTheory;
2846
2847    impl TheoryCallback for NullTheory {
2848        fn on_assignment(&mut self, _lit: Lit) -> TheoryCheckResult {
2849            TheoryCheckResult::Sat
2850        }
2851
2852        fn final_check(&mut self) -> TheoryCheckResult {
2853            TheoryCheckResult::Sat
2854        }
2855
2856        fn on_backtrack(&mut self, _level: u32) {}
2857    }
2858
2859    #[test]
2860    fn test_solve_with_theory_sat() {
2861        let mut solver = Solver::new();
2862        let mut theory = NullTheory;
2863
2864        let _x = solver.new_var();
2865        let _y = solver.new_var();
2866
2867        // x or y
2868        solver.add_clause_dimacs(&[1, 2]);
2869        // not x or y
2870        solver.add_clause_dimacs(&[-1, 2]);
2871
2872        assert_eq!(solver.solve_with_theory(&mut theory), SolverResult::Sat);
2873        assert!(solver.model_value(Var::new(1)).is_true()); // y must be true
2874    }
2875
2876    #[test]
2877    fn test_solve_with_theory_unsat() {
2878        let mut solver = Solver::new();
2879        let mut theory = NullTheory;
2880
2881        let _x = solver.new_var();
2882
2883        // x
2884        solver.add_clause_dimacs(&[1]);
2885        // not x
2886        solver.add_clause_dimacs(&[-1]);
2887
2888        assert_eq!(solver.solve_with_theory(&mut theory), SolverResult::Unsat);
2889    }
2890
2891    /// A theory that forces x0 => x1 (if x0 is true, x1 must be true)
2892    struct ImplicationTheory {
2893        /// Track if x0 is assigned true
2894        x0_true: bool,
2895    }
2896
2897    impl ImplicationTheory {
2898        fn new() -> Self {
2899            Self { x0_true: false }
2900        }
2901    }
2902
2903    impl TheoryCallback for ImplicationTheory {
2904        fn on_assignment(&mut self, lit: Lit) -> TheoryCheckResult {
2905            // If x0 becomes true, propagate x1
2906            if lit.var().index() == 0 && lit.is_pos() {
2907                self.x0_true = true;
2908                // Propagate: x1 must be true because x0 is true
2909                // The reason is: ~x0 (if x0 were false, we wouldn't need x1)
2910                let reason: SmallVec<[Lit; 8]> = smallvec::smallvec![Lit::pos(Var::new(0))];
2911                return TheoryCheckResult::Propagated(vec![(Lit::pos(Var::new(1)), reason)]);
2912            }
2913            TheoryCheckResult::Sat
2914        }
2915
2916        fn final_check(&mut self) -> TheoryCheckResult {
2917            TheoryCheckResult::Sat
2918        }
2919
2920        fn on_backtrack(&mut self, _level: u32) {
2921            self.x0_true = false;
2922        }
2923    }
2924
2925    #[test]
2926    fn test_theory_propagation() {
2927        let mut solver = Solver::new();
2928        let mut theory = ImplicationTheory::new();
2929
2930        let _x0 = solver.new_var();
2931        let _x1 = solver.new_var();
2932
2933        // Force x0 to be true
2934        solver.add_clause_dimacs(&[1]);
2935
2936        let result = solver.solve_with_theory(&mut theory);
2937        assert_eq!(result, SolverResult::Sat);
2938
2939        // x0 should be true (forced by clause)
2940        assert!(solver.model_value(Var::new(0)).is_true());
2941        // x1 should also be true (propagated by theory)
2942        assert!(solver.model_value(Var::new(1)).is_true());
2943    }
2944
2945    /// Theory that says x0 and x1 can't both be true
2946    struct MutexTheory {
2947        x0_true: Option<Lit>,
2948        x1_true: Option<Lit>,
2949    }
2950
2951    impl MutexTheory {
2952        fn new() -> Self {
2953            Self {
2954                x0_true: None,
2955                x1_true: None,
2956            }
2957        }
2958    }
2959
2960    impl TheoryCallback for MutexTheory {
2961        fn on_assignment(&mut self, lit: Lit) -> TheoryCheckResult {
2962            if lit.var().index() == 0 && lit.is_pos() {
2963                self.x0_true = Some(lit);
2964            }
2965            if lit.var().index() == 1 && lit.is_pos() {
2966                self.x1_true = Some(lit);
2967            }
2968
2969            // If both are true, conflict
2970            if self.x0_true.is_some() && self.x1_true.is_some() {
2971                // Conflict clause: ~x0 or ~x1 (at least one must be false)
2972                let conflict: SmallVec<[Lit; 8]> = smallvec::smallvec![
2973                    Lit::pos(Var::new(0)), // x0 is true (we negate in conflict)
2974                    Lit::pos(Var::new(1))  // x1 is true
2975                ];
2976                return TheoryCheckResult::Conflict(conflict);
2977            }
2978            TheoryCheckResult::Sat
2979        }
2980
2981        fn final_check(&mut self) -> TheoryCheckResult {
2982            if self.x0_true.is_some() && self.x1_true.is_some() {
2983                let conflict: SmallVec<[Lit; 8]> =
2984                    smallvec::smallvec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))];
2985                return TheoryCheckResult::Conflict(conflict);
2986            }
2987            TheoryCheckResult::Sat
2988        }
2989
2990        fn on_backtrack(&mut self, _level: u32) {
2991            self.x0_true = None;
2992            self.x1_true = None;
2993        }
2994    }
2995
2996    #[test]
2997    fn test_theory_conflict() {
2998        let mut solver = Solver::new();
2999        let mut theory = MutexTheory::new();
3000
3001        let _x0 = solver.new_var();
3002        let _x1 = solver.new_var();
3003
3004        // Force both x0 and x1 to be true (should cause theory conflict)
3005        solver.add_clause_dimacs(&[1]);
3006        solver.add_clause_dimacs(&[2]);
3007
3008        let result = solver.solve_with_theory(&mut theory);
3009        assert_eq!(result, SolverResult::Unsat);
3010    }
3011
3012    #[test]
3013    fn test_solve_with_assumptions_sat() {
3014        let mut solver = Solver::new();
3015
3016        let x0 = solver.new_var();
3017        let x1 = solver.new_var();
3018
3019        // x0 \/ x1
3020        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
3021
3022        // Assume x0 = true
3023        let assumptions = [Lit::pos(x0)];
3024        let (result, core) = solver.solve_with_assumptions(&assumptions);
3025
3026        assert_eq!(result, SolverResult::Sat);
3027        assert!(core.is_none());
3028    }
3029
3030    #[test]
3031    fn test_solve_with_assumptions_unsat() {
3032        let mut solver = Solver::new();
3033
3034        let x0 = solver.new_var();
3035        let x1 = solver.new_var();
3036
3037        // x0 -> ~x1 (encoded as ~x0 \/ ~x1)
3038        solver.add_clause([Lit::neg(x0), Lit::neg(x1)]);
3039
3040        // Assume both x0 = true and x1 = true (should be UNSAT)
3041        let assumptions = [Lit::pos(x0), Lit::pos(x1)];
3042        let (result, core) = solver.solve_with_assumptions(&assumptions);
3043
3044        assert_eq!(result, SolverResult::Unsat);
3045        assert!(core.is_some());
3046        let core = core.expect("UNSAT result must have conflict core");
3047        // Core should contain at least one of the conflicting assumptions
3048        assert!(!core.is_empty());
3049    }
3050
3051    #[test]
3052    fn test_solve_with_assumptions_core_extraction() {
3053        let mut solver = Solver::new();
3054
3055        let x0 = solver.new_var();
3056        let x1 = solver.new_var();
3057        let x2 = solver.new_var();
3058
3059        // ~x0 (x0 must be false)
3060        solver.add_clause([Lit::neg(x0)]);
3061
3062        // Assume x0 = true, x1 = true, x2 = true
3063        // Only x0 should be in the core
3064        let assumptions = [Lit::pos(x0), Lit::pos(x1), Lit::pos(x2)];
3065        let (result, core) = solver.solve_with_assumptions(&assumptions);
3066
3067        assert_eq!(result, SolverResult::Unsat);
3068        assert!(core.is_some());
3069        let core = core.expect("UNSAT result must have conflict core");
3070        // x0 should be in the core
3071        assert!(core.contains(&Lit::pos(x0)));
3072    }
3073
3074    #[test]
3075    fn test_solve_with_assumptions_incremental() {
3076        let mut solver = Solver::new();
3077
3078        let x0 = solver.new_var();
3079        let x1 = solver.new_var();
3080
3081        // x0 \/ x1
3082        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
3083
3084        // First: assume ~x0 (should be SAT with x1 = true)
3085        let (result1, _) = solver.solve_with_assumptions(&[Lit::neg(x0)]);
3086        assert_eq!(result1, SolverResult::Sat);
3087
3088        // Second: assume ~x0 and ~x1 (should be UNSAT)
3089        let (result2, core2) = solver.solve_with_assumptions(&[Lit::neg(x0), Lit::neg(x1)]);
3090        assert_eq!(result2, SolverResult::Unsat);
3091        assert!(core2.is_some());
3092
3093        // Third: assume x0 (should be SAT again)
3094        let (result3, _) = solver.solve_with_assumptions(&[Lit::pos(x0)]);
3095        assert_eq!(result3, SolverResult::Sat);
3096    }
3097
3098    #[test]
3099    fn test_push_pop_simple() {
3100        let mut solver = Solver::new();
3101
3102        let x0 = solver.new_var();
3103
3104        // Should be SAT (x0 can be true or false)
3105        assert_eq!(solver.solve(), SolverResult::Sat);
3106
3107        // Push and add unit clause: x0
3108        solver.push();
3109        solver.add_clause([Lit::pos(x0)]);
3110        assert_eq!(solver.solve(), SolverResult::Sat);
3111        assert!(solver.model_value(x0).is_true());
3112
3113        // Pop - should be SAT again
3114        solver.pop();
3115        let result = solver.solve();
3116        assert_eq!(
3117            result,
3118            SolverResult::Sat,
3119            "After pop, expected SAT but got {:?}. trivially_unsat={}",
3120            result,
3121            solver.trivially_unsat
3122        );
3123    }
3124
3125    #[test]
3126    fn test_push_pop_incremental() {
3127        let mut solver = Solver::new();
3128
3129        let x0 = solver.new_var();
3130        let x1 = solver.new_var();
3131        let x2 = solver.new_var();
3132
3133        // Base level: x0 \/ x1
3134        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
3135        assert_eq!(solver.solve(), SolverResult::Sat);
3136
3137        // Push and add: ~x0
3138        solver.push();
3139        solver.add_clause([Lit::neg(x0)]);
3140        assert_eq!(solver.solve(), SolverResult::Sat);
3141        // x1 must be true
3142        assert!(solver.model_value(x1).is_true());
3143
3144        // Push again and add: ~x1 (should be UNSAT)
3145        solver.push();
3146        solver.add_clause([Lit::neg(x1)]);
3147        assert_eq!(solver.solve(), SolverResult::Unsat);
3148
3149        // Pop back one level (remove ~x1, keep ~x0)
3150        solver.pop();
3151        assert_eq!(solver.solve(), SolverResult::Sat);
3152        assert!(solver.model_value(x1).is_true());
3153
3154        // Pop back to base level (remove ~x0)
3155        solver.pop();
3156        assert_eq!(solver.solve(), SolverResult::Sat);
3157        // Either x0 or x1 can be true now
3158
3159        // Push and add different clause: x0 /\ x2
3160        solver.push();
3161        solver.add_clause([Lit::pos(x0)]);
3162        solver.add_clause([Lit::pos(x2)]);
3163        assert_eq!(solver.solve(), SolverResult::Sat);
3164        assert!(solver.model_value(x0).is_true());
3165        assert!(solver.model_value(x2).is_true());
3166
3167        // Pop and verify clauses are removed
3168        solver.pop();
3169        assert_eq!(solver.solve(), SolverResult::Sat);
3170    }
3171
3172    #[test]
3173    fn test_push_pop_with_learned_clauses() {
3174        let mut solver = Solver::new();
3175
3176        let x0 = solver.new_var();
3177        let x1 = solver.new_var();
3178        let x2 = solver.new_var();
3179
3180        // Create a formula that will cause learning
3181        // (x0 \/ x1) /\ (~x0 \/ x2) /\ (~x1 \/ x2)
3182        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
3183        solver.add_clause([Lit::neg(x0), Lit::pos(x2)]);
3184        solver.add_clause([Lit::neg(x1), Lit::pos(x2)]);
3185
3186        assert_eq!(solver.solve(), SolverResult::Sat);
3187
3188        // Push and add conflicting clause
3189        solver.push();
3190        solver.add_clause([Lit::neg(x2)]);
3191
3192        // This should be UNSAT and cause clause learning
3193        assert_eq!(solver.solve(), SolverResult::Unsat);
3194
3195        // Pop - learned clauses from this level should be removed
3196        solver.pop();
3197
3198        // Should be SAT again
3199        assert_eq!(solver.solve(), SolverResult::Sat);
3200    }
3201}