1use crate::metadata::ProofMetadata;
4use rustc_hash::FxHashMap;
5use smallvec::SmallVec;
6use std::fmt;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct ProofNodeId(pub u32);
13
14impl fmt::Display for ProofNodeId {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "p{}", self.0)
17 }
18}
19
20#[derive(Debug, Clone)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Proof {
27 nodes: Vec<ProofNode>,
29 roots: Vec<ProofNodeId>,
31 #[cfg_attr(feature = "serde", serde(skip))]
33 term_cache: FxHashMap<String, ProofNodeId>,
34 metadata: FxHashMap<ProofNodeId, ProofMetadata>,
36 next_id: u32,
38}
39
40#[derive(Debug, Clone)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct ProofNode {
44 pub id: ProofNodeId,
46 pub step: ProofStep,
48 pub dependents: SmallVec<[ProofNodeId; 2]>,
50 pub depth: u32,
52}
53
54#[derive(Debug, Clone)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub enum ProofStep {
58 Axiom {
60 conclusion: String,
62 },
63 Inference {
65 rule: String,
67 premises: SmallVec<[ProofNodeId; 4]>,
69 conclusion: String,
71 args: SmallVec<[String; 2]>,
73 },
74}
75
76impl ProofNode {
77 pub fn conclusion(&self) -> &str {
79 match &self.step {
80 ProofStep::Axiom { conclusion } => conclusion,
81 ProofStep::Inference { conclusion, .. } => conclusion,
82 }
83 }
84}
85
86impl Proof {
87 #[must_use]
89 pub fn new() -> Self {
90 Self {
91 nodes: Vec::new(),
92 roots: Vec::new(),
93 term_cache: FxHashMap::default(),
94 metadata: FxHashMap::default(),
95 next_id: 0,
96 }
97 }
98
99 fn alloc_id(&mut self) -> ProofNodeId {
101 let id = ProofNodeId(self.next_id);
102 self.next_id += 1;
103 id
104 }
105
106 pub fn add_axiom(&mut self, conclusion: impl Into<String>) -> ProofNodeId {
108 let conclusion = conclusion.into();
109
110 if let Some(&existing_id) = self.term_cache.get(&conclusion) {
112 return existing_id;
113 }
114
115 let id = self.alloc_id();
116 let node = ProofNode {
117 id,
118 step: ProofStep::Axiom {
119 conclusion: conclusion.clone(),
120 },
121 dependents: SmallVec::new(),
122 depth: 0,
123 };
124
125 self.nodes.push(node);
126 self.term_cache.insert(conclusion, id);
127 self.roots.push(id);
128 id
129 }
130
131 pub fn add_inference(
133 &mut self,
134 rule: impl Into<String>,
135 premises: Vec<ProofNodeId>,
136 conclusion: impl Into<String>,
137 ) -> ProofNodeId {
138 self.add_inference_with_args(rule, premises, Vec::new(), conclusion)
139 }
140
141 pub fn add_inference_with_args(
143 &mut self,
144 rule: impl Into<String>,
145 premises: Vec<ProofNodeId>,
146 args: Vec<String>,
147 conclusion: impl Into<String>,
148 ) -> ProofNodeId {
149 let conclusion = conclusion.into();
150 let rule = rule.into();
151
152 if let Some(&existing_id) = self.term_cache.get(&conclusion) {
154 return existing_id;
155 }
156
157 let id = self.alloc_id();
158
159 let depth = premises
161 .iter()
162 .filter_map(|p| self.get_node(*p))
163 .map(|n| n.depth)
164 .max()
165 .unwrap_or(0)
166 + 1;
167
168 let node = ProofNode {
169 id,
170 step: ProofStep::Inference {
171 rule,
172 premises: SmallVec::from_vec(premises.clone()),
173 conclusion: conclusion.clone(),
174 args: SmallVec::from_vec(args),
175 },
176 dependents: SmallVec::new(),
177 depth,
178 };
179
180 for premise_id in &premises {
182 if let Some(premise_node) = self.get_node_mut(*premise_id) {
183 premise_node.dependents.push(id);
184 }
185 }
186
187 self.nodes.push(node);
188 self.term_cache.insert(conclusion, id);
189 self.roots.push(id);
190 id
191 }
192
193 #[must_use]
195 pub fn get_node(&self, id: ProofNodeId) -> Option<&ProofNode> {
196 self.nodes.get(id.0 as usize)
197 }
198
199 fn get_node_mut(&mut self, id: ProofNodeId) -> Option<&mut ProofNode> {
201 self.nodes.get_mut(id.0 as usize)
202 }
203
204 pub fn update_conclusion(
209 &mut self,
210 id: ProofNodeId,
211 new_conclusion: impl Into<String>,
212 ) -> bool {
213 if let Some(node) = self.get_node_mut(id) {
214 let new_conclusion = new_conclusion.into();
215 match &mut node.step {
216 ProofStep::Axiom { conclusion } => *conclusion = new_conclusion,
217 ProofStep::Inference { conclusion, .. } => *conclusion = new_conclusion,
218 }
219 if let Some(old_term) = self
221 .term_cache
222 .iter()
223 .find_map(|(k, v)| if *v == id { Some(k.clone()) } else { None })
224 {
225 self.term_cache.remove(&old_term);
226 }
227 true
228 } else {
229 false
230 }
231 }
232
233 #[must_use]
235 pub fn nodes(&self) -> &[ProofNode] {
236 &self.nodes
237 }
238
239 #[must_use]
241 pub fn roots(&self) -> &[ProofNodeId] {
242 &self.roots
243 }
244
245 #[must_use]
247 pub fn root(&self) -> Option<ProofNodeId> {
248 self.roots.last().copied()
249 }
250
251 #[must_use]
253 pub fn root_node(&self) -> Option<&ProofNode> {
254 self.root().and_then(|id| self.get_node(id))
255 }
256
257 #[must_use]
259 pub fn len(&self) -> usize {
260 self.nodes.len()
261 }
262
263 #[must_use]
265 pub fn depth(&self) -> u32 {
266 self.stats().max_depth
267 }
268
269 #[must_use]
271 pub fn node_count(&self) -> usize {
272 self.len()
273 }
274
275 #[must_use]
277 pub fn leaf_nodes(&self) -> Vec<ProofNodeId> {
278 self.nodes
279 .iter()
280 .filter(|node| matches!(node.step, ProofStep::Axiom { .. }))
281 .map(|node| node.id)
282 .collect()
283 }
284
285 #[must_use]
287 pub fn premises(&self, node_id: ProofNodeId) -> Option<&[ProofNodeId]> {
288 self.get_node(node_id).and_then(|node| {
289 if let ProofStep::Inference { premises, .. } = &node.step {
290 Some(premises.as_slice())
291 } else {
292 None
293 }
294 })
295 }
296
297 #[must_use]
299 pub fn is_empty(&self) -> bool {
300 self.nodes.is_empty()
301 }
302
303 pub fn clear(&mut self) {
305 self.nodes.clear();
306 self.roots.clear();
307 self.term_cache.clear();
308 self.metadata.clear();
309 self.next_id = 0;
310 }
311
312 #[must_use]
314 pub fn stats(&self) -> ProofStats {
315 let mut stats = ProofStats {
316 total_nodes: self.nodes.len(),
317 axiom_count: 0,
318 inference_count: 0,
319 max_depth: 0,
320 avg_depth: 0.0,
321 root_count: self.roots.len(),
322 max_premises: 0,
323 avg_premises: 0.0,
324 leaf_count: 0,
325 };
326
327 let mut total_depth = 0u64;
328 let mut total_premises = 0usize;
329
330 for node in &self.nodes {
331 match &node.step {
332 ProofStep::Axiom { .. } => stats.axiom_count += 1,
333 ProofStep::Inference { premises, .. } => {
334 stats.inference_count += 1;
335 let premise_count = premises.len();
336 stats.max_premises = stats.max_premises.max(premise_count);
337 total_premises += premise_count;
338 }
339 }
340 stats.max_depth = stats.max_depth.max(node.depth);
341 total_depth += u64::from(node.depth);
342
343 if node.dependents.is_empty() {
345 stats.leaf_count += 1;
346 }
347 }
348
349 if !self.nodes.is_empty() {
350 stats.avg_depth = total_depth as f64 / self.nodes.len() as f64;
351 }
352
353 if stats.inference_count > 0 {
354 stats.avg_premises = total_premises as f64 / stats.inference_count as f64;
355 }
356
357 stats
358 }
359
360 pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
362 writeln!(writer, "; Proof with {} nodes", self.len())?;
363 writeln!(writer)?;
364
365 for node in &self.nodes {
366 match &node.step {
367 ProofStep::Axiom { conclusion } => {
368 writeln!(writer, "{}: (axiom {})", node.id, conclusion)?;
369 }
370 ProofStep::Inference {
371 rule,
372 premises,
373 conclusion,
374 args,
375 } => {
376 write!(writer, "{}: ({} [", node.id, rule)?;
377 for (i, p) in premises.iter().enumerate() {
378 if i > 0 {
379 write!(writer, ", ")?;
380 }
381 write!(writer, "{}", p)?;
382 }
383 write!(writer, "]")?;
384 if !args.is_empty() {
385 write!(writer, " :args [")?;
386 for (i, arg) in args.iter().enumerate() {
387 if i > 0 {
388 write!(writer, ", ")?;
389 }
390 write!(writer, "{}", arg)?;
391 }
392 write!(writer, "]")?;
393 }
394 writeln!(writer, " => {})", conclusion)?;
395 }
396 }
397 }
398
399 Ok(())
400 }
401
402 #[must_use]
404 pub fn to_string_repr(&self) -> String {
405 let mut buf = Vec::new();
406 self.write(&mut buf)
407 .expect("writing to Vec should not fail");
408 String::from_utf8(buf).expect("proof output is UTF-8")
409 }
410
411 pub fn add_axioms(
413 &mut self,
414 conclusions: impl IntoIterator<Item = impl Into<String>>,
415 ) -> Vec<ProofNodeId> {
416 conclusions.into_iter().map(|c| self.add_axiom(c)).collect()
417 }
418
419 #[must_use]
421 pub fn find_nodes_by_rule(&self, rule: &str) -> Vec<ProofNodeId> {
422 self.nodes
423 .iter()
424 .filter(|node| matches!(&node.step, ProofStep::Inference { rule: r, .. } if r == rule))
425 .map(|node| node.id)
426 .collect()
427 }
428
429 #[must_use]
431 pub fn find_nodes_by_conclusion(&self, conclusion: &str) -> Vec<ProofNodeId> {
432 self.nodes
433 .iter()
434 .filter(|node| match &node.step {
435 ProofStep::Axiom { conclusion: c } => c == conclusion,
436 ProofStep::Inference { conclusion: c, .. } => c == conclusion,
437 })
438 .map(|node| node.id)
439 .collect()
440 }
441
442 #[must_use]
444 pub fn get_children(&self, node_id: ProofNodeId) -> Vec<ProofNodeId> {
445 self.premises(node_id)
446 .map(|p| p.to_vec())
447 .unwrap_or_default()
448 }
449
450 #[must_use]
452 pub fn get_parents(&self, node_id: ProofNodeId) -> Vec<ProofNodeId> {
453 self.get_node(node_id)
454 .map(|node| node.dependents.to_vec())
455 .unwrap_or_default()
456 }
457
458 #[must_use]
460 pub fn is_ancestor(&self, ancestor: ProofNodeId, descendant: ProofNodeId) -> bool {
461 if ancestor == descendant {
462 return false;
463 }
464
465 let mut visited = rustc_hash::FxHashSet::default();
466 let mut queue = std::collections::VecDeque::new();
467 queue.push_back(descendant);
468
469 while let Some(current) = queue.pop_front() {
470 if !visited.insert(current) {
471 continue;
472 }
473
474 if current == ancestor {
475 return true;
476 }
477
478 if let Some(premises) = self.premises(current) {
479 for &premise in premises {
480 queue.push_back(premise);
481 }
482 }
483 }
484
485 false
486 }
487
488 #[must_use]
490 pub fn get_all_ancestors(&self, node_id: ProofNodeId) -> Vec<ProofNodeId> {
491 let mut ancestors = Vec::new();
492 let mut visited = rustc_hash::FxHashSet::default();
493 let mut queue = std::collections::VecDeque::new();
494 queue.push_back(node_id);
495
496 while let Some(current) = queue.pop_front() {
497 if !visited.insert(current) {
498 continue;
499 }
500
501 if current != node_id {
502 ancestors.push(current);
503 }
504
505 if let Some(premises) = self.premises(current) {
506 for &premise in premises {
507 queue.push_back(premise);
508 }
509 }
510 }
511
512 ancestors
513 }
514
515 #[must_use]
517 pub fn count_rule(&self, rule: &str) -> usize {
518 self.nodes
519 .iter()
520 .filter(|node| matches!(&node.step, ProofStep::Inference { rule: r, .. } if r == rule))
521 .count()
522 }
523
524 #[cfg(feature = "serde")]
526 pub fn rebuild_cache(&mut self) {
527 self.term_cache.clear();
528 for node in &self.nodes {
529 let conclusion = match &node.step {
530 ProofStep::Axiom { conclusion } => conclusion,
531 ProofStep::Inference { conclusion, .. } => conclusion,
532 };
533 self.term_cache.insert(conclusion.clone(), node.id);
534 }
535 }
536
537 pub fn set_metadata(&mut self, node_id: ProofNodeId, metadata: ProofMetadata) {
541 self.metadata.insert(node_id, metadata);
542 }
543
544 #[must_use]
546 pub fn get_metadata(&self, node_id: ProofNodeId) -> Option<&ProofMetadata> {
547 self.metadata.get(&node_id)
548 }
549
550 pub fn get_metadata_mut(&mut self, node_id: ProofNodeId) -> Option<&mut ProofMetadata> {
552 self.metadata.get_mut(&node_id)
553 }
554
555 pub fn remove_metadata(&mut self, node_id: ProofNodeId) -> Option<ProofMetadata> {
557 self.metadata.remove(&node_id)
558 }
559
560 #[must_use]
562 pub fn has_metadata(&self, node_id: ProofNodeId) -> bool {
563 self.metadata.contains_key(&node_id)
564 }
565
566 #[must_use]
568 pub fn nodes_with_tag(&self, tag: &str) -> Vec<ProofNodeId> {
569 self.metadata
570 .iter()
571 .filter(|(_, meta)| meta.has_tag(tag))
572 .map(|(id, _)| *id)
573 .collect()
574 }
575
576 #[must_use]
578 pub fn nodes_with_priority(&self, priority: crate::metadata::Priority) -> Vec<ProofNodeId> {
579 self.metadata
580 .iter()
581 .filter(|(_, meta)| meta.priority() == priority)
582 .map(|(id, _)| *id)
583 .collect()
584 }
585
586 #[must_use]
588 pub fn nodes_with_difficulty(
589 &self,
590 difficulty: crate::metadata::Difficulty,
591 ) -> Vec<ProofNodeId> {
592 self.metadata
593 .iter()
594 .filter(|(_, meta)| meta.difficulty() == difficulty)
595 .map(|(id, _)| *id)
596 .collect()
597 }
598
599 #[must_use]
601 pub fn nodes_with_strategy(&self, strategy: crate::metadata::Strategy) -> Vec<ProofNodeId> {
602 self.metadata
603 .iter()
604 .filter(|(_, meta)| meta.has_strategy(strategy))
605 .map(|(id, _)| *id)
606 .collect()
607 }
608
609 pub fn get_or_create_metadata(&mut self, node_id: ProofNodeId) -> &mut ProofMetadata {
611 self.metadata.entry(node_id).or_default()
612 }
613}
614
615impl Default for Proof {
616 fn default() -> Self {
617 Self::new()
618 }
619}
620
621#[derive(Debug, Clone, Copy)]
623#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
624pub struct ProofStats {
625 pub total_nodes: usize,
627 pub axiom_count: usize,
629 pub inference_count: usize,
631 pub max_depth: u32,
633 pub avg_depth: f64,
635 pub root_count: usize,
637 pub max_premises: usize,
639 pub avg_premises: f64,
641 pub leaf_count: usize,
643}
644
645impl fmt::Display for ProofStats {
646 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647 writeln!(f, "Proof Statistics:")?;
648 writeln!(f, " Total nodes: {}", self.total_nodes)?;
649 writeln!(f, " Axioms: {}", self.axiom_count)?;
650 writeln!(f, " Inferences: {}", self.inference_count)?;
651 writeln!(f, " Roots: {}", self.root_count)?;
652 writeln!(f, " Leaves: {}", self.leaf_count)?;
653 writeln!(f, " Max depth: {}", self.max_depth)?;
654 writeln!(f, " Avg depth: {:.2}", self.avg_depth)?;
655 writeln!(f, " Max premises: {}", self.max_premises)?;
656 writeln!(f, " Avg premises: {:.2}", self.avg_premises)
657 }
658}
659
660impl fmt::Display for ProofNode {
661 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662 write!(f, "{}: {}", self.id, self.step)
663 }
664}
665
666impl fmt::Display for ProofStep {
667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668 match self {
669 ProofStep::Axiom { conclusion } => write!(f, "(axiom {})", conclusion),
670 ProofStep::Inference {
671 rule,
672 premises,
673 conclusion,
674 args,
675 } => {
676 write!(f, "({} [", rule)?;
677 for (i, p) in premises.iter().enumerate() {
678 if i > 0 {
679 write!(f, ", ")?;
680 }
681 write!(f, "{}", p)?;
682 }
683 write!(f, "]")?;
684 if !args.is_empty() {
685 write!(f, " :args {:?}", args)?;
686 }
687 write!(f, " => {})", conclusion)
688 }
689 }
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_proof_creation() {
699 let proof = Proof::new();
700 assert!(proof.is_empty());
701 assert_eq!(proof.len(), 0);
702 }
703
704 #[test]
705 fn test_add_axiom() {
706 let mut proof = Proof::new();
707 let id = proof.add_axiom("p");
708
709 assert_eq!(proof.len(), 1);
710 let node = proof.get_node(id).expect("test operation should succeed");
711 assert_eq!(node.id, ProofNodeId(0));
712 assert_eq!(node.depth, 0);
713 }
714
715 #[test]
716 fn test_add_inference() {
717 let mut proof = Proof::new();
718 let a1 = proof.add_axiom("p");
719 let a2 = proof.add_axiom("q");
720 let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
721
722 assert_eq!(proof.len(), 3);
723 let node = proof.get_node(i1).expect("test operation should succeed");
724 assert_eq!(node.depth, 1);
725 }
726
727 #[test]
728 fn test_proof_deduplication() {
729 let mut proof = Proof::new();
730 let id1 = proof.add_axiom("p");
731 let id2 = proof.add_axiom("p"); assert_eq!(id1, id2);
734 assert_eq!(proof.len(), 1); }
736
737 #[test]
738 fn test_proof_depth() {
739 let mut proof = Proof::new();
740 let a1 = proof.add_axiom("p");
741 let a2 = proof.add_axiom("q");
742 let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
743 let i2 = proof.add_inference("not", vec![i1], "~(p /\\ q)");
744
745 let node = proof.get_node(i2).expect("test operation should succeed");
746 assert_eq!(node.depth, 2);
747 }
748
749 #[test]
750 fn test_proof_stats() {
751 let mut proof = Proof::new();
752 proof.add_axiom("p");
753 proof.add_axiom("q");
754 let a1 = proof.add_axiom("r");
755 let a2 = proof.add_axiom("s");
756 proof.add_inference("and", vec![a1, a2], "r /\\ s");
757
758 let stats = proof.stats();
759 assert_eq!(stats.total_nodes, 5);
760 assert_eq!(stats.axiom_count, 4);
761 assert_eq!(stats.inference_count, 1);
762 assert_eq!(stats.max_depth, 1);
763 }
764
765 #[test]
766 fn test_proof_clear() {
767 let mut proof = Proof::new();
768 proof.add_axiom("p");
769 proof.add_axiom("q");
770
771 assert_eq!(proof.len(), 2);
772 proof.clear();
773 assert!(proof.is_empty());
774 }
775
776 #[test]
777 fn test_proof_display() {
778 let mut proof = Proof::new();
779 let a1 = proof.add_axiom("p");
780 proof.add_inference("not", vec![a1], "~p");
781
782 let output = proof.to_string_repr();
783 assert!(output.contains("axiom"));
784 assert!(output.contains("not"));
785 }
786
787 #[test]
788 fn test_dependent_tracking() {
789 let mut proof = Proof::new();
790 let a1 = proof.add_axiom("p");
791 let a2 = proof.add_axiom("q");
792 let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
793
794 let node_a1 = proof.get_node(a1).expect("test operation should succeed");
795 assert!(node_a1.dependents.contains(&i1));
796
797 let node_a2 = proof.get_node(a2).expect("test operation should succeed");
798 assert!(node_a2.dependents.contains(&i1));
799 }
800
801 #[test]
803 fn test_metadata_set_get() {
804 use crate::metadata::{Difficulty, Priority};
805
806 let mut proof = Proof::new();
807 let id = proof.add_axiom("p");
808
809 let meta = ProofMetadata::new()
810 .with_priority(Priority::High)
811 .with_difficulty(Difficulty::Easy);
812
813 proof.set_metadata(id, meta);
814
815 let retrieved = proof
816 .get_metadata(id)
817 .expect("test operation should succeed");
818 assert_eq!(retrieved.priority(), Priority::High);
819 assert_eq!(retrieved.difficulty(), Difficulty::Easy);
820 }
821
822 #[test]
823 fn test_metadata_tags() {
824 let mut proof = Proof::new();
825 let id1 = proof.add_axiom("p");
826 let id2 = proof.add_axiom("q");
827
828 proof.set_metadata(id1, ProofMetadata::new().with_tag("important"));
829 proof.set_metadata(id2, ProofMetadata::new().with_tag("trivial"));
830
831 let important_nodes = proof.nodes_with_tag("important");
832 assert_eq!(important_nodes.len(), 1);
833 assert_eq!(important_nodes[0], id1);
834 }
835
836 #[test]
837 fn test_metadata_priority_filter() {
838 use crate::metadata::Priority;
839
840 let mut proof = Proof::new();
841 let id1 = proof.add_axiom("p");
842 let id2 = proof.add_axiom("q");
843 let id3 = proof.add_axiom("r");
844
845 proof.set_metadata(id1, ProofMetadata::new().with_priority(Priority::High));
846 proof.set_metadata(id2, ProofMetadata::new().with_priority(Priority::High));
847 proof.set_metadata(id3, ProofMetadata::new().with_priority(Priority::Low));
848
849 let high_priority = proof.nodes_with_priority(Priority::High);
850 assert_eq!(high_priority.len(), 2);
851 }
852
853 #[test]
854 fn test_metadata_strategy_filter() {
855 use crate::metadata::Strategy;
856
857 let mut proof = Proof::new();
858 let id1 = proof.add_axiom("p");
859 let id2 = proof.add_axiom("q");
860
861 proof.set_metadata(
862 id1,
863 ProofMetadata::new().with_strategy(Strategy::Resolution),
864 );
865 proof.set_metadata(id2, ProofMetadata::new().with_strategy(Strategy::Theory));
866
867 let resolution_nodes = proof.nodes_with_strategy(Strategy::Resolution);
868 assert_eq!(resolution_nodes.len(), 1);
869 assert_eq!(resolution_nodes[0], id1);
870 }
871
872 #[test]
873 fn test_metadata_remove() {
874 let mut proof = Proof::new();
875 let id = proof.add_axiom("p");
876
877 proof.set_metadata(id, ProofMetadata::new().with_tag("temp"));
878 assert!(proof.has_metadata(id));
879
880 proof.remove_metadata(id);
881 assert!(!proof.has_metadata(id));
882 }
883
884 #[test]
885 fn test_metadata_get_or_create() {
886 use crate::metadata::Priority;
887
888 let mut proof = Proof::new();
889 let id = proof.add_axiom("p");
890
891 let meta = proof.get_or_create_metadata(id);
892 meta.set_priority(Priority::VeryHigh);
893
894 let retrieved = proof
895 .get_metadata(id)
896 .expect("test operation should succeed");
897 assert_eq!(retrieved.priority(), Priority::VeryHigh);
898 }
899
900 #[test]
901 fn test_metadata_clear() {
902 let mut proof = Proof::new();
903 let id = proof.add_axiom("p");
904 proof.set_metadata(id, ProofMetadata::new().with_tag("test"));
905
906 proof.clear();
907 assert!(!proof.has_metadata(id));
908 }
909
910 mod proptests {
912 use super::*;
913 use proptest::prelude::*;
914
915 fn var_name() -> impl Strategy<Value = String> {
917 "[a-z][0-9]*".prop_map(|s| s.to_string())
918 }
919
920 fn rule_name() -> impl Strategy<Value = String> {
922 prop_oneof![
923 Just("and".to_string()),
924 Just("or".to_string()),
925 Just("not".to_string()),
926 Just("imp".to_string()),
927 Just("resolution".to_string()),
928 ]
929 }
930
931 proptest! {
932 #[test]
934 fn prop_axiom_increases_size(conclusion in var_name()) {
935 let mut proof = Proof::new();
936 let initial_len = proof.len();
937 proof.add_axiom(&conclusion);
938 prop_assert!(proof.len() > initial_len || proof.len() == 1);
939 }
940
941 #[test]
943 fn prop_axiom_depth_zero(conclusion in var_name()) {
944 let mut proof = Proof::new();
945 let id = proof.add_axiom(&conclusion);
946 let node = proof.get_node(id).expect("test operation should succeed");
947 prop_assert_eq!(node.depth, 0);
948 }
949
950 #[test]
952 fn prop_axiom_deduplication(conclusion in var_name()) {
953 let mut proof = Proof::new();
954 let id1 = proof.add_axiom(&conclusion);
955 let id2 = proof.add_axiom(&conclusion);
956 prop_assert_eq!(id1, id2);
957 prop_assert_eq!(proof.len(), 1);
958 }
959
960 #[test]
962 fn prop_inference_depth(
963 rule in rule_name(),
964 conclusions in prop::collection::vec(var_name(), 1..5)
965 ) {
966 let mut proof = Proof::new();
967 let mut axiom_ids = Vec::new();
968
969 for conclusion in &conclusions {
970 let id = proof.add_axiom(conclusion);
971 axiom_ids.push(id);
972 }
973
974 if !axiom_ids.is_empty() {
975 let inf_id = proof.add_inference(
976 &rule,
977 axiom_ids.clone(),
978 format!("({} {})", rule, conclusions.join(" "))
979 );
980 let inf_node = proof.get_node(inf_id).expect("test operation should succeed");
981
982 for &premise_id in &axiom_ids {
983 let premise_node = proof.get_node(premise_id).expect("test operation should succeed");
984 prop_assert!(inf_node.depth > premise_node.depth);
985 }
986 }
987 }
988
989 #[test]
991 fn prop_stats_consistency(
992 axiom_count in 1..10_usize,
993 inference_count in 0..5_usize
994 ) {
995 let mut proof = Proof::new();
996 let mut axiom_ids = Vec::new();
997
998 for i in 0..axiom_count {
1000 let id = proof.add_axiom(format!("p{}", i));
1001 axiom_ids.push(id);
1002 }
1003
1004 for i in 0..inference_count {
1006 if axiom_ids.len() >= 2 {
1007 let premises = vec![axiom_ids[0], axiom_ids[1]];
1008 let id = proof.add_inference(
1009 "and",
1010 premises,
1011 format!("q{}", i)
1012 );
1013 axiom_ids.push(id);
1014 }
1015 }
1016
1017 let stats = proof.stats();
1018 prop_assert_eq!(stats.axiom_count, axiom_count);
1019 prop_assert!(stats.inference_count <= inference_count);
1020 prop_assert_eq!(stats.total_nodes, stats.axiom_count + stats.inference_count);
1021 }
1022
1023 #[test]
1025 fn prop_node_count_invariant(
1026 axioms in prop::collection::vec(var_name(), 0..10)
1027 ) {
1028 let mut proof = Proof::new();
1029 for axiom in &axioms {
1030 proof.add_axiom(axiom);
1031 }
1032
1033 let stats = proof.stats();
1034 prop_assert_eq!(proof.len(), stats.total_nodes);
1035 prop_assert_eq!(stats.total_nodes, stats.axiom_count + stats.inference_count);
1036 }
1037
1038 #[test]
1040 fn prop_all_nodes_reachable(
1041 conclusions in prop::collection::vec(var_name(), 1..8)
1042 ) {
1043 let mut proof = Proof::new();
1044 for conclusion in &conclusions {
1045 proof.add_axiom(conclusion);
1046 }
1047
1048 for i in 0..proof.len() {
1049 let id = ProofNodeId(i as u32);
1050 prop_assert!(proof.get_node(id).is_some());
1051 }
1052 }
1053
1054 #[test]
1056 fn prop_clear_resets(
1057 conclusions in prop::collection::vec(var_name(), 1..10)
1058 ) {
1059 let mut proof = Proof::new();
1060 for conclusion in &conclusions {
1061 proof.add_axiom(conclusion);
1062 }
1063
1064 proof.clear();
1065 prop_assert!(proof.is_empty());
1066 prop_assert_eq!(proof.len(), 0);
1067 prop_assert_eq!(proof.next_id, 0);
1068 }
1069
1070 #[test]
1072 fn prop_max_depth_bound(
1073 conclusions in prop::collection::vec(var_name(), 1..8)
1074 ) {
1075 let mut proof = Proof::new();
1076 let mut ids = Vec::new();
1077
1078 for conclusion in &conclusions {
1079 ids.push(proof.add_axiom(conclusion));
1080 }
1081
1082 if ids.len() >= 2 {
1084 let id = proof.add_inference("and", vec![ids[0], ids[1]], "result");
1085 ids.push(id);
1086 }
1087
1088 let stats = proof.stats();
1089 for id in &ids {
1090 if let Some(node) = proof.get_node(*id) {
1091 prop_assert!(stats.max_depth >= node.depth);
1092 }
1093 }
1094 }
1095
1096 #[test]
1098 fn prop_premise_validity(
1099 rule in rule_name(),
1100 conclusions in prop::collection::vec(var_name(), 2..6)
1101 ) {
1102 let mut proof = Proof::new();
1103 let mut premise_ids = Vec::new();
1104
1105 for conclusion in &conclusions {
1106 premise_ids.push(proof.add_axiom(conclusion));
1107 }
1108
1109 if premise_ids.len() >= 2 {
1110 let inf_id = proof.add_inference(
1111 &rule,
1112 premise_ids.clone(),
1113 "result"
1114 );
1115
1116 if let Some(premises) = proof.premises(inf_id) {
1117 for &premise_id in premises {
1118 prop_assert!(proof.get_node(premise_id).is_some());
1119 }
1120 }
1121 }
1122 }
1123 }
1124 }
1125}