1use alloc::{format, string::String};
2
3use crate::diagram::{Diagram, DiagramKind, NormalizationRule, NormalizationTrace};
4
5pub struct TextRenderer;
6pub struct SvgRenderer;
7
8impl TextRenderer {
9 pub fn render(diagram: &Diagram) -> String {
10 let mut out = String::new();
11 out.push_str(&format!(
12 "diagram {} -> {}\n",
13 diagram.input_arity, diagram.output_arity
14 ));
15 for (index, stage) in diagram.sequence_chain().iter().enumerate() {
16 out.push_str(&format!("{index}: {}\n", Self::stage(stage)));
17 }
18 out
19 }
20
21 pub fn render_trace(trace: &NormalizationTrace) -> String {
22 let mut out = String::new();
23 out.push_str("normalization trace\n");
24 out.push_str("rules:\n");
25 if trace.rules.is_empty() {
26 out.push_str("- none\n");
27 } else {
28 for rule in &trace.rules {
29 out.push_str(&format!("- {}\n", Self::rule_name(*rule)));
30 }
31 }
32 out.push_str("normalized:\n");
33 out.push_str(&Self::render(&trace.normalized));
34 out
35 }
36
37 fn stage(diagram: &Diagram) -> String {
38 match &diagram.kind {
39 DiagramKind::Identity => format!("id[{}]", diagram.input_arity),
40 DiagramKind::Box { label } => format!(
41 "box({label}) {} -> {}",
42 diagram.input_arity, diagram.output_arity
43 ),
44 DiagramKind::Parallel(left, right) => {
45 format!("parallel({}, {})", Self::stage(left), Self::stage(right))
46 }
47 DiagramKind::Swap { left, right } => format!("swap[{left}|{right}]"),
48 DiagramKind::Cup { arity } => format!("cup[{arity}]"),
49 DiagramKind::Cap { arity } => format!("cap[{arity}]"),
50 DiagramKind::Sequence(_, _) => {
51 unreachable!("sequence chains are flattened before stage rendering")
52 }
53 }
54 }
55
56 fn rule_name(rule: NormalizationRule) -> &'static str {
57 match rule {
58 NormalizationRule::FlattenSequence => "flatten-sequence",
59 NormalizationRule::FlattenParallel => "flatten-parallel",
60 NormalizationRule::ElideIdentitySequenceStage => "elide-identity-sequence-stage",
61 NormalizationRule::CollapseIdentityParallel => "collapse-identity-parallel",
62 NormalizationRule::CancelAdjacentSwaps => "cancel-adjacent-swaps",
63 NormalizationRule::YankCupCap => "yank-cup-cap",
64 }
65 }
66}
67
68impl SvgRenderer {
69 pub fn render(diagram: &Diagram) -> String {
70 let stages = diagram.sequence_chain();
71 let width = 180 * stages.len().max(1);
72 let height = 80 + 40 * diagram.input_arity.max(diagram.output_arity).max(1);
73 let mut out =
74 format!("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\">");
75 out.push_str("<style>text{font-family:monospace;font-size:12px}rect{fill:#1f2937;stroke:#94a3b8}line{stroke:#64748b;stroke-width:2}</style>");
76
77 for wire in 0..diagram.input_arity.max(1) {
78 let y = 40 + wire * 30;
79 out.push_str(&format!(
80 "<line x1=\"10\" y1=\"{y}\" x2=\"{}\" y2=\"{y}\" />",
81 width - 10
82 ));
83 }
84
85 for (index, stage) in stages.iter().enumerate() {
86 let x = 30 + index * 160;
87 out.push_str(&format!(
88 "<rect x=\"{x}\" y=\"20\" width=\"120\" height=\"40\" rx=\"6\" />"
89 ));
90 out.push_str(&format!(
91 "<text x=\"{}\" y=\"45\">{}</text>",
92 x + 10,
93 TextRenderer::stage(stage)
94 ));
95 }
96
97 out.push_str("</svg>");
98 out
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::diagram::Diagram;
106
107 #[test]
108 fn text_renderer_includes_swap_and_boxes() {
109 let diagram = Diagram::box_("double", 1, 1)
110 .parallel(Diagram::box_("increment", 1, 1))
111 .then(Diagram::swap(1, 1));
112 let rendered = TextRenderer::render(&diagram);
113 assert!(rendered.contains("parallel"));
114 assert!(rendered.contains("swap[1|1]"));
115 }
116
117 #[test]
118 fn svg_renderer_emits_svg_tag() {
119 let diagram = Diagram::box_("double", 1, 1);
120 let rendered = SvgRenderer::render(&diagram);
121 assert!(rendered.starts_with("<svg"));
122 assert!(rendered.contains("box(double)"));
123 }
124
125 #[test]
126 fn text_renderer_trace_lists_applied_rules() {
127 let trace = Diagram::identity(2)
128 .then(Diagram::swap(1, 1))
129 .then(Diagram::swap(1, 1))
130 .normalize_with_trace();
131
132 let rendered = TextRenderer::render_trace(&trace);
133 assert!(rendered.contains("normalization trace"));
134 assert!(rendered.contains("flatten-sequence"));
135 assert!(rendered.contains("cancel-adjacent-swaps"));
136 assert!(rendered.contains("diagram 2 -> 2"));
137 }
138
139 #[test]
140 fn text_renderer_includes_cups_and_caps() {
141 let diagram = Diagram::cup(1)
142 .parallel(Diagram::identity(1))
143 .then(Diagram::identity(1).parallel(Diagram::cap(1)));
144
145 let rendered = TextRenderer::render(&diagram);
146 assert!(rendered.contains("cup[1]"));
147 assert!(rendered.contains("cap[1]"));
148 }
149
150 #[test]
151 fn text_renderer_trace_lists_yanking_rule() {
152 let trace = Diagram::cup(1)
153 .parallel(Diagram::identity(1))
154 .then(Diagram::identity(1).parallel(Diagram::cap(1)))
155 .normalize_with_trace();
156
157 let rendered = TextRenderer::render_trace(&trace);
158 assert!(rendered.contains("yank-cup-cap"));
159 }
160}