1use std::{collections::HashSet, fmt};
2
3use crate::{ExprGraph, ExprId, ExprMetadata, ExprNode, expression::node_children};
4
5#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
7pub enum RepeatedSubtrees {
8 #[default]
10 Expand,
11 Reference,
13}
14
15#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
17pub enum ExprNodeKind {
18 RealConst,
20 ComplexConst,
22 ScalarParam,
24 EventScalar,
26 EventP4Component,
28 Unary,
30 Binary,
32 NaryAdd,
34 NaryMul,
36 Complex,
38 Vector,
40 Matrix,
42 Component,
44 MatrixElement,
46 MatMul,
48 MatVec,
50 Dot,
52 Solve,
54}
55
56impl ExprNodeKind {
57 pub fn of(node: &ExprNode) -> Self {
59 match node {
60 ExprNode::RealConst(_) => Self::RealConst,
61 ExprNode::ComplexConst(_) => Self::ComplexConst,
62 ExprNode::ScalarParam(_) => Self::ScalarParam,
63 ExprNode::EventScalar(_) => Self::EventScalar,
64 ExprNode::EventP4Component { .. } => Self::EventP4Component,
65 ExprNode::Unary { .. } => Self::Unary,
66 ExprNode::Binary { .. } => Self::Binary,
67 ExprNode::NaryAdd { .. } => Self::NaryAdd,
68 ExprNode::NaryMul { .. } => Self::NaryMul,
69 ExprNode::Complex { .. } => Self::Complex,
70 ExprNode::Vector { .. } => Self::Vector,
71 ExprNode::Matrix { .. } => Self::Matrix,
72 ExprNode::Component { .. } => Self::Component,
73 ExprNode::MatrixElement { .. } => Self::MatrixElement,
74 ExprNode::MatMul { .. } => Self::MatMul,
75 ExprNode::MatVec { .. } => Self::MatVec,
76 ExprNode::Dot { .. } => Self::Dot,
77 ExprNode::Solve { .. } => Self::Solve,
78 }
79 }
80}
81
82#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub struct DisplayColor {
85 red: u8,
86 green: u8,
87 blue: u8,
88}
89
90impl DisplayColor {
91 pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
93 Self { red, green, blue }
94 }
95
96 fn dot(self) -> String {
97 format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
98 }
99
100 fn ansi_foreground(self) -> String {
101 format!("\x1b[38;2;{};{};{}m", self.red, self.green, self.blue)
102 }
103}
104
105#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
107pub struct NodeStyle {
108 pub foreground: Option<DisplayColor>,
110 pub fill: Option<DisplayColor>,
112 pub border: Option<DisplayColor>,
114}
115
116impl NodeStyle {
117 pub const fn new() -> Self {
119 Self {
120 foreground: None,
121 fill: None,
122 border: None,
123 }
124 }
125
126 pub const fn with_foreground(mut self, color: DisplayColor) -> Self {
128 self.foreground = Some(color);
129 self
130 }
131
132 pub const fn with_fill(mut self, color: DisplayColor) -> Self {
134 self.fill = Some(color);
135 self
136 }
137
138 pub const fn with_border(mut self, color: DisplayColor) -> Self {
140 self.border = Some(color);
141 self
142 }
143
144 fn overlay(&mut self, other: Self) {
145 if other.foreground.is_some() {
146 self.foreground = other.foreground;
147 }
148 if other.fill.is_some() {
149 self.fill = other.fill;
150 }
151 if other.border.is_some() {
152 self.border = other.border;
153 }
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
159pub enum NodeSelector {
160 Any,
162 Kind(ExprNodeKind),
164 Name(String),
166 Tag(String),
168}
169
170impl NodeSelector {
171 fn matches(&self, node: &ExprNode, metadata: Option<&ExprMetadata>) -> bool {
172 match self {
173 Self::Any => true,
174 Self::Kind(kind) => *kind == ExprNodeKind::of(node),
175 Self::Name(name) => {
176 metadata.and_then(ExprMetadata::name) == Some(name.as_str())
177 || match node {
178 ExprNode::ScalarParam(parameter) => parameter.name() == name,
179 ExprNode::EventScalar(node_name)
180 | ExprNode::EventP4Component {
181 name: node_name, ..
182 } => node_name.as_ref() == name,
183 _ => false,
184 }
185 }
186 Self::Tag(tag) => metadata.is_some_and(|metadata| metadata.has_tag(tag)),
187 }
188 }
189}
190
191#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct NodeStyleRule {
194 pub selector: NodeSelector,
196 pub style: NodeStyle,
198}
199
200impl NodeStyleRule {
201 pub fn new(selector: NodeSelector, style: NodeStyle) -> Self {
203 Self { selector, style }
204 }
205}
206
207#[derive(Copy, Clone, Debug, PartialEq, Eq)]
209pub enum ColorPreset {
210 Light,
212 Dark,
214}
215
216#[derive(Clone, Debug, Default)]
217struct DisplayOptions {
218 repeated_subtrees: RepeatedSubtrees,
219 rules: Vec<NodeStyleRule>,
220}
221
222impl DisplayOptions {
223 fn with_preset(&mut self, preset: ColorPreset) {
224 let (constant, parameter, event, operation, linear_algebra) = match preset {
225 ColorPreset::Light => (
226 DisplayColor::rgb(88, 96, 105),
227 DisplayColor::rgb(0, 92, 197),
228 DisplayColor::rgb(3, 102, 214),
229 DisplayColor::rgb(130, 80, 223),
230 DisplayColor::rgb(207, 34, 46),
231 ),
232 ColorPreset::Dark => (
233 DisplayColor::rgb(139, 148, 158),
234 DisplayColor::rgb(88, 166, 255),
235 DisplayColor::rgb(121, 192, 255),
236 DisplayColor::rgb(210, 168, 255),
237 DisplayColor::rgb(255, 123, 114),
238 ),
239 };
240 let style = |color| NodeStyle::new().with_foreground(color).with_border(color);
241 for kind in [ExprNodeKind::RealConst, ExprNodeKind::ComplexConst] {
242 self.rules.push(NodeStyleRule::new(
243 NodeSelector::Kind(kind),
244 style(constant),
245 ));
246 }
247 self.rules.push(NodeStyleRule::new(
248 NodeSelector::Kind(ExprNodeKind::ScalarParam),
249 style(parameter),
250 ));
251 for kind in [ExprNodeKind::EventScalar, ExprNodeKind::EventP4Component] {
252 self.rules
253 .push(NodeStyleRule::new(NodeSelector::Kind(kind), style(event)));
254 }
255 for kind in [
256 ExprNodeKind::Unary,
257 ExprNodeKind::Binary,
258 ExprNodeKind::NaryAdd,
259 ExprNodeKind::NaryMul,
260 ExprNodeKind::Complex,
261 ExprNodeKind::Vector,
262 ExprNodeKind::Matrix,
263 ExprNodeKind::Component,
264 ExprNodeKind::MatrixElement,
265 ] {
266 self.rules.push(NodeStyleRule::new(
267 NodeSelector::Kind(kind),
268 style(operation),
269 ));
270 }
271 for kind in [
272 ExprNodeKind::MatMul,
273 ExprNodeKind::MatVec,
274 ExprNodeKind::Dot,
275 ExprNodeKind::Solve,
276 ] {
277 self.rules.push(NodeStyleRule::new(
278 NodeSelector::Kind(kind),
279 style(linear_algebra),
280 ));
281 }
282 }
283
284 fn resolve(&self, graph: &ExprGraph, id: ExprId, node: &ExprNode) -> NodeStyle {
285 let mut style = NodeStyle::default();
286 let metadata = graph.metadata(id);
287 for rule in &self.rules {
288 if rule.selector.matches(node, metadata) {
289 style.overlay(rule.style);
290 }
291 }
292 style
293 }
294}
295
296macro_rules! display_builder_methods {
297 () => {
298 pub fn repeated_subtrees(mut self, repeated_subtrees: RepeatedSubtrees) -> Self {
300 self.options.repeated_subtrees = repeated_subtrees;
301 self
302 }
303
304 pub fn expand_repeated(self, expand: bool) -> Self {
306 self.repeated_subtrees(if expand {
307 RepeatedSubtrees::Expand
308 } else {
309 RepeatedSubtrees::Reference
310 })
311 }
312
313 pub fn with_preset(mut self, preset: ColorPreset) -> Self {
315 self.options.with_preset(preset);
316 self
317 }
318
319 pub fn with_style_rule(mut self, rule: NodeStyleRule) -> Self {
323 self.options.rules.push(rule);
324 self
325 }
326 };
327}
328
329pub struct ExprGraphTreeDisplay<'a> {
331 graph: &'a ExprGraph,
332 options: DisplayOptions,
333}
334
335impl<'a> ExprGraphTreeDisplay<'a> {
336 pub(crate) fn new(graph: &'a ExprGraph) -> Self {
337 Self {
338 graph,
339 options: DisplayOptions::default(),
340 }
341 }
342
343 display_builder_methods!();
344
345 fn fmt_node(
346 &self,
347 f: &mut fmt::Formatter<'_>,
348 id: ExprId,
349 prefix: &str,
350 edge: Option<(&str, bool)>,
351 visited: &mut HashSet<ExprId>,
352 ) -> fmt::Result {
353 let Some(node) = self.graph.node(id) else {
354 return write_tree_line(f, prefix, edge, &format!("#{} <missing node>", id.index()));
355 };
356 let repeated = !visited.insert(id);
357 let mut line = if repeated && self.options.repeated_subtrees == RepeatedSubtrees::Reference
358 {
359 format!("#{0} <reference to #{0}>", id.index())
360 } else {
361 self.graph.node_label(id, node)
362 };
363 if let Some(color) = self.options.resolve(self.graph, id, node).foreground {
364 line = format!("{}{line}\x1b[0m", color.ansi_foreground());
365 }
366 write_tree_line(f, prefix, edge, &line)?;
367 if repeated && self.options.repeated_subtrees == RepeatedSubtrees::Reference {
368 return Ok(());
369 }
370
371 let children = node_children(node);
372 let child_prefix = match edge {
373 Some((_, true)) => format!("{prefix} "),
374 Some((_, false)) => format!("{prefix}┃ "),
375 None => prefix.to_owned(),
376 };
377 for (index, (label, child)) in children.iter().enumerate() {
378 self.fmt_node(
379 f,
380 *child,
381 &child_prefix,
382 Some((label, index + 1 == children.len())),
383 visited,
384 )?;
385 }
386 Ok(())
387 }
388}
389
390impl fmt::Display for ExprGraphTreeDisplay<'_> {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 writeln!(f, "ExprGraph(root=#{})", self.graph.root().index())?;
393 self.fmt_node(f, self.graph.root(), "", None, &mut HashSet::new())
394 }
395}
396
397pub struct ExprGraphDotDisplay<'a> {
399 graph: &'a ExprGraph,
400 options: DisplayOptions,
401}
402
403impl<'a> ExprGraphDotDisplay<'a> {
404 pub(crate) fn new(graph: &'a ExprGraph) -> Self {
405 Self {
406 graph,
407 options: DisplayOptions::default(),
408 }
409 }
410
411 display_builder_methods!();
412
413 #[cfg(feature = "svg")]
414 pub fn render_svg(&self) -> Result<String, GraphRenderError> {
421 use layout::{backends::svg::SVGWriter, gv};
422
423 let dot = self.to_string();
424 let mut parser = gv::DotParser::new(&dot);
425 let graph = parser.process().map_err(GraphRenderError::Dot)?;
426 let mut builder = gv::GraphBuilder::new();
427 builder.visit_graph(&graph);
428 let mut graph = builder.get();
429 let mut svg = SVGWriter::new();
430 graph.do_it(false, false, false, &mut svg);
431 Ok(svg.finalize())
432 }
433
434 fn node_attributes(&self, id: ExprId, node: &ExprNode) -> String {
435 let mut attributes = vec![format!(
436 "label=\"{}\"",
437 escape_dot(&self.graph.node_label(id, node))
438 )];
439 let style = self.options.resolve(self.graph, id, node);
440 if let Some(color) = style.foreground {
441 attributes.push(format!("fontcolor=\"{}\"", color.dot()));
442 }
443 if let Some(color) = style.border {
444 attributes.push(format!("color=\"{}\"", color.dot()));
445 }
446 if let Some(color) = style.fill {
447 attributes.push(format!("fillcolor=\"{}\"", color.dot()));
448 attributes.push("style=filled".to_owned());
449 }
450 attributes.join(", ")
451 }
452
453 fn write_expanded(
454 &self,
455 f: &mut fmt::Formatter<'_>,
456 id: ExprId,
457 occurrence: &mut usize,
458 ) -> fmt::Result {
459 let current = *occurrence;
460 *occurrence += 1;
461 let Some(node) = self.graph.node(id) else {
462 return Ok(());
463 };
464 writeln!(f, " n{current} [{}];", self.node_attributes(id, node))?;
465 for (label, child) in node_children(node) {
466 let child_occurrence = *occurrence;
467 self.write_expanded(f, child, occurrence)?;
468 writeln!(
469 f,
470 " n{current} -> n{child_occurrence} [label=\"{}\"];",
471 escape_dot(&label)
472 )?;
473 }
474 Ok(())
475 }
476
477 fn write_shared(
478 &self,
479 f: &mut fmt::Formatter<'_>,
480 id: ExprId,
481 visited: &mut HashSet<ExprId>,
482 ) -> fmt::Result {
483 if !visited.insert(id) {
484 return Ok(());
485 }
486 let Some(node) = self.graph.node(id) else {
487 return Ok(());
488 };
489 writeln!(f, " n{} [{}];", id.index(), self.node_attributes(id, node))?;
490 for (label, child) in node_children(node) {
491 self.write_shared(f, child, visited)?;
492 writeln!(
493 f,
494 " n{} -> n{} [label=\"{}\"];",
495 id.index(),
496 child.index(),
497 escape_dot(&label)
498 )?;
499 }
500 Ok(())
501 }
502}
503
504#[cfg(feature = "svg")]
505#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
507pub enum GraphRenderError {
508 #[error("failed to parse generated DOT: {0}")]
510 Dot(String),
511}
512
513impl fmt::Display for ExprGraphDotDisplay<'_> {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 writeln!(f, "digraph ExprGraph {{")?;
516 match self.options.repeated_subtrees {
517 RepeatedSubtrees::Expand => self.write_expanded(f, self.graph.root(), &mut 0)?,
518 RepeatedSubtrees::Reference => {
519 self.write_shared(f, self.graph.root(), &mut HashSet::new())?
520 }
521 }
522 writeln!(f, "}}")
523 }
524}
525
526fn write_tree_line(
527 f: &mut fmt::Formatter<'_>,
528 prefix: &str,
529 edge: Option<(&str, bool)>,
530 text: &str,
531) -> fmt::Result {
532 if let Some((label, is_last)) = edge {
533 let connector = if is_last { "┗" } else { "┣" };
534 writeln!(f, "{prefix}{connector} {label}: {text}")
535 } else {
536 writeln!(f, "{text}")
537 }
538}
539
540fn escape_dot(value: &str) -> String {
541 value
542 .replace('\\', "\\\\")
543 .replace('"', "\\\"")
544 .replace('\n', "\\n")
545 .replace('\r', "\\r")
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use crate::event_scalar;
552
553 fn shared_graph() -> ExprGraph {
554 let shared = event_scalar("x").named("shared").tagged("data");
555 ((shared.clone() + 1.0) * (shared + 2.0)).to_graph()
556 }
557
558 #[test]
559 fn tree_and_dot_expand_repeated_subtrees_without_color_by_default() {
560 let graph = shared_graph();
561 let shared_id = graph
562 .nodes()
563 .iter()
564 .position(|node| matches!(node, ExprNode::EventScalar(name) if name.as_ref() == "x"))
565 .unwrap();
566 let needle = format!("#{shared_id} EventScalar(x)");
567 let tree = graph.display_tree().to_string();
568 let dot = graph.display_dot().to_string();
569
570 assert_eq!(tree.matches(&needle).count(), 2);
571 assert_eq!(dot.matches(&needle).count(), 2);
572 assert!(!tree.contains("\x1b["));
573 assert!(!dot.contains("fontcolor="));
574 assert!(!dot.contains("fillcolor="));
575 }
576
577 #[test]
578 fn reference_mode_suppresses_repeated_tree_expansion_and_emits_a_shared_dag() {
579 let graph = shared_graph();
580 let tree = graph
581 .display_tree()
582 .repeated_subtrees(RepeatedSubtrees::Reference)
583 .to_string();
584 let dot = graph.display_dot().expand_repeated(false).to_string();
585
586 assert_eq!(tree.matches("EventScalar(x)").count(), 1);
587 assert_eq!(tree.matches("<reference to #").count(), 1);
588 assert_eq!(dot.matches("EventScalar(x)").count(), 1);
589 assert_eq!(dot.matches(" -> ").count(), 6);
590 }
591
592 #[test]
593 fn later_style_rules_override_matching_preset_fields() {
594 let graph = shared_graph();
595 let override_color = DisplayColor::rgb(1, 2, 3);
596 let rule = NodeStyleRule::new(
597 NodeSelector::Tag("data".to_owned()),
598 NodeStyle::new().with_foreground(override_color),
599 );
600 let tree = graph
601 .display_tree()
602 .with_preset(ColorPreset::Light)
603 .with_style_rule(rule.clone())
604 .to_string();
605 let dot = graph
606 .display_dot()
607 .with_preset(ColorPreset::Light)
608 .with_style_rule(rule)
609 .to_string();
610
611 assert!(tree.contains("\x1b[38;2;1;2;3m"));
612 assert!(dot.contains("fontcolor=\"#010203\""));
613 }
614
615 #[test]
616 fn dot_escapes_metadata_and_event_labels() {
617 let graph = event_scalar("x\\\"y").named("quoted\"name").to_graph();
618 let dot = graph.display_dot().to_string();
619
620 assert!(dot.contains("x\\\\\\\"y"));
621 assert!(dot.contains("quoted\\\"name"));
622 }
623
624 #[cfg(feature = "svg")]
625 #[test]
626 fn dot_display_renders_svg_in_process() {
627 let svg = shared_graph()
628 .display_dot()
629 .with_preset(ColorPreset::Light)
630 .render_svg()
631 .unwrap();
632
633 assert!(svg.contains("<svg"));
634 assert!(svg.contains("</svg>"));
635 }
636}