Skip to main content

oxiz_proof/
incremental.rs

1//! Incremental proof construction.
2//!
3//! This module provides infrastructure for building proofs incrementally during
4//! solving, with support for backtracking, scoping, and efficient recording.
5
6use crate::proof::{Proof, ProofNodeId};
7use rustc_hash::FxHashMap;
8use std::collections::VecDeque;
9
10/// A scoped proof builder for incremental construction
11///
12/// Supports:
13/// - Push/pop scoping (for backtracking)
14/// - Efficient deduplication
15/// - Premise tracking
16#[derive(Debug)]
17pub struct IncrementalProofBuilder {
18    /// The main proof being constructed
19    proof: Proof,
20    /// Stack of scope markers (number of nodes when scope was pushed)
21    scopes: Vec<ScopeFrame>,
22    /// Mapping from solver-specific clause IDs to proof nodes
23    clause_map: FxHashMap<u32, ProofNodeId>,
24    /// Whether proof recording is enabled
25    enabled: bool,
26}
27
28/// A scope frame in the incremental builder
29#[derive(Debug, Clone)]
30struct ScopeFrame {
31    /// Number of nodes in proof when scope was created
32    /// (Reserved for future use in optimization)
33    #[allow(dead_code)]
34    proof_size: usize,
35    /// Clause mappings added in this scope
36    clause_mappings: Vec<(u32, ProofNodeId)>,
37}
38
39impl IncrementalProofBuilder {
40    /// Create a new incremental proof builder
41    #[must_use]
42    pub fn new() -> Self {
43        Self {
44            proof: Proof::new(),
45            scopes: Vec::new(),
46            clause_map: FxHashMap::default(),
47            enabled: true,
48        }
49    }
50
51    /// Enable proof recording
52    pub fn enable(&mut self) {
53        self.enabled = true;
54    }
55
56    /// Disable proof recording
57    pub fn disable(&mut self) {
58        self.enabled = false;
59    }
60
61    /// Check if proof recording is enabled
62    #[must_use]
63    pub const fn is_enabled(&self) -> bool {
64        self.enabled
65    }
66
67    /// Push a new scope (for backtracking)
68    pub fn push_scope(&mut self) {
69        self.scopes.push(ScopeFrame {
70            proof_size: self.proof.len(),
71            clause_mappings: Vec::new(),
72        });
73    }
74
75    /// Pop a scope (backtrack)
76    pub fn pop_scope(&mut self) {
77        if let Some(frame) = self.scopes.pop() {
78            // Remove clause mappings added in this scope
79            for (clause_id, _) in &frame.clause_mappings {
80                self.clause_map.remove(clause_id);
81            }
82
83            // Note: We don't remove proof nodes as they might be referenced
84            // by nodes in outer scopes. Compression will clean them up later.
85        }
86    }
87
88    /// Get the current scope level
89    #[must_use]
90    pub fn scope_level(&self) -> usize {
91        self.scopes.len()
92    }
93
94    /// Record an axiom (input clause)
95    pub fn record_axiom(&mut self, clause_id: u32, conclusion: impl Into<String>) -> ProofNodeId {
96        if !self.enabled {
97            return ProofNodeId(0); // Dummy ID
98        }
99
100        let node_id = self.proof.add_axiom(conclusion);
101        self.clause_map.insert(clause_id, node_id);
102
103        // Track in current scope if any
104        if let Some(scope) = self.scopes.last_mut() {
105            scope.clause_mappings.push((clause_id, node_id));
106        }
107
108        node_id
109    }
110
111    /// Record an inference step
112    pub fn record_inference(
113        &mut self,
114        clause_id: u32,
115        rule: impl Into<String>,
116        premise_ids: &[u32],
117        conclusion: impl Into<String>,
118    ) -> ProofNodeId {
119        if !self.enabled {
120            return ProofNodeId(0); // Dummy ID
121        }
122
123        // Map solver clause IDs to proof node IDs
124        let premises: Vec<ProofNodeId> = premise_ids
125            .iter()
126            .filter_map(|id| self.clause_map.get(id).copied())
127            .collect();
128
129        let node_id = self.proof.add_inference(rule, premises, conclusion);
130        self.clause_map.insert(clause_id, node_id);
131
132        // Track in current scope if any
133        if let Some(scope) = self.scopes.last_mut() {
134            scope.clause_mappings.push((clause_id, node_id));
135        }
136
137        node_id
138    }
139
140    /// Record an inference step with arguments
141    pub fn record_inference_with_args(
142        &mut self,
143        clause_id: u32,
144        rule: impl Into<String>,
145        premise_ids: &[u32],
146        args: Vec<String>,
147        conclusion: impl Into<String>,
148    ) -> ProofNodeId {
149        if !self.enabled {
150            return ProofNodeId(0); // Dummy ID
151        }
152
153        let premises: Vec<ProofNodeId> = premise_ids
154            .iter()
155            .filter_map(|id| self.clause_map.get(id).copied())
156            .collect();
157
158        let node_id = self
159            .proof
160            .add_inference_with_args(rule, premises, args, conclusion);
161        self.clause_map.insert(clause_id, node_id);
162
163        if let Some(scope) = self.scopes.last_mut() {
164            scope.clause_mappings.push((clause_id, node_id));
165        }
166
167        node_id
168    }
169
170    /// Get the proof node for a solver clause ID
171    #[must_use]
172    pub fn get_clause_proof(&self, clause_id: u32) -> Option<ProofNodeId> {
173        self.clause_map.get(&clause_id).copied()
174    }
175
176    /// Get the proof (immutable)
177    #[must_use]
178    pub const fn proof(&self) -> &Proof {
179        &self.proof
180    }
181
182    /// Take the proof, leaving an empty one
183    pub fn take_proof(&mut self) -> Proof {
184        self.clause_map.clear();
185        self.scopes.clear();
186        std::mem::take(&mut self.proof)
187    }
188
189    /// Reset the builder
190    pub fn reset(&mut self) {
191        self.proof.clear();
192        self.scopes.clear();
193        self.clause_map.clear();
194    }
195
196    /// Get statistics about the incremental construction
197    #[must_use]
198    pub fn stats(&self) -> IncrementalStats {
199        IncrementalStats {
200            total_nodes: self.proof.len(),
201            scope_level: self.scopes.len(),
202            clause_mappings: self.clause_map.len(),
203            enabled: self.enabled,
204        }
205    }
206}
207
208impl Default for IncrementalProofBuilder {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214/// Statistics about incremental proof construction
215#[derive(Debug, Clone, Copy)]
216pub struct IncrementalStats {
217    /// Total number of proof nodes
218    pub total_nodes: usize,
219    /// Current scope level
220    pub scope_level: usize,
221    /// Number of clause mappings
222    pub clause_mappings: usize,
223    /// Whether recording is enabled
224    pub enabled: bool,
225}
226
227/// A proof recorder that can be used during SAT/SMT solving
228///
229/// This provides a simple interface for recording proof steps during search
230#[derive(Debug)]
231pub struct ProofRecorder {
232    /// The incremental builder
233    builder: IncrementalProofBuilder,
234    /// Next clause ID
235    next_clause_id: u32,
236    /// Queue of recorded steps (for batching)
237    queue: VecDeque<RecordedStep>,
238    /// Whether to batch steps
239    batch_mode: bool,
240}
241
242/// A recorded proof step
243#[derive(Debug, Clone)]
244enum RecordedStep {
245    Axiom {
246        clause_id: u32,
247        conclusion: String,
248    },
249    Inference {
250        clause_id: u32,
251        rule: String,
252        premises: Vec<u32>,
253        conclusion: String,
254        args: Vec<String>,
255    },
256}
257
258impl ProofRecorder {
259    /// Create a new proof recorder
260    #[must_use]
261    pub fn new() -> Self {
262        Self {
263            builder: IncrementalProofBuilder::new(),
264            next_clause_id: 0,
265            queue: VecDeque::new(),
266            batch_mode: false,
267        }
268    }
269
270    /// Enable batch mode (queue steps instead of adding immediately)
271    pub fn enable_batch_mode(&mut self) {
272        self.batch_mode = true;
273    }
274
275    /// Disable batch mode
276    pub fn disable_batch_mode(&mut self) {
277        self.batch_mode = false;
278    }
279
280    /// Allocate a new clause ID
281    pub fn alloc_clause_id(&mut self) -> u32 {
282        let id = self.next_clause_id;
283        self.next_clause_id += 1;
284        id
285    }
286
287    /// Record an input clause
288    pub fn record_input(&mut self, conclusion: impl Into<String>) -> u32 {
289        let clause_id = self.alloc_clause_id();
290        let conclusion = conclusion.into();
291
292        if self.batch_mode {
293            self.queue.push_back(RecordedStep::Axiom {
294                clause_id,
295                conclusion,
296            });
297        } else {
298            self.builder.record_axiom(clause_id, conclusion);
299        }
300
301        clause_id
302    }
303
304    /// Record a derived clause
305    pub fn record_derived(
306        &mut self,
307        rule: impl Into<String>,
308        premises: &[u32],
309        conclusion: impl Into<String>,
310    ) -> u32 {
311        let clause_id = self.alloc_clause_id();
312        let rule = rule.into();
313        let conclusion = conclusion.into();
314
315        if self.batch_mode {
316            self.queue.push_back(RecordedStep::Inference {
317                clause_id,
318                rule,
319                premises: premises.to_vec(),
320                conclusion,
321                args: Vec::new(),
322            });
323        } else {
324            self.builder
325                .record_inference(clause_id, rule, premises, conclusion);
326        }
327
328        clause_id
329    }
330
331    /// Flush the batched steps
332    pub fn flush(&mut self) {
333        while let Some(step) = self.queue.pop_front() {
334            match step {
335                RecordedStep::Axiom {
336                    clause_id,
337                    conclusion,
338                } => {
339                    self.builder.record_axiom(clause_id, conclusion);
340                }
341                RecordedStep::Inference {
342                    clause_id,
343                    rule,
344                    premises,
345                    conclusion,
346                    args,
347                } => {
348                    self.builder
349                        .record_inference_with_args(clause_id, rule, &premises, args, conclusion);
350                }
351            }
352        }
353    }
354
355    /// Get the builder
356    #[must_use]
357    pub const fn builder(&self) -> &IncrementalProofBuilder {
358        &self.builder
359    }
360
361    /// Get the mutable builder
362    pub fn builder_mut(&mut self) -> &mut IncrementalProofBuilder {
363        &mut self.builder
364    }
365
366    /// Take the proof
367    pub fn take_proof(&mut self) -> Proof {
368        self.flush();
369        self.builder.take_proof()
370    }
371}
372
373impl Default for ProofRecorder {
374    fn default() -> Self {
375        Self::new()
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_incremental_builder_creation() {
385        let builder = IncrementalProofBuilder::new();
386        assert!(builder.is_enabled());
387        assert_eq!(builder.scope_level(), 0);
388    }
389
390    #[test]
391    fn test_record_axiom() {
392        let mut builder = IncrementalProofBuilder::new();
393        let clause_id = 1;
394        builder.record_axiom(clause_id, "p");
395
396        assert!(builder.get_clause_proof(clause_id).is_some());
397        assert_eq!(builder.proof().len(), 1);
398    }
399
400    #[test]
401    fn test_record_inference() {
402        let mut builder = IncrementalProofBuilder::new();
403        let c1 = 1;
404        let c2 = 2;
405        let c3 = 3;
406
407        builder.record_axiom(c1, "p");
408        builder.record_axiom(c2, "q");
409        builder.record_inference(c3, "and", &[c1, c2], "p /\\ q");
410
411        assert_eq!(builder.proof().len(), 3);
412    }
413
414    #[test]
415    fn test_push_pop_scope() {
416        let mut builder = IncrementalProofBuilder::new();
417
418        builder.push_scope();
419        assert_eq!(builder.scope_level(), 1);
420
421        let c1 = 1;
422        builder.record_axiom(c1, "p");
423
424        builder.pop_scope();
425        assert_eq!(builder.scope_level(), 0);
426        assert!(builder.get_clause_proof(c1).is_none()); // Mapping removed
427    }
428
429    #[test]
430    fn test_disabled_recording() {
431        let mut builder = IncrementalProofBuilder::new();
432        builder.disable();
433
434        let c1 = 1;
435        builder.record_axiom(c1, "p");
436
437        // Proof should still be empty when disabled
438        assert_eq!(builder.proof().len(), 0);
439    }
440
441    #[test]
442    fn test_take_proof() {
443        let mut builder = IncrementalProofBuilder::new();
444        builder.record_axiom(1, "p");
445        builder.record_axiom(2, "q");
446
447        let proof = builder.take_proof();
448        assert_eq!(proof.len(), 2);
449        assert_eq!(builder.proof().len(), 0);
450    }
451
452    #[test]
453    fn test_proof_recorder() {
454        let mut recorder = ProofRecorder::new();
455
456        let c1 = recorder.record_input("p");
457        let c2 = recorder.record_input("q");
458        let c3 = recorder.record_derived("and", &[c1, c2], "p /\\ q");
459
460        assert!(c3 > c2);
461
462        let proof = recorder.take_proof();
463        assert_eq!(proof.len(), 3);
464    }
465
466    #[test]
467    fn test_proof_recorder_batch() {
468        let mut recorder = ProofRecorder::new();
469        recorder.enable_batch_mode();
470
471        let _c1 = recorder.record_input("p");
472        let _c2 = recorder.record_input("q");
473
474        // Nothing added yet (batched)
475        assert_eq!(recorder.builder().proof().len(), 0);
476
477        recorder.flush();
478
479        // Now added
480        assert_eq!(recorder.builder().proof().len(), 2);
481    }
482
483    #[test]
484    fn test_incremental_stats() {
485        let mut builder = IncrementalProofBuilder::new();
486        builder.record_axiom(1, "p");
487        builder.push_scope();
488        builder.record_axiom(2, "q");
489
490        let stats = builder.stats();
491        assert_eq!(stats.total_nodes, 2);
492        assert_eq!(stats.scope_level, 1);
493        assert!(stats.enabled);
494    }
495
496    // Property-based tests
497    mod proptests {
498        use super::*;
499        use proptest::prelude::*;
500
501        fn var_name() -> impl Strategy<Value = String> {
502            "[a-z][0-9]*".prop_map(|s| s.to_string())
503        }
504
505        proptest! {
506            /// Recording axioms should increase proof size (unless duplicated)
507            #[test]
508            fn prop_record_axiom_increases_size(
509                conclusions in prop::collection::vec(var_name(), 1..10)
510            ) {
511                let mut builder = IncrementalProofBuilder::new();
512                let mut seen = std::collections::HashSet::new();
513
514                for (i, conclusion) in conclusions.iter().enumerate() {
515                    let initial_len = builder.proof().len();
516                    builder.record_axiom(i as u32 + 1, conclusion);
517
518                    // Size increases only if this is a new unique conclusion
519                    if seen.insert(conclusion.clone()) {
520                        prop_assert!(builder.proof().len() > initial_len);
521                    } else {
522                        // Duplicate conclusions are deduplicated
523                        prop_assert_eq!(builder.proof().len(), initial_len);
524                    }
525                }
526            }
527
528            /// Scope level should track push/pop correctly
529            #[test]
530            fn prop_scope_level_tracking(depth in 0..10_usize) {
531                let mut builder = IncrementalProofBuilder::new();
532                prop_assert_eq!(builder.scope_level(), 0);
533
534                for i in 0..depth {
535                    builder.push_scope();
536                    prop_assert_eq!(builder.scope_level(), i + 1);
537                }
538
539                for i in (0..depth).rev() {
540                    builder.pop_scope();
541                    prop_assert_eq!(builder.scope_level(), i);
542                }
543            }
544
545            /// Pop should remove clause mappings added in that scope
546            #[test]
547            fn prop_pop_removes_scope_mappings(
548                base_count in 1..4_usize,
549                scope_count in 1..4_usize
550            ) {
551                let mut builder = IncrementalProofBuilder::new();
552
553                // Add some base clauses with unique IDs
554                for i in 0..base_count {
555                    builder.record_axiom(i as u32 + 1, format!("base{}", i));
556                }
557                let base_mappings = builder.stats().clause_mappings;
558
559                // Push scope and add more with unique IDs
560                builder.push_scope();
561                for i in 0..scope_count {
562                    builder.record_axiom((base_count + i) as u32 + 100, format!("scope{}", i));
563                }
564
565                // Pop should revert clause mappings to base count
566                builder.pop_scope();
567                prop_assert_eq!(builder.stats().clause_mappings, base_mappings);
568            }
569
570            /// Disabled builder should not add to proof
571            #[test]
572            fn prop_disabled_no_recording(
573                conclusions in prop::collection::vec(var_name(), 1..10)
574            ) {
575                let mut builder = IncrementalProofBuilder::new();
576                builder.disable();
577
578                for (i, conclusion) in conclusions.iter().enumerate() {
579                    builder.record_axiom(i as u32 + 1, conclusion);
580                }
581
582                prop_assert_eq!(builder.proof().len(), 0);
583            }
584
585            /// Stats should be consistent with builder state
586            #[test]
587            fn prop_stats_consistency(
588                conclusions in prop::collection::vec(var_name(), 1..8),
589                scope_depth in 0..5_usize
590            ) {
591                let mut builder = IncrementalProofBuilder::new();
592
593                for _ in 0..scope_depth {
594                    builder.push_scope();
595                }
596
597                for (i, conclusion) in conclusions.iter().enumerate() {
598                    builder.record_axiom(i as u32 + 1, conclusion);
599                }
600
601                let stats = builder.stats();
602                prop_assert_eq!(stats.scope_level, scope_depth);
603                prop_assert!(stats.enabled);
604                prop_assert_eq!(stats.total_nodes, builder.proof().len());
605            }
606
607            /// Take proof should empty the builder
608            #[test]
609            fn prop_take_proof_empties(
610                conclusions in prop::collection::vec(var_name(), 1..8)
611            ) {
612                let mut builder = IncrementalProofBuilder::new();
613
614                for (i, conclusion) in conclusions.iter().enumerate() {
615                    builder.record_axiom(i as u32 + 1, conclusion);
616                }
617
618                let proof_len = builder.proof().len();
619                let taken = builder.take_proof();
620
621                prop_assert_eq!(taken.len(), proof_len);
622                prop_assert_eq!(builder.proof().len(), 0);
623            }
624
625            /// ProofRecorder should maintain clause ID ordering for unique conclusions
626            #[test]
627            fn prop_recorder_id_ordering(count in 2..8_usize) {
628                let mut recorder = ProofRecorder::new();
629                let mut last_id = None;
630
631                // Use unique conclusions to avoid deduplication
632                for i in 0..count {
633                    let id = recorder.record_input(format!("unique{}", i));
634                    if let Some(prev_id) = last_id {
635                        prop_assert!(id > prev_id);
636                    }
637                    last_id = Some(id);
638                }
639            }
640
641            /// Batch mode should delay additions
642            #[test]
643            fn prop_batch_mode_delays(
644                conclusions in prop::collection::vec(var_name(), 1..8)
645            ) {
646                let mut recorder = ProofRecorder::new();
647                recorder.enable_batch_mode();
648
649                for conclusion in &conclusions {
650                    recorder.record_input(conclusion);
651                }
652
653                // Nothing added yet in batch mode
654                prop_assert_eq!(recorder.builder().proof().len(), 0);
655
656                recorder.flush();
657
658                // Now everything is added (accounting for deduplication)
659                let unique_count = conclusions.iter().collect::<std::collections::HashSet<_>>().len();
660                prop_assert_eq!(recorder.builder().proof().len(), unique_count);
661            }
662
663            /// Recording derived clauses maintains dependencies
664            #[test]
665            fn prop_derived_dependencies(
666                conclusions in prop::collection::vec(var_name(), 2..6)
667            ) {
668                let mut recorder = ProofRecorder::new();
669                let mut clause_ids = Vec::new();
670
671                // Record inputs
672                for conclusion in &conclusions {
673                    clause_ids.push(recorder.record_input(conclusion));
674                }
675
676                // Record derived clause from first two inputs
677                if clause_ids.len() >= 2 {
678                    let derived_id = recorder.record_derived(
679                        "and",
680                        &clause_ids[..2],
681                        "derived"
682                    );
683
684                    // Derived ID should be larger than inputs
685                    prop_assert!(derived_id > clause_ids[0]);
686                    prop_assert!(derived_id > clause_ids[1]);
687                }
688            }
689        }
690    }
691}