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