oxiz_proof/
proof.rs

1//! Core proof representation.
2
3use crate::metadata::ProofMetadata;
4use rustc_hash::FxHashMap;
5use smallvec::SmallVec;
6use std::fmt;
7use std::io::{self, Write};
8
9/// Unique identifier for a proof node
10#[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/// A proof tree with explicit node tracking.
21///
22/// This structure maintains a DAG (Directed Acyclic Graph) of proof steps,
23/// allowing for efficient premise tracking and proof compression.
24#[derive(Debug, Clone)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Proof {
27    /// All nodes in the proof
28    nodes: Vec<ProofNode>,
29    /// Root nodes (conclusions with no dependents)
30    roots: Vec<ProofNodeId>,
31    /// Mapping from terms to proof nodes (for deduplication)
32    #[cfg_attr(feature = "serde", serde(skip))]
33    term_cache: FxHashMap<String, ProofNodeId>,
34    /// Metadata for proof nodes
35    metadata: FxHashMap<ProofNodeId, ProofMetadata>,
36    /// Next available node ID
37    next_id: u32,
38}
39
40/// A single node in the proof tree
41#[derive(Debug, Clone)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct ProofNode {
44    /// Unique identifier
45    pub id: ProofNodeId,
46    /// The proof step
47    pub step: ProofStep,
48    /// IDs of nodes that depend on this one (optimized for 0-2 dependents)
49    pub dependents: SmallVec<[ProofNodeId; 2]>,
50    /// Depth in the proof tree (for compression)
51    pub depth: u32,
52}
53
54/// A single proof step.
55#[derive(Debug, Clone)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub enum ProofStep {
58    /// Axiom (no premises)
59    Axiom {
60        /// The conclusion term
61        conclusion: String,
62    },
63    /// Inference rule application
64    Inference {
65        /// Rule name
66        rule: String,
67        /// Premise node IDs (optimized for 1-4 premises)
68        premises: SmallVec<[ProofNodeId; 4]>,
69        /// The conclusion term
70        conclusion: String,
71        /// Optional arguments to the rule (optimized for 0-2 args)
72        args: SmallVec<[String; 2]>,
73    },
74}
75
76impl ProofNode {
77    /// Get the conclusion of this proof node.
78    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    /// Create a new empty proof
88    #[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    /// Allocate a new node ID
100    fn alloc_id(&mut self) -> ProofNodeId {
101        let id = ProofNodeId(self.next_id);
102        self.next_id += 1;
103        id
104    }
105
106    /// Add an axiom to the proof
107    pub fn add_axiom(&mut self, conclusion: impl Into<String>) -> ProofNodeId {
108        let conclusion = conclusion.into();
109
110        // Check cache for deduplication
111        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    /// Add an inference step to the proof
132    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    /// Add an inference step with arguments
142    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        // Check cache for deduplication
153        if let Some(&existing_id) = self.term_cache.get(&conclusion) {
154            return existing_id;
155        }
156
157        let id = self.alloc_id();
158
159        // Calculate depth (max of premise depths + 1)
160        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        // Update premise dependents
181        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    /// Get a proof node by ID
194    #[must_use]
195    pub fn get_node(&self, id: ProofNodeId) -> Option<&ProofNode> {
196        self.nodes.get(id.0 as usize)
197    }
198
199    /// Get a mutable proof node by ID
200    fn get_node_mut(&mut self, id: ProofNodeId) -> Option<&mut ProofNode> {
201        self.nodes.get_mut(id.0 as usize)
202    }
203
204    /// Update the conclusion of a proof node.
205    ///
206    /// Returns `true` if the node was found and updated, `false` otherwise.
207    /// This is useful for proof simplification and transformation.
208    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            // Update term cache
220            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    /// Get all proof nodes
234    #[must_use]
235    pub fn nodes(&self) -> &[ProofNode] {
236        &self.nodes
237    }
238
239    /// Get the root nodes
240    #[must_use]
241    pub fn roots(&self) -> &[ProofNodeId] {
242        &self.roots
243    }
244
245    /// Get the primary root (last added root)
246    #[must_use]
247    pub fn root(&self) -> Option<ProofNodeId> {
248        self.roots.last().copied()
249    }
250
251    /// Get the primary root node
252    #[must_use]
253    pub fn root_node(&self) -> Option<&ProofNode> {
254        self.root().and_then(|id| self.get_node(id))
255    }
256
257    /// Get the number of nodes
258    #[must_use]
259    pub fn len(&self) -> usize {
260        self.nodes.len()
261    }
262
263    /// Get the maximum depth of the proof tree
264    #[must_use]
265    pub fn depth(&self) -> u32 {
266        self.stats().max_depth
267    }
268
269    /// Get the node count (alias for len())
270    #[must_use]
271    pub fn node_count(&self) -> usize {
272        self.len()
273    }
274
275    /// Get all leaf nodes (axioms - nodes with no premises)
276    #[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    /// Get the premises of a node (if it's an inference)
286    #[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    /// Check if the proof is empty
298    #[must_use]
299    pub fn is_empty(&self) -> bool {
300        self.nodes.is_empty()
301    }
302
303    /// Clear the proof
304    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    /// Get proof statistics
313    #[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            // Count leaves (nodes with no dependents)
344            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    /// Write the proof in a human-readable format
361    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    /// Convert to string
403    #[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    /// Add multiple axioms at once and return their IDs
412    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    /// Find all nodes with a specific rule
420    #[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    /// Find all nodes with a specific conclusion
430    #[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    /// Get all direct children (premises) of a node
443    #[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    /// Get all direct parents (dependents) of a node
451    #[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    /// Check if one node is an ancestor of another
459    #[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    /// Get all ancestors of a node (all nodes it depends on)
489    #[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    /// Count nodes of a specific rule type
516    #[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    /// Rebuild the internal term cache (useful after deserialization)
525    #[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    // Metadata API
538
539    /// Set metadata for a proof node.
540    pub fn set_metadata(&mut self, node_id: ProofNodeId, metadata: ProofMetadata) {
541        self.metadata.insert(node_id, metadata);
542    }
543
544    /// Get metadata for a proof node.
545    #[must_use]
546    pub fn get_metadata(&self, node_id: ProofNodeId) -> Option<&ProofMetadata> {
547        self.metadata.get(&node_id)
548    }
549
550    /// Get mutable metadata for a proof node.
551    pub fn get_metadata_mut(&mut self, node_id: ProofNodeId) -> Option<&mut ProofMetadata> {
552        self.metadata.get_mut(&node_id)
553    }
554
555    /// Remove metadata for a proof node.
556    pub fn remove_metadata(&mut self, node_id: ProofNodeId) -> Option<ProofMetadata> {
557        self.metadata.remove(&node_id)
558    }
559
560    /// Check if a node has metadata.
561    #[must_use]
562    pub fn has_metadata(&self, node_id: ProofNodeId) -> bool {
563        self.metadata.contains_key(&node_id)
564    }
565
566    /// Get all nodes with a specific tag.
567    #[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    /// Get all nodes with a specific priority.
577    #[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    /// Get all nodes with a specific difficulty.
587    #[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    /// Get all nodes with a specific strategy.
600    #[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    /// Get or create metadata for a node.
610    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/// Statistics about a proof
622#[derive(Debug, Clone, Copy)]
623#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
624pub struct ProofStats {
625    /// Total number of nodes
626    pub total_nodes: usize,
627    /// Number of axiom nodes
628    pub axiom_count: usize,
629    /// Number of inference nodes
630    pub inference_count: usize,
631    /// Maximum depth in the proof tree
632    pub max_depth: u32,
633    /// Average depth
634    pub avg_depth: f64,
635    /// Number of root nodes
636    pub root_count: usize,
637    /// Maximum number of premises for a single inference
638    pub max_premises: usize,
639    /// Average number of premises per inference
640    pub avg_premises: f64,
641    /// Number of nodes with no dependents (leaves)
642    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).unwrap();
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).unwrap();
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"); // Same conclusion
732
733        assert_eq!(id1, id2);
734        assert_eq!(proof.len(), 1); // Only one node created
735    }
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).unwrap();
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).unwrap();
795        assert!(node_a1.dependents.contains(&i1));
796
797        let node_a2 = proof.get_node(a2).unwrap();
798        assert!(node_a2.dependents.contains(&i1));
799    }
800
801    // Metadata tests
802    #[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.get_metadata(id).unwrap();
816        assert_eq!(retrieved.priority(), Priority::High);
817        assert_eq!(retrieved.difficulty(), Difficulty::Easy);
818    }
819
820    #[test]
821    fn test_metadata_tags() {
822        let mut proof = Proof::new();
823        let id1 = proof.add_axiom("p");
824        let id2 = proof.add_axiom("q");
825
826        proof.set_metadata(id1, ProofMetadata::new().with_tag("important"));
827        proof.set_metadata(id2, ProofMetadata::new().with_tag("trivial"));
828
829        let important_nodes = proof.nodes_with_tag("important");
830        assert_eq!(important_nodes.len(), 1);
831        assert_eq!(important_nodes[0], id1);
832    }
833
834    #[test]
835    fn test_metadata_priority_filter() {
836        use crate::metadata::Priority;
837
838        let mut proof = Proof::new();
839        let id1 = proof.add_axiom("p");
840        let id2 = proof.add_axiom("q");
841        let id3 = proof.add_axiom("r");
842
843        proof.set_metadata(id1, ProofMetadata::new().with_priority(Priority::High));
844        proof.set_metadata(id2, ProofMetadata::new().with_priority(Priority::High));
845        proof.set_metadata(id3, ProofMetadata::new().with_priority(Priority::Low));
846
847        let high_priority = proof.nodes_with_priority(Priority::High);
848        assert_eq!(high_priority.len(), 2);
849    }
850
851    #[test]
852    fn test_metadata_strategy_filter() {
853        use crate::metadata::Strategy;
854
855        let mut proof = Proof::new();
856        let id1 = proof.add_axiom("p");
857        let id2 = proof.add_axiom("q");
858
859        proof.set_metadata(
860            id1,
861            ProofMetadata::new().with_strategy(Strategy::Resolution),
862        );
863        proof.set_metadata(id2, ProofMetadata::new().with_strategy(Strategy::Theory));
864
865        let resolution_nodes = proof.nodes_with_strategy(Strategy::Resolution);
866        assert_eq!(resolution_nodes.len(), 1);
867        assert_eq!(resolution_nodes[0], id1);
868    }
869
870    #[test]
871    fn test_metadata_remove() {
872        let mut proof = Proof::new();
873        let id = proof.add_axiom("p");
874
875        proof.set_metadata(id, ProofMetadata::new().with_tag("temp"));
876        assert!(proof.has_metadata(id));
877
878        proof.remove_metadata(id);
879        assert!(!proof.has_metadata(id));
880    }
881
882    #[test]
883    fn test_metadata_get_or_create() {
884        use crate::metadata::Priority;
885
886        let mut proof = Proof::new();
887        let id = proof.add_axiom("p");
888
889        let meta = proof.get_or_create_metadata(id);
890        meta.set_priority(Priority::VeryHigh);
891
892        let retrieved = proof.get_metadata(id).unwrap();
893        assert_eq!(retrieved.priority(), Priority::VeryHigh);
894    }
895
896    #[test]
897    fn test_metadata_clear() {
898        let mut proof = Proof::new();
899        let id = proof.add_axiom("p");
900        proof.set_metadata(id, ProofMetadata::new().with_tag("test"));
901
902        proof.clear();
903        assert!(!proof.has_metadata(id));
904    }
905
906    // Property-based tests
907    mod proptests {
908        use super::*;
909        use proptest::prelude::*;
910
911        // Strategy for generating variable names
912        fn var_name() -> impl Strategy<Value = String> {
913            "[a-z][0-9]*".prop_map(|s| s.to_string())
914        }
915
916        // Strategy for generating rule names
917        fn rule_name() -> impl Strategy<Value = String> {
918            prop_oneof![
919                Just("and".to_string()),
920                Just("or".to_string()),
921                Just("not".to_string()),
922                Just("imp".to_string()),
923                Just("resolution".to_string()),
924            ]
925        }
926
927        proptest! {
928            /// Adding axioms should never fail and always increase size
929            #[test]
930            fn prop_axiom_increases_size(conclusion in var_name()) {
931                let mut proof = Proof::new();
932                let initial_len = proof.len();
933                proof.add_axiom(&conclusion);
934                prop_assert!(proof.len() > initial_len || proof.len() == 1);
935            }
936
937            /// Axiom depth is always 0
938            #[test]
939            fn prop_axiom_depth_zero(conclusion in var_name()) {
940                let mut proof = Proof::new();
941                let id = proof.add_axiom(&conclusion);
942                let node = proof.get_node(id).unwrap();
943                prop_assert_eq!(node.depth, 0);
944            }
945
946            /// Deduplication: Adding same axiom twice gives same ID
947            #[test]
948            fn prop_axiom_deduplication(conclusion in var_name()) {
949                let mut proof = Proof::new();
950                let id1 = proof.add_axiom(&conclusion);
951                let id2 = proof.add_axiom(&conclusion);
952                prop_assert_eq!(id1, id2);
953                prop_assert_eq!(proof.len(), 1);
954            }
955
956            /// Inference depth is always greater than max premise depth
957            #[test]
958            fn prop_inference_depth(
959                rule in rule_name(),
960                conclusions in prop::collection::vec(var_name(), 1..5)
961            ) {
962                let mut proof = Proof::new();
963                let mut axiom_ids = Vec::new();
964
965                for conclusion in &conclusions {
966                    let id = proof.add_axiom(conclusion);
967                    axiom_ids.push(id);
968                }
969
970                if !axiom_ids.is_empty() {
971                    let inf_id = proof.add_inference(
972                        &rule,
973                        axiom_ids.clone(),
974                        format!("({} {})", rule, conclusions.join(" "))
975                    );
976                    let inf_node = proof.get_node(inf_id).unwrap();
977
978                    for &premise_id in &axiom_ids {
979                        let premise_node = proof.get_node(premise_id).unwrap();
980                        prop_assert!(inf_node.depth > premise_node.depth);
981                    }
982                }
983            }
984
985            /// Stats should be consistent with actual proof structure
986            #[test]
987            fn prop_stats_consistency(
988                axiom_count in 1..10_usize,
989                inference_count in 0..5_usize
990            ) {
991                let mut proof = Proof::new();
992                let mut axiom_ids = Vec::new();
993
994                // Add axioms
995                for i in 0..axiom_count {
996                    let id = proof.add_axiom(format!("p{}", i));
997                    axiom_ids.push(id);
998                }
999
1000                // Add inferences
1001                for i in 0..inference_count {
1002                    if axiom_ids.len() >= 2 {
1003                        let premises = vec![axiom_ids[0], axiom_ids[1]];
1004                        let id = proof.add_inference(
1005                            "and",
1006                            premises,
1007                            format!("q{}", i)
1008                        );
1009                        axiom_ids.push(id);
1010                    }
1011                }
1012
1013                let stats = proof.stats();
1014                prop_assert_eq!(stats.axiom_count, axiom_count);
1015                prop_assert!(stats.inference_count <= inference_count);
1016                prop_assert_eq!(stats.total_nodes, stats.axiom_count + stats.inference_count);
1017            }
1018
1019            /// Node count equals axioms plus inferences
1020            #[test]
1021            fn prop_node_count_invariant(
1022                axioms in prop::collection::vec(var_name(), 0..10)
1023            ) {
1024                let mut proof = Proof::new();
1025                for axiom in &axioms {
1026                    proof.add_axiom(axiom);
1027                }
1028
1029                let stats = proof.stats();
1030                prop_assert_eq!(proof.len(), stats.total_nodes);
1031                prop_assert_eq!(stats.total_nodes, stats.axiom_count + stats.inference_count);
1032            }
1033
1034            /// All nodes should be reachable
1035            #[test]
1036            fn prop_all_nodes_reachable(
1037                conclusions in prop::collection::vec(var_name(), 1..8)
1038            ) {
1039                let mut proof = Proof::new();
1040                for conclusion in &conclusions {
1041                    proof.add_axiom(conclusion);
1042                }
1043
1044                for i in 0..proof.len() {
1045                    let id = ProofNodeId(i as u32);
1046                    prop_assert!(proof.get_node(id).is_some());
1047                }
1048            }
1049
1050            /// Clearing proof resets everything
1051            #[test]
1052            fn prop_clear_resets(
1053                conclusions in prop::collection::vec(var_name(), 1..10)
1054            ) {
1055                let mut proof = Proof::new();
1056                for conclusion in &conclusions {
1057                    proof.add_axiom(conclusion);
1058                }
1059
1060                proof.clear();
1061                prop_assert!(proof.is_empty());
1062                prop_assert_eq!(proof.len(), 0);
1063                prop_assert_eq!(proof.next_id, 0);
1064            }
1065
1066            /// Max depth is at least as large as any individual node depth
1067            #[test]
1068            fn prop_max_depth_bound(
1069                conclusions in prop::collection::vec(var_name(), 1..8)
1070            ) {
1071                let mut proof = Proof::new();
1072                let mut ids = Vec::new();
1073
1074                for conclusion in &conclusions {
1075                    ids.push(proof.add_axiom(conclusion));
1076                }
1077
1078                // Add some inferences
1079                if ids.len() >= 2 {
1080                    let id = proof.add_inference("and", vec![ids[0], ids[1]], "result");
1081                    ids.push(id);
1082                }
1083
1084                let stats = proof.stats();
1085                for id in &ids {
1086                    if let Some(node) = proof.get_node(*id) {
1087                        prop_assert!(stats.max_depth >= node.depth);
1088                    }
1089                }
1090            }
1091
1092            /// Premise IDs must exist in the proof
1093            #[test]
1094            fn prop_premise_validity(
1095                rule in rule_name(),
1096                conclusions in prop::collection::vec(var_name(), 2..6)
1097            ) {
1098                let mut proof = Proof::new();
1099                let mut premise_ids = Vec::new();
1100
1101                for conclusion in &conclusions {
1102                    premise_ids.push(proof.add_axiom(conclusion));
1103                }
1104
1105                if premise_ids.len() >= 2 {
1106                    let inf_id = proof.add_inference(
1107                        &rule,
1108                        premise_ids.clone(),
1109                        "result"
1110                    );
1111
1112                    if let Some(premises) = proof.premises(inf_id) {
1113                        for &premise_id in premises {
1114                            prop_assert!(proof.get_node(premise_id).is_some());
1115                        }
1116                    }
1117                }
1118            }
1119        }
1120    }
1121}