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
464                let lit = clause_lits[0];
465                if self.trail.lit_value(lit).is_false() {
466                    self.trivially_unsat = true;
467                    return false;
468                }
469                if !self.trail.is_assigned(lit.var()) {
470                    self.trail.assign_decision(lit);
471                }
472                return true;
473            }
474            2 => {
475                // Binary clause - add to binary implication graph
476                let clause_id = self.clauses.add_original(clause_lits.iter().copied());
477
478                // Track clause for incremental solving
479                if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
480                    current_level_clauses.push(clause_id);
481                }
482
483                let lit0 = clause_lits[0];
484                let lit1 = clause_lits[1];
485
486                // (~lit0 => lit1) and (~lit1 => lit0)
487                self.binary_graph.add(lit0.negate(), lit1, clause_id);
488                self.binary_graph.add(lit1.negate(), lit0, clause_id);
489
490                // Also set up watches for consistency
491                self.watches
492                    .add(lit0.negate(), Watcher::new(clause_id, lit1));
493                self.watches
494                    .add(lit1.negate(), Watcher::new(clause_id, lit0));
495
496                return true;
497            }
498            _ => {}
499        }
500
501        // Add clause (3+ literals)
502        let clause_id = self.clauses.add_original(clause_lits.iter().copied());
503
504        // Track clause for incremental solving
505        if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
506            current_level_clauses.push(clause_id);
507        }
508
509        // Set up watches
510        let lit0 = clause_lits[0];
511        let lit1 = clause_lits[1];
512
513        self.watches
514            .add(lit0.negate(), Watcher::new(clause_id, lit1));
515        self.watches
516            .add(lit1.negate(), Watcher::new(clause_id, lit0));
517
518        true
519    }
520
521    /// Add a clause from DIMACS literals
522    pub fn add_clause_dimacs(&mut self, lits: &[i32]) -> bool {
523        self.add_clause(lits.iter().map(|&l| Lit::from_dimacs(l)))
524    }
525
526    /// Solve the SAT problem
527    pub fn solve(&mut self) -> SolverResult {
528        // Check if trivially unsatisfiable
529        if self.trivially_unsat {
530            return SolverResult::Unsat;
531        }
532
533        // Initial propagation
534        if self.propagate().is_some() {
535            return SolverResult::Unsat;
536        }
537
538        loop {
539            // Propagate
540            if let Some(conflict) = self.propagate() {
541                self.stats.conflicts += 1;
542                self.conflicts_since_inprocessing += 1;
543
544                if self.trail.decision_level() == 0 {
545                    return SolverResult::Unsat;
546                }
547
548                // Analyze conflict
549                let (backtrack_level, learnt_clause) = self.analyze(conflict);
550
551                // Backtrack with phase saving
552                self.backtrack_with_phase_saving(backtrack_level);
553
554                // Learn clause
555                if learnt_clause.len() == 1 {
556                    self.trail.assign_decision(learnt_clause[0]);
557                } else {
558                    // Compute LBD for the learned clause
559                    let lbd = self.compute_lbd(&learnt_clause);
560
561                    // Track recent LBD for Glucose-style and local restarts
562                    self.recent_lbd_sum += u64::from(lbd);
563                    self.recent_lbd_count += 1;
564                    self.global_lbd_sum += u64::from(lbd);
565                    self.global_lbd_count += 1;
566
567                    // Reset recent LBD tracking periodically
568                    if self.recent_lbd_count >= 5000 {
569                        self.recent_lbd_sum /= 2;
570                        self.recent_lbd_count /= 2;
571                    }
572
573                    let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
574                    self.stats.learned_clauses += 1;
575
576                    // Set LBD score for the clause
577                    if let Some(clause) = self.clauses.get_mut(clause_id) {
578                        clause.lbd = lbd;
579                    }
580
581                    // Track learned clause for potential deletion
582                    self.learned_clause_ids.push(clause_id);
583
584                    // Track clause for incremental solving
585                    if let Some(current_level_clauses) = self.assertion_clause_ids.last_mut() {
586                        current_level_clauses.push(clause_id);
587                    }
588
589                    // Watch first two literals
590                    let lit0 = learnt_clause[0];
591                    let lit1 = learnt_clause[1];
592                    self.watches
593                        .add(lit0.negate(), Watcher::new(clause_id, lit1));
594                    self.watches
595                        .add(lit1.negate(), Watcher::new(clause_id, lit0));
596
597                    // Propagate the asserting literal
598                    self.trail.assign_propagation(learnt_clause[0], clause_id);
599                }
600
601                // Decay activities
602                self.vsids.decay();
603                self.chb.decay();
604                self.lrb.decay();
605                self.lrb.on_conflict();
606                self.clauses.decay_activity(self.config.clause_decay);
607                // Increase clause bump increment (inverse of decay)
608                self.clause_bump_increment /= self.config.clause_decay;
609
610                // Track conflicts for clause deletion
611                self.conflicts_since_deletion += 1;
612
613                // Periodic clause database reduction
614                if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
615                    self.reduce_clause_database();
616                    self.conflicts_since_deletion = 0;
617
618                    // Vivification after clause database reduction (at level 0 after restart)
619                    if self.stats.restarts.is_multiple_of(10) {
620                        let saved_level = self.trail.decision_level();
621                        if saved_level == 0 {
622                            self.vivify_clauses();
623                        }
624                    }
625                }
626
627                // Check for restart
628                if self.stats.conflicts >= self.restart_threshold {
629                    self.restart();
630                }
631
632                // Periodic inprocessing
633                if self.config.enable_inprocessing
634                    && self.conflicts_since_inprocessing >= self.config.inprocessing_interval
635                {
636                    self.inprocess();
637                    self.conflicts_since_inprocessing = 0;
638                }
639            } else {
640                // No conflict - try to decide
641                if let Some(var) = self.pick_branch_var() {
642                    self.stats.decisions += 1;
643                    self.trail.new_decision_level();
644
645                    // Use phase saving with random polarity
646                    let polarity = if self.rand_bool(self.config.random_polarity_prob) {
647                        // Random polarity
648                        self.rand_bool(0.5)
649                    } else {
650                        // Saved phase
651                        self.phase[var.index()]
652                    };
653                    let lit = if polarity {
654                        Lit::pos(var)
655                    } else {
656                        Lit::neg(var)
657                    };
658                    self.trail.assign_decision(lit);
659                } else {
660                    // All variables assigned - SAT
661                    self.save_model();
662                    return SolverResult::Sat;
663                }
664            }
665        }
666    }
667
668    /// Solve with assumptions and return unsat core if UNSAT
669    ///
670    /// This is the key method for MaxSAT: it solves under assumptions and
671    /// if the result is UNSAT, returns the subset of assumptions in the core.
672    ///
673    /// # Arguments
674    /// * `assumptions` - Literals that must be true
675    ///
676    /// # Returns
677    /// * `(SolverResult, Option<Vec<Lit>>)` - Result and unsat core (if UNSAT)
678    pub fn solve_with_assumptions(
679        &mut self,
680        assumptions: &[Lit],
681    ) -> (SolverResult, Option<Vec<Lit>>) {
682        if self.trivially_unsat {
683            return (SolverResult::Unsat, Some(Vec::new()));
684        }
685
686        // Ensure all assumption variables exist
687        for &lit in assumptions {
688            while self.num_vars <= lit.var().index() {
689                self.new_var();
690            }
691        }
692
693        // Initial propagation at level 0
694        if self.propagate().is_some() {
695            return (SolverResult::Unsat, Some(Vec::new()));
696        }
697
698        // Create a new decision level for assumptions
699        let assumption_level_start = self.trail.decision_level();
700
701        // Assign assumptions as decisions
702        for (i, &lit) in assumptions.iter().enumerate() {
703            // Check if already assigned
704            let value = self.trail.lit_value(lit);
705            if value.is_true() {
706                continue; // Already satisfied
707            }
708            if value.is_false() {
709                // Conflict with assumption - extract core from conflicting assumptions
710                let core = self.extract_assumption_core(assumptions, i);
711                self.backtrack(assumption_level_start);
712                return (SolverResult::Unsat, Some(core));
713            }
714
715            // Make decision for assumption
716            self.trail.new_decision_level();
717            self.trail.assign_decision(lit);
718
719            // Propagate after each assumption
720            if let Some(_conflict) = self.propagate() {
721                // Conflict during assumption propagation
722                let core = self.analyze_assumption_conflict(assumptions);
723                self.backtrack(assumption_level_start);
724                return (SolverResult::Unsat, Some(core));
725            }
726        }
727
728        // Now solve normally
729        loop {
730            if let Some(conflict) = self.propagate() {
731                self.stats.conflicts += 1;
732
733                // Check if conflict involves assumptions
734                let backtrack_level = self.analyze_conflict_level(conflict);
735
736                if backtrack_level <= assumption_level_start {
737                    // Conflict forces backtracking past assumptions - UNSAT
738                    let core = self.analyze_assumption_conflict(assumptions);
739                    self.backtrack(assumption_level_start);
740                    return (SolverResult::Unsat, Some(core));
741                }
742
743                let (bt_level, learnt_clause) = self.analyze(conflict);
744                self.backtrack_with_phase_saving(bt_level.max(assumption_level_start + 1));
745                self.learn_clause(learnt_clause);
746
747                self.vsids.decay();
748                self.clauses.decay_activity(self.config.clause_decay);
749                self.handle_clause_deletion_and_restart_limited(assumption_level_start);
750            } else {
751                // No conflict - try to decide
752                if let Some(var) = self.pick_branch_var() {
753                    self.stats.decisions += 1;
754                    self.trail.new_decision_level();
755
756                    let polarity = if self.rand_bool(self.config.random_polarity_prob) {
757                        self.rand_bool(0.5)
758                    } else {
759                        self.phase.get(var.index()).copied().unwrap_or(false)
760                    };
761                    let lit = if polarity {
762                        Lit::pos(var)
763                    } else {
764                        Lit::neg(var)
765                    };
766                    self.trail.assign_decision(lit);
767                } else {
768                    // All variables assigned - SAT
769                    self.save_model();
770                    self.backtrack(assumption_level_start);
771                    return (SolverResult::Sat, None);
772                }
773            }
774        }
775    }
776
777    /// Extract a core of assumptions that caused a conflict
778    fn extract_assumption_core(&self, assumptions: &[Lit], conflict_idx: usize) -> Vec<Lit> {
779        // The conflicting assumption and any assumptions it depends on
780        let mut core = Vec::new();
781        let conflict_lit = assumptions[conflict_idx];
782
783        // Find assumptions that led to this conflict
784        for &lit in &assumptions[..=conflict_idx] {
785            if self.seen.get(lit.var().index()).copied().unwrap_or(false) || lit == conflict_lit {
786                core.push(lit);
787            }
788        }
789
790        // If core is empty, just return the conflicting assumption
791        if core.is_empty() {
792            core.push(conflict_lit);
793        }
794
795        core
796    }
797
798    /// Analyze conflict to find assumptions in the unsat core
799    fn analyze_assumption_conflict(&mut self, assumptions: &[Lit]) -> Vec<Lit> {
800        // Use seen flags to mark which assumptions are in the conflict
801        let mut core = Vec::new();
802
803        // Walk back through the trail to find conflicting assumptions
804        for &lit in assumptions {
805            let var = lit.var();
806            if var.index() < self.trail.assignments().len() {
807                let value = self.trail.lit_value(lit);
808                // If the negation of an assumption is implied, it's in the core
809                if value.is_false() || self.seen.get(var.index()).copied().unwrap_or(false) {
810                    core.push(lit);
811                }
812            }
813        }
814
815        // If no specific core found, return all assumptions
816        if core.is_empty() {
817            core.extend(assumptions.iter().copied());
818        }
819
820        core
821    }
822
823    /// Get the minimum backtrack level for a conflict
824    fn analyze_conflict_level(&self, conflict: ClauseId) -> u32 {
825        let clause = match self.clauses.get(conflict) {
826            Some(c) => c,
827            None => return 0,
828        };
829
830        let mut min_level = u32::MAX;
831        for lit in clause.lits.iter().copied() {
832            let level = self.trail.level(lit.var());
833            if level > 0 && level < min_level {
834                min_level = level;
835            }
836        }
837
838        if min_level == u32::MAX { 0 } else { min_level }
839    }
840
841    /// Handle clause deletion and restart, but don't backtrack past assumptions
842    fn handle_clause_deletion_and_restart_limited(&mut self, min_level: u32) {
843        self.conflicts_since_deletion += 1;
844
845        if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
846            self.reduce_clause_database();
847            self.conflicts_since_deletion = 0;
848        }
849
850        if self.stats.conflicts >= self.restart_threshold {
851            // Limited restart - don't backtrack past assumptions
852            self.backtrack(min_level);
853            self.stats.restarts += 1;
854            self.luby_index += 1;
855            self.restart_threshold =
856                self.stats.conflicts + self.config.restart_interval * Self::luby(self.luby_index);
857        }
858    }
859
860    /// Unit propagation using two-watched literals
861    fn propagate(&mut self) -> Option<ClauseId> {
862        while let Some(lit) = self.trail.next_to_propagate() {
863            self.stats.propagations += 1;
864
865            // First, propagate binary implications (faster)
866            let binary_implications = self.binary_graph.get(lit).to_vec();
867            for &(implied_lit, clause_id) in &binary_implications {
868                let value = self.trail.lit_value(implied_lit);
869                if value.is_false() {
870                    // Conflict in binary clause
871                    return Some(clause_id);
872                } else if !value.is_defined() {
873                    // Propagate
874                    self.trail.assign_propagation(implied_lit, clause_id);
875
876                    // Lazy hyper-binary resolution: check if we can learn a binary clause
877                    if self.config.enable_lazy_hyper_binary {
878                        self.check_hyper_binary_resolution(lit, implied_lit, clause_id);
879                    }
880                }
881            }
882
883            // Get watches for the negation of the propagated literal
884            let watches = std::mem::take(self.watches.get_mut(lit));
885
886            let mut i = 0;
887            while i < watches.len() {
888                let watcher = watches[i];
889
890                // Check blocker
891                if self.trail.lit_value(watcher.blocker).is_true() {
892                    i += 1;
893                    continue;
894                }
895
896                let clause = match self.clauses.get_mut(watcher.clause) {
897                    Some(c) if !c.deleted => c,
898                    _ => {
899                        i += 1;
900                        continue;
901                    }
902                };
903
904                // Make sure the false literal is at position 1
905                if clause.lits[0] == lit.negate() {
906                    clause.swap(0, 1);
907                }
908
909                // If first watch is true, clause is satisfied
910                let first = clause.lits[0];
911                if self.trail.lit_value(first).is_true() {
912                    // Update blocker
913                    self.watches
914                        .get_mut(lit)
915                        .push(Watcher::new(watcher.clause, first));
916                    i += 1;
917                    continue;
918                }
919
920                // Look for a new watch
921                let mut found = false;
922                for j in 2..clause.lits.len() {
923                    let l = clause.lits[j];
924                    if !self.trail.lit_value(l).is_false() {
925                        clause.swap(1, j);
926                        self.watches
927                            .add(clause.lits[1].negate(), Watcher::new(watcher.clause, first));
928                        found = true;
929                        break;
930                    }
931                }
932
933                if found {
934                    i += 1;
935                    continue;
936                }
937
938                // No new watch found - clause is unit or conflicting
939                self.watches
940                    .get_mut(lit)
941                    .push(Watcher::new(watcher.clause, first));
942
943                if self.trail.lit_value(first).is_false() {
944                    // Conflict
945                    // Put remaining watches back
946                    for j in (i + 1)..watches.len() {
947                        self.watches.get_mut(lit).push(watches[j]);
948                    }
949                    return Some(watcher.clause);
950                } else {
951                    // Unit propagation
952                    self.trail.assign_propagation(first, watcher.clause);
953
954                    // Lazy hyper-binary resolution
955                    if self.config.enable_lazy_hyper_binary {
956                        self.check_hyper_binary_resolution(lit, first, watcher.clause);
957                    }
958                }
959
960                i += 1;
961            }
962        }
963
964        None
965    }
966
967    /// Analyze conflict and learn clause
968    fn analyze(&mut self, conflict: ClauseId) -> (u32, SmallVec<[Lit; 16]>) {
969        self.learnt.clear();
970        self.learnt.push(Lit::from_code(0)); // Placeholder for asserting literal
971
972        let mut counter = 0;
973        let mut p = None;
974        let mut index = self.trail.assignments().len();
975        let current_level = self.trail.decision_level();
976
977        // Reset seen flags
978        for s in &mut self.seen {
979            *s = false;
980        }
981
982        let mut reason_clause = conflict;
983
984        loop {
985            // Process reason clause (must exist, as it's either conflict or a propagation reason)
986            let Some(clause) = self.clauses.get(reason_clause) else {
987                break; // Should not happen in valid state
988            };
989            let start = if p.is_some() { 1 } else { 0 };
990            let is_learned = clause.learned;
991
992            // Record clause usage for tier promotion and bump activity (if it's a learned clause)
993            if is_learned && let Some(clause_mut) = self.clauses.get_mut(reason_clause) {
994                clause_mut.record_usage();
995                // Promote to Core if LBD ≤ 2 (GLUE clause)
996                if clause_mut.lbd <= 2 {
997                    clause_mut.promote_to_core();
998                }
999                // Bump clause activity (MapleSAT-style)
1000                clause_mut.activity += self.clause_bump_increment;
1001            }
1002
1003            let Some(clause) = self.clauses.get(reason_clause) else {
1004                break;
1005            };
1006            for &lit in &clause.lits[start..] {
1007                let var = lit.var();
1008                let level = self.trail.level(var);
1009
1010                if !self.seen[var.index()] && level > 0 {
1011                    self.seen[var.index()] = true;
1012                    self.vsids.bump(var);
1013                    self.chb.bump(var);
1014                    self.lrb.on_reason(var);
1015
1016                    if level == current_level {
1017                        counter += 1;
1018                    } else {
1019                        self.learnt.push(lit.negate());
1020                    }
1021                }
1022            }
1023
1024            // Find next literal to analyze
1025            let mut current_lit;
1026            loop {
1027                index -= 1;
1028                current_lit = self.trail.assignments()[index];
1029                p = Some(current_lit);
1030                if self.seen[current_lit.var().index()] {
1031                    break;
1032                }
1033            }
1034
1035            counter -= 1;
1036            if counter == 0 {
1037                break;
1038            }
1039
1040            let var = current_lit.var();
1041            match self.trail.reason(var) {
1042                Reason::Propagation(c) => reason_clause = c,
1043                _ => break,
1044            }
1045        }
1046
1047        // Set asserting literal (p is guaranteed to be Some at this point)
1048        if let Some(lit) = p {
1049            self.learnt[0] = lit.negate();
1050        }
1051
1052        // Minimize learnt clause using recursive resolution
1053        self.minimize_learnt_clause();
1054
1055        // Calculate assertion level (traditional backtrack level)
1056        let assertion_level = if self.learnt.len() == 1 {
1057            0
1058        } else {
1059            // Find second highest level
1060            let mut max_level = 0;
1061            let mut max_idx = 1;
1062            for (i, &lit) in self.learnt.iter().enumerate().skip(1) {
1063                let level = self.trail.level(lit.var());
1064                if level > max_level {
1065                    max_level = level;
1066                    max_idx = i;
1067                }
1068            }
1069            // Move second watch to position 1
1070            self.learnt.swap(1, max_idx);
1071            max_level
1072        };
1073
1074        // Apply chronological backtracking if enabled
1075        let backtrack_level = self.chrono_backtrack.compute_backtrack_level(
1076            &self.trail,
1077            &self.learnt,
1078            assertion_level,
1079        );
1080
1081        // Track chronological vs non-chronological backtracks
1082        if backtrack_level != assertion_level {
1083            self.stats.chrono_backtracks += 1;
1084        } else {
1085            self.stats.non_chrono_backtracks += 1;
1086        }
1087
1088        (backtrack_level, self.learnt.clone())
1089    }
1090
1091    /// Pick next variable to branch on
1092    fn pick_branch_var(&mut self) -> Option<Var> {
1093        if self.config.use_lrb_branching {
1094            // Use LRB branching
1095            while let Some(var) = self.lrb.select() {
1096                if !self.trail.is_assigned(var) {
1097                    self.lrb.on_assign(var);
1098                    return Some(var);
1099                }
1100            }
1101        } else if self.config.use_chb_branching {
1102            // Use CHB branching
1103            // Rebuild heap periodically to reflect score changes
1104            if self.stats.decisions.is_multiple_of(100) {
1105                self.chb.rebuild_heap();
1106            }
1107
1108            while let Some(var) = self.chb.pop_max() {
1109                if !self.trail.is_assigned(var) {
1110                    return Some(var);
1111                }
1112            }
1113        } else {
1114            // Use VSIDS branching
1115            while let Some(var) = self.vsids.pop_max() {
1116                if !self.trail.is_assigned(var) {
1117                    return Some(var);
1118                }
1119            }
1120        }
1121        None
1122    }
1123
1124    /// Minimize the learned clause by removing redundant literals
1125    ///
1126    /// A literal can be removed if it is implied by the remaining literals.
1127    /// We use a recursive check: a literal l is redundant if its reason clause
1128    /// contains only literals that are either:
1129    /// - Already in the learnt clause (marked as seen)
1130    /// - At decision level 0 (always true in the learned clause context)
1131    /// - Themselves redundant (recursive check)
1132    ///
1133    /// This also performs clause strengthening by checking for stronger implications
1134    fn minimize_learnt_clause(&mut self) {
1135        if self.learnt.len() <= 2 {
1136            // Don't minimize very small clauses
1137            return;
1138        }
1139
1140        let original_len = self.learnt.len();
1141
1142        // Mark all literals in the learned clause as "in clause"
1143        // We use analyze_stack to track literals to check
1144        self.analyze_stack.clear();
1145
1146        // Phase 1: Basic minimization - remove redundant literals
1147        let mut j = 1; // Write position
1148        for i in 1..self.learnt.len() {
1149            let lit = self.learnt[i];
1150            if self.lit_is_redundant(lit) {
1151                // Skip this literal (it's redundant)
1152            } else {
1153                // Keep this literal
1154                self.learnt[j] = lit;
1155                j += 1;
1156            }
1157        }
1158        self.learnt.truncate(j);
1159
1160        // Phase 2: Clause strengthening - check for self-subsuming resolution
1161        // If the clause contains both l and ~l' where l' is in a reason clause,
1162        // we might be able to strengthen the clause
1163        self.strengthen_learnt_clause();
1164
1165        // Track minimization statistics
1166        let final_len = self.learnt.len();
1167        if final_len < original_len {
1168            self.stats.minimizations += 1;
1169            self.stats.literals_removed += (original_len - final_len) as u64;
1170        }
1171    }
1172
1173    /// Strengthen the learned clause using on-the-fly self-subsuming resolution
1174    fn strengthen_learnt_clause(&mut self) {
1175        if self.learnt.len() <= 2 {
1176            return;
1177        }
1178
1179        // Check each literal to see if we can strengthen by resolution
1180        let mut j = 1;
1181        for i in 1..self.learnt.len() {
1182            let lit = self.learnt[i];
1183            let var = lit.var();
1184
1185            // Check if this literal can be strengthened
1186            if let Reason::Propagation(reason_id) = self.trail.reason(var)
1187                && let Some(reason_clause) = self.clauses.get(reason_id)
1188                && reason_clause.lits.len() == 2
1189            {
1190                // Binary reason: one literal is lit, the other is the implied literal
1191                let other_lit = if reason_clause.lits[0] == lit.negate() {
1192                    reason_clause.lits[1]
1193                } else if reason_clause.lits[1] == lit.negate() {
1194                    reason_clause.lits[0]
1195                } else {
1196                    // Keep the literal
1197                    self.learnt[j] = lit;
1198                    j += 1;
1199                    continue;
1200                };
1201
1202                // If other_lit is already in the learned clause at level 0,
1203                // we can remove lit
1204                if self.trail.level(other_lit.var()) == 0 && self.seen[other_lit.var().index()] {
1205                    // Skip this literal (strengthened)
1206                    continue;
1207                }
1208            }
1209
1210            // Keep this literal
1211            self.learnt[j] = lit;
1212            j += 1;
1213        }
1214        self.learnt.truncate(j);
1215    }
1216
1217    /// Check if a literal is redundant in the learned clause
1218    ///
1219    /// A literal is redundant if its reason clause only contains:
1220    /// - Literals marked as seen (in the learned clause)
1221    /// - Literals at decision level 0
1222    /// - Literals that are themselves redundant (recursive)
1223    fn lit_is_redundant(&mut self, lit: Lit) -> bool {
1224        let var = lit.var();
1225
1226        // Decision variables and theory propagations are not redundant
1227        let reason = match self.trail.reason(var) {
1228            Reason::Decision => return false,
1229            Reason::Theory => return false, // Theory propagations can't be minimized
1230            Reason::Propagation(c) => c,
1231        };
1232
1233        let reason_clause = match self.clauses.get(reason) {
1234            Some(c) => c,
1235            None => return false,
1236        };
1237
1238        // Check all literals in the reason clause
1239        for &reason_lit in &reason_clause.lits {
1240            if reason_lit == lit.negate() {
1241                // Skip the literal we're analyzing
1242                continue;
1243            }
1244
1245            let reason_var = reason_lit.var();
1246
1247            // Level 0 literals are always OK
1248            if self.trail.level(reason_var) == 0 {
1249                continue;
1250            }
1251
1252            // If the literal is in the learned clause (seen), it's OK
1253            if self.seen[reason_var.index()] {
1254                continue;
1255            }
1256
1257            // Otherwise, this literal prevents minimization
1258            // (A full recursive check would be more powerful but more expensive)
1259            return false;
1260        }
1261
1262        true
1263    }
1264
1265    /// Backtrack with phase saving
1266    fn backtrack_with_phase_saving(&mut self, level: u32) {
1267        // Save phases before backtracking
1268        let phase = &mut self.phase;
1269        let lrb = &mut self.lrb;
1270        self.trail.backtrack_to_with_callback(level, |lit| {
1271            let var = lit.var();
1272            if var.index() < phase.len() {
1273                phase[var.index()] = lit.is_pos();
1274            }
1275            // Re-insert variable into LRB heap
1276            lrb.unassign(var);
1277        });
1278    }
1279
1280    /// Backtrack to a given level without saving phases
1281    fn backtrack(&mut self, level: u32) {
1282        self.trail.backtrack_to(level);
1283    }
1284
1285    /// Compute the Luby sequence value for index i (1-indexed: luby(1)=1, luby(2)=1, ...)
1286    /// Sequence: 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, ...
1287    /// For 0-indexed input, we add 1 internally.
1288    fn luby(i: u64) -> u64 {
1289        let i = i + 1; // Convert to 1-indexed
1290
1291        // Find k such that 2^k - 1 >= i
1292        let mut k = 1u32;
1293        while (1u64 << k) - 1 < i {
1294            k += 1;
1295        }
1296
1297        let seq_len = (1u64 << k) - 1;
1298
1299        if i == seq_len {
1300            // i is exactly 2^k - 1, return 2^(k-1)
1301            1u64 << (k - 1)
1302        } else {
1303            // Recurse: luby(i) = luby(i - (2^(k-1) - 1))
1304            // The sequence up to 2^k - 1 is: luby(1..2^(k-1)-1), luby(1..2^(k-1)-1), 2^(k-1)
1305            let half_len = (1u64 << (k - 1)) - 1;
1306            if i <= half_len {
1307                Self::luby(i - 1) // Already 0-indexed internally
1308            } else if i <= 2 * half_len {
1309                Self::luby(i - half_len - 1)
1310            } else {
1311                1u64 << (k - 1)
1312            }
1313        }
1314    }
1315
1316    /// Restart
1317    fn restart(&mut self) {
1318        self.stats.restarts += 1;
1319        self.backtrack_with_phase_saving(0);
1320
1321        // Calculate next restart threshold based on strategy
1322        match self.config.restart_strategy {
1323            RestartStrategy::Luby => {
1324                self.luby_index += 1;
1325                self.restart_threshold = self.stats.conflicts
1326                    + Self::luby(self.luby_index) * self.config.restart_interval;
1327            }
1328            RestartStrategy::Geometric => {
1329                let current_interval = if self.restart_threshold > self.stats.conflicts {
1330                    self.restart_threshold - self.stats.conflicts
1331                } else {
1332                    self.config.restart_interval
1333                };
1334                let next_interval =
1335                    (current_interval as f64 * self.config.restart_multiplier) as u64;
1336                self.restart_threshold = self.stats.conflicts + next_interval;
1337            }
1338            RestartStrategy::Glucose => {
1339                // Glucose-style dynamic restarts based on LBD
1340                // Restart when recent average LBD is higher than global average
1341                // For now, use geometric with dynamic adjustment
1342                let current_interval = if self.restart_threshold > self.stats.conflicts {
1343                    self.restart_threshold - self.stats.conflicts
1344                } else {
1345                    self.config.restart_interval
1346                };
1347
1348                // Adjust based on recent LBD trend
1349                let next_interval = if self.recent_lbd_count > 50 {
1350                    let recent_avg = self.recent_lbd_sum / self.recent_lbd_count.max(1);
1351                    // If recent LBD is low (good), increase interval; if high, decrease
1352                    if recent_avg < 5 {
1353                        // Good quality clauses - increase interval
1354                        ((current_interval as f64) * 1.1) as u64
1355                    } else {
1356                        // Poor quality clauses - decrease interval
1357                        ((current_interval as f64) * 0.9) as u64
1358                    }
1359                } else {
1360                    current_interval
1361                };
1362
1363                self.restart_threshold = self.stats.conflicts + next_interval.max(100);
1364            }
1365            RestartStrategy::LocalLbd => {
1366                // Local restarts based on LBD
1367                // Check if we should do a local restart
1368                self.conflicts_since_local_restart += 1;
1369
1370                if self.conflicts_since_local_restart >= 50 && self.should_local_restart() {
1371                    // Perform local restart - backtrack to a safe level, not to 0
1372                    let local_level = self.compute_local_restart_level();
1373                    self.backtrack_with_phase_saving(local_level);
1374                    self.conflicts_since_local_restart = 0;
1375                    // Reset recent LBD for next window
1376                    self.recent_lbd_sum = 0;
1377                    self.recent_lbd_count = 0;
1378                } else {
1379                    // Standard restart if too many conflicts
1380                    let current_interval = if self.restart_threshold > self.stats.conflicts {
1381                        self.restart_threshold - self.stats.conflicts
1382                    } else {
1383                        self.config.restart_interval
1384                    };
1385                    self.restart_threshold = self.stats.conflicts + current_interval;
1386                }
1387                return; // Don't do full backtrack to 0
1388            }
1389        }
1390
1391        // Re-add all unassigned variables to VSIDS heap
1392        for i in 0..self.num_vars {
1393            let var = Var::new(i as u32);
1394            if !self.trail.is_assigned(var) && !self.vsids.contains(var) {
1395                self.vsids.insert(var);
1396            }
1397        }
1398    }
1399
1400    /// Check if we should perform a local restart
1401    /// Returns true if recent average LBD is significantly higher than global average
1402    fn should_local_restart(&self) -> bool {
1403        if self.recent_lbd_count < 50 || self.global_lbd_count < 100 {
1404            return false;
1405        }
1406
1407        let recent_avg = self.recent_lbd_sum / self.recent_lbd_count.max(1);
1408        let global_avg = self.global_lbd_sum / self.global_lbd_count.max(1);
1409
1410        // Local restart if recent average is 1.25x higher than global average
1411        recent_avg * 4 > global_avg * 5
1412    }
1413
1414    /// Compute the level to backtrack to for local restart
1415    /// Use a level that preserves some of the search progress
1416    fn compute_local_restart_level(&self) -> u32 {
1417        let current_level = self.trail.decision_level();
1418
1419        // Backtrack to about 20% of current depth to preserve some work
1420        if current_level > 5 {
1421            current_level / 5
1422        } else {
1423            0
1424        }
1425    }
1426
1427    /// Compute LBD (Literal Block Distance) of a clause
1428    /// LBD is the number of distinct decision levels in the clause
1429    fn compute_lbd(&mut self, lits: &[Lit]) -> u32 {
1430        self.lbd_mark += 1;
1431        let mark = self.lbd_mark;
1432
1433        let mut count = 0u32;
1434        for &lit in lits {
1435            let level = self.trail.level(lit.var()) as usize;
1436            if level < self.level_marks.len() && self.level_marks[level] != mark {
1437                self.level_marks[level] = mark;
1438                count += 1;
1439            }
1440        }
1441
1442        count
1443    }
1444
1445    /// Reduce the learned clause database using tier-based deletion strategy
1446    /// - Core tier (Tier 1): Rarely deleted, only if very inactive
1447    /// - Mid tier (Tier 2): Delete ~30% based on activity
1448    /// - Local tier (Tier 3): Delete ~75% based on activity
1449    fn reduce_clause_database(&mut self) {
1450        use crate::clause::ClauseTier;
1451
1452        let mut core_candidates: Vec<(ClauseId, f64)> = Vec::new();
1453        let mut mid_candidates: Vec<(ClauseId, f64)> = Vec::new();
1454        let mut local_candidates: Vec<(ClauseId, f64)> = Vec::new();
1455
1456        for &cid in &self.learned_clause_ids {
1457            if let Some(clause) = self.clauses.get(cid) {
1458                if clause.deleted {
1459                    continue;
1460                }
1461
1462                // Don't delete binary clauses (very useful)
1463                if clause.lits.len() <= 2 {
1464                    continue;
1465                }
1466
1467                // Check if clause is currently a reason for any assignment
1468                // (We can't delete reason clauses)
1469                let is_reason = clause.lits.iter().any(|&lit| {
1470                    let var = lit.var();
1471                    if self.trail.is_assigned(var) {
1472                        matches!(self.trail.reason(var), Reason::Propagation(r) if r == cid)
1473                    } else {
1474                        false
1475                    }
1476                });
1477
1478                if !is_reason {
1479                    match clause.tier {
1480                        ClauseTier::Core => core_candidates.push((cid, clause.activity)),
1481                        ClauseTier::Mid => mid_candidates.push((cid, clause.activity)),
1482                        ClauseTier::Local => local_candidates.push((cid, clause.activity)),
1483                    }
1484                }
1485            }
1486        }
1487
1488        // Sort by activity (ascending) - delete low-activity clauses first
1489        core_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1490        mid_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1491        local_candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1492
1493        // Delete different percentages from each tier
1494        // Core: Delete bottom 10% (very conservative)
1495        let num_core_delete = core_candidates.len() / 10;
1496        // Mid: Delete bottom 30%
1497        let num_mid_delete = (mid_candidates.len() * 3) / 10;
1498        // Local: Delete bottom 75% (very aggressive)
1499        let num_local_delete = (local_candidates.len() * 3) / 4;
1500
1501        for (cid, _) in core_candidates.iter().take(num_core_delete) {
1502            self.clauses.remove(*cid);
1503            self.stats.deleted_clauses += 1;
1504        }
1505
1506        for (cid, _) in mid_candidates.iter().take(num_mid_delete) {
1507            self.clauses.remove(*cid);
1508            self.stats.deleted_clauses += 1;
1509        }
1510
1511        for (cid, _) in local_candidates.iter().take(num_local_delete) {
1512            self.clauses.remove(*cid);
1513            self.stats.deleted_clauses += 1;
1514        }
1515
1516        // Clean up learned_clause_ids (remove deleted clauses)
1517        self.learned_clause_ids
1518            .retain(|&cid| self.clauses.get(cid).is_some_and(|c| !c.deleted));
1519    }
1520
1521    /// Save the model
1522    fn save_model(&mut self) {
1523        self.model.resize(self.num_vars, LBool::Undef);
1524        for i in 0..self.num_vars {
1525            self.model[i] = self.trail.value(Var::new(i as u32));
1526        }
1527    }
1528
1529    /// Get the model (if sat)
1530    #[must_use]
1531    pub fn model(&self) -> &[LBool] {
1532        &self.model
1533    }
1534
1535    /// Get the value of a variable in the model
1536    #[must_use]
1537    pub fn model_value(&self, var: Var) -> LBool {
1538        self.model.get(var.index()).copied().unwrap_or(LBool::Undef)
1539    }
1540
1541    /// Get statistics
1542    #[must_use]
1543    pub fn stats(&self) -> &SolverStats {
1544        &self.stats
1545    }
1546
1547    /// Get number of variables
1548    #[must_use]
1549    pub fn num_vars(&self) -> usize {
1550        self.num_vars
1551    }
1552
1553    /// Get number of clauses
1554    #[must_use]
1555    pub fn num_clauses(&self) -> usize {
1556        self.clauses.len()
1557    }
1558
1559    /// Push a new assertion level (for incremental solving)
1560    ///
1561    /// This saves the current state so that clauses added after this point
1562    /// can be removed with pop(). Automatically backtracks to decision level 0
1563    /// to ensure a clean state for adding new constraints.
1564    pub fn push(&mut self) {
1565        // Backtrack to level 0 to ensure clean state
1566        // This is necessary because solve() may leave assignments on the trail
1567        self.trail.backtrack_to(0);
1568
1569        self.assertion_levels.push(self.clauses.num_original());
1570        self.assertion_trail_sizes.push(self.trail.size());
1571        self.assertion_clause_ids.push(Vec::new());
1572    }
1573
1574    /// Pop to previous assertion level
1575    pub fn pop(&mut self) {
1576        if self.assertion_levels.len() > 1 {
1577            self.assertion_levels.pop();
1578
1579            // Get the trail size to backtrack to
1580            let trail_size = self.assertion_trail_sizes.pop().unwrap_or(0);
1581
1582            // Remove all clauses added at this assertion level
1583            if let Some(clause_ids_to_remove) = self.assertion_clause_ids.pop() {
1584                for clause_id in clause_ids_to_remove {
1585                    // Remove from clause database
1586                    self.clauses.remove(clause_id);
1587
1588                    // Remove from learned clause tracking if it's a learned clause
1589                    self.learned_clause_ids.retain(|&id| id != clause_id);
1590
1591                    // Note: Watch lists will be cleaned up naturally during propagation
1592                    // as they check if clauses are deleted before using them
1593                }
1594            }
1595
1596            // Backtrack trail to the exact size it was at push()
1597            // This properly handles unit clauses that were added after push
1598            self.trail.backtrack_to_size(trail_size);
1599
1600            // Ensure we're at decision level 0
1601            self.trail.backtrack_to(0);
1602
1603            // Clear the trivially_unsat flag as we've removed problematic clauses
1604            self.trivially_unsat = false;
1605        }
1606    }
1607
1608    /// Backtrack to decision level 0 (for AllSAT enumeration)
1609    ///
1610    /// This is necessary after a SAT result before adding blocking clauses
1611    /// to ensure the new clauses can trigger propagation correctly.
1612    pub fn backtrack_to_root(&mut self) {
1613        self.trail.backtrack_to(0);
1614    }
1615
1616    /// Reset the solver
1617    pub fn reset(&mut self) {
1618        self.clauses = ClauseDatabase::new();
1619        self.trail.clear();
1620        self.watches.clear();
1621        self.vsids.clear();
1622        self.chb.clear();
1623        self.stats = SolverStats::default();
1624        self.learnt.clear();
1625        self.seen.clear();
1626        self.analyze_stack.clear();
1627        self.assertion_levels.clear();
1628        self.assertion_levels.push(0);
1629        self.assertion_trail_sizes.clear();
1630        self.assertion_trail_sizes.push(0);
1631        self.assertion_clause_ids.clear();
1632        self.assertion_clause_ids.push(Vec::new());
1633        self.model.clear();
1634        self.num_vars = 0;
1635        self.restart_threshold = self.config.restart_interval;
1636        self.trivially_unsat = false;
1637        self.phase.clear();
1638        self.luby_index = 0;
1639        self.level_marks.clear();
1640        self.lbd_mark = 0;
1641        self.learned_clause_ids.clear();
1642        self.conflicts_since_deletion = 0;
1643        self.rng_state = 0x853c_49e6_748f_ea9b;
1644        self.recent_lbd_sum = 0;
1645        self.recent_lbd_count = 0;
1646        self.binary_graph.clear();
1647        self.global_lbd_sum = 0;
1648        self.global_lbd_count = 0;
1649        self.conflicts_since_local_restart = 0;
1650    }
1651
1652    /// Solve with theory integration via callbacks
1653    ///
1654    /// This implements the CDCL(T) loop:
1655    /// 1. BCP (Boolean Constraint Propagation)
1656    /// 2. Theory propagation (via callback)
1657    /// 3. On conflict: analyze and learn
1658    /// 4. Decision
1659    /// 5. Final theory check when all vars assigned
1660    pub fn solve_with_theory<T: TheoryCallback>(&mut self, theory: &mut T) -> SolverResult {
1661        if self.trivially_unsat {
1662            return SolverResult::Unsat;
1663        }
1664
1665        // Initial propagation
1666        if self.propagate().is_some() {
1667            return SolverResult::Unsat;
1668        }
1669
1670        loop {
1671            // Boolean propagation
1672            if let Some(conflict) = self.propagate() {
1673                self.stats.conflicts += 1;
1674
1675                if self.trail.decision_level() == 0 {
1676                    return SolverResult::Unsat;
1677                }
1678
1679                let (backtrack_level, learnt_clause) = self.analyze(conflict);
1680                theory.on_backtrack(backtrack_level);
1681                self.backtrack_with_phase_saving(backtrack_level);
1682                self.learn_clause(learnt_clause);
1683
1684                self.vsids.decay();
1685                self.clauses.decay_activity(self.config.clause_decay);
1686                self.handle_clause_deletion_and_restart();
1687                continue;
1688            }
1689
1690            // Theory propagation check after each assignment
1691            loop {
1692                // Get newly assigned literals and notify theory
1693                let assignments = self.trail.assignments().to_vec();
1694                let mut theory_conflict = None;
1695                let mut theory_propagations = Vec::new();
1696
1697                // Check each new assignment with theory
1698                for &lit in &assignments {
1699                    match theory.on_assignment(lit) {
1700                        TheoryCheckResult::Sat => {}
1701                        TheoryCheckResult::Conflict(conflict_lits) => {
1702                            theory_conflict = Some(conflict_lits);
1703                            break;
1704                        }
1705                        TheoryCheckResult::Propagated(props) => {
1706                            theory_propagations.extend(props);
1707                        }
1708                    }
1709                }
1710
1711                // Handle theory conflict
1712                if let Some(conflict_lits) = theory_conflict {
1713                    self.stats.conflicts += 1;
1714
1715                    if self.trail.decision_level() == 0 {
1716                        return SolverResult::Unsat;
1717                    }
1718
1719                    let (backtrack_level, learnt_clause) =
1720                        self.analyze_theory_conflict(&conflict_lits);
1721                    theory.on_backtrack(backtrack_level);
1722                    self.backtrack_with_phase_saving(backtrack_level);
1723                    self.learn_clause(learnt_clause);
1724
1725                    self.vsids.decay();
1726                    self.clauses.decay_activity(self.config.clause_decay);
1727                    self.handle_clause_deletion_and_restart();
1728                    continue;
1729                }
1730
1731                // Handle theory propagations
1732                let mut made_propagation = false;
1733                for (lit, reason_lits) in theory_propagations {
1734                    if !self.trail.is_assigned(lit.var()) {
1735                        // Add reason clause and propagate
1736                        let clause_id = self.add_theory_reason_clause(&reason_lits, lit);
1737                        self.trail.assign_propagation(lit, clause_id);
1738                        made_propagation = true;
1739                    }
1740                }
1741
1742                if made_propagation {
1743                    // Re-run Boolean propagation
1744                    if let Some(conflict) = self.propagate() {
1745                        self.stats.conflicts += 1;
1746
1747                        if self.trail.decision_level() == 0 {
1748                            return SolverResult::Unsat;
1749                        }
1750
1751                        let (backtrack_level, learnt_clause) = self.analyze(conflict);
1752                        theory.on_backtrack(backtrack_level);
1753                        self.backtrack_with_phase_saving(backtrack_level);
1754                        self.learn_clause(learnt_clause);
1755
1756                        self.vsids.decay();
1757                        self.clauses.decay_activity(self.config.clause_decay);
1758                        self.handle_clause_deletion_and_restart();
1759                    }
1760                    continue;
1761                }
1762
1763                break;
1764            }
1765
1766            // Try to decide
1767            if let Some(var) = self.pick_branch_var() {
1768                self.stats.decisions += 1;
1769                self.trail.new_decision_level();
1770                let new_level = self.trail.decision_level();
1771                theory.on_new_level(new_level);
1772
1773                let polarity = if self.rand_bool(self.config.random_polarity_prob) {
1774                    self.rand_bool(0.5)
1775                } else {
1776                    self.phase[var.index()]
1777                };
1778                let lit = if polarity {
1779                    Lit::pos(var)
1780                } else {
1781                    Lit::neg(var)
1782                };
1783                self.trail.assign_decision(lit);
1784            } else {
1785                // All variables assigned - do final theory check
1786                match theory.final_check() {
1787                    TheoryCheckResult::Sat => {
1788                        self.save_model();
1789                        return SolverResult::Sat;
1790                    }
1791                    TheoryCheckResult::Conflict(conflict_lits) => {
1792                        self.stats.conflicts += 1;
1793
1794                        if self.trail.decision_level() == 0 {
1795                            return SolverResult::Unsat;
1796                        }
1797
1798                        let (backtrack_level, learnt_clause) =
1799                            self.analyze_theory_conflict(&conflict_lits);
1800                        theory.on_backtrack(backtrack_level);
1801                        self.backtrack_with_phase_saving(backtrack_level);
1802                        self.learn_clause(learnt_clause);
1803
1804                        self.vsids.decay();
1805                        self.clauses.decay_activity(self.config.clause_decay);
1806                        self.handle_clause_deletion_and_restart();
1807                    }
1808                    TheoryCheckResult::Propagated(props) => {
1809                        // Handle late propagations
1810                        for (lit, reason_lits) in props {
1811                            if !self.trail.is_assigned(lit.var()) {
1812                                let clause_id = self.add_theory_reason_clause(&reason_lits, lit);
1813                                self.trail.assign_propagation(lit, clause_id);
1814                            }
1815                        }
1816                    }
1817                }
1818            }
1819        }
1820    }
1821
1822    /// Analyze a theory conflict (given as a list of literals that are all false)
1823    fn analyze_theory_conflict(&mut self, conflict_lits: &[Lit]) -> (u32, SmallVec<[Lit; 16]>) {
1824        self.learnt.clear();
1825        self.learnt.push(Lit::from_code(0)); // Placeholder
1826
1827        let mut counter = 0;
1828        let current_level = self.trail.decision_level();
1829
1830        // Reset seen flags
1831        for s in &mut self.seen {
1832            *s = false;
1833        }
1834
1835        // Process conflict literals
1836        for &lit in conflict_lits {
1837            let var = lit.var();
1838            let level = self.trail.level(var);
1839
1840            if !self.seen[var.index()] && level > 0 {
1841                self.seen[var.index()] = true;
1842                self.vsids.bump(var);
1843                self.chb.bump(var);
1844
1845                if level == current_level {
1846                    counter += 1;
1847                } else {
1848                    self.learnt.push(lit.negate());
1849                }
1850            }
1851        }
1852
1853        // Find UIP by walking back through trail
1854        let mut index = self.trail.assignments().len();
1855        let mut p = None;
1856
1857        while counter > 0 {
1858            index -= 1;
1859            let current_lit = self.trail.assignments()[index];
1860            p = Some(current_lit);
1861            let var = current_lit.var();
1862
1863            if self.seen[var.index()] {
1864                counter -= 1;
1865
1866                if counter > 0
1867                    && let Reason::Propagation(reason_clause) = self.trail.reason(var)
1868                    && let Some(clause) = self.clauses.get(reason_clause)
1869                {
1870                    // Get reason and process its literals
1871                    for &lit in &clause.lits[1..] {
1872                        let reason_var = lit.var();
1873                        let level = self.trail.level(reason_var);
1874
1875                        if !self.seen[reason_var.index()] && level > 0 {
1876                            self.seen[reason_var.index()] = true;
1877                            self.vsids.bump(reason_var);
1878                            self.chb.bump(reason_var);
1879
1880                            if level == current_level {
1881                                counter += 1;
1882                            } else {
1883                                self.learnt.push(lit.negate());
1884                            }
1885                        }
1886                    }
1887                }
1888            }
1889        }
1890
1891        // Set asserting literal
1892        if let Some(uip) = p {
1893            self.learnt[0] = uip.negate();
1894        }
1895
1896        // Minimize
1897        self.minimize_learnt_clause();
1898
1899        // Calculate backtrack level
1900        let backtrack_level = if self.learnt.len() == 1 {
1901            0
1902        } else {
1903            let mut max_level = 0;
1904            let mut max_idx = 1;
1905            for (i, &lit) in self.learnt.iter().enumerate().skip(1) {
1906                let level = self.trail.level(lit.var());
1907                if level > max_level {
1908                    max_level = level;
1909                    max_idx = i;
1910                }
1911            }
1912            self.learnt.swap(1, max_idx);
1913            max_level
1914        };
1915
1916        (backtrack_level, self.learnt.clone())
1917    }
1918
1919    /// Add a theory reason clause
1920    /// The clause is: reason_lits[0] OR reason_lits[1] OR ... OR propagated_lit
1921    fn add_theory_reason_clause(&mut self, reason_lits: &[Lit], propagated_lit: Lit) -> ClauseId {
1922        let mut clause_lits: SmallVec<[Lit; 8]> = SmallVec::new();
1923        clause_lits.push(propagated_lit);
1924        for &lit in reason_lits {
1925            clause_lits.push(lit.negate());
1926        }
1927
1928        let clause_id = self.clauses.add_learned(clause_lits.iter().copied());
1929
1930        // Set up watches
1931        if clause_lits.len() >= 2 {
1932            let lit0 = clause_lits[0];
1933            let lit1 = clause_lits[1];
1934            self.watches
1935                .add(lit0.negate(), Watcher::new(clause_id, lit1));
1936            self.watches
1937                .add(lit1.negate(), Watcher::new(clause_id, lit0));
1938        }
1939
1940        clause_id
1941    }
1942
1943    /// Learn a clause and set up watches
1944    /// Includes on-the-fly subsumption check
1945    fn learn_clause(&mut self, learnt_clause: SmallVec<[Lit; 16]>) {
1946        if learnt_clause.len() == 1 {
1947            self.trail.assign_decision(learnt_clause[0]);
1948            self.stats.unit_clauses += 1;
1949            self.stats.learned_clauses += 1;
1950        } else if learnt_clause.len() == 2 {
1951            // Binary learned clause - add to binary implication graph
1952            let lbd = self.compute_lbd(&learnt_clause);
1953            let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
1954            self.stats.learned_clauses += 1;
1955            self.stats.binary_clauses += 1;
1956            self.stats.total_lbd += lbd as u64;
1957
1958            if let Some(clause) = self.clauses.get_mut(clause_id) {
1959                clause.lbd = lbd;
1960            }
1961
1962            self.learned_clause_ids.push(clause_id);
1963
1964            let lit0 = learnt_clause[0];
1965            let lit1 = learnt_clause[1];
1966
1967            // Add to binary graph
1968            self.binary_graph.add(lit0.negate(), lit1, clause_id);
1969            self.binary_graph.add(lit1.negate(), lit0, clause_id);
1970
1971            self.watches
1972                .add(lit0.negate(), Watcher::new(clause_id, lit1));
1973            self.watches
1974                .add(lit1.negate(), Watcher::new(clause_id, lit0));
1975
1976            self.trail.assign_propagation(learnt_clause[0], clause_id);
1977        } else {
1978            let lbd = self.compute_lbd(&learnt_clause);
1979            self.stats.total_lbd += lbd as u64;
1980            let clause_id = self.clauses.add_learned(learnt_clause.iter().copied());
1981            self.stats.learned_clauses += 1;
1982
1983            if let Some(clause) = self.clauses.get_mut(clause_id) {
1984                clause.lbd = lbd;
1985            }
1986
1987            self.learned_clause_ids.push(clause_id);
1988
1989            let lit0 = learnt_clause[0];
1990            let lit1 = learnt_clause[1];
1991            self.watches
1992                .add(lit0.negate(), Watcher::new(clause_id, lit1));
1993            self.watches
1994                .add(lit1.negate(), Watcher::new(clause_id, lit0));
1995
1996            self.trail.assign_propagation(learnt_clause[0], clause_id);
1997
1998            // On-the-fly subsumption: check if this new clause subsumes existing clauses
1999            if learnt_clause.len() <= 5 && lbd <= 3 {
2000                self.check_subsumption(clause_id);
2001            }
2002        }
2003    }
2004
2005    /// Check if the given clause subsumes any existing clauses
2006    /// A clause C subsumes C' if all literals of C are in C'
2007    fn check_subsumption(&mut self, new_clause_id: ClauseId) {
2008        let new_clause = match self.clauses.get(new_clause_id) {
2009            Some(c) => c.lits.clone(),
2010            None => return,
2011        };
2012
2013        if new_clause.len() > 10 {
2014            return; // Don't check subsumption for large clauses (too expensive)
2015        }
2016
2017        // Check against learned clauses only
2018        let mut to_remove = Vec::new();
2019        for &cid in &self.learned_clause_ids {
2020            if cid == new_clause_id {
2021                continue;
2022            }
2023
2024            if let Some(clause) = self.clauses.get(cid) {
2025                if clause.deleted || clause.lits.len() < new_clause.len() {
2026                    continue;
2027                }
2028
2029                // Check if new_clause subsumes clause
2030                if new_clause.iter().all(|&lit| clause.lits.contains(&lit)) {
2031                    to_remove.push(cid);
2032                }
2033            }
2034        }
2035
2036        // Remove subsumed clauses
2037        for cid in to_remove {
2038            self.clauses.remove(cid);
2039            self.stats.deleted_clauses += 1;
2040        }
2041    }
2042
2043    /// Handle clause deletion check and restart check
2044    fn handle_clause_deletion_and_restart(&mut self) {
2045        self.conflicts_since_deletion += 1;
2046
2047        if self.conflicts_since_deletion >= self.config.clause_deletion_threshold as u64 {
2048            self.reduce_clause_database();
2049            self.conflicts_since_deletion = 0;
2050        }
2051
2052        if self.stats.conflicts >= self.restart_threshold {
2053            self.restart();
2054        }
2055    }
2056
2057    /// Get the current trail (for theory solvers)
2058    #[must_use]
2059    pub fn trail(&self) -> &Trail {
2060        &self.trail
2061    }
2062
2063    /// Get the current decision level
2064    #[must_use]
2065    pub fn decision_level(&self) -> u32 {
2066        self.trail.decision_level()
2067    }
2068
2069    /// Generate a random u64 using xorshift64
2070    fn rand_u64(&mut self) -> u64 {
2071        let mut x = self.rng_state;
2072        x ^= x << 13;
2073        x ^= x >> 7;
2074        x ^= x << 17;
2075        self.rng_state = x;
2076        x
2077    }
2078
2079    /// Generate a random f64 in [0, 1)
2080    fn rand_f64(&mut self) -> f64 {
2081        const MAX: f64 = u64::MAX as f64;
2082        (self.rand_u64() as f64) / MAX
2083    }
2084
2085    /// Generate a random boolean with given probability of being true
2086    fn rand_bool(&mut self, probability: f64) -> bool {
2087        self.rand_f64() < probability
2088    }
2089
2090    /// Check for hyper-binary resolution opportunity
2091    /// When propagating `implied` due to `lit` being assigned, check if we can
2092    /// learn a binary clause by resolving the reason clauses
2093    fn check_hyper_binary_resolution(&mut self, _lit: Lit, implied: Lit, reason_id: ClauseId) {
2094        // Only check at higher decision levels to avoid overhead
2095        if self.trail.decision_level() < 2 {
2096            return;
2097        }
2098
2099        // Get the reason clause
2100        let reason_clause = match self.clauses.get(reason_id) {
2101            Some(c) if c.lits.len() >= 2 && c.lits.len() <= 4 => c.lits.clone(),
2102            _ => return,
2103        };
2104
2105        // Check if we can derive a binary clause
2106        // Look for literals in the reason clause that are assigned at the current level
2107        let current_level = self.trail.decision_level();
2108        let mut current_level_lits = SmallVec::<[Lit; 4]>::new();
2109
2110        for &reason_lit in &reason_clause {
2111            if reason_lit != implied {
2112                let var = reason_lit.var();
2113                if self.trail.level(var) == current_level {
2114                    current_level_lits.push(reason_lit);
2115                }
2116            }
2117        }
2118
2119        // If there's exactly one literal from the current level besides the implied one,
2120        // and all others are at level 0 or earlier, we might have a hyper-binary opportunity
2121        if current_level_lits.len() == 1 {
2122            let other_lit = current_level_lits[0];
2123
2124            // Check if we can create a useful binary clause
2125            // We can learn: ~other_lit => implied (or equivalently: other_lit v implied)
2126            let binary_clause_lits = [other_lit.negate(), implied];
2127
2128            // Check if this binary clause is new and useful
2129            if !self.has_binary_implication(other_lit, implied) {
2130                // Learn this binary clause on-the-fly
2131                let clause_id = self.clauses.add_learned(binary_clause_lits.iter().copied());
2132                self.binary_graph.add(other_lit, implied, clause_id);
2133                self.binary_graph
2134                    .add(implied.negate(), other_lit.negate(), clause_id);
2135                self.stats.learned_clauses += 1;
2136            }
2137        }
2138    }
2139
2140    /// Check if a binary implication already exists
2141    fn has_binary_implication(&self, from_lit: Lit, to_lit: Lit) -> bool {
2142        self.binary_graph
2143            .get(from_lit)
2144            .iter()
2145            .any(|(lit, _)| *lit == to_lit)
2146    }
2147
2148    /// Vivification: try to strengthen clauses by checking if some literals are redundant
2149    /// This is an inprocessing technique that should be called periodically
2150    fn vivify_clauses(&mut self) {
2151        if self.trail.decision_level() != 0 {
2152            return; // Only vivify at decision level 0
2153        }
2154
2155        let mut vivified_count = 0;
2156        let max_vivifications = 100; // Limit to avoid too much overhead
2157
2158        // Try to vivify some learned clauses
2159        let clause_ids: Vec<ClauseId> = self
2160            .learned_clause_ids
2161            .iter()
2162            .copied()
2163            .take(max_vivifications)
2164            .collect();
2165
2166        for clause_id in clause_ids {
2167            if vivified_count >= max_vivifications {
2168                break;
2169            }
2170
2171            let clause_lits = match self.clauses.get(clause_id) {
2172                Some(c) if !c.deleted && c.lits.len() > 2 => c.lits.clone(),
2173                _ => continue,
2174            };
2175
2176            // Try to find redundant literals in the clause
2177            // Assign all literals except one to false and see if we can derive the last one
2178            for skip_idx in 0..clause_lits.len() {
2179                // Save current state
2180                let saved_level = self.trail.decision_level();
2181
2182                // Assign all literals except skip_idx to false
2183                self.trail.new_decision_level();
2184                let mut conflict = false;
2185
2186                for (i, &lit) in clause_lits.iter().enumerate() {
2187                    if i == skip_idx {
2188                        continue;
2189                    }
2190
2191                    let value = self.trail.lit_value(lit);
2192                    if value.is_true() {
2193                        // Clause is already satisfied
2194                        conflict = false;
2195                        break;
2196                    } else if value.is_false() {
2197                        // Already false
2198                        continue;
2199                    } else {
2200                        // Assign to false
2201                        self.trail.assign_decision(lit.negate());
2202
2203                        // Propagate
2204                        if self.propagate().is_some() {
2205                            conflict = true;
2206                            break;
2207                        }
2208                    }
2209                }
2210
2211                // Backtrack
2212                self.backtrack(saved_level);
2213
2214                if conflict
2215                    && let Some(clause) = self.clauses.get_mut(clause_id)
2216                    && clause.lits.len() > 2
2217                {
2218                    // The literal at skip_idx is implied by the rest
2219                    // We can remove it from the clause (vivification succeeded)
2220                    clause.lits.remove(skip_idx);
2221                    vivified_count += 1;
2222                    break; // Done with this clause
2223                }
2224            }
2225        }
2226    }
2227
2228    /// Perform inprocessing (apply preprocessing during search)
2229    fn inprocess(&mut self) {
2230        use crate::preprocessing::Preprocessor;
2231
2232        // Only inprocess at decision level 0
2233        if self.trail.decision_level() != 0 {
2234            return;
2235        }
2236
2237        // Create preprocessor with current number of variables
2238        let mut preprocessor = Preprocessor::new(self.num_vars);
2239
2240        // Apply lightweight preprocessing techniques
2241        let _pure_elim = preprocessor.pure_literal_elimination(&mut self.clauses);
2242        let _subsumption = preprocessor.subsumption_elimination(&mut self.clauses);
2243
2244        // On-the-fly clause strengthening
2245        self.strengthen_clauses_inprocessing();
2246
2247        // Rebuild watch lists for any modified clauses
2248        // This is a simplified approach - in a full implementation,
2249        // we would track which clauses were removed and update watches incrementally
2250    }
2251
2252    /// On-the-fly clause strengthening during inprocessing
2253    ///
2254    /// Try to remove literals from clauses by checking if they're redundant.
2255    /// A literal is redundant if the clause is satisfied when it's assigned to false.
2256    fn strengthen_clauses_inprocessing(&mut self) {
2257        if self.trail.decision_level() != 0 {
2258            return;
2259        }
2260
2261        let max_clauses_to_strengthen = 50; // Limit to avoid overhead
2262        let mut strengthened_count = 0;
2263
2264        // Collect candidate clauses (learned clauses with LBD > 2)
2265        let mut candidates: Vec<(ClauseId, u32)> = Vec::new();
2266
2267        for &clause_id in &self.learned_clause_ids {
2268            if let Some(clause) = self.clauses.get(clause_id)
2269                && !clause.deleted
2270                && clause.lits.len() > 3
2271                && clause.lbd > 2
2272            {
2273                candidates.push((clause_id, clause.lbd));
2274            }
2275        }
2276
2277        // Sort by LBD (prioritize higher LBD clauses for strengthening)
2278        candidates.sort_by_key(|(_, lbd)| std::cmp::Reverse(*lbd));
2279
2280        for (clause_id, _) in candidates.iter().take(max_clauses_to_strengthen) {
2281            if strengthened_count >= max_clauses_to_strengthen {
2282                break;
2283            }
2284
2285            let clause_lits = match self.clauses.get(*clause_id) {
2286                Some(c) if !c.deleted && c.lits.len() > 3 => c.lits.clone(),
2287                _ => continue,
2288            };
2289
2290            // Try to remove each literal by checking if the remaining clause is still valid
2291            let mut literals_to_remove = Vec::new();
2292
2293            for (i, &lit) in clause_lits.iter().enumerate() {
2294                // Save current trail state
2295                let saved_level = self.trail.decision_level();
2296
2297                // Try assigning this literal to false
2298                self.trail.new_decision_level();
2299                self.trail.assign_decision(lit.negate());
2300
2301                // Propagate
2302                let conflict = self.propagate();
2303
2304                // Backtrack
2305                self.backtrack(saved_level);
2306
2307                if conflict.is_some() {
2308                    // Assigning this literal to false causes a conflict
2309                    // This means the rest of the clause implies this literal
2310                    // So this literal can potentially be removed (strengthening)
2311
2312                    // But we need to be careful: only remove if the remaining clause
2313                    // is still non-tautological and non-empty
2314                    let mut remaining: Vec<Lit> = clause_lits
2315                        .iter()
2316                        .enumerate()
2317                        .filter(|(j, _)| *j != i)
2318                        .map(|(_, &l)| l)
2319                        .collect();
2320
2321                    // Check if remaining clause is still valid (at least 2 literals)
2322                    if remaining.len() >= 2 {
2323                        // Check it's not a tautology
2324                        remaining.sort_by_key(|l| l.code());
2325                        let mut is_tautology = false;
2326                        for k in 0..remaining.len() - 1 {
2327                            if remaining[k] == remaining[k + 1].negate() {
2328                                is_tautology = true;
2329                                break;
2330                            }
2331                        }
2332
2333                        if !is_tautology {
2334                            literals_to_remove.push(i);
2335                            break; // Only remove one literal at a time
2336                        }
2337                    }
2338                }
2339            }
2340
2341            // Apply strengthening if we found literals to remove
2342            if !literals_to_remove.is_empty() {
2343                // First, remove literals
2344                if let Some(clause) = self.clauses.get_mut(*clause_id) {
2345                    // Remove literals in reverse order to preserve indices
2346                    for &idx in literals_to_remove.iter().rev() {
2347                        if idx < clause.lits.len() {
2348                            clause.lits.remove(idx);
2349                        }
2350                    }
2351                }
2352
2353                // Then, recompute LBD (after the mutable borrow ends)
2354                if let Some(clause) = self.clauses.get(*clause_id) {
2355                    let lits_clone = clause.lits.clone();
2356                    let new_lbd = self.compute_lbd(&lits_clone);
2357
2358                    // Now update the LBD
2359                    if let Some(clause) = self.clauses.get_mut(*clause_id) {
2360                        clause.lbd = new_lbd;
2361                    }
2362
2363                    strengthened_count += 1;
2364                }
2365            }
2366        }
2367    }
2368}
2369
2370#[cfg(test)]
2371mod tests {
2372    use super::*;
2373
2374    #[test]
2375    fn test_empty_sat() {
2376        let mut solver = Solver::new();
2377        assert_eq!(solver.solve(), SolverResult::Sat);
2378    }
2379
2380    #[test]
2381    fn test_simple_sat() {
2382        let mut solver = Solver::new();
2383        let _x = solver.new_var();
2384        let _y = solver.new_var();
2385
2386        // x or y
2387        solver.add_clause_dimacs(&[1, 2]);
2388        // not x or y
2389        solver.add_clause_dimacs(&[-1, 2]);
2390
2391        assert_eq!(solver.solve(), SolverResult::Sat);
2392        assert!(solver.model_value(Var::new(1)).is_true()); // y must be true
2393    }
2394
2395    #[test]
2396    fn test_simple_unsat() {
2397        let mut solver = Solver::new();
2398        let _x = solver.new_var();
2399
2400        // x
2401        solver.add_clause_dimacs(&[1]);
2402        // not x
2403        solver.add_clause_dimacs(&[-1]);
2404
2405        assert_eq!(solver.solve(), SolverResult::Unsat);
2406    }
2407
2408    #[test]
2409    fn test_pigeonhole_2_1() {
2410        // 2 pigeons, 1 hole - UNSAT
2411        let mut solver = Solver::new();
2412        let _p1h1 = solver.new_var(); // pigeon 1 in hole 1
2413        let _p2h1 = solver.new_var(); // pigeon 2 in hole 1
2414
2415        // Each pigeon must be in some hole
2416        solver.add_clause_dimacs(&[1]); // p1 in h1
2417        solver.add_clause_dimacs(&[2]); // p2 in h1
2418
2419        // No hole can have two pigeons
2420        solver.add_clause_dimacs(&[-1, -2]); // not (p1h1 and p2h1)
2421
2422        assert_eq!(solver.solve(), SolverResult::Unsat);
2423    }
2424
2425    #[test]
2426    fn test_3sat_random() {
2427        let mut solver = Solver::new();
2428        for _ in 0..10 {
2429            solver.new_var();
2430        }
2431
2432        // Random 3-SAT instance (likely SAT)
2433        solver.add_clause_dimacs(&[1, 2, 3]);
2434        solver.add_clause_dimacs(&[-1, 4, 5]);
2435        solver.add_clause_dimacs(&[2, -3, 6]);
2436        solver.add_clause_dimacs(&[-4, 7, 8]);
2437        solver.add_clause_dimacs(&[5, -6, 9]);
2438        solver.add_clause_dimacs(&[-7, 8, 10]);
2439        solver.add_clause_dimacs(&[1, -8, -9]);
2440        solver.add_clause_dimacs(&[-2, 3, -10]);
2441
2442        let result = solver.solve();
2443        assert_eq!(result, SolverResult::Sat);
2444    }
2445
2446    #[test]
2447    fn test_luby_sequence() {
2448        // Luby sequence: 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, ...
2449        assert_eq!(Solver::luby(0), 1);
2450        assert_eq!(Solver::luby(1), 1);
2451        assert_eq!(Solver::luby(2), 2);
2452        assert_eq!(Solver::luby(3), 1);
2453        assert_eq!(Solver::luby(4), 1);
2454        assert_eq!(Solver::luby(5), 2);
2455        assert_eq!(Solver::luby(6), 4);
2456        assert_eq!(Solver::luby(7), 1);
2457    }
2458
2459    #[test]
2460    fn test_phase_saving() {
2461        let mut solver = Solver::new();
2462        for _ in 0..5 {
2463            solver.new_var();
2464        }
2465
2466        // Set up a problem where phase saving helps
2467        solver.add_clause_dimacs(&[1, 2]);
2468        solver.add_clause_dimacs(&[-1, 3]);
2469        solver.add_clause_dimacs(&[-2, 4]);
2470        solver.add_clause_dimacs(&[-3, -4, 5]);
2471        solver.add_clause_dimacs(&[-5, 1]);
2472
2473        let result = solver.solve();
2474        assert_eq!(result, SolverResult::Sat);
2475    }
2476
2477    #[test]
2478    fn test_lbd_computation() {
2479        // Test that clause deletion can handle a problem that generates learned clauses
2480        let mut solver = Solver::with_config(SolverConfig {
2481            clause_deletion_threshold: 5, // Trigger deletion quickly
2482            ..SolverConfig::default()
2483        });
2484
2485        for _ in 0..20 {
2486            solver.new_var();
2487        }
2488
2489        // A harder problem to generate more conflicts and learned clauses
2490        // PHP(3,2): 3 pigeons, 2 holes - UNSAT
2491        // Variables: p_i_h (pigeon i in hole h)
2492        // p11=1, p12=2, p21=3, p22=4, p31=5, p32=6
2493
2494        // Each pigeon must be in some hole
2495        solver.add_clause_dimacs(&[1, 2]); // p1 in h1 or h2
2496        solver.add_clause_dimacs(&[3, 4]); // p2 in h1 or h2
2497        solver.add_clause_dimacs(&[5, 6]); // p3 in h1 or h2
2498
2499        // No hole can have two pigeons
2500        solver.add_clause_dimacs(&[-1, -3]); // not (p1h1 and p2h1)
2501        solver.add_clause_dimacs(&[-1, -5]); // not (p1h1 and p3h1)
2502        solver.add_clause_dimacs(&[-3, -5]); // not (p2h1 and p3h1)
2503        solver.add_clause_dimacs(&[-2, -4]); // not (p1h2 and p2h2)
2504        solver.add_clause_dimacs(&[-2, -6]); // not (p1h2 and p3h2)
2505        solver.add_clause_dimacs(&[-4, -6]); // not (p2h2 and p3h2)
2506
2507        let result = solver.solve();
2508        assert_eq!(result, SolverResult::Unsat);
2509        // Verify we had some conflicts (and thus learned clauses)
2510        assert!(solver.stats().conflicts > 0);
2511    }
2512
2513    #[test]
2514    fn test_clause_activity_decay() {
2515        let mut solver = Solver::new();
2516        for _ in 0..10 {
2517            solver.new_var();
2518        }
2519
2520        // Add some clauses
2521        solver.add_clause_dimacs(&[1, 2, 3]);
2522        solver.add_clause_dimacs(&[-1, 4, 5]);
2523        solver.add_clause_dimacs(&[-2, -3, 6]);
2524
2525        // Solve (should be SAT)
2526        let result = solver.solve();
2527        assert_eq!(result, SolverResult::Sat);
2528    }
2529
2530    #[test]
2531    fn test_clause_minimization() {
2532        // Test that clause minimization works correctly on a problem
2533        // that will generate learned clauses
2534        let mut solver = Solver::new();
2535
2536        for _ in 0..15 {
2537            solver.new_var();
2538        }
2539
2540        // A problem structure that generates conflicts and learned clauses
2541        // Graph coloring with 3 colors on 5 vertices
2542        // Vertices: 1-5, Colors: R(0-4), G(5-9), B(10-14)
2543
2544        // Each vertex has at least one color
2545        solver.add_clause_dimacs(&[1, 6, 11]); // v1: R or G or B
2546        solver.add_clause_dimacs(&[2, 7, 12]); // v2
2547        solver.add_clause_dimacs(&[3, 8, 13]); // v3
2548        solver.add_clause_dimacs(&[4, 9, 14]); // v4
2549        solver.add_clause_dimacs(&[5, 10, 15]); // v5
2550
2551        // At most one color per vertex (pairwise exclusion)
2552        solver.add_clause_dimacs(&[-1, -6]); // v1: not (R and G)
2553        solver.add_clause_dimacs(&[-1, -11]); // v1: not (R and B)
2554        solver.add_clause_dimacs(&[-6, -11]); // v1: not (G and B)
2555
2556        solver.add_clause_dimacs(&[-2, -7]);
2557        solver.add_clause_dimacs(&[-2, -12]);
2558        solver.add_clause_dimacs(&[-7, -12]);
2559
2560        solver.add_clause_dimacs(&[-3, -8]);
2561        solver.add_clause_dimacs(&[-3, -13]);
2562        solver.add_clause_dimacs(&[-8, -13]);
2563
2564        // Adjacent vertices have different colors (edges: 1-2, 2-3, 3-4, 4-5)
2565        solver.add_clause_dimacs(&[-1, -2]); // edge 1-2: not both R
2566        solver.add_clause_dimacs(&[-6, -7]); // edge 1-2: not both G
2567        solver.add_clause_dimacs(&[-11, -12]); // edge 1-2: not both B
2568
2569        solver.add_clause_dimacs(&[-2, -3]); // edge 2-3
2570        solver.add_clause_dimacs(&[-7, -8]);
2571        solver.add_clause_dimacs(&[-12, -13]);
2572
2573        let result = solver.solve();
2574        assert_eq!(result, SolverResult::Sat);
2575
2576        // Verify we had some learning (and thus minimization opportunities)
2577        assert!(solver.stats().conflicts > 0 || solver.stats().learned_clauses > 0);
2578    }
2579
2580    /// A simple theory callback that does nothing (pure SAT)
2581    struct NullTheory;
2582
2583    impl TheoryCallback for NullTheory {
2584        fn on_assignment(&mut self, _lit: Lit) -> TheoryCheckResult {
2585            TheoryCheckResult::Sat
2586        }
2587
2588        fn final_check(&mut self) -> TheoryCheckResult {
2589            TheoryCheckResult::Sat
2590        }
2591
2592        fn on_backtrack(&mut self, _level: u32) {}
2593    }
2594
2595    #[test]
2596    fn test_solve_with_theory_sat() {
2597        let mut solver = Solver::new();
2598        let mut theory = NullTheory;
2599
2600        let _x = solver.new_var();
2601        let _y = solver.new_var();
2602
2603        // x or y
2604        solver.add_clause_dimacs(&[1, 2]);
2605        // not x or y
2606        solver.add_clause_dimacs(&[-1, 2]);
2607
2608        assert_eq!(solver.solve_with_theory(&mut theory), SolverResult::Sat);
2609        assert!(solver.model_value(Var::new(1)).is_true()); // y must be true
2610    }
2611
2612    #[test]
2613    fn test_solve_with_theory_unsat() {
2614        let mut solver = Solver::new();
2615        let mut theory = NullTheory;
2616
2617        let _x = solver.new_var();
2618
2619        // x
2620        solver.add_clause_dimacs(&[1]);
2621        // not x
2622        solver.add_clause_dimacs(&[-1]);
2623
2624        assert_eq!(solver.solve_with_theory(&mut theory), SolverResult::Unsat);
2625    }
2626
2627    /// A theory that forces x0 => x1 (if x0 is true, x1 must be true)
2628    struct ImplicationTheory {
2629        /// Track if x0 is assigned true
2630        x0_true: bool,
2631    }
2632
2633    impl ImplicationTheory {
2634        fn new() -> Self {
2635            Self { x0_true: false }
2636        }
2637    }
2638
2639    impl TheoryCallback for ImplicationTheory {
2640        fn on_assignment(&mut self, lit: Lit) -> TheoryCheckResult {
2641            // If x0 becomes true, propagate x1
2642            if lit.var().index() == 0 && lit.is_pos() {
2643                self.x0_true = true;
2644                // Propagate: x1 must be true because x0 is true
2645                // The reason is: ~x0 (if x0 were false, we wouldn't need x1)
2646                let reason: SmallVec<[Lit; 8]> = smallvec::smallvec![Lit::pos(Var::new(0))];
2647                return TheoryCheckResult::Propagated(vec![(Lit::pos(Var::new(1)), reason)]);
2648            }
2649            TheoryCheckResult::Sat
2650        }
2651
2652        fn final_check(&mut self) -> TheoryCheckResult {
2653            TheoryCheckResult::Sat
2654        }
2655
2656        fn on_backtrack(&mut self, _level: u32) {
2657            self.x0_true = false;
2658        }
2659    }
2660
2661    #[test]
2662    fn test_theory_propagation() {
2663        let mut solver = Solver::new();
2664        let mut theory = ImplicationTheory::new();
2665
2666        let _x0 = solver.new_var();
2667        let _x1 = solver.new_var();
2668
2669        // Force x0 to be true
2670        solver.add_clause_dimacs(&[1]);
2671
2672        let result = solver.solve_with_theory(&mut theory);
2673        assert_eq!(result, SolverResult::Sat);
2674
2675        // x0 should be true (forced by clause)
2676        assert!(solver.model_value(Var::new(0)).is_true());
2677        // x1 should also be true (propagated by theory)
2678        assert!(solver.model_value(Var::new(1)).is_true());
2679    }
2680
2681    /// Theory that says x0 and x1 can't both be true
2682    struct MutexTheory {
2683        x0_true: Option<Lit>,
2684        x1_true: Option<Lit>,
2685    }
2686
2687    impl MutexTheory {
2688        fn new() -> Self {
2689            Self {
2690                x0_true: None,
2691                x1_true: None,
2692            }
2693        }
2694    }
2695
2696    impl TheoryCallback for MutexTheory {
2697        fn on_assignment(&mut self, lit: Lit) -> TheoryCheckResult {
2698            if lit.var().index() == 0 && lit.is_pos() {
2699                self.x0_true = Some(lit);
2700            }
2701            if lit.var().index() == 1 && lit.is_pos() {
2702                self.x1_true = Some(lit);
2703            }
2704
2705            // If both are true, conflict
2706            if self.x0_true.is_some() && self.x1_true.is_some() {
2707                // Conflict clause: ~x0 or ~x1 (at least one must be false)
2708                let conflict: SmallVec<[Lit; 8]> = smallvec::smallvec![
2709                    Lit::pos(Var::new(0)), // x0 is true (we negate in conflict)
2710                    Lit::pos(Var::new(1))  // x1 is true
2711                ];
2712                return TheoryCheckResult::Conflict(conflict);
2713            }
2714            TheoryCheckResult::Sat
2715        }
2716
2717        fn final_check(&mut self) -> TheoryCheckResult {
2718            if self.x0_true.is_some() && self.x1_true.is_some() {
2719                let conflict: SmallVec<[Lit; 8]> =
2720                    smallvec::smallvec![Lit::pos(Var::new(0)), Lit::pos(Var::new(1))];
2721                return TheoryCheckResult::Conflict(conflict);
2722            }
2723            TheoryCheckResult::Sat
2724        }
2725
2726        fn on_backtrack(&mut self, _level: u32) {
2727            self.x0_true = None;
2728            self.x1_true = None;
2729        }
2730    }
2731
2732    #[test]
2733    fn test_theory_conflict() {
2734        let mut solver = Solver::new();
2735        let mut theory = MutexTheory::new();
2736
2737        let _x0 = solver.new_var();
2738        let _x1 = solver.new_var();
2739
2740        // Force both x0 and x1 to be true (should cause theory conflict)
2741        solver.add_clause_dimacs(&[1]);
2742        solver.add_clause_dimacs(&[2]);
2743
2744        let result = solver.solve_with_theory(&mut theory);
2745        assert_eq!(result, SolverResult::Unsat);
2746    }
2747
2748    #[test]
2749    fn test_solve_with_assumptions_sat() {
2750        let mut solver = Solver::new();
2751
2752        let x0 = solver.new_var();
2753        let x1 = solver.new_var();
2754
2755        // x0 \/ x1
2756        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
2757
2758        // Assume x0 = true
2759        let assumptions = [Lit::pos(x0)];
2760        let (result, core) = solver.solve_with_assumptions(&assumptions);
2761
2762        assert_eq!(result, SolverResult::Sat);
2763        assert!(core.is_none());
2764    }
2765
2766    #[test]
2767    fn test_solve_with_assumptions_unsat() {
2768        let mut solver = Solver::new();
2769
2770        let x0 = solver.new_var();
2771        let x1 = solver.new_var();
2772
2773        // x0 -> ~x1 (encoded as ~x0 \/ ~x1)
2774        solver.add_clause([Lit::neg(x0), Lit::neg(x1)]);
2775
2776        // Assume both x0 = true and x1 = true (should be UNSAT)
2777        let assumptions = [Lit::pos(x0), Lit::pos(x1)];
2778        let (result, core) = solver.solve_with_assumptions(&assumptions);
2779
2780        assert_eq!(result, SolverResult::Unsat);
2781        assert!(core.is_some());
2782        let core = core.unwrap();
2783        // Core should contain at least one of the conflicting assumptions
2784        assert!(!core.is_empty());
2785    }
2786
2787    #[test]
2788    fn test_solve_with_assumptions_core_extraction() {
2789        let mut solver = Solver::new();
2790
2791        let x0 = solver.new_var();
2792        let x1 = solver.new_var();
2793        let x2 = solver.new_var();
2794
2795        // ~x0 (x0 must be false)
2796        solver.add_clause([Lit::neg(x0)]);
2797
2798        // Assume x0 = true, x1 = true, x2 = true
2799        // Only x0 should be in the core
2800        let assumptions = [Lit::pos(x0), Lit::pos(x1), Lit::pos(x2)];
2801        let (result, core) = solver.solve_with_assumptions(&assumptions);
2802
2803        assert_eq!(result, SolverResult::Unsat);
2804        assert!(core.is_some());
2805        let core = core.unwrap();
2806        // x0 should be in the core
2807        assert!(core.contains(&Lit::pos(x0)));
2808    }
2809
2810    #[test]
2811    fn test_solve_with_assumptions_incremental() {
2812        let mut solver = Solver::new();
2813
2814        let x0 = solver.new_var();
2815        let x1 = solver.new_var();
2816
2817        // x0 \/ x1
2818        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
2819
2820        // First: assume ~x0 (should be SAT with x1 = true)
2821        let (result1, _) = solver.solve_with_assumptions(&[Lit::neg(x0)]);
2822        assert_eq!(result1, SolverResult::Sat);
2823
2824        // Second: assume ~x0 and ~x1 (should be UNSAT)
2825        let (result2, core2) = solver.solve_with_assumptions(&[Lit::neg(x0), Lit::neg(x1)]);
2826        assert_eq!(result2, SolverResult::Unsat);
2827        assert!(core2.is_some());
2828
2829        // Third: assume x0 (should be SAT again)
2830        let (result3, _) = solver.solve_with_assumptions(&[Lit::pos(x0)]);
2831        assert_eq!(result3, SolverResult::Sat);
2832    }
2833
2834    #[test]
2835    fn test_push_pop_simple() {
2836        let mut solver = Solver::new();
2837
2838        let x0 = solver.new_var();
2839
2840        // Should be SAT (x0 can be true or false)
2841        assert_eq!(solver.solve(), SolverResult::Sat);
2842
2843        // Push and add unit clause: x0
2844        solver.push();
2845        solver.add_clause([Lit::pos(x0)]);
2846        assert_eq!(solver.solve(), SolverResult::Sat);
2847        assert!(solver.model_value(x0).is_true());
2848
2849        // Pop - should be SAT again
2850        solver.pop();
2851        let result = solver.solve();
2852        assert_eq!(
2853            result,
2854            SolverResult::Sat,
2855            "After pop, expected SAT but got {:?}. trivially_unsat={}",
2856            result,
2857            solver.trivially_unsat
2858        );
2859    }
2860
2861    #[test]
2862    fn test_push_pop_incremental() {
2863        let mut solver = Solver::new();
2864
2865        let x0 = solver.new_var();
2866        let x1 = solver.new_var();
2867        let x2 = solver.new_var();
2868
2869        // Base level: x0 \/ x1
2870        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
2871        assert_eq!(solver.solve(), SolverResult::Sat);
2872
2873        // Push and add: ~x0
2874        solver.push();
2875        solver.add_clause([Lit::neg(x0)]);
2876        assert_eq!(solver.solve(), SolverResult::Sat);
2877        // x1 must be true
2878        assert!(solver.model_value(x1).is_true());
2879
2880        // Push again and add: ~x1 (should be UNSAT)
2881        solver.push();
2882        solver.add_clause([Lit::neg(x1)]);
2883        assert_eq!(solver.solve(), SolverResult::Unsat);
2884
2885        // Pop back one level (remove ~x1, keep ~x0)
2886        solver.pop();
2887        assert_eq!(solver.solve(), SolverResult::Sat);
2888        assert!(solver.model_value(x1).is_true());
2889
2890        // Pop back to base level (remove ~x0)
2891        solver.pop();
2892        assert_eq!(solver.solve(), SolverResult::Sat);
2893        // Either x0 or x1 can be true now
2894
2895        // Push and add different clause: x0 /\ x2
2896        solver.push();
2897        solver.add_clause([Lit::pos(x0)]);
2898        solver.add_clause([Lit::pos(x2)]);
2899        assert_eq!(solver.solve(), SolverResult::Sat);
2900        assert!(solver.model_value(x0).is_true());
2901        assert!(solver.model_value(x2).is_true());
2902
2903        // Pop and verify clauses are removed
2904        solver.pop();
2905        assert_eq!(solver.solve(), SolverResult::Sat);
2906    }
2907
2908    #[test]
2909    fn test_push_pop_with_learned_clauses() {
2910        let mut solver = Solver::new();
2911
2912        let x0 = solver.new_var();
2913        let x1 = solver.new_var();
2914        let x2 = solver.new_var();
2915
2916        // Create a formula that will cause learning
2917        // (x0 \/ x1) /\ (~x0 \/ x2) /\ (~x1 \/ x2)
2918        solver.add_clause([Lit::pos(x0), Lit::pos(x1)]);
2919        solver.add_clause([Lit::neg(x0), Lit::pos(x2)]);
2920        solver.add_clause([Lit::neg(x1), Lit::pos(x2)]);
2921
2922        assert_eq!(solver.solve(), SolverResult::Sat);
2923
2924        // Push and add conflicting clause
2925        solver.push();
2926        solver.add_clause([Lit::neg(x2)]);
2927
2928        // This should be UNSAT and cause clause learning
2929        assert_eq!(solver.solve(), SolverResult::Unsat);
2930
2931        // Pop - learned clauses from this level should be removed
2932        solver.pop();
2933
2934        // Should be SAT again
2935        assert_eq!(solver.solve(), SolverResult::Sat);
2936    }
2937}