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