Skip to main content

oxiz_solver/combination/
conflict_resolution.rs

1//! Theory Conflict Resolution for Combination.
2//!
3//! This module handles conflicts that arise from theory combination:
4//! - Multi-theory conflict analysis
5//! - Minimal explanation generation
6//! - Conflict minimization
7//! - Theory blame assignment
8//! - Conflict-driven clause learning (CDCL) for theory combination
9//!
10//! ## Theory Conflicts
11//!
12//! In theory combination, conflicts can arise from:
13//! - A single theory detecting inconsistency
14//! - Incompatible propagations from multiple theories
15//! - Violation of shared term constraints
16//!
17//! ## Conflict Analysis
18//!
19//! When a conflict occurs, we perform analysis to:
20//! - Identify the root cause
21//! - Generate a minimal explanation
22//! - Learn conflict clauses to prevent similar conflicts
23//! - Determine the backtrack level
24//!
25//! ## References
26//!
27//! - Silva & Sakallah (1996): "GRASP: A Search Algorithm for Propositional Satisfiability"
28//! - Nieuwenhuis, Oliveras, Tinelli (2006): "Solving SAT and SAT Modulo Theories"
29//! - Z3's `smt/theory_combination.cpp`, `smt/smt_conflict.cpp`
30
31/// Term identifier.
32#[allow(unused_imports)]
33use crate::prelude::*;
34/// Term identifier for conflict resolution.
35pub type TermId = u32;
36
37/// Theory identifier.
38pub type TheoryId = u32;
39
40/// Decision level.
41pub type DecisionLevel = u32;
42
43/// Literal (term with polarity).
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct Literal {
46    /// Term.
47    pub term: TermId,
48    /// Polarity.
49    pub polarity: bool,
50}
51
52impl Literal {
53    /// Create positive literal.
54    pub fn positive(term: TermId) -> Self {
55        Self {
56            term,
57            polarity: true,
58        }
59    }
60
61    /// Create negative literal.
62    pub fn negative(term: TermId) -> Self {
63        Self {
64            term,
65            polarity: false,
66        }
67    }
68
69    /// Negate literal.
70    pub fn negate(self) -> Self {
71        Self {
72            term: self.term,
73            polarity: !self.polarity,
74        }
75    }
76}
77
78/// Equality between terms.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub struct Equality {
81    /// Left-hand side.
82    pub lhs: TermId,
83    /// Right-hand side.
84    pub rhs: TermId,
85}
86
87impl Equality {
88    /// Create new equality.
89    pub fn new(lhs: TermId, rhs: TermId) -> Self {
90        if lhs <= rhs {
91            Self { lhs, rhs }
92        } else {
93            Self { lhs: rhs, rhs: lhs }
94        }
95    }
96}
97
98/// Explanation for a propagation or conflict.
99#[derive(Debug, Clone)]
100pub enum Explanation {
101    /// Given as input.
102    Given,
103
104    /// Theory propagation.
105    TheoryPropagation {
106        /// Source theory.
107        theory: TheoryId,
108        /// Antecedent literals.
109        antecedents: Vec<Literal>,
110    },
111
112    /// Equality propagation.
113    EqualityPropagation {
114        /// Equalities used.
115        equalities: Vec<Equality>,
116        /// Supporting literals.
117        support: Vec<Literal>,
118    },
119
120    /// Transitivity.
121    Transitivity {
122        /// Chain of equalities.
123        chain: Vec<Equality>,
124    },
125
126    /// Congruence.
127    Congruence {
128        /// Function applications.
129        function: TermId,
130        /// Argument equalities.
131        arg_equalities: Vec<Equality>,
132    },
133}
134
135/// Theory conflict.
136#[derive(Debug, Clone)]
137pub struct TheoryConflict {
138    /// Theory that detected the conflict.
139    pub theory: TheoryId,
140
141    /// Conflicting literals.
142    pub literals: Vec<Literal>,
143
144    /// Explanation for the conflict.
145    pub explanation: Explanation,
146
147    /// Decision level where conflict occurred.
148    pub level: DecisionLevel,
149}
150
151/// Conflict clause learned from analysis.
152#[derive(Debug, Clone)]
153pub struct ConflictClause {
154    /// Literals in the clause.
155    pub literals: Vec<Literal>,
156
157    /// UIP (unique implication point) literal.
158    pub uip: Option<Literal>,
159
160    /// Backtrack level.
161    pub backtrack_level: DecisionLevel,
162
163    /// Theories involved.
164    pub theories: FxHashSet<TheoryId>,
165
166    /// Activity score (for clause deletion).
167    pub activity: f64,
168}
169
170/// Conflict analysis result.
171#[derive(Debug, Clone)]
172pub struct ConflictAnalysis {
173    /// Learned clause.
174    pub clause: ConflictClause,
175
176    /// Minimal explanation.
177    pub explanation: Explanation,
178
179    /// Theories responsible.
180    pub blamed_theories: FxHashSet<TheoryId>,
181}
182
183/// Configuration for conflict resolution.
184#[derive(Debug, Clone)]
185pub struct ConflictResolutionConfig {
186    /// Enable conflict minimization.
187    pub enable_minimization: bool,
188
189    /// Enable UIP-based learning.
190    pub enable_uip: bool,
191
192    /// Minimization algorithm.
193    pub minimization_algorithm: MinimizationAlgorithm,
194
195    /// Maximum resolution steps.
196    pub max_resolution_steps: usize,
197
198    /// Enable theory blame tracking.
199    pub track_theory_blame: bool,
200
201    /// Enable conflict clause learning.
202    pub enable_learning: bool,
203}
204
205impl Default for ConflictResolutionConfig {
206    fn default() -> Self {
207        Self {
208            enable_minimization: true,
209            enable_uip: true,
210            minimization_algorithm: MinimizationAlgorithm::Recursive,
211            max_resolution_steps: 1000,
212            track_theory_blame: true,
213            enable_learning: true,
214        }
215    }
216}
217
218/// Minimization algorithm for conflict clauses.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MinimizationAlgorithm {
221    /// No minimization.
222    None,
223    /// Simple minimization (remove redundant literals).
224    Simple,
225    /// Recursive minimization.
226    Recursive,
227    /// Binary resolution minimization.
228    BinaryResolution,
229}
230
231/// Statistics for conflict resolution.
232#[derive(Debug, Clone, Default)]
233pub struct ConflictResolutionStats {
234    /// Total conflicts analyzed.
235    pub conflicts_analyzed: u64,
236    /// Clauses learned.
237    pub clauses_learned: u64,
238    /// Literals minimized away.
239    pub literals_minimized: u64,
240    /// UIP conflicts.
241    pub uip_conflicts: u64,
242    /// Resolution steps performed.
243    pub resolution_steps: u64,
244    /// Theory blames assigned.
245    pub theory_blames: u64,
246}
247
248/// Conflict resolution engine.
249pub struct ConflictResolver {
250    /// Configuration.
251    config: ConflictResolutionConfig,
252
253    /// Statistics.
254    stats: ConflictResolutionStats,
255
256    /// Assignment trail.
257    trail: Vec<(Literal, DecisionLevel, Explanation)>,
258
259    /// Literal to trail position.
260    literal_position: FxHashMap<Literal, usize>,
261
262    /// Decision level boundaries in trail.
263    level_boundaries: FxHashMap<DecisionLevel, usize>,
264
265    /// Current decision level.
266    current_level: DecisionLevel,
267
268    /// Learned clauses.
269    learned_clauses: Vec<ConflictClause>,
270
271    /// Theory blame counters.
272    theory_blame: FxHashMap<TheoryId, u64>,
273}
274
275impl ConflictResolver {
276    /// Create new conflict resolver.
277    pub fn new() -> Self {
278        Self::with_config(ConflictResolutionConfig::default())
279    }
280
281    /// Create with configuration.
282    pub fn with_config(config: ConflictResolutionConfig) -> Self {
283        let mut level_boundaries = FxHashMap::default();
284        // Initialize level 0 boundary at position 0
285        level_boundaries.insert(0, 0);
286
287        Self {
288            config,
289            stats: ConflictResolutionStats::default(),
290            trail: Vec::new(),
291            literal_position: FxHashMap::default(),
292            level_boundaries,
293            current_level: 0,
294            learned_clauses: Vec::new(),
295            theory_blame: FxHashMap::default(),
296        }
297    }
298
299    /// Get statistics.
300    pub fn stats(&self) -> &ConflictResolutionStats {
301        &self.stats
302    }
303
304    /// Add assignment to trail.
305    pub fn add_assignment(
306        &mut self,
307        literal: Literal,
308        level: DecisionLevel,
309        explanation: Explanation,
310    ) {
311        let position = self.trail.len();
312        self.trail.push((literal, level, explanation));
313        self.literal_position.insert(literal, position);
314
315        self.level_boundaries.entry(level).or_insert(position);
316    }
317
318    /// Push decision level.
319    pub fn push_decision_level(&mut self) {
320        self.current_level += 1;
321    }
322
323    /// Backtrack to decision level.
324    pub fn backtrack(&mut self, level: DecisionLevel) -> Result<(), String> {
325        if level > self.current_level {
326            return Err("Cannot backtrack to future level".to_string());
327        }
328
329        // Find position to backtrack to
330        let backtrack_pos = self
331            .level_boundaries
332            .get(&level)
333            .copied()
334            .unwrap_or(self.trail.len());
335
336        // Remove assignments above this level
337        self.trail.truncate(backtrack_pos);
338
339        // Rebuild literal position map
340        self.literal_position.clear();
341        for (i, &(literal, _, _)) in self.trail.iter().enumerate() {
342            self.literal_position.insert(literal, i);
343        }
344
345        // Remove level boundaries above this level
346        self.level_boundaries.retain(|&l, _| l <= level);
347
348        self.current_level = level;
349        Ok(())
350    }
351
352    /// Analyze a theory conflict.
353    pub fn analyze_conflict(
354        &mut self,
355        conflict: TheoryConflict,
356    ) -> Result<ConflictAnalysis, String> {
357        self.stats.conflicts_analyzed += 1;
358
359        if self.config.track_theory_blame {
360            *self.theory_blame.entry(conflict.theory).or_insert(0) += 1;
361            self.stats.theory_blames += 1;
362        }
363
364        // Extract conflict literals
365        let mut conflict_literals = conflict.literals.clone();
366
367        // Perform resolution to find UIP if enabled
368        if self.config.enable_uip {
369            conflict_literals = self.find_uip(&conflict_literals, conflict.level)?;
370            self.stats.uip_conflicts += 1;
371        }
372
373        // Minimize conflict clause
374        if self.config.enable_minimization {
375            let before_size = conflict_literals.len();
376            conflict_literals = self.minimize_conflict(&conflict_literals)?;
377            let after_size = conflict_literals.len();
378            self.stats.literals_minimized += (before_size - after_size) as u64;
379        }
380
381        // Determine backtrack level
382        let backtrack_level = self.compute_backtrack_level(&conflict_literals, conflict.level)?;
383
384        // Build learned clause
385        let clause = ConflictClause {
386            literals: conflict_literals.clone(),
387            uip: self.find_uip_literal(&conflict_literals),
388            backtrack_level,
389            theories: {
390                let mut theories = FxHashSet::default();
391                theories.insert(conflict.theory);
392                theories
393            },
394            activity: 1.0,
395        };
396
397        // Learn clause if enabled
398        if self.config.enable_learning {
399            self.learned_clauses.push(clause.clone());
400            self.stats.clauses_learned += 1;
401        }
402
403        Ok(ConflictAnalysis {
404            clause,
405            explanation: conflict.explanation,
406            blamed_theories: {
407                let mut theories = FxHashSet::default();
408                theories.insert(conflict.theory);
409                theories
410            },
411        })
412    }
413
414    /// Find UIP (Unique Implication Point) using resolution.
415    fn find_uip(
416        &mut self,
417        literals: &[Literal],
418        level: DecisionLevel,
419    ) -> Result<Vec<Literal>, String> {
420        let mut current_clause: FxHashSet<Literal> = literals.iter().copied().collect();
421        let mut seen = FxHashSet::default();
422        let mut counter = 0;
423
424        // Count literals at current level
425        for &lit in &current_clause {
426            if self.get_decision_level(lit) == Some(level) {
427                counter += 1;
428            }
429        }
430
431        // Resolution loop
432        for _ in 0..self.config.max_resolution_steps {
433            self.stats.resolution_steps += 1;
434
435            if counter <= 1 {
436                break; // Found UIP
437            }
438
439            // Find next literal to resolve
440            let resolve_lit = self.find_resolution_literal(&current_clause, level, &seen)?;
441            seen.insert(resolve_lit);
442
443            // Get reason for this literal
444            let reason = self.get_reason(resolve_lit)?;
445
446            // Perform resolution
447            current_clause.remove(&resolve_lit);
448            counter -= 1;
449
450            for &lit in &reason {
451                if !current_clause.contains(&lit) {
452                    current_clause.insert(lit);
453                    if self.get_decision_level(lit) == Some(level) {
454                        counter += 1;
455                    }
456                }
457            }
458        }
459
460        Ok(current_clause.into_iter().collect())
461    }
462
463    /// Find literal for resolution.
464    fn find_resolution_literal(
465        &self,
466        clause: &FxHashSet<Literal>,
467        level: DecisionLevel,
468        seen: &FxHashSet<Literal>,
469    ) -> Result<Literal, String> {
470        // Find last assigned literal at current level that hasn't been seen
471        for &(literal, lit_level, _) in self.trail.iter().rev() {
472            if lit_level == level && clause.contains(&literal) && !seen.contains(&literal) {
473                return Ok(literal);
474            }
475        }
476
477        Err("No resolution literal found".to_string())
478    }
479
480    /// Get decision level for a literal.
481    fn get_decision_level(&self, literal: Literal) -> Option<DecisionLevel> {
482        self.literal_position
483            .get(&literal)
484            .and_then(|&pos| self.trail.get(pos))
485            .map(|(_, level, _)| *level)
486    }
487
488    /// Get reason (explanation) for a literal.
489    fn get_reason(&self, literal: Literal) -> Result<Vec<Literal>, String> {
490        let position = self
491            .literal_position
492            .get(&literal)
493            .ok_or("Literal not in trail")?;
494
495        let (_, _, explanation) = &self.trail[*position];
496
497        match explanation {
498            Explanation::TheoryPropagation { antecedents, .. } => Ok(antecedents.clone()),
499            Explanation::EqualityPropagation { support, .. } => Ok(support.clone()),
500            _ => Ok(Vec::new()),
501        }
502    }
503
504    /// Minimize conflict clause.
505    fn minimize_conflict(&self, literals: &[Literal]) -> Result<Vec<Literal>, String> {
506        match self.config.minimization_algorithm {
507            MinimizationAlgorithm::None => Ok(literals.to_vec()),
508            MinimizationAlgorithm::Simple => self.minimize_simple(literals),
509            MinimizationAlgorithm::Recursive => self.minimize_recursive(literals),
510            MinimizationAlgorithm::BinaryResolution => self.minimize_binary_resolution(literals),
511        }
512    }
513
514    /// Simple minimization (remove obviously redundant literals).
515    fn minimize_simple(&self, literals: &[Literal]) -> Result<Vec<Literal>, String> {
516        // Remove duplicates and keep only necessary literals
517        let mut minimal = Vec::new();
518        let mut seen = FxHashSet::default();
519
520        for &lit in literals {
521            if !seen.contains(&lit) {
522                seen.insert(lit);
523                minimal.push(lit);
524            }
525        }
526
527        Ok(minimal)
528    }
529
530    /// Recursive minimization.
531    fn minimize_recursive(&self, literals: &[Literal]) -> Result<Vec<Literal>, String> {
532        let mut minimal = Vec::new();
533        let mut redundant = FxHashSet::default();
534
535        for &lit in literals {
536            if self.is_redundant(lit, literals, &mut redundant)? {
537                continue;
538            }
539            minimal.push(lit);
540        }
541
542        Ok(minimal)
543    }
544
545    /// Check if a literal is redundant.
546    fn is_redundant(
547        &self,
548        literal: Literal,
549        clause: &[Literal],
550        redundant: &mut FxHashSet<Literal>,
551    ) -> Result<bool, String> {
552        if redundant.contains(&literal) {
553            return Ok(true);
554        }
555
556        let reason = self.get_reason(literal).ok().unwrap_or_default();
557
558        for &reason_lit in &reason {
559            if !clause.contains(&reason_lit)
560                && !redundant.contains(&reason_lit)
561                && !self.is_redundant(reason_lit, clause, redundant)?
562            {
563                return Ok(false);
564            }
565        }
566
567        redundant.insert(literal);
568        Ok(true)
569    }
570
571    /// Binary resolution minimization.
572    fn minimize_binary_resolution(&self, literals: &[Literal]) -> Result<Vec<Literal>, String> {
573        // Simplified: same as simple for now
574        self.minimize_simple(literals)
575    }
576
577    /// Compute backtrack level.
578    fn compute_backtrack_level(
579        &self,
580        literals: &[Literal],
581        _conflict_level: DecisionLevel,
582    ) -> Result<DecisionLevel, String> {
583        // Find second-highest decision level in the clause
584        let mut levels: Vec<DecisionLevel> = literals
585            .iter()
586            .filter_map(|&lit| self.get_decision_level(lit))
587            .collect();
588
589        levels.sort_unstable();
590        levels.dedup();
591
592        if levels.len() >= 2 {
593            Ok(levels[levels.len() - 2])
594        } else if !levels.is_empty() {
595            Ok(levels[0].saturating_sub(1))
596        } else {
597            Ok(0)
598        }
599    }
600
601    /// Find UIP literal in clause.
602    fn find_uip_literal(&self, literals: &[Literal]) -> Option<Literal> {
603        // Find literal at highest decision level
604        literals
605            .iter()
606            .max_by_key(|&&lit| self.get_decision_level(lit).unwrap_or(0))
607            .copied()
608    }
609
610    /// Get learned clauses.
611    pub fn learned_clauses(&self) -> &[ConflictClause] {
612        &self.learned_clauses
613    }
614
615    /// Get theory blame statistics.
616    pub fn theory_blame(&self) -> &FxHashMap<TheoryId, u64> {
617        &self.theory_blame
618    }
619
620    /// Clear all state.
621    pub fn clear(&mut self) {
622        self.trail.clear();
623        self.literal_position.clear();
624        self.level_boundaries.clear();
625        self.current_level = 0;
626        self.learned_clauses.clear();
627        self.theory_blame.clear();
628    }
629
630    /// Reset statistics.
631    pub fn reset_stats(&mut self) {
632        self.stats = ConflictResolutionStats::default();
633    }
634}
635
636impl Default for ConflictResolver {
637    fn default() -> Self {
638        Self::new()
639    }
640}
641
642/// Explanation generator for theory conflicts.
643pub struct ExplanationGenerator {
644    /// Explanation cache.
645    cache: FxHashMap<Literal, Explanation>,
646}
647
648impl ExplanationGenerator {
649    /// Create new explanation generator.
650    pub fn new() -> Self {
651        Self {
652            cache: FxHashMap::default(),
653        }
654    }
655
656    /// Add explanation for a literal.
657    pub fn add_explanation(&mut self, literal: Literal, explanation: Explanation) {
658        self.cache.insert(literal, explanation);
659    }
660
661    /// Get explanation for a literal.
662    pub fn get_explanation(&self, literal: Literal) -> Option<&Explanation> {
663        self.cache.get(&literal)
664    }
665
666    /// Build explanation chain.
667    pub fn build_chain(&self, literals: &[Literal]) -> Explanation {
668        let mut antecedents = Vec::new();
669
670        for &lit in literals {
671            if let Some(explanation) = self.cache.get(&lit)
672                && let Explanation::TheoryPropagation {
673                    antecedents: ants, ..
674                } = explanation
675            {
676                antecedents.extend_from_slice(ants);
677            }
678        }
679
680        Explanation::TheoryPropagation {
681            theory: 0,
682            antecedents,
683        }
684    }
685
686    /// Clear cache.
687    pub fn clear(&mut self) {
688        self.cache.clear();
689    }
690}
691
692impl Default for ExplanationGenerator {
693    fn default() -> Self {
694        Self::new()
695    }
696}
697
698/// Multi-theory conflict analyzer.
699pub struct MultiTheoryConflictAnalyzer {
700    /// Individual theory resolvers.
701    resolvers: FxHashMap<TheoryId, ConflictResolver>,
702
703    /// Combined conflict statistics.
704    combined_stats: ConflictResolutionStats,
705}
706
707impl MultiTheoryConflictAnalyzer {
708    /// Create new multi-theory analyzer.
709    pub fn new() -> Self {
710        Self {
711            resolvers: FxHashMap::default(),
712            combined_stats: ConflictResolutionStats::default(),
713        }
714    }
715
716    /// Register theory.
717    pub fn register_theory(&mut self, theory: TheoryId, config: ConflictResolutionConfig) {
718        self.resolvers
719            .insert(theory, ConflictResolver::with_config(config));
720    }
721
722    /// Analyze conflict from a theory.
723    pub fn analyze(&mut self, conflict: TheoryConflict) -> Result<ConflictAnalysis, String> {
724        let resolver = self
725            .resolvers
726            .get_mut(&conflict.theory)
727            .ok_or("Theory not registered")?;
728
729        let analysis = resolver.analyze_conflict(conflict)?;
730
731        // Update combined stats
732        self.combined_stats.conflicts_analyzed += 1;
733
734        Ok(analysis)
735    }
736
737    /// Get combined statistics.
738    pub fn combined_stats(&self) -> &ConflictResolutionStats {
739        &self.combined_stats
740    }
741
742    /// Get resolver for a theory.
743    pub fn get_resolver(&self, theory: TheoryId) -> Option<&ConflictResolver> {
744        self.resolvers.get(&theory)
745    }
746
747    /// Clear all resolvers.
748    pub fn clear(&mut self) {
749        for resolver in self.resolvers.values_mut() {
750            resolver.clear();
751        }
752        self.combined_stats = ConflictResolutionStats::default();
753    }
754}
755
756impl Default for MultiTheoryConflictAnalyzer {
757    fn default() -> Self {
758        Self::new()
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_literal_creation() {
768        let lit = Literal::positive(1);
769        assert_eq!(lit.term, 1);
770        assert!(lit.polarity);
771    }
772
773    #[test]
774    fn test_literal_negation() {
775        let lit = Literal::positive(1);
776        let neg = lit.negate();
777        assert!(!neg.polarity);
778    }
779
780    #[test]
781    fn test_resolver_creation() {
782        let resolver = ConflictResolver::new();
783        assert_eq!(resolver.stats().conflicts_analyzed, 0);
784    }
785
786    #[test]
787    fn test_add_assignment() {
788        let mut resolver = ConflictResolver::new();
789        let lit = Literal::positive(1);
790
791        resolver.add_assignment(lit, 0, Explanation::Given);
792        assert_eq!(resolver.trail.len(), 1);
793    }
794
795    #[test]
796    fn test_decision_level() {
797        let mut resolver = ConflictResolver::new();
798
799        resolver.push_decision_level();
800        assert_eq!(resolver.current_level, 1);
801    }
802
803    #[test]
804    fn test_backtrack() {
805        let mut resolver = ConflictResolver::new();
806
807        resolver.push_decision_level();
808        resolver.add_assignment(Literal::positive(1), 1, Explanation::Given);
809
810        resolver.backtrack(0).expect("Backtrack failed");
811        assert_eq!(resolver.trail.len(), 0);
812    }
813
814    #[test]
815    fn test_conflict_analysis() {
816        let mut resolver = ConflictResolver::new();
817
818        let conflict = TheoryConflict {
819            theory: 0,
820            literals: vec![Literal::positive(1), Literal::negative(2)],
821            explanation: Explanation::Given,
822            level: 0,
823        };
824
825        let analysis = resolver.analyze_conflict(conflict);
826        assert!(analysis.is_ok());
827    }
828
829    #[test]
830    fn test_explanation_generator() {
831        let mut generator = ExplanationGenerator::new();
832        let lit = Literal::positive(1);
833
834        generator.add_explanation(lit, Explanation::Given);
835        assert!(generator.get_explanation(lit).is_some());
836    }
837
838    #[test]
839    fn test_multi_theory_analyzer() {
840        let mut analyzer = MultiTheoryConflictAnalyzer::new();
841        analyzer.register_theory(0, ConflictResolutionConfig::default());
842
843        let conflict = TheoryConflict {
844            theory: 0,
845            literals: vec![Literal::positive(1)],
846            explanation: Explanation::Given,
847            level: 0,
848        };
849
850        let result = analyzer.analyze(conflict);
851        assert!(result.is_ok());
852    }
853
854    #[test]
855    fn test_simple_minimization() {
856        let resolver = ConflictResolver::new();
857
858        let literals = vec![
859            Literal::positive(1),
860            Literal::positive(2),
861            Literal::positive(1), // Duplicate
862        ];
863
864        let minimized = resolver
865            .minimize_simple(&literals)
866            .expect("Minimization failed");
867        assert_eq!(minimized.len(), 2);
868    }
869
870    #[test]
871    fn test_backtrack_level_computation() {
872        let mut resolver = ConflictResolver::new();
873
874        resolver.add_assignment(Literal::positive(1), 0, Explanation::Given);
875        resolver.push_decision_level();
876        resolver.add_assignment(Literal::positive(2), 1, Explanation::Given);
877        resolver.push_decision_level();
878        resolver.add_assignment(Literal::positive(3), 2, Explanation::Given);
879
880        let literals = vec![
881            Literal::positive(1),
882            Literal::positive(2),
883            Literal::positive(3),
884        ];
885
886        let level = resolver.compute_backtrack_level(&literals, 2);
887        assert!(level.is_ok());
888    }
889}