1use std::collections::{HashMap, HashSet};
4use std::fmt::Write;
5
6use crate::ir::{Nav, NavKind};
7
8use super::graph::{BuildEffect, BuildGraph, BuildMatcher, NodeId, RefMarker};
9
10pub struct GraphPrinter<'a, 'src> {
12 graph: &'a BuildGraph<'src>,
13 dead_nodes: Option<&'a HashSet<NodeId>>,
14 show_dead: bool,
15}
16
17impl<'a, 'src> GraphPrinter<'a, 'src> {
18 pub fn new(graph: &'a BuildGraph<'src>) -> Self {
19 Self {
20 graph,
21 dead_nodes: None,
22 show_dead: false,
23 }
24 }
25
26 pub fn with_dead_nodes(mut self, dead: &'a HashSet<NodeId>) -> Self {
27 self.dead_nodes = Some(dead);
28 self
29 }
30
31 pub fn show_dead(mut self, show: bool) -> Self {
32 self.show_dead = show;
33 self
34 }
35
36 pub fn dump(&self) -> String {
37 let mut out = String::new();
38 self.format(&mut out).expect("String write never fails");
39 out
40 }
41
42 fn node_width(&self) -> usize {
43 let max_id = self.graph.iter().map(|(id, _)| id).max().unwrap_or(0);
44 if max_id == 0 {
45 1
46 } else {
47 ((max_id as f64).log10().floor() as usize) + 1
48 }
49 }
50
51 fn format_node_id(&self, id: NodeId, width: usize) -> String {
52 format!("({:0width$})", id, width = width)
53 }
54
55 fn format(&self, w: &mut String) -> std::fmt::Result {
56 let width = self.node_width();
57
58 let ref_names: HashMap<u32, &str> = self
60 .graph
61 .iter()
62 .filter_map(|(_, node)| {
63 if let RefMarker::Enter { ref_id } = &node.ref_marker {
64 Some((*ref_id, node.ref_name.unwrap_or("?")))
65 } else {
66 None
67 }
68 })
69 .collect();
70
71 for (name, entry) in self.graph.definitions() {
72 writeln!(w, "{} = {}", name, self.format_node_id(entry, width))?;
73 }
74 if self.graph.definitions().next().is_some() {
75 writeln!(w)?;
76 }
77
78 for (id, node) in self.graph.iter() {
79 let is_dead = self.dead_nodes.map(|d| d.contains(&id)).unwrap_or(false);
80
81 if is_dead && !self.show_dead {
82 continue;
83 }
84
85 write!(w, "{}", self.format_node_id(id, width))?;
87
88 if is_dead {
90 writeln!(w, " → (⨯)")?;
91 continue;
92 }
93
94 write!(w, " —")?;
95
96 if !node.nav.is_stay() {
98 write!(w, "{}—", format_nav(&node.nav))?;
99 }
100
101 if let RefMarker::Enter { .. } = &node.ref_marker {
103 let name = node.ref_name.unwrap_or("?");
104 write!(w, "<{}>—", name)?;
105 }
106
107 self.format_matcher(w, &node.matcher)?;
109
110 if let RefMarker::Exit { ref_id } = &node.ref_marker {
112 let name = ref_names.get(ref_id).copied().unwrap_or("?");
113 write!(w, "—<{}>", name)?;
114 }
115
116 if !node.effects.is_empty() {
118 write!(w, "—[")?;
119 for (i, effect) in node.effects.iter().enumerate() {
120 if i > 0 {
121 write!(w, ", ")?;
122 }
123 write!(w, "{}", format_effect(effect))?;
124 }
125 write!(w, "]")?;
126 }
127
128 self.format_successors(w, &node.successors, width)?;
130
131 writeln!(w)?;
132 }
133
134 Ok(())
135 }
136
137 fn format_matcher(&self, w: &mut String, matcher: &BuildMatcher<'src>) -> std::fmt::Result {
138 match matcher {
139 BuildMatcher::Epsilon => write!(w, "𝜀"),
140 BuildMatcher::Node {
141 kind,
142 field,
143 negated_fields,
144 } => {
145 write!(w, "({})", kind)?;
146 if let Some(f) = field {
147 write!(w, "@{}", f)?;
148 }
149 for neg in negated_fields {
150 write!(w, "!{}", neg)?;
151 }
152 Ok(())
153 }
154 BuildMatcher::Anonymous { literal, field } => {
155 write!(w, "\"{}\"", literal)?;
156 if let Some(f) = field {
157 write!(w, "@{}", f)?;
158 }
159 Ok(())
160 }
161 BuildMatcher::Wildcard { field } => {
162 write!(w, "(🞵)")?;
163 if let Some(f) = field {
164 write!(w, "@{}", f)?;
165 }
166 Ok(())
167 }
168 }
169 }
170
171 fn format_successors(
172 &self,
173 w: &mut String,
174 successors: &[NodeId],
175 width: usize,
176 ) -> std::fmt::Result {
177 let live_succs: Vec<_> = successors
178 .iter()
179 .filter(|s| self.dead_nodes.map(|d| !d.contains(s)).unwrap_or(true))
180 .collect();
181
182 if live_succs.is_empty() {
183 write!(w, "→ (✓)")
184 } else {
185 write!(w, "→ ")?;
186 for (i, s) in live_succs.iter().enumerate() {
187 if i > 0 {
188 write!(w, ", ")?;
189 }
190 write!(w, "{}", self.format_node_id(**s, width))?;
191 }
192 Ok(())
193 }
194 }
195}
196
197fn format_nav(nav: &Nav) -> String {
198 match nav.kind {
199 NavKind::Stay => "{˟}".to_string(),
200 NavKind::Next => "{→}".to_string(),
201 NavKind::NextSkipTrivia => "{→·}".to_string(),
202 NavKind::NextExact => "{→!}".to_string(),
203 NavKind::Down => "{↘}".to_string(),
204 NavKind::DownSkipTrivia => "{↘.}".to_string(),
205 NavKind::DownExact => "{↘!}".to_string(),
206 NavKind::Up => format!("{{↗{}}}", to_superscript(nav.level)),
207 NavKind::UpSkipTrivia => format!("{{↗·{}}}", to_superscript(nav.level)),
208 NavKind::UpExact => format!("{{↗!{}}}", to_superscript(nav.level)),
209 }
210}
211
212fn to_superscript(n: u8) -> String {
213 const SUPERSCRIPTS: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
214 if n == 0 {
215 return "⁰".to_string();
216 }
217 let mut result = String::new();
218 let mut num = n;
219 while num > 0 {
220 result.insert(0, SUPERSCRIPTS[(num % 10) as usize]);
221 num /= 10;
222 }
223 result
224}
225
226fn format_effect(effect: &BuildEffect) -> String {
227 match effect {
228 BuildEffect::CaptureNode => "CaptureNode".to_string(),
229 BuildEffect::ClearCurrent => "ClearCurrent".to_string(),
230 BuildEffect::StartArray { .. } => "StartArray".to_string(),
231 BuildEffect::PushElement => "PushElement".to_string(),
232 BuildEffect::EndArray => "EndArray".to_string(),
233 BuildEffect::StartObject { .. } => "StartObject".to_string(),
234 BuildEffect::EndObject => "EndObject".to_string(),
235 BuildEffect::Field { name, .. } => format!("Field({})", name),
236 BuildEffect::StartVariant(v) => format!("StartVariant({})", v),
237 BuildEffect::EndVariant => "EndVariant".to_string(),
238 BuildEffect::ToString => "ToString".to_string(),
239 }
240}
241
242impl<'src> BuildGraph<'src> {
243 pub fn printer(&self) -> GraphPrinter<'_, 'src> {
244 GraphPrinter::new(self)
245 }
246
247 pub fn dump(&self) -> String {
248 self.printer().dump()
249 }
250
251 pub fn dump_with_dead(&self, dead_nodes: &HashSet<NodeId>) -> String {
252 self.printer()
253 .with_dead_nodes(dead_nodes)
254 .show_dead(true)
255 .dump()
256 }
257
258 pub fn dump_live(&self, dead_nodes: &HashSet<NodeId>) -> String {
259 self.printer().with_dead_nodes(dead_nodes).dump()
260 }
261}