plotnik_lib/query/
graph_dump.rs

1//! Dump helpers for graph inspection and testing.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt::Write;
5
6use crate::ir::{Nav, NavKind};
7
8use super::graph::{BuildEffect, BuildGraph, BuildMatcher, NodeId, RefMarker};
9
10/// Printer for `BuildGraph` with configurable output options.
11pub 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        // Build ref_id → name lookup from Enter nodes
59        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            // Source node
86            write!(w, "{}", self.format_node_id(id, width))?;
87
88            // Dead node short-circuit
89            if is_dead {
90                writeln!(w, " → (⨯)")?;
91                continue;
92            }
93
94            write!(w, " —")?;
95
96            // Navigation (omit for Stay)
97            if !node.nav.is_stay() {
98                write!(w, "{}—", format_nav(&node.nav))?;
99            }
100
101            // Enter ref marker (before matcher)
102            if let RefMarker::Enter { .. } = &node.ref_marker {
103                let name = node.ref_name.unwrap_or("?");
104                write!(w, "<{}>—", name)?;
105            }
106
107            // Matcher
108            self.format_matcher(w, &node.matcher)?;
109
110            // Exit ref marker (after matcher)
111            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            // Effects
117            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            // Successors
129            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}