1use crate::proof::{Proof, ProofNode, ProofNodeId, ProofStep};
7use std::collections::HashSet;
8use std::fmt::Write as FmtWrite;
9use std::io::{self, Write};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum VisualizationFormat {
14 Dot,
16 AsciiTree,
18 IndentedText,
20 Json,
22}
23
24#[derive(Debug)]
26pub struct ProofVisualizer {
27 max_depth: Option<usize>,
29 show_ids: bool,
31 show_full_conclusions: bool,
33 max_conclusion_length: usize,
35}
36
37impl ProofVisualizer {
38 #[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 pub fn with_max_depth(mut self, depth: usize) -> Self {
51 self.max_depth = Some(depth);
52 self
53 }
54
55 pub fn with_show_ids(mut self, show: bool) -> Self {
57 self.show_ids = show;
58 self
59 }
60
61 pub fn with_full_conclusions(mut self, show: bool) -> Self {
63 self.show_full_conclusions = show;
64 self
65 }
66
67 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 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 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 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 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 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 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 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 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
358fn 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 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}