Skip to main content

oxiz_proof/
visualization.rs

1//! Proof visualization utilities.
2//!
3//! This module provides tools for visualizing proof trees in various formats,
4//! including DOT (Graphviz), ASCII art, and structured text.
5
6use crate::proof::{Proof, ProofNode, ProofNodeId, ProofStep};
7use std::collections::HashSet;
8use std::fmt::Write as FmtWrite;
9use std::io::{self, Write};
10
11/// Visualization format for proofs.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum VisualizationFormat {
14    /// DOT format for Graphviz.
15    Dot,
16    /// ASCII tree format.
17    AsciiTree,
18    /// Indented text format.
19    IndentedText,
20    /// JSON format.
21    Json,
22}
23
24/// Proof visualizer.
25#[derive(Debug)]
26pub struct ProofVisualizer {
27    /// Maximum depth to visualize (None = unlimited).
28    max_depth: Option<usize>,
29    /// Whether to show node IDs.
30    show_ids: bool,
31    /// Whether to show full conclusions (or truncate).
32    show_full_conclusions: bool,
33    /// Maximum conclusion length (if not showing full).
34    max_conclusion_length: usize,
35}
36
37impl ProofVisualizer {
38    /// Create a new proof visualizer with default settings.
39    #[must_use]
40    pub fn new() -> Self {
41        Self {
42            max_depth: None,
43            show_ids: true,
44            show_full_conclusions: false,
45            max_conclusion_length: 40,
46        }
47    }
48
49    /// Set the maximum depth to visualize.
50    pub fn with_max_depth(mut self, depth: usize) -> Self {
51        self.max_depth = Some(depth);
52        self
53    }
54
55    /// Set whether to show node IDs.
56    pub fn with_show_ids(mut self, show: bool) -> Self {
57        self.show_ids = show;
58        self
59    }
60
61    /// Set whether to show full conclusions.
62    pub fn with_full_conclusions(mut self, show: bool) -> Self {
63        self.show_full_conclusions = show;
64        self
65    }
66
67    /// Visualize a proof in the specified format.
68    pub fn visualize<W: Write>(
69        &self,
70        proof: &Proof,
71        format: VisualizationFormat,
72        writer: &mut W,
73    ) -> io::Result<()> {
74        match format {
75            VisualizationFormat::Dot => self.visualize_dot(proof, writer),
76            VisualizationFormat::AsciiTree => self.visualize_ascii_tree(proof, writer),
77            VisualizationFormat::IndentedText => self.visualize_indented(proof, writer),
78            VisualizationFormat::Json => self.visualize_json(proof, writer),
79        }
80    }
81
82    /// Visualize proof as DOT format for Graphviz.
83    fn visualize_dot<W: Write>(&self, proof: &Proof, writer: &mut W) -> io::Result<()> {
84        writeln!(writer, "digraph Proof {{")?;
85        writeln!(writer, "  rankdir=BT;")?;
86        writeln!(writer, "  node [shape=box];")?;
87
88        // Write nodes
89        let mut visited = HashSet::new();
90        if let Some(root) = proof.root() {
91            self.write_dot_nodes(proof, root, writer, &mut visited, 0)?;
92        }
93
94        writeln!(writer, "}}")?;
95        Ok(())
96    }
97
98    fn write_dot_nodes<W: Write>(
99        &self,
100        proof: &Proof,
101        node_id: ProofNodeId,
102        writer: &mut W,
103        visited: &mut HashSet<ProofNodeId>,
104        depth: usize,
105    ) -> io::Result<()> {
106        if visited.contains(&node_id) {
107            return Ok(());
108        }
109        if let Some(max_depth) = self.max_depth
110            && depth >= max_depth
111        {
112            return Ok(());
113        }
114
115        visited.insert(node_id);
116
117        if let Some(node) = proof.get_node(node_id) {
118            let label = self.format_node_label(node);
119            let color = match &node.step {
120                ProofStep::Axiom { .. } => "lightblue",
121                ProofStep::Inference { .. } => "lightgreen",
122            };
123
124            writeln!(
125                writer,
126                "  {} [label=\"{}\", fillcolor={}, style=filled];",
127                node_id.0, label, color
128            )?;
129
130            // Write edges to premises
131            if let ProofStep::Inference { premises, .. } = &node.step {
132                for &premise_id in premises {
133                    writeln!(writer, "  {} -> {};", premise_id.0, node_id.0)?;
134                    self.write_dot_nodes(proof, premise_id, writer, visited, depth + 1)?;
135                }
136            }
137        }
138
139        Ok(())
140    }
141
142    /// Visualize proof as ASCII tree.
143    fn visualize_ascii_tree<W: Write>(&self, proof: &Proof, writer: &mut W) -> io::Result<()> {
144        if let Some(root) = proof.root_node() {
145            self.write_ascii_node(proof, root, writer, "", true, 0)?;
146        }
147        Ok(())
148    }
149
150    fn write_ascii_node<W: Write>(
151        &self,
152        proof: &Proof,
153        node: &ProofNode,
154        writer: &mut W,
155        prefix: &str,
156        is_last: bool,
157        depth: usize,
158    ) -> io::Result<()> {
159        if let Some(max_depth) = self.max_depth
160            && depth >= max_depth
161        {
162            return Ok(());
163        }
164
165        let connector = if is_last { "└─" } else { "├─" };
166        let label = self.format_node_label(node);
167
168        writeln!(writer, "{}{} {}", prefix, connector, label)?;
169
170        if let ProofStep::Inference { premises, .. } = &node.step {
171            let new_prefix = format!("{}{}  ", prefix, if is_last { " " } else { "│" });
172
173            for (i, &premise_id) in premises.iter().enumerate() {
174                if let Some(premise_node) = proof.get_node(premise_id) {
175                    let is_last_premise = i == premises.len() - 1;
176                    self.write_ascii_node(
177                        proof,
178                        premise_node,
179                        writer,
180                        &new_prefix,
181                        is_last_premise,
182                        depth + 1,
183                    )?;
184                }
185            }
186        }
187
188        Ok(())
189    }
190
191    /// Visualize proof as indented text.
192    fn visualize_indented<W: Write>(&self, proof: &Proof, writer: &mut W) -> io::Result<()> {
193        if let Some(root) = proof.root_node() {
194            self.write_indented_node(proof, root, writer, 0, 0)?;
195        }
196        Ok(())
197    }
198
199    fn write_indented_node<W: Write>(
200        &self,
201        proof: &Proof,
202        node: &ProofNode,
203        writer: &mut W,
204        indent: usize,
205        depth: usize,
206    ) -> io::Result<()> {
207        if let Some(max_depth) = self.max_depth
208            && depth >= max_depth
209        {
210            return Ok(());
211        }
212
213        let indent_str = "  ".repeat(indent);
214        let label = self.format_node_label(node);
215
216        writeln!(writer, "{}{}", indent_str, label)?;
217
218        if let ProofStep::Inference { premises, .. } = &node.step {
219            for &premise_id in premises {
220                if let Some(premise_node) = proof.get_node(premise_id) {
221                    self.write_indented_node(proof, premise_node, writer, indent + 1, depth + 1)?;
222                }
223            }
224        }
225
226        Ok(())
227    }
228
229    /// Visualize proof as JSON.
230    fn visualize_json<W: Write>(&self, proof: &Proof, writer: &mut W) -> io::Result<()> {
231        writeln!(writer, "{{")?;
232        writeln!(writer, "  \"type\": \"proof\",")?;
233        writeln!(writer, "  \"node_count\": {},", proof.node_count())?;
234        writeln!(writer, "  \"depth\": {},", proof.depth())?;
235        writeln!(writer, "  \"root\": {{")?;
236
237        if let Some(root) = proof.root_node() {
238            self.write_json_node(proof, root, writer, 2, 0)?;
239        }
240
241        writeln!(writer, "  }}")?;
242        writeln!(writer, "}}")?;
243        Ok(())
244    }
245
246    fn write_json_node<W: Write>(
247        &self,
248        proof: &Proof,
249        node: &ProofNode,
250        writer: &mut W,
251        indent: usize,
252        depth: usize,
253    ) -> io::Result<()> {
254        if let Some(max_depth) = self.max_depth
255            && depth >= max_depth
256        {
257            return Ok(());
258        }
259
260        let indent_str = "  ".repeat(indent);
261
262        if self.show_ids {
263            writeln!(writer, "{}\"id\": \"{}\",", indent_str, node.id)?;
264        }
265
266        match &node.step {
267            ProofStep::Axiom { conclusion } => {
268                writeln!(writer, "{}\"type\": \"axiom\",", indent_str)?;
269                writeln!(
270                    writer,
271                    "{}\"conclusion\": \"{}\"",
272                    indent_str,
273                    escape_json(conclusion)
274                )?;
275            }
276            ProofStep::Inference {
277                rule,
278                premises,
279                conclusion,
280                ..
281            } => {
282                writeln!(writer, "{}\"type\": \"inference\",", indent_str)?;
283                writeln!(writer, "{}\"rule\": \"{}\",", indent_str, escape_json(rule))?;
284                writeln!(
285                    writer,
286                    "{}\"conclusion\": \"{}\",",
287                    indent_str,
288                    escape_json(conclusion)
289                )?;
290
291                if !premises.is_empty() {
292                    writeln!(writer, "{}\"premises\": [", indent_str)?;
293                    for (i, &premise_id) in premises.iter().enumerate() {
294                        if let Some(premise_node) = proof.get_node(premise_id) {
295                            writeln!(writer, "{}  {{", indent_str)?;
296                            self.write_json_node(
297                                proof,
298                                premise_node,
299                                writer,
300                                indent + 2,
301                                depth + 1,
302                            )?;
303                            if i < premises.len() - 1 {
304                                writeln!(writer, "{}  }},", indent_str)?;
305                            } else {
306                                writeln!(writer, "{}  }}", indent_str)?;
307                            }
308                        }
309                    }
310                    writeln!(writer, "{}]", indent_str)?;
311                }
312            }
313        }
314
315        Ok(())
316    }
317
318    /// Format a node label for display.
319    fn format_node_label(&self, node: &ProofNode) -> String {
320        let mut label = String::new();
321
322        if self.show_ids {
323            let _ = write!(label, "{}: ", node.id);
324        }
325
326        match &node.step {
327            ProofStep::Axiom { conclusion } => {
328                let _ = write!(label, "axiom ");
329                label.push_str(&self.format_conclusion(conclusion));
330            }
331            ProofStep::Inference {
332                rule, conclusion, ..
333            } => {
334                let _ = write!(label, "{} ", rule);
335                label.push_str(&self.format_conclusion(conclusion));
336            }
337        }
338
339        label
340    }
341
342    /// Format a conclusion, possibly truncating it.
343    fn format_conclusion(&self, conclusion: &str) -> String {
344        if self.show_full_conclusions || conclusion.len() <= self.max_conclusion_length {
345            conclusion.to_string()
346        } else {
347            format!("{}...", &conclusion[..self.max_conclusion_length])
348        }
349    }
350}
351
352impl Default for ProofVisualizer {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358/// Escape a string for JSON output.
359fn escape_json(s: &str) -> String {
360    s.replace('\\', "\\\\")
361        .replace('"', "\\\"")
362        .replace('\n', "\\n")
363        .replace('\r', "\\r")
364        .replace('\t', "\\t")
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_visualizer_new() {
373        let viz = ProofVisualizer::new();
374        assert!(viz.show_ids);
375        assert!(!viz.show_full_conclusions);
376        assert_eq!(viz.max_conclusion_length, 40);
377        assert!(viz.max_depth.is_none());
378    }
379
380    #[test]
381    fn test_visualizer_with_options() {
382        let viz = ProofVisualizer::new()
383            .with_max_depth(5)
384            .with_show_ids(false)
385            .with_full_conclusions(true);
386
387        assert_eq!(viz.max_depth, Some(5));
388        assert!(!viz.show_ids);
389        assert!(viz.show_full_conclusions);
390    }
391
392    #[test]
393    fn test_visualize_dot() {
394        let mut proof = Proof::new();
395        proof.add_axiom("test");
396        let viz = ProofVisualizer::new();
397
398        let mut output = Vec::new();
399        viz.visualize(&proof, VisualizationFormat::Dot, &mut output)
400            .expect("test operation should succeed");
401
402        let dot = String::from_utf8(output).expect("test operation should succeed");
403        assert!(dot.contains("digraph Proof"));
404        assert!(dot.contains("axiom"));
405        assert!(dot.contains("test"));
406    }
407
408    #[test]
409    fn test_visualize_ascii_tree() {
410        let mut proof = Proof::new();
411        let p = proof.add_axiom("p");
412        let q = proof.add_axiom("q");
413        let _and_node = proof.add_inference("and", vec![p, q], "(and p q)");
414
415        let viz = ProofVisualizer::new();
416        let mut output = Vec::new();
417        viz.visualize(&proof, VisualizationFormat::AsciiTree, &mut output)
418            .expect("test operation should succeed");
419
420        let tree = String::from_utf8(output).expect("test operation should succeed");
421        assert!(tree.contains("and"));
422        assert!(tree.contains("axiom"));
423    }
424
425    #[test]
426    fn test_visualize_indented() {
427        let mut proof = Proof::new();
428        proof.add_axiom("test");
429        let viz = ProofVisualizer::new();
430
431        let mut output = Vec::new();
432        viz.visualize(&proof, VisualizationFormat::IndentedText, &mut output)
433            .expect("test operation should succeed");
434
435        let text = String::from_utf8(output).expect("test operation should succeed");
436        assert!(text.contains("axiom"));
437        assert!(text.contains("test"));
438    }
439
440    #[test]
441    fn test_visualize_json() {
442        let mut proof = Proof::new();
443        proof.add_axiom("test");
444        let viz = ProofVisualizer::new();
445
446        let mut output = Vec::new();
447        viz.visualize(&proof, VisualizationFormat::Json, &mut output)
448            .expect("test operation should succeed");
449
450        let json = String::from_utf8(output).expect("test operation should succeed");
451        assert!(json.contains("\"type\": \"proof\""));
452        assert!(json.contains("\"type\": \"axiom\""));
453        assert!(json.contains("test"));
454    }
455
456    #[test]
457    fn test_escape_json() {
458        assert_eq!(escape_json("hello"), "hello");
459        assert_eq!(escape_json("hello\"world"), "hello\\\"world");
460        assert_eq!(escape_json("line1\nline2"), "line1\\nline2");
461        assert_eq!(escape_json("path\\to\\file"), "path\\\\to\\\\file");
462    }
463
464    #[test]
465    fn test_visualize_with_max_depth() {
466        let mut proof = Proof::new();
467        let p = proof.add_axiom("p");
468        let q = proof.add_axiom("q");
469        let r = proof.add_axiom("r");
470        let and1 = proof.add_inference("and", vec![q, r], "(and q r)");
471        let _and2 = proof.add_inference("and", vec![p, and1], "(and p (and q r))");
472
473        let viz = ProofVisualizer::new().with_max_depth(1);
474        let mut output = Vec::new();
475        viz.visualize(&proof, VisualizationFormat::IndentedText, &mut output)
476            .expect("test operation should succeed");
477
478        let text = String::from_utf8(output).expect("test operation should succeed");
479        // Should only show root and its immediate children
480        assert!(text.contains("and"));
481    }
482
483    #[test]
484    fn test_format_conclusion_truncate() {
485        let viz = ProofVisualizer::new();
486
487        let short = "short";
488        assert_eq!(viz.format_conclusion(short), "short");
489
490        let long = "a".repeat(50);
491        let formatted = viz.format_conclusion(&long);
492        assert!(formatted.ends_with("..."));
493        assert!(formatted.len() < long.len());
494    }
495
496    #[test]
497    fn test_format_conclusion_full() {
498        let viz = ProofVisualizer::new().with_full_conclusions(true);
499
500        let long = "a".repeat(50);
501        let formatted = viz.format_conclusion(&long);
502        assert_eq!(formatted, long);
503        assert!(!formatted.contains("..."));
504    }
505}