Skip to main content

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).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"); // 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).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    // 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
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    // Property-based tests
911    mod proptests {
912        use super::*;
913        use proptest::prelude::*;
914
915        // Strategy for generating variable names
916        fn var_name() -> impl Strategy<Value = String> {
917            "[a-z][0-9]*".prop_map(|s| s.to_string())
918        }
919
920        // Strategy for generating rule names
921        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            /// Adding axioms should never fail and always increase size
933            #[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            /// Axiom depth is always 0
942            #[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            /// Deduplication: Adding same axiom twice gives same ID
951            #[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            /// Inference depth is always greater than max premise depth
961            #[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            /// Stats should be consistent with actual proof structure
990            #[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                // Add axioms
999                for i in 0..axiom_count {
1000                    let id = proof.add_axiom(format!("p{}", i));
1001                    axiom_ids.push(id);
1002                }
1003
1004                // Add inferences
1005                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            /// Node count equals axioms plus inferences
1024            #[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            /// All nodes should be reachable
1039            #[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            /// Clearing proof resets everything
1055            #[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            /// Max depth is at least as large as any individual node depth
1071            #[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                // Add some inferences
1083                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            /// Premise IDs must exist in the proof
1097            #[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}