Skip to main content

oxiz_solver/combination/
propagation_opt.rs

1//! Optimized Theory Propagation.
2//!
3//! This module implements advanced propagation strategies for theory combination:
4//! - Incremental propagation with dependency tracking
5//! - Lazy propagation using watched literals
6//! - Priority-based propagation scheduling
7//! - Propagation caching and memoization
8//! - Backtracking support with trail management
9//!
10//! ## Incremental Propagation
11//!
12//! Rather than re-propagating everything on each change:
13//! - Track dependencies between propagations
14//! - Only re-propagate affected constraints
15//! - Use timestamps to detect stale propagations
16//!
17//! ## Lazy Propagation
18//!
19//! Inspired by watched literals in SAT solving:
20//! - Maintain watch lists for terms
21//! - Trigger propagation only when watched terms change
22//! - Minimize redundant propagation work
23//!
24//! ## Priority-Based Propagation
25//!
26//! Not all propagations are equally important:
27//! - Assign priorities based on clause activity
28//! - Propagate high-priority constraints first
29//! - Defer low-priority propagations
30//!
31//! ## References
32//!
33//! - Nieuwenhuis, Oliveras, Tinelli: "Solving SAT and SAT Modulo Theories" (2006)
34//! - Z3's `smt/theory_propagation.cpp`
35
36#![allow(missing_docs)]
37#![allow(dead_code)]
38
39use rustc_hash::{FxHashMap, FxHashSet};
40use std::cmp::Ordering;
41use std::collections::BinaryHeap;
42
43/// Term identifier.
44pub type TermId = u32;
45
46/// Theory identifier.
47pub type TheoryId = u32;
48
49/// Decision level.
50pub type DecisionLevel = u32;
51
52/// Timestamp for incremental propagation.
53pub type Timestamp = u64;
54
55/// Literal (positive or negative term).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct Literal {
58    /// Term identifier.
59    pub term: TermId,
60    /// Polarity (true = positive, false = negative).
61    pub polarity: bool,
62}
63
64impl Literal {
65    /// Create positive literal.
66    pub fn positive(term: TermId) -> Self {
67        Self {
68            term,
69            polarity: true,
70        }
71    }
72
73    /// Create negative literal.
74    pub fn negative(term: TermId) -> Self {
75        Self {
76            term,
77            polarity: false,
78        }
79    }
80
81    /// Negate literal.
82    pub fn negate(self) -> Self {
83        Self {
84            term: self.term,
85            polarity: !self.polarity,
86        }
87    }
88}
89
90/// Propagation event.
91#[derive(Debug, Clone)]
92pub struct PropagationEvent {
93    /// The propagated literal.
94    pub literal: Literal,
95    /// Decision level where propagated.
96    pub level: DecisionLevel,
97    /// Timestamp of propagation.
98    pub timestamp: Timestamp,
99    /// Theory that performed propagation.
100    pub theory: TheoryId,
101    /// Reason for propagation.
102    pub reason: PropagationReason,
103}
104
105/// Reason for a propagation.
106#[derive(Debug, Clone)]
107pub enum PropagationReason {
108    /// Decision (no reason).
109    Decision,
110    /// Unit propagation.
111    UnitPropagation {
112        /// Clause that became unit.
113        clause: ClauseId,
114    },
115    /// Theory propagation.
116    TheoryPropagation {
117        /// Explanation literals.
118        explanation: Vec<Literal>,
119    },
120    /// Equality propagation.
121    EqualityPropagation {
122        /// Left-hand side.
123        lhs: TermId,
124        /// Right-hand side.
125        rhs: TermId,
126    },
127}
128
129/// Clause identifier.
130pub type ClauseId = u32;
131
132/// Propagation priority.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct PropagationPriority {
135    /// Priority level (higher = more urgent).
136    pub level: u32,
137    /// Clause activity (VSIDS-style).
138    pub activity: u32,
139    /// Decision level.
140    pub decision_level: DecisionLevel,
141}
142
143impl Ord for PropagationPriority {
144    fn cmp(&self, other: &Self) -> Ordering {
145        self.level
146            .cmp(&other.level)
147            .then_with(|| self.activity.cmp(&other.activity))
148            .then_with(|| other.decision_level.cmp(&self.decision_level))
149    }
150}
151
152impl PartialOrd for PropagationPriority {
153    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
154        Some(self.cmp(other))
155    }
156}
157
158/// Pending propagation.
159#[derive(Debug, Clone)]
160struct PendingPropagation {
161    /// Event to propagate.
162    event: PropagationEvent,
163    /// Priority.
164    priority: PropagationPriority,
165}
166
167impl PartialEq for PendingPropagation {
168    fn eq(&self, other: &Self) -> bool {
169        self.priority == other.priority
170    }
171}
172
173impl Eq for PendingPropagation {}
174
175impl Ord for PendingPropagation {
176    fn cmp(&self, other: &Self) -> Ordering {
177        self.priority.cmp(&other.priority)
178    }
179}
180
181impl PartialOrd for PendingPropagation {
182    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187/// Watch list for a term.
188#[derive(Debug, Clone)]
189pub struct WatchList {
190    /// Clauses watching this term.
191    clauses: Vec<ClauseId>,
192    /// Theories watching this term.
193    theories: FxHashSet<TheoryId>,
194}
195
196impl WatchList {
197    /// Create empty watch list.
198    fn new() -> Self {
199        Self {
200            clauses: Vec::new(),
201            theories: FxHashSet::default(),
202        }
203    }
204
205    /// Add clause to watch list.
206    fn add_clause(&mut self, clause: ClauseId) {
207        self.clauses.push(clause);
208    }
209
210    /// Add theory to watch list.
211    fn add_theory(&mut self, theory: TheoryId) {
212        self.theories.insert(theory);
213    }
214
215    /// Get watching clauses.
216    fn watching_clauses(&self) -> &[ClauseId] {
217        &self.clauses
218    }
219
220    /// Get watching theories.
221    fn watching_theories(&self) -> impl Iterator<Item = TheoryId> + '_ {
222        self.theories.iter().copied()
223    }
224}
225
226/// Propagation trail for backtracking.
227#[derive(Debug, Clone)]
228pub struct PropagationTrail {
229    /// Trail of propagation events.
230    trail: Vec<PropagationEvent>,
231    /// Decision level boundaries.
232    level_boundaries: Vec<usize>,
233    /// Current decision level.
234    current_level: DecisionLevel,
235}
236
237impl PropagationTrail {
238    /// Create new trail.
239    fn new() -> Self {
240        Self {
241            trail: Vec::new(),
242            level_boundaries: vec![0],
243            current_level: 0,
244        }
245    }
246
247    /// Push new decision level.
248    fn push_level(&mut self) {
249        self.current_level += 1;
250        self.level_boundaries.push(self.trail.len());
251    }
252
253    /// Add propagation event.
254    fn add_event(&mut self, event: PropagationEvent) {
255        self.trail.push(event);
256    }
257
258    /// Backtrack to decision level.
259    fn backtrack(&mut self, level: DecisionLevel) -> Vec<PropagationEvent> {
260        if level >= self.current_level {
261            return Vec::new();
262        }
263
264        // When backtracking to level k, we want to keep everything up to
265        // (but not including) the start of level k+1
266        let boundary = if (level as usize + 1) < self.level_boundaries.len() {
267            self.level_boundaries[level as usize + 1]
268        } else {
269            self.trail.len()
270        };
271
272        let undone = self.trail.split_off(boundary);
273        self.level_boundaries.truncate(level as usize + 1);
274        self.current_level = level;
275
276        undone
277    }
278
279    /// Get current decision level.
280    fn current_level(&self) -> DecisionLevel {
281        self.current_level
282    }
283
284    /// Get trail.
285    fn trail(&self) -> &[PropagationEvent] {
286        &self.trail
287    }
288
289    /// Clear trail.
290    fn clear(&mut self) {
291        self.trail.clear();
292        self.level_boundaries.clear();
293        self.level_boundaries.push(0);
294        self.current_level = 0;
295    }
296}
297
298/// Dependency graph for incremental propagation.
299#[derive(Debug, Clone)]
300pub struct DependencyGraph {
301    /// Forward dependencies: term → terms that depend on it.
302    forward: FxHashMap<TermId, FxHashSet<TermId>>,
303    /// Backward dependencies: term → terms it depends on.
304    backward: FxHashMap<TermId, FxHashSet<TermId>>,
305}
306
307impl DependencyGraph {
308    /// Create new dependency graph.
309    fn new() -> Self {
310        Self {
311            forward: FxHashMap::default(),
312            backward: FxHashMap::default(),
313        }
314    }
315
316    /// Add dependency: dependent depends on dependency.
317    fn add_dependency(&mut self, dependency: TermId, dependent: TermId) {
318        self.forward
319            .entry(dependency)
320            .or_default()
321            .insert(dependent);
322        self.backward
323            .entry(dependent)
324            .or_default()
325            .insert(dependency);
326    }
327
328    /// Get terms that depend on a term.
329    fn get_dependents(&self, term: TermId) -> impl Iterator<Item = TermId> + '_ {
330        self.forward
331            .get(&term)
332            .into_iter()
333            .flat_map(|set| set.iter().copied())
334    }
335
336    /// Get terms a term depends on.
337    fn get_dependencies(&self, term: TermId) -> impl Iterator<Item = TermId> + '_ {
338        self.backward
339            .get(&term)
340            .into_iter()
341            .flat_map(|set| set.iter().copied())
342    }
343
344    /// Clear all dependencies.
345    fn clear(&mut self) {
346        self.forward.clear();
347        self.backward.clear();
348    }
349}
350
351/// Propagation cache for memoization.
352#[derive(Debug, Clone)]
353pub struct PropagationCache {
354    /// Cached propagations: (term, theory) → (result, timestamp).
355    cache: FxHashMap<(TermId, TheoryId), (Vec<Literal>, Timestamp)>,
356    /// Current timestamp.
357    current_timestamp: Timestamp,
358}
359
360impl PropagationCache {
361    /// Create new cache.
362    fn new() -> Self {
363        Self {
364            cache: FxHashMap::default(),
365            current_timestamp: 0,
366        }
367    }
368
369    /// Increment timestamp.
370    fn tick(&mut self) {
371        self.current_timestamp += 1;
372    }
373
374    /// Get cached propagation.
375    fn get(&self, term: TermId, theory: TheoryId) -> Option<&Vec<Literal>> {
376        self.cache
377            .get(&(term, theory))
378            .map(|(result, _timestamp)| result)
379    }
380
381    /// Cache propagation result.
382    fn cache_result(&mut self, term: TermId, theory: TheoryId, result: Vec<Literal>) {
383        self.cache
384            .insert((term, theory), (result, self.current_timestamp));
385    }
386
387    /// Invalidate cache entry.
388    fn invalidate(&mut self, term: TermId, theory: TheoryId) {
389        self.cache.remove(&(term, theory));
390    }
391
392    /// Clear entire cache.
393    fn clear(&mut self) {
394        self.cache.clear();
395        self.current_timestamp = 0;
396    }
397}
398
399/// Configuration for optimized propagation.
400#[derive(Debug, Clone)]
401pub struct PropagationConfig {
402    /// Enable incremental propagation.
403    pub incremental: bool,
404    /// Enable lazy propagation.
405    pub lazy: bool,
406    /// Enable priority-based scheduling.
407    pub priority_based: bool,
408    /// Enable propagation caching.
409    pub caching: bool,
410    /// Maximum propagation queue size.
411    pub max_queue_size: usize,
412    /// Propagation batch size.
413    pub batch_size: usize,
414    /// Enable dependency tracking.
415    pub track_dependencies: bool,
416}
417
418impl Default for PropagationConfig {
419    fn default() -> Self {
420        Self {
421            incremental: true,
422            lazy: true,
423            priority_based: true,
424            caching: true,
425            max_queue_size: 100000,
426            batch_size: 1000,
427            track_dependencies: true,
428        }
429    }
430}
431
432/// Statistics for propagation.
433#[derive(Debug, Clone, Default)]
434pub struct PropagationStats {
435    /// Total propagations performed.
436    pub propagations: u64,
437    /// Lazy propagations triggered.
438    pub lazy_propagations: u64,
439    /// Cache hits.
440    pub cache_hits: u64,
441    /// Cache misses.
442    pub cache_misses: u64,
443    /// Incremental propagations.
444    pub incremental_propagations: u64,
445    /// Backtracking operations.
446    pub backtracks: u64,
447    /// Dependency graph updates.
448    pub dependency_updates: u64,
449}
450
451/// Optimized propagation engine.
452pub struct OptimizedPropagationEngine {
453    /// Configuration.
454    config: PropagationConfig,
455
456    /// Statistics.
457    stats: PropagationStats,
458
459    /// Propagation queue (priority queue).
460    queue: BinaryHeap<PendingPropagation>,
461
462    /// Watch lists for terms.
463    watch_lists: FxHashMap<TermId, WatchList>,
464
465    /// Propagation trail for backtracking.
466    trail: PropagationTrail,
467
468    /// Dependency graph.
469    dependencies: DependencyGraph,
470
471    /// Propagation cache.
472    cache: PropagationCache,
473
474    /// Assignment timestamps.
475    assignment_timestamps: FxHashMap<TermId, Timestamp>,
476
477    /// Current assignments.
478    assignments: FxHashMap<TermId, bool>,
479
480    /// Clause activity scores (VSIDS-style).
481    clause_activities: FxHashMap<ClauseId, u32>,
482
483    /// Activity decay factor.
484    activity_decay: f64,
485}
486
487impl OptimizedPropagationEngine {
488    /// Create new propagation engine.
489    pub fn new() -> Self {
490        Self::with_config(PropagationConfig::default())
491    }
492
493    /// Create with configuration.
494    pub fn with_config(config: PropagationConfig) -> Self {
495        Self {
496            config,
497            stats: PropagationStats::default(),
498            queue: BinaryHeap::new(),
499            watch_lists: FxHashMap::default(),
500            trail: PropagationTrail::new(),
501            dependencies: DependencyGraph::new(),
502            cache: PropagationCache::new(),
503            assignment_timestamps: FxHashMap::default(),
504            assignments: FxHashMap::default(),
505            clause_activities: FxHashMap::default(),
506            activity_decay: 0.95,
507        }
508    }
509
510    /// Get statistics.
511    pub fn stats(&self) -> &PropagationStats {
512        &self.stats
513    }
514
515    /// Get current decision level.
516    pub fn decision_level(&self) -> DecisionLevel {
517        self.trail.current_level()
518    }
519
520    /// Push new decision level.
521    pub fn push_decision_level(&mut self) {
522        self.trail.push_level();
523    }
524
525    /// Backtrack to decision level.
526    pub fn backtrack(&mut self, level: DecisionLevel) -> Result<(), String> {
527        if level > self.decision_level() {
528            return Err("Cannot backtrack to future level".to_string());
529        }
530
531        let undone = self.trail.backtrack(level);
532
533        // Undo assignments
534        for event in undone {
535            self.assignments.remove(&event.literal.term);
536            self.assignment_timestamps.remove(&event.literal.term);
537
538            // Invalidate cache for affected terms
539            if self.config.caching {
540                self.invalidate_cache_for_term(event.literal.term);
541            }
542        }
543
544        self.stats.backtracks += 1;
545        Ok(())
546    }
547
548    /// Add watch for a term.
549    pub fn add_watch(&mut self, term: TermId, clause: ClauseId) {
550        let watch_list = self.watch_lists.entry(term).or_insert_with(WatchList::new);
551        watch_list.add_clause(clause);
552    }
553
554    /// Add theory watch for a term.
555    pub fn add_theory_watch(&mut self, term: TermId, theory: TheoryId) {
556        let watch_list = self.watch_lists.entry(term).or_insert_with(WatchList::new);
557        watch_list.add_theory(theory);
558    }
559
560    /// Propagate a literal.
561    pub fn propagate(
562        &mut self,
563        literal: Literal,
564        theory: TheoryId,
565        reason: PropagationReason,
566        priority: u32,
567    ) -> Result<(), String> {
568        // Check if already assigned
569        if let Some(&assigned_value) = self.assignments.get(&literal.term) {
570            if assigned_value != literal.polarity {
571                return Err("Conflict: literal already assigned with opposite polarity".to_string());
572            }
573            return Ok(()); // Already propagated
574        }
575
576        let event = PropagationEvent {
577            literal,
578            level: self.decision_level(),
579            timestamp: self.cache.current_timestamp,
580            theory,
581            reason,
582        };
583
584        let activity = self.clause_activities.get(&0).copied().unwrap_or(0);
585
586        let pending = PendingPropagation {
587            event,
588            priority: PropagationPriority {
589                level: priority,
590                activity,
591                decision_level: self.decision_level(),
592            },
593        };
594
595        if self.config.priority_based {
596            self.queue.push(pending);
597        } else {
598            self.propagate_immediate(pending.event)?;
599        }
600
601        Ok(())
602    }
603
604    /// Propagate immediately (non-lazy).
605    fn propagate_immediate(&mut self, event: PropagationEvent) -> Result<(), String> {
606        // Assign literal
607        self.assignments
608            .insert(event.literal.term, event.literal.polarity);
609        self.assignment_timestamps
610            .insert(event.literal.term, event.timestamp);
611
612        // Add to trail
613        self.trail.add_event(event.clone());
614
615        self.stats.propagations += 1;
616        self.cache.tick();
617
618        // Trigger watches if lazy propagation enabled
619        if self.config.lazy {
620            self.trigger_watches(event.literal.term)?;
621        }
622
623        Ok(())
624    }
625
626    /// Process propagation queue.
627    pub fn process_queue(&mut self) -> Result<(), String> {
628        let mut count = 0;
629
630        while let Some(pending) = self.queue.pop() {
631            self.propagate_immediate(pending.event)?;
632
633            count += 1;
634            if count >= self.config.batch_size {
635                break;
636            }
637        }
638
639        Ok(())
640    }
641
642    /// Trigger watches for a term.
643    fn trigger_watches(&mut self, term: TermId) -> Result<(), String> {
644        if let Some(watch_list) = self.watch_lists.get(&term) {
645            self.stats.lazy_propagations += 1;
646
647            // Trigger clause watches
648            for &_clause_id in watch_list.watching_clauses() {
649                // Would trigger clause propagation here
650            }
651
652            // Trigger theory watches
653            for _theory_id in watch_list.watching_theories() {
654                // Would notify theory here
655            }
656        }
657
658        Ok(())
659    }
660
661    /// Add dependency between terms.
662    pub fn add_dependency(&mut self, dependency: TermId, dependent: TermId) {
663        if !self.config.track_dependencies {
664            return;
665        }
666
667        self.dependencies.add_dependency(dependency, dependent);
668        self.stats.dependency_updates += 1;
669    }
670
671    /// Get terms affected by change to a term.
672    pub fn get_affected_terms(&self, term: TermId) -> Vec<TermId> {
673        if !self.config.track_dependencies {
674            return Vec::new();
675        }
676
677        self.dependencies.get_dependents(term).collect()
678    }
679
680    /// Incremental propagation for changed term.
681    pub fn incremental_propagate(&mut self, term: TermId, theory: TheoryId) -> Result<(), String> {
682        if !self.config.incremental {
683            return Ok(());
684        }
685
686        // Invalidate cache for this term
687        if self.config.caching {
688            self.invalidate_cache_for_term(term);
689        }
690
691        // Get affected terms via dependency graph
692        let affected: Vec<_> = self.get_affected_terms(term);
693
694        for affected_term in affected {
695            // Re-propagate affected terms
696            if self.config.caching {
697                self.cache.invalidate(affected_term, theory);
698            }
699
700            self.stats.incremental_propagations += 1;
701        }
702
703        Ok(())
704    }
705
706    /// Invalidate cache for a term.
707    fn invalidate_cache_for_term(&mut self, term: TermId) {
708        // Invalidate for all theories
709        // (In practice, we'd track which theories to invalidate)
710        for theory_id in 0..10 {
711            self.cache.invalidate(term, theory_id);
712        }
713    }
714
715    /// Check cache for propagation result.
716    pub fn check_cache(&mut self, term: TermId, theory: TheoryId) -> Option<Vec<Literal>> {
717        if !self.config.caching {
718            return None;
719        }
720
721        if let Some(result) = self.cache.get(term, theory) {
722            self.stats.cache_hits += 1;
723            Some(result.clone())
724        } else {
725            self.stats.cache_misses += 1;
726            None
727        }
728    }
729
730    /// Cache propagation result.
731    pub fn cache_propagation(&mut self, term: TermId, theory: TheoryId, result: Vec<Literal>) {
732        if !self.config.caching {
733            return;
734        }
735
736        self.cache.cache_result(term, theory, result);
737    }
738
739    /// Update clause activity (VSIDS-style).
740    pub fn bump_clause_activity(&mut self, clause: ClauseId) {
741        let activity = self.clause_activities.entry(clause).or_insert(0);
742        *activity += 1;
743    }
744
745    /// Decay all clause activities.
746    pub fn decay_activities(&mut self) {
747        for activity in self.clause_activities.values_mut() {
748            *activity = (*activity as f64 * self.activity_decay) as u32;
749        }
750    }
751
752    /// Get assignment for term.
753    pub fn get_assignment(&self, term: TermId) -> Option<bool> {
754        self.assignments.get(&term).copied()
755    }
756
757    /// Check if term is assigned.
758    pub fn is_assigned(&self, term: TermId) -> bool {
759        self.assignments.contains_key(&term)
760    }
761
762    /// Get propagation trail.
763    pub fn trail(&self) -> &[PropagationEvent] {
764        self.trail.trail()
765    }
766
767    /// Clear all state.
768    pub fn clear(&mut self) {
769        self.queue.clear();
770        self.watch_lists.clear();
771        self.trail.clear();
772        self.dependencies.clear();
773        self.cache.clear();
774        self.assignment_timestamps.clear();
775        self.assignments.clear();
776        self.clause_activities.clear();
777    }
778
779    /// Reset statistics.
780    pub fn reset_stats(&mut self) {
781        self.stats = PropagationStats::default();
782    }
783}
784
785impl Default for OptimizedPropagationEngine {
786    fn default() -> Self {
787        Self::new()
788    }
789}
790
791/// Lazy propagator using watch lists.
792pub struct LazyPropagator {
793    /// Watch lists.
794    watches: FxHashMap<Literal, Vec<ClauseId>>,
795    /// Clauses.
796    clauses: FxHashMap<ClauseId, Vec<Literal>>,
797    /// Next clause ID.
798    next_clause_id: ClauseId,
799}
800
801impl LazyPropagator {
802    /// Create new lazy propagator.
803    pub fn new() -> Self {
804        Self {
805            watches: FxHashMap::default(),
806            clauses: FxHashMap::default(),
807            next_clause_id: 0,
808        }
809    }
810
811    /// Add clause with watched literals.
812    pub fn add_clause(&mut self, literals: Vec<Literal>) -> ClauseId {
813        let clause_id = self.next_clause_id;
814        self.next_clause_id += 1;
815
816        // Watch first two literals (if they exist)
817        if literals.len() >= 2 {
818            self.watches.entry(literals[0]).or_default().push(clause_id);
819            self.watches.entry(literals[1]).or_default().push(clause_id);
820        }
821
822        self.clauses.insert(clause_id, literals);
823        clause_id
824    }
825
826    /// Propagate assignment.
827    pub fn propagate_assignment(
828        &mut self,
829        literal: Literal,
830        _assignments: &FxHashMap<TermId, bool>,
831    ) -> Vec<ClauseId> {
832        let negated = literal.negate();
833        let mut triggered = Vec::new();
834
835        if let Some(watching_clauses) = self.watches.get(&negated) {
836            for &clause_id in watching_clauses {
837                triggered.push(clause_id);
838            }
839        }
840
841        triggered
842    }
843
844    /// Get clause literals.
845    pub fn get_clause(&self, clause_id: ClauseId) -> Option<&Vec<Literal>> {
846        self.clauses.get(&clause_id)
847    }
848
849    /// Clear all watches and clauses.
850    pub fn clear(&mut self) {
851        self.watches.clear();
852        self.clauses.clear();
853        self.next_clause_id = 0;
854    }
855}
856
857impl Default for LazyPropagator {
858    fn default() -> Self {
859        Self::new()
860    }
861}
862
863/// Incremental propagator with dependency tracking.
864pub struct IncrementalPropagator {
865    /// Dependency graph.
866    dependencies: DependencyGraph,
867    /// Dirty terms (need re-propagation).
868    dirty: FxHashSet<TermId>,
869    /// Propagation order (topological).
870    propagation_order: Vec<TermId>,
871}
872
873impl IncrementalPropagator {
874    /// Create new incremental propagator.
875    pub fn new() -> Self {
876        Self {
877            dependencies: DependencyGraph::new(),
878            dirty: FxHashSet::default(),
879            propagation_order: Vec::new(),
880        }
881    }
882
883    /// Add dependency.
884    pub fn add_dependency(&mut self, dependency: TermId, dependent: TermId) {
885        self.dependencies.add_dependency(dependency, dependent);
886    }
887
888    /// Mark term as dirty.
889    pub fn mark_dirty(&mut self, term: TermId) {
890        self.dirty.insert(term);
891
892        // Mark dependents as dirty
893        for dependent in self.dependencies.get_dependents(term) {
894            self.dirty.insert(dependent);
895        }
896    }
897
898    /// Get dirty terms.
899    pub fn get_dirty_terms(&self) -> impl Iterator<Item = TermId> + '_ {
900        self.dirty.iter().copied()
901    }
902
903    /// Clear dirty marks.
904    pub fn clear_dirty(&mut self) {
905        self.dirty.clear();
906    }
907
908    /// Compute propagation order (topological sort).
909    pub fn compute_propagation_order(&mut self) -> Vec<TermId> {
910        // Simplified topological sort
911        self.propagation_order.clear();
912        self.propagation_order.extend(self.dirty.iter().copied());
913        self.propagation_order.clone()
914    }
915
916    /// Clear all state.
917    pub fn clear(&mut self) {
918        self.dependencies.clear();
919        self.dirty.clear();
920        self.propagation_order.clear();
921    }
922}
923
924impl Default for IncrementalPropagator {
925    fn default() -> Self {
926        Self::new()
927    }
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    #[test]
935    fn test_literal_creation() {
936        let lit = Literal::positive(1);
937        assert_eq!(lit.term, 1);
938        assert!(lit.polarity);
939    }
940
941    #[test]
942    fn test_literal_negation() {
943        let lit = Literal::positive(1);
944        let neg = lit.negate();
945        assert_eq!(neg.term, 1);
946        assert!(!neg.polarity);
947    }
948
949    #[test]
950    fn test_propagation_engine_creation() {
951        let engine = OptimizedPropagationEngine::new();
952        assert_eq!(engine.decision_level(), 0);
953    }
954
955    #[test]
956    fn test_decision_level() {
957        let mut engine = OptimizedPropagationEngine::new();
958        engine.push_decision_level();
959        assert_eq!(engine.decision_level(), 1);
960    }
961
962    #[test]
963    fn test_backtrack() {
964        let mut engine = OptimizedPropagationEngine::new();
965        engine.push_decision_level();
966        engine.push_decision_level();
967        assert_eq!(engine.decision_level(), 2);
968
969        engine.backtrack(1).expect("Backtrack failed");
970        assert_eq!(engine.decision_level(), 1);
971    }
972
973    #[test]
974    fn test_watch_list() {
975        let mut watch_list = WatchList::new();
976        watch_list.add_clause(1);
977        watch_list.add_theory(0);
978
979        assert_eq!(watch_list.watching_clauses().len(), 1);
980    }
981
982    #[test]
983    fn test_propagation_trail() {
984        let mut trail = PropagationTrail::new();
985        trail.push_level();
986
987        let event = PropagationEvent {
988            literal: Literal::positive(1),
989            level: 1,
990            timestamp: 0,
991            theory: 0,
992            reason: PropagationReason::Decision,
993        };
994
995        trail.add_event(event);
996        assert_eq!(trail.trail().len(), 1);
997    }
998
999    #[test]
1000    fn test_trail_backtrack() {
1001        let mut trail = PropagationTrail::new();
1002        trail.push_level();
1003
1004        let event1 = PropagationEvent {
1005            literal: Literal::positive(1),
1006            level: 1,
1007            timestamp: 0,
1008            theory: 0,
1009            reason: PropagationReason::Decision,
1010        };
1011
1012        trail.add_event(event1);
1013        trail.push_level();
1014
1015        let event2 = PropagationEvent {
1016            literal: Literal::positive(2),
1017            level: 2,
1018            timestamp: 1,
1019            theory: 0,
1020            reason: PropagationReason::Decision,
1021        };
1022
1023        trail.add_event(event2);
1024
1025        let undone = trail.backtrack(1);
1026        assert_eq!(undone.len(), 1);
1027        assert_eq!(trail.trail().len(), 1);
1028    }
1029
1030    #[test]
1031    fn test_dependency_graph() {
1032        let mut deps = DependencyGraph::new();
1033        deps.add_dependency(1, 2);
1034        deps.add_dependency(1, 3);
1035
1036        let dependents: Vec<_> = deps.get_dependents(1).collect();
1037        assert_eq!(dependents.len(), 2);
1038    }
1039
1040    #[test]
1041    fn test_propagation_cache() {
1042        let mut cache = PropagationCache::new();
1043        let result = vec![Literal::positive(1)];
1044
1045        cache.cache_result(1, 0, result.clone());
1046        assert_eq!(cache.get(1, 0), Some(&result));
1047    }
1048
1049    #[test]
1050    fn test_cache_invalidation() {
1051        let mut cache = PropagationCache::new();
1052        let result = vec![Literal::positive(1)];
1053
1054        cache.cache_result(1, 0, result);
1055        cache.invalidate(1, 0);
1056        assert_eq!(cache.get(1, 0), None);
1057    }
1058
1059    #[test]
1060    fn test_lazy_propagator() {
1061        let mut prop = LazyPropagator::new();
1062        let clause = vec![Literal::positive(1), Literal::positive(2)];
1063        let clause_id = prop.add_clause(clause);
1064
1065        assert_eq!(prop.get_clause(clause_id).map(|c| c.len()), Some(2));
1066    }
1067
1068    #[test]
1069    fn test_incremental_propagator() {
1070        let mut prop = IncrementalPropagator::new();
1071        prop.add_dependency(1, 2);
1072        prop.mark_dirty(1);
1073
1074        let dirty: Vec<_> = prop.get_dirty_terms().collect();
1075        assert!(dirty.contains(&1));
1076        assert!(dirty.contains(&2));
1077    }
1078
1079    #[test]
1080    fn test_priority_ordering() {
1081        let p1 = PropagationPriority {
1082            level: 1,
1083            activity: 0,
1084            decision_level: 0,
1085        };
1086        let p2 = PropagationPriority {
1087            level: 2,
1088            activity: 0,
1089            decision_level: 0,
1090        };
1091
1092        assert!(p2 > p1);
1093    }
1094
1095    #[test]
1096    fn test_propagate() {
1097        let mut engine = OptimizedPropagationEngine::new();
1098        let literal = Literal::positive(1);
1099
1100        engine
1101            .propagate(literal, 0, PropagationReason::Decision, 100)
1102            .expect("Propagation failed");
1103
1104        // Process the queue to actually execute the propagation
1105        engine.process_queue().expect("Process queue failed");
1106
1107        assert_eq!(engine.stats().propagations, 1);
1108    }
1109
1110    #[test]
1111    fn test_assignment_tracking() {
1112        let mut engine = OptimizedPropagationEngine::new();
1113        let literal = Literal::positive(1);
1114
1115        engine
1116            .propagate(literal, 0, PropagationReason::Decision, 100)
1117            .expect("Propagation failed");
1118        engine.process_queue().expect("Queue processing failed");
1119
1120        assert_eq!(engine.get_assignment(1), Some(true));
1121    }
1122}