Skip to main content

oxiz_spacer/
frames.rs

1//! Frame management for PDR (Property Directed Reachability).
2//!
3//! Frames F_0, F_1, ..., F_N represent over-approximations of reachable states.
4//!
5//! Key invariants:
6//! - F_0 = Init (initial states)
7//! - F_i ⊇ F_{i+1} (monotonicity: each frame is an over-approximation of the next)
8//! - F_i ∧ T ⊆ F_{i+1} (consecution: transitions from F_i stay in F_{i+1})
9//!
10//! Reference: Z3's `muz/spacer/spacer_context.h` frames class.
11
12use crate::chc::PredId;
13use oxiz_core::TermId;
14use rustc_hash::FxHashSet;
15use smallvec::SmallVec;
16use std::cmp::Ordering;
17use std::collections::BinaryHeap;
18
19/// Represents infinity level for inductive lemmas
20pub const INFTY_LEVEL: u32 = u32::MAX;
21
22/// Check if a level is infinity
23#[inline]
24pub fn is_infty_level(level: u32) -> bool {
25    level == INFTY_LEVEL
26}
27
28/// A lemma in the frame sequence
29#[derive(Debug, Clone)]
30pub struct Lemma {
31    /// Unique identifier for this lemma
32    pub id: LemmaId,
33    /// The lemma formula (negation of blocked states)
34    pub formula: TermId,
35    /// Current level of the lemma
36    level: u32,
37    /// Initial level when the lemma was created
38    init_level: u32,
39    /// Number of times this lemma has been bumped (priority boost)
40    bumped: u16,
41    /// Weakness score for prioritization
42    weakness: u16,
43    /// Whether this lemma came from an external source
44    external: bool,
45    /// Whether this lemma is blocked by a counter-example to pushing
46    blocked: bool,
47    /// Whether this is a background assumption
48    background: bool,
49}
50
51impl Lemma {
52    /// Create a new lemma at a given level
53    pub fn new(id: LemmaId, formula: TermId, level: u32) -> Self {
54        Self {
55            id,
56            formula,
57            level,
58            init_level: level,
59            bumped: 0,
60            weakness: 0,
61            external: false,
62            blocked: false,
63            background: false,
64        }
65    }
66
67    /// Check if this lemma is inductive (at infinity level)
68    #[inline]
69    #[must_use]
70    pub fn is_inductive(&self) -> bool {
71        is_infty_level(self.level)
72    }
73
74    /// Get the current level
75    #[inline]
76    #[must_use]
77    pub fn level(&self) -> u32 {
78        self.level
79    }
80
81    /// Get the initial level
82    #[inline]
83    #[must_use]
84    pub fn init_level(&self) -> u32 {
85        self.init_level
86    }
87
88    /// Set the level (for propagation)
89    #[inline]
90    pub fn set_level(&mut self, level: u32) {
91        self.level = level;
92    }
93
94    /// Bump the lemma priority
95    #[inline]
96    pub fn bump(&mut self) {
97        self.bumped = self.bumped.saturating_add(1);
98    }
99
100    /// Get the bump count
101    #[inline]
102    #[must_use]
103    pub fn bumped(&self) -> u16 {
104        self.bumped
105    }
106
107    /// Get the weakness score
108    #[inline]
109    #[must_use]
110    pub fn weakness(&self) -> u16 {
111        self.weakness
112    }
113
114    /// Set the weakness score
115    #[inline]
116    pub fn set_weakness(&mut self, weakness: u16) {
117        self.weakness = weakness;
118    }
119
120    /// Check if this is an external lemma
121    #[inline]
122    #[must_use]
123    pub fn is_external(&self) -> bool {
124        self.external
125    }
126
127    /// Mark as external
128    #[inline]
129    pub fn set_external(&mut self, external: bool) {
130        self.external = external;
131    }
132
133    /// Check if blocked by CTP
134    #[inline]
135    #[must_use]
136    pub fn is_blocked(&self) -> bool {
137        self.blocked
138    }
139
140    /// Set blocked status
141    #[inline]
142    pub fn set_blocked(&mut self, blocked: bool) {
143        self.blocked = blocked;
144    }
145
146    /// Check if this is a background assumption
147    #[inline]
148    #[must_use]
149    pub fn is_background(&self) -> bool {
150        self.background
151    }
152
153    /// Mark as background
154    #[inline]
155    pub fn set_background(&mut self, background: bool) {
156        self.background = background;
157    }
158}
159
160/// Unique identifier for a lemma
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
162pub struct LemmaId(pub u32);
163
164impl LemmaId {
165    /// Create a new lemma ID
166    #[must_use]
167    pub const fn new(id: u32) -> Self {
168        Self(id)
169    }
170
171    /// Get the raw ID value
172    #[must_use]
173    pub const fn raw(self) -> u32 {
174        self.0
175    }
176}
177
178/// Comparison for lemmas by level (for sorting)
179impl PartialEq for Lemma {
180    fn eq(&self, other: &Self) -> bool {
181        self.id == other.id
182    }
183}
184
185impl Eq for Lemma {}
186
187impl PartialOrd for Lemma {
188    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189        Some(self.cmp(other))
190    }
191}
192
193impl Ord for Lemma {
194    fn cmp(&self, other: &Self) -> Ordering {
195        self.level
196            .cmp(&other.level)
197            .then_with(|| self.id.cmp(&other.id))
198    }
199}
200
201/// Frame sequence for a single predicate transformer
202#[derive(Debug)]
203pub struct PredicateFrames {
204    /// The predicate this frame sequence is for
205    pub pred: PredId,
206    /// All lemmas (both active and pinned)
207    lemmas: Vec<Lemma>,
208    /// Active lemma IDs (not subsumed)
209    active: FxHashSet<LemmaId>,
210    /// Background invariants
211    bg_invariants: SmallVec<[LemmaId; 4]>,
212    /// Number of frames
213    num_frames: u32,
214    /// Next lemma ID
215    next_lemma_id: u32,
216    /// Whether lemmas are sorted
217    sorted: bool,
218}
219
220impl PredicateFrames {
221    /// Create a new frame sequence for a predicate
222    pub fn new(pred: PredId) -> Self {
223        Self {
224            pred,
225            lemmas: Vec::new(),
226            active: FxHashSet::default(),
227            bg_invariants: SmallVec::new(),
228            num_frames: 1, // Start with F_0
229            next_lemma_id: 0,
230            sorted: true,
231        }
232    }
233
234    /// Get the number of frames
235    #[inline]
236    #[must_use]
237    pub fn num_frames(&self) -> u32 {
238        self.num_frames
239    }
240
241    /// Add a new frame
242    pub fn add_frame(&mut self) {
243        self.num_frames += 1;
244    }
245
246    /// Add a lemma at a given level
247    pub fn add_lemma(&mut self, formula: TermId, level: u32) -> LemmaId {
248        let id = LemmaId(self.next_lemma_id);
249        self.next_lemma_id += 1;
250
251        let lemma = Lemma::new(id, formula, level);
252        self.lemmas.push(lemma);
253        self.active.insert(id);
254        self.sorted = false;
255
256        id
257    }
258
259    /// Add a background invariant
260    pub fn add_background(&mut self, formula: TermId) -> LemmaId {
261        let id = self.add_lemma(formula, INFTY_LEVEL);
262        if let Some(lemma) = self.get_lemma_mut(id) {
263            lemma.set_background(true);
264        }
265        self.bg_invariants.push(id);
266        id
267    }
268
269    /// Get a lemma by ID
270    #[must_use]
271    pub fn get_lemma(&self, id: LemmaId) -> Option<&Lemma> {
272        self.lemmas.get(id.0 as usize)
273    }
274
275    /// Get a mutable lemma by ID
276    pub fn get_lemma_mut(&mut self, id: LemmaId) -> Option<&mut Lemma> {
277        self.lemmas.get_mut(id.0 as usize)
278    }
279
280    /// Get all active lemmas
281    #[inline]
282    pub fn active_lemmas(&self) -> impl Iterator<Item = &Lemma> {
283        self.active.iter().filter_map(|&id| self.get_lemma(id))
284    }
285
286    /// Get lemmas at a specific level
287    pub fn lemmas_at_level(&self, level: u32) -> impl Iterator<Item = &Lemma> {
288        self.active_lemmas().filter(move |l| l.level() == level)
289    }
290
291    /// Get lemmas at or above a level (for frame queries)
292    pub fn lemmas_geq_level(&self, level: u32) -> impl Iterator<Item = &Lemma> {
293        self.active_lemmas().filter(move |l| l.level() >= level)
294    }
295
296    /// Get all inductive lemmas
297    #[inline]
298    pub fn inductive_lemmas(&self) -> impl Iterator<Item = &Lemma> {
299        self.active_lemmas().filter(|l| l.is_inductive())
300    }
301
302    /// Get background invariants
303    pub fn background_invariants(&self) -> impl Iterator<Item = &Lemma> {
304        self.bg_invariants
305            .iter()
306            .filter_map(|&id| self.get_lemma(id))
307    }
308
309    /// Propagate a lemma to a higher level
310    pub fn propagate(&mut self, id: LemmaId, new_level: u32) -> bool {
311        if let Some(lemma) = self.get_lemma_mut(id)
312            && new_level > lemma.level()
313        {
314            lemma.set_level(new_level);
315            self.sorted = false;
316            return true;
317        }
318        false
319    }
320
321    /// Propagate lemmas to infinity (inductive)
322    pub fn propagate_to_infinity(&mut self, from_level: u32) {
323        for lemma in &mut self.lemmas {
324            if self.active.contains(&lemma.id) && lemma.level() >= from_level {
325                lemma.set_level(INFTY_LEVEL);
326            }
327        }
328        self.sorted = false;
329    }
330
331    /// Try to propagate all lemmas at a level to the next level
332    /// Returns true if all lemmas were successfully propagated
333    pub fn propagate_level(&mut self, level: u32) -> bool {
334        let mut all_propagated = true;
335        let lemma_ids: Vec<_> = self.lemmas_at_level(level).map(|l| l.id).collect();
336
337        for id in lemma_ids {
338            if !self.propagate(id, level + 1) {
339                all_propagated = false;
340            }
341        }
342
343        all_propagated
344    }
345
346    /// Deactivate a lemma (mark as subsumed)
347    pub fn deactivate(&mut self, id: LemmaId) {
348        self.active.remove(&id);
349    }
350
351    /// Check if a lemma is active
352    #[must_use]
353    pub fn is_active(&self, id: LemmaId) -> bool {
354        self.active.contains(&id)
355    }
356
357    /// Get number of active lemmas
358    #[must_use]
359    pub fn num_active_lemmas(&self) -> usize {
360        self.active.len()
361    }
362
363    /// Get number of inductive lemmas
364    #[must_use]
365    pub fn num_inductive(&self) -> usize {
366        self.inductive_lemmas().count()
367    }
368
369    /// Remove subsumed lemmas using syntactic subsumption
370    /// Returns the number of lemmas removed
371    pub fn remove_subsumed_syntactic(&mut self) -> usize {
372        let mut to_deactivate = Vec::new();
373
374        // Collect all active lemma IDs and formulas
375        let active_lemmas: Vec<_> = self
376            .active
377            .iter()
378            .filter_map(|&id| self.get_lemma(id).map(|l| (id, l.formula)))
379            .collect();
380
381        // Check for syntactic duplicates
382        for i in 0..active_lemmas.len() {
383            for j in (i + 1)..active_lemmas.len() {
384                if active_lemmas[i].1 == active_lemmas[j].1 {
385                    // Same formula - remove one (keep the one with lower level)
386                    let lemma_i = self.get_lemma(active_lemmas[i].0);
387                    let lemma_j = self.get_lemma(active_lemmas[j].0);
388
389                    if let (Some(li), Some(lj)) = (lemma_i, lemma_j) {
390                        if li.level() > lj.level() {
391                            to_deactivate.push(active_lemmas[i].0);
392                        } else {
393                            to_deactivate.push(active_lemmas[j].0);
394                        }
395                    }
396                }
397            }
398        }
399
400        // Remove duplicates from deactivation list
401        to_deactivate.sort_unstable_by_key(|id| id.0);
402        to_deactivate.dedup();
403
404        // Actually deactivate
405        let count = to_deactivate.len();
406        for id in to_deactivate {
407            self.deactivate(id);
408        }
409
410        count
411    }
412
413    /// Sort lemmas by level (for efficient queries)
414    pub fn sort_lemmas(&mut self) {
415        if !self.sorted {
416            // We don't actually need to sort the storage, just mark as sorted
417            // since we filter through active set anyway
418            self.sorted = true;
419        }
420    }
421
422    /// Clear all lemmas (reset)
423    pub fn clear(&mut self) {
424        self.lemmas.clear();
425        self.active.clear();
426        self.bg_invariants.clear();
427        self.num_frames = 1;
428        self.next_lemma_id = 0;
429        self.sorted = true;
430    }
431
432    /// Compress frames by removing lemmas at lower levels that have been pushed
433    /// This is a memory optimization to reduce the number of stored lemmas
434    /// Returns the number of lemmas removed
435    pub fn compress(&mut self, keep_above_level: u32) -> usize {
436        let mut removed = 0;
437
438        // Collect lemmas to remove (those below keep_above_level and not inductive)
439        let to_remove: Vec<_> = self
440            .active
441            .iter()
442            .filter_map(|&id| {
443                self.get_lemma(id).and_then(|l| {
444                    if l.level() < keep_above_level && !l.is_inductive() {
445                        Some(id)
446                    } else {
447                        None
448                    }
449                })
450            })
451            .collect();
452
453        // Deactivate these lemmas
454        for id in to_remove {
455            self.deactivate(id);
456            removed += 1;
457        }
458
459        removed
460    }
461
462    /// Get memory usage statistics
463    #[must_use]
464    pub fn memory_stats(&self) -> (usize, usize, usize) {
465        (
466            self.lemmas.len(),        // Total lemmas
467            self.active.len(),        // Active lemmas
468            self.bg_invariants.len(), // Background invariants
469        )
470    }
471}
472
473/// Global frame manager for all predicates
474#[derive(Debug)]
475pub struct FrameManager {
476    /// Per-predicate frame sequences
477    frames: rustc_hash::FxHashMap<PredId, PredicateFrames>,
478    /// Current global frame level
479    current_level: u32,
480}
481
482impl Default for FrameManager {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488impl FrameManager {
489    /// Create a new frame manager
490    pub fn new() -> Self {
491        Self {
492            frames: rustc_hash::FxHashMap::default(),
493            current_level: 0,
494        }
495    }
496
497    /// Get or create frames for a predicate
498    pub fn get_or_create(&mut self, pred: PredId) -> &mut PredicateFrames {
499        self.frames
500            .entry(pred)
501            .or_insert_with(|| PredicateFrames::new(pred))
502    }
503
504    /// Get frames for a predicate
505    #[must_use]
506    pub fn get(&self, pred: PredId) -> Option<&PredicateFrames> {
507        self.frames.get(&pred)
508    }
509
510    /// Get mutable frames for a predicate
511    pub fn get_mut(&mut self, pred: PredId) -> Option<&mut PredicateFrames> {
512        self.frames.get_mut(&pred)
513    }
514
515    /// Get the current global level
516    #[inline]
517    #[must_use]
518    pub fn current_level(&self) -> u32 {
519        self.current_level
520    }
521
522    /// Advance to the next level
523    pub fn next_level(&mut self) {
524        self.current_level += 1;
525        for frames in self.frames.values_mut() {
526            frames.add_frame();
527        }
528    }
529
530    /// Add a lemma for a predicate at a level
531    pub fn add_lemma(&mut self, pred: PredId, formula: TermId, level: u32) -> LemmaId {
532        self.get_or_create(pred).add_lemma(formula, level)
533    }
534
535    /// Check for fixpoint: all lemmas at current level are inductive
536    #[must_use]
537    pub fn is_fixpoint(&self) -> bool {
538        let level = self.current_level;
539        for frames in self.frames.values() {
540            // Check if there are any non-inductive lemmas at current level
541            if frames.lemmas_at_level(level).any(|l| !l.is_inductive()) {
542                return false;
543            }
544        }
545        true
546    }
547
548    /// Try to propagate all frames
549    /// Returns true if a fixpoint is detected
550    pub fn propagate(&mut self) -> bool {
551        // Try to push all lemmas to higher levels
552        for level in 1..=self.current_level {
553            let mut all_propagated = true;
554
555            for frames in self.frames.values_mut() {
556                if !frames.propagate_level(level) {
557                    all_propagated = false;
558                }
559            }
560
561            // If all lemmas at this level were pushed, we have a fixpoint
562            if all_propagated && level == self.current_level {
563                // Promote all lemmas at this level to infinity
564                for frames in self.frames.values_mut() {
565                    frames.propagate_to_infinity(level);
566                }
567                return true;
568            }
569        }
570
571        false
572    }
573
574    /// Compress all frames by removing old lemmas
575    /// Returns the total number of lemmas removed
576    pub fn compress(&mut self, keep_above_level: u32) -> usize {
577        let mut total_removed = 0;
578        for frames in self.frames.values_mut() {
579            total_removed += frames.compress(keep_above_level);
580        }
581        total_removed
582    }
583
584    /// Get total memory usage statistics across all predicates
585    #[must_use]
586    pub fn total_memory_stats(&self) -> (usize, usize, usize) {
587        let mut total_lemmas = 0;
588        let mut total_active = 0;
589        let mut total_bg = 0;
590
591        for frames in self.frames.values() {
592            let (lemmas, active, bg) = frames.memory_stats();
593            total_lemmas += lemmas;
594            total_active += active;
595            total_bg += bg;
596        }
597
598        (total_lemmas, total_active, total_bg)
599    }
600
601    /// Reset all frames
602    pub fn reset(&mut self) {
603        for frames in self.frames.values_mut() {
604            frames.clear();
605        }
606        self.current_level = 0;
607    }
608
609    /// Get statistics
610    pub fn stats(&self) -> FrameStats {
611        let mut total_lemmas = 0;
612        let mut total_inductive = 0;
613        let mut max_level = 0;
614
615        for frames in self.frames.values() {
616            total_lemmas += frames.num_active_lemmas();
617            total_inductive += frames.num_inductive();
618            max_level = max_level.max(frames.num_frames());
619        }
620
621        FrameStats {
622            total_lemmas,
623            total_inductive,
624            num_predicates: self.frames.len(),
625            max_level,
626            current_level: self.current_level,
627        }
628    }
629}
630
631/// Statistics about the frame sequence
632#[derive(Debug, Clone)]
633pub struct FrameStats {
634    /// Total number of active lemmas
635    pub total_lemmas: usize,
636    /// Number of inductive lemmas
637    pub total_inductive: usize,
638    /// Number of predicates with frames
639    pub num_predicates: usize,
640    /// Maximum frame level
641    pub max_level: u32,
642    /// Current level
643    pub current_level: u32,
644}
645
646/// Priority queue for lemmas (by level, lower first)
647#[derive(Debug)]
648pub struct LemmaQueue {
649    heap: BinaryHeap<std::cmp::Reverse<(u32, LemmaId, PredId)>>,
650}
651
652impl Default for LemmaQueue {
653    fn default() -> Self {
654        Self::new()
655    }
656}
657
658impl LemmaQueue {
659    /// Create a new lemma queue
660    pub fn new() -> Self {
661        Self {
662            heap: BinaryHeap::new(),
663        }
664    }
665
666    /// Push a lemma to the queue
667    pub fn push(&mut self, level: u32, lemma: LemmaId, pred: PredId) {
668        self.heap.push(std::cmp::Reverse((level, lemma, pred)));
669    }
670
671    /// Pop the lemma with lowest level
672    pub fn pop(&mut self) -> Option<(u32, LemmaId, PredId)> {
673        self.heap.pop().map(|r| r.0)
674    }
675
676    /// Check if empty
677    #[must_use]
678    pub fn is_empty(&self) -> bool {
679        self.heap.is_empty()
680    }
681
682    /// Get the number of items
683    #[must_use]
684    pub fn len(&self) -> usize {
685        self.heap.len()
686    }
687
688    /// Clear the queue
689    pub fn clear(&mut self) {
690        self.heap.clear();
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    #[test]
699    fn test_lemma_creation() {
700        let id = LemmaId::new(0);
701        let formula = oxiz_core::TermId::new(42);
702        let lemma = Lemma::new(id, formula, 1);
703
704        assert_eq!(lemma.level(), 1);
705        assert_eq!(lemma.init_level(), 1);
706        assert!(!lemma.is_inductive());
707    }
708
709    #[test]
710    fn test_inductive_lemma() {
711        let id = LemmaId::new(0);
712        let formula = oxiz_core::TermId::new(42);
713        let mut lemma = Lemma::new(id, formula, 1);
714
715        lemma.set_level(INFTY_LEVEL);
716        assert!(lemma.is_inductive());
717    }
718
719    #[test]
720    fn test_predicate_frames() {
721        let pred = PredId::new(0);
722        let mut frames = PredicateFrames::new(pred);
723
724        let f1 = oxiz_core::TermId::new(1);
725        let f2 = oxiz_core::TermId::new(2);
726
727        let id1 = frames.add_lemma(f1, 1);
728        let id2 = frames.add_lemma(f2, 2);
729
730        assert_eq!(frames.num_active_lemmas(), 2);
731        assert_eq!(frames.lemmas_at_level(1).count(), 1);
732        assert_eq!(frames.lemmas_at_level(2).count(), 1);
733
734        // Propagate lemma 1 to level 2
735        assert!(frames.propagate(id1, 2));
736        assert_eq!(frames.lemmas_at_level(2).count(), 2);
737        assert_eq!(frames.lemmas_at_level(1).count(), 0);
738
739        // Deactivate lemma 2
740        frames.deactivate(id2);
741        assert!(!frames.is_active(id2));
742        assert_eq!(frames.num_active_lemmas(), 1);
743    }
744
745    #[test]
746    fn test_frame_manager() {
747        let mut manager = FrameManager::new();
748        let pred = PredId::new(0);
749
750        let f1 = oxiz_core::TermId::new(1);
751        manager.add_lemma(pred, f1, 0);
752
753        assert_eq!(manager.current_level(), 0);
754
755        manager.next_level();
756        assert_eq!(manager.current_level(), 1);
757
758        let stats = manager.stats();
759        assert_eq!(stats.total_lemmas, 1);
760        assert_eq!(stats.num_predicates, 1);
761    }
762
763    #[test]
764    fn test_lemma_queue() {
765        let mut queue = LemmaQueue::new();
766
767        queue.push(3, LemmaId::new(0), PredId::new(0));
768        queue.push(1, LemmaId::new(1), PredId::new(0));
769        queue.push(2, LemmaId::new(2), PredId::new(0));
770
771        // Should pop in order of level (lowest first)
772        let (level, _, _) = queue.pop().expect("collection should not be empty");
773        assert_eq!(level, 1);
774
775        let (level, _, _) = queue.pop().expect("collection should not be empty");
776        assert_eq!(level, 2);
777
778        let (level, _, _) = queue.pop().expect("collection should not be empty");
779        assert_eq!(level, 3);
780
781        assert!(queue.is_empty());
782    }
783
784    #[test]
785    fn test_propagate_to_infinity() {
786        let pred = PredId::new(0);
787        let mut frames = PredicateFrames::new(pred);
788
789        let f1 = oxiz_core::TermId::new(1);
790        let f2 = oxiz_core::TermId::new(2);
791        let f3 = oxiz_core::TermId::new(3);
792
793        frames.add_lemma(f1, 1);
794        frames.add_lemma(f2, 2);
795        frames.add_lemma(f3, 3);
796
797        // Propagate all lemmas at level >= 2 to infinity
798        frames.propagate_to_infinity(2);
799
800        assert_eq!(frames.lemmas_at_level(1).count(), 1);
801        assert_eq!(frames.inductive_lemmas().count(), 2);
802    }
803}