1#![allow(missing_docs)]
37#![allow(dead_code)]
38
39use rustc_hash::{FxHashMap, FxHashSet};
40use std::cmp::Ordering;
41use std::collections::BinaryHeap;
42
43pub type TermId = u32;
45
46pub type TheoryId = u32;
48
49pub type DecisionLevel = u32;
51
52pub type Timestamp = u64;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct Literal {
58 pub term: TermId,
60 pub polarity: bool,
62}
63
64impl Literal {
65 pub fn positive(term: TermId) -> Self {
67 Self {
68 term,
69 polarity: true,
70 }
71 }
72
73 pub fn negative(term: TermId) -> Self {
75 Self {
76 term,
77 polarity: false,
78 }
79 }
80
81 pub fn negate(self) -> Self {
83 Self {
84 term: self.term,
85 polarity: !self.polarity,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct PropagationEvent {
93 pub literal: Literal,
95 pub level: DecisionLevel,
97 pub timestamp: Timestamp,
99 pub theory: TheoryId,
101 pub reason: PropagationReason,
103}
104
105#[derive(Debug, Clone)]
107pub enum PropagationReason {
108 Decision,
110 UnitPropagation {
112 clause: ClauseId,
114 },
115 TheoryPropagation {
117 explanation: Vec<Literal>,
119 },
120 EqualityPropagation {
122 lhs: TermId,
124 rhs: TermId,
126 },
127}
128
129pub type ClauseId = u32;
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct PropagationPriority {
135 pub level: u32,
137 pub activity: u32,
139 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#[derive(Debug, Clone)]
160struct PendingPropagation {
161 event: PropagationEvent,
163 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#[derive(Debug, Clone)]
189pub struct WatchList {
190 clauses: Vec<ClauseId>,
192 theories: FxHashSet<TheoryId>,
194}
195
196impl WatchList {
197 fn new() -> Self {
199 Self {
200 clauses: Vec::new(),
201 theories: FxHashSet::default(),
202 }
203 }
204
205 fn add_clause(&mut self, clause: ClauseId) {
207 self.clauses.push(clause);
208 }
209
210 fn add_theory(&mut self, theory: TheoryId) {
212 self.theories.insert(theory);
213 }
214
215 fn watching_clauses(&self) -> &[ClauseId] {
217 &self.clauses
218 }
219
220 fn watching_theories(&self) -> impl Iterator<Item = TheoryId> + '_ {
222 self.theories.iter().copied()
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct PropagationTrail {
229 trail: Vec<PropagationEvent>,
231 level_boundaries: Vec<usize>,
233 current_level: DecisionLevel,
235}
236
237impl PropagationTrail {
238 fn new() -> Self {
240 Self {
241 trail: Vec::new(),
242 level_boundaries: vec![0],
243 current_level: 0,
244 }
245 }
246
247 fn push_level(&mut self) {
249 self.current_level += 1;
250 self.level_boundaries.push(self.trail.len());
251 }
252
253 fn add_event(&mut self, event: PropagationEvent) {
255 self.trail.push(event);
256 }
257
258 fn backtrack(&mut self, level: DecisionLevel) -> Vec<PropagationEvent> {
260 if level >= self.current_level {
261 return Vec::new();
262 }
263
264 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 fn current_level(&self) -> DecisionLevel {
281 self.current_level
282 }
283
284 fn trail(&self) -> &[PropagationEvent] {
286 &self.trail
287 }
288
289 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#[derive(Debug, Clone)]
300pub struct DependencyGraph {
301 forward: FxHashMap<TermId, FxHashSet<TermId>>,
303 backward: FxHashMap<TermId, FxHashSet<TermId>>,
305}
306
307impl DependencyGraph {
308 fn new() -> Self {
310 Self {
311 forward: FxHashMap::default(),
312 backward: FxHashMap::default(),
313 }
314 }
315
316 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 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 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 fn clear(&mut self) {
346 self.forward.clear();
347 self.backward.clear();
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct PropagationCache {
354 cache: FxHashMap<(TermId, TheoryId), (Vec<Literal>, Timestamp)>,
356 current_timestamp: Timestamp,
358}
359
360impl PropagationCache {
361 fn new() -> Self {
363 Self {
364 cache: FxHashMap::default(),
365 current_timestamp: 0,
366 }
367 }
368
369 fn tick(&mut self) {
371 self.current_timestamp += 1;
372 }
373
374 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 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 fn invalidate(&mut self, term: TermId, theory: TheoryId) {
389 self.cache.remove(&(term, theory));
390 }
391
392 fn clear(&mut self) {
394 self.cache.clear();
395 self.current_timestamp = 0;
396 }
397}
398
399#[derive(Debug, Clone)]
401pub struct PropagationConfig {
402 pub incremental: bool,
404 pub lazy: bool,
406 pub priority_based: bool,
408 pub caching: bool,
410 pub max_queue_size: usize,
412 pub batch_size: usize,
414 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#[derive(Debug, Clone, Default)]
434pub struct PropagationStats {
435 pub propagations: u64,
437 pub lazy_propagations: u64,
439 pub cache_hits: u64,
441 pub cache_misses: u64,
443 pub incremental_propagations: u64,
445 pub backtracks: u64,
447 pub dependency_updates: u64,
449}
450
451pub struct OptimizedPropagationEngine {
453 config: PropagationConfig,
455
456 stats: PropagationStats,
458
459 queue: BinaryHeap<PendingPropagation>,
461
462 watch_lists: FxHashMap<TermId, WatchList>,
464
465 trail: PropagationTrail,
467
468 dependencies: DependencyGraph,
470
471 cache: PropagationCache,
473
474 assignment_timestamps: FxHashMap<TermId, Timestamp>,
476
477 assignments: FxHashMap<TermId, bool>,
479
480 clause_activities: FxHashMap<ClauseId, u32>,
482
483 activity_decay: f64,
485}
486
487impl OptimizedPropagationEngine {
488 pub fn new() -> Self {
490 Self::with_config(PropagationConfig::default())
491 }
492
493 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 pub fn stats(&self) -> &PropagationStats {
512 &self.stats
513 }
514
515 pub fn decision_level(&self) -> DecisionLevel {
517 self.trail.current_level()
518 }
519
520 pub fn push_decision_level(&mut self) {
522 self.trail.push_level();
523 }
524
525 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 for event in undone {
535 self.assignments.remove(&event.literal.term);
536 self.assignment_timestamps.remove(&event.literal.term);
537
538 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 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 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 pub fn propagate(
562 &mut self,
563 literal: Literal,
564 theory: TheoryId,
565 reason: PropagationReason,
566 priority: u32,
567 ) -> Result<(), String> {
568 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(()); }
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 fn propagate_immediate(&mut self, event: PropagationEvent) -> Result<(), String> {
606 self.assignments
608 .insert(event.literal.term, event.literal.polarity);
609 self.assignment_timestamps
610 .insert(event.literal.term, event.timestamp);
611
612 self.trail.add_event(event.clone());
614
615 self.stats.propagations += 1;
616 self.cache.tick();
617
618 if self.config.lazy {
620 self.trigger_watches(event.literal.term)?;
621 }
622
623 Ok(())
624 }
625
626 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 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 for &_clause_id in watch_list.watching_clauses() {
649 }
651
652 for _theory_id in watch_list.watching_theories() {
654 }
656 }
657
658 Ok(())
659 }
660
661 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 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 pub fn incremental_propagate(&mut self, term: TermId, theory: TheoryId) -> Result<(), String> {
682 if !self.config.incremental {
683 return Ok(());
684 }
685
686 if self.config.caching {
688 self.invalidate_cache_for_term(term);
689 }
690
691 let affected: Vec<_> = self.get_affected_terms(term);
693
694 for affected_term in affected {
695 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 fn invalidate_cache_for_term(&mut self, term: TermId) {
708 for theory_id in 0..10 {
711 self.cache.invalidate(term, theory_id);
712 }
713 }
714
715 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 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 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 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 pub fn get_assignment(&self, term: TermId) -> Option<bool> {
754 self.assignments.get(&term).copied()
755 }
756
757 pub fn is_assigned(&self, term: TermId) -> bool {
759 self.assignments.contains_key(&term)
760 }
761
762 pub fn trail(&self) -> &[PropagationEvent] {
764 self.trail.trail()
765 }
766
767 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 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
791pub struct LazyPropagator {
793 watches: FxHashMap<Literal, Vec<ClauseId>>,
795 clauses: FxHashMap<ClauseId, Vec<Literal>>,
797 next_clause_id: ClauseId,
799}
800
801impl LazyPropagator {
802 pub fn new() -> Self {
804 Self {
805 watches: FxHashMap::default(),
806 clauses: FxHashMap::default(),
807 next_clause_id: 0,
808 }
809 }
810
811 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 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 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 pub fn get_clause(&self, clause_id: ClauseId) -> Option<&Vec<Literal>> {
846 self.clauses.get(&clause_id)
847 }
848
849 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
863pub struct IncrementalPropagator {
865 dependencies: DependencyGraph,
867 dirty: FxHashSet<TermId>,
869 propagation_order: Vec<TermId>,
871}
872
873impl IncrementalPropagator {
874 pub fn new() -> Self {
876 Self {
877 dependencies: DependencyGraph::new(),
878 dirty: FxHashSet::default(),
879 propagation_order: Vec::new(),
880 }
881 }
882
883 pub fn add_dependency(&mut self, dependency: TermId, dependent: TermId) {
885 self.dependencies.add_dependency(dependency, dependent);
886 }
887
888 pub fn mark_dirty(&mut self, term: TermId) {
890 self.dirty.insert(term);
891
892 for dependent in self.dependencies.get_dependents(term) {
894 self.dirty.insert(dependent);
895 }
896 }
897
898 pub fn get_dirty_terms(&self) -> impl Iterator<Item = TermId> + '_ {
900 self.dirty.iter().copied()
901 }
902
903 pub fn clear_dirty(&mut self) {
905 self.dirty.clear();
906 }
907
908 pub fn compute_propagation_order(&mut self) -> Vec<TermId> {
910 self.propagation_order.clear();
912 self.propagation_order.extend(self.dirty.iter().copied());
913 self.propagation_order.clone()
914 }
915
916 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 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}