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