1use crate::proof::{Proof, ProofNodeId};
7use rustc_hash::FxHashMap;
8use std::collections::VecDeque;
9
10#[derive(Debug)]
17pub struct IncrementalProofBuilder {
18 proof: Proof,
20 scopes: Vec<ScopeFrame>,
22 clause_map: FxHashMap<u32, ProofNodeId>,
24 enabled: bool,
26}
27
28#[derive(Debug, Clone)]
30struct ScopeFrame {
31 #[allow(dead_code)]
34 proof_size: usize,
35 clause_mappings: Vec<(u32, ProofNodeId)>,
37}
38
39impl IncrementalProofBuilder {
40 #[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 pub fn enable(&mut self) {
53 self.enabled = true;
54 }
55
56 pub fn disable(&mut self) {
58 self.enabled = false;
59 }
60
61 #[must_use]
63 pub const fn is_enabled(&self) -> bool {
64 self.enabled
65 }
66
67 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 pub fn pop_scope(&mut self) {
77 if let Some(frame) = self.scopes.pop() {
78 for (clause_id, _) in &frame.clause_mappings {
80 self.clause_map.remove(clause_id);
81 }
82
83 }
86 }
87
88 #[must_use]
90 pub fn scope_level(&self) -> usize {
91 self.scopes.len()
92 }
93
94 pub fn record_axiom(&mut self, clause_id: u32, conclusion: impl Into<String>) -> ProofNodeId {
96 if !self.enabled {
97 return ProofNodeId(0); }
99
100 let node_id = self.proof.add_axiom(conclusion);
101 self.clause_map.insert(clause_id, node_id);
102
103 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 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); }
122
123 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 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 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); }
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 #[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 #[must_use]
178 pub const fn proof(&self) -> &Proof {
179 &self.proof
180 }
181
182 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 pub fn reset(&mut self) {
191 self.proof.clear();
192 self.scopes.clear();
193 self.clause_map.clear();
194 }
195
196 #[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#[derive(Debug, Clone, Copy)]
216pub struct IncrementalStats {
217 pub total_nodes: usize,
219 pub scope_level: usize,
221 pub clause_mappings: usize,
223 pub enabled: bool,
225}
226
227#[derive(Debug)]
231pub struct ProofRecorder {
232 builder: IncrementalProofBuilder,
234 next_clause_id: u32,
236 queue: VecDeque<RecordedStep>,
238 batch_mode: bool,
240}
241
242#[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 #[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 pub fn enable_batch_mode(&mut self) {
272 self.batch_mode = true;
273 }
274
275 pub fn disable_batch_mode(&mut self) {
277 self.batch_mode = false;
278 }
279
280 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 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 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 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 #[must_use]
357 pub const fn builder(&self) -> &IncrementalProofBuilder {
358 &self.builder
359 }
360
361 pub fn builder_mut(&mut self) -> &mut IncrementalProofBuilder {
363 &mut self.builder
364 }
365
366 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()); }
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 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 assert_eq!(recorder.builder().proof().len(), 0);
476
477 recorder.flush();
478
479 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 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 #[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 if seen.insert(conclusion.clone()) {
520 prop_assert!(builder.proof().len() > initial_len);
521 } else {
522 prop_assert_eq!(builder.proof().len(), initial_len);
524 }
525 }
526 }
527
528 #[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 #[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 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 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 builder.pop_scope();
567 prop_assert_eq!(builder.stats().clause_mappings, base_mappings);
568 }
569
570 #[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 #[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 #[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 #[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 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 #[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 prop_assert_eq!(recorder.builder().proof().len(), 0);
655
656 recorder.flush();
657
658 let unique_count = conclusions.iter().collect::<std::collections::HashSet<_>>().len();
660 prop_assert_eq!(recorder.builder().proof().len(), unique_count);
661 }
662
663 #[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 for conclusion in &conclusions {
673 clause_ids.push(recorder.record_input(conclusion));
674 }
675
676 if clause_ids.len() >= 2 {
678 let derived_id = recorder.record_derived(
679 "and",
680 &clause_ids[..2],
681 "derived"
682 );
683
684 prop_assert!(derived_id > clause_ids[0]);
686 prop_assert!(derived_id > clause_ids[1]);
687 }
688 }
689 }
690 }
691}