hydro_lang/viz/
graphviz.rs

1use std::borrow::Cow;
2use std::fmt::Write;
3
4use super::render::{HydroEdgeProp, HydroGraphWrite, HydroNodeType, IndentedGraphWriter};
5use crate::viz::render::VizNodeKey;
6
7/// Escapes a string for use in a DOT graph label.
8pub fn escape_dot(string: &str, newline: &str) -> String {
9    string.replace('"', "\\\"").replace('\n', newline)
10}
11
12/// DOT/Graphviz graph writer for Hydro IR.
13pub struct HydroDot<W> {
14    base: IndentedGraphWriter<W>,
15}
16
17impl<W> HydroDot<W> {
18    pub fn new(write: W) -> Self {
19        Self {
20            base: IndentedGraphWriter::new(write),
21        }
22    }
23
24    pub fn new_with_config(write: W, config: &super::render::HydroWriteConfig) -> Self {
25        Self {
26            base: IndentedGraphWriter::new_with_config(write, config),
27        }
28    }
29}
30
31impl<W> HydroGraphWrite for HydroDot<W>
32where
33    W: Write,
34{
35    type Err = super::render::GraphWriteError;
36
37    fn write_prologue(&mut self) -> Result<(), Self::Err> {
38        writeln!(
39            self.base.write,
40            "{b:i$}digraph HydroIR {{",
41            b = "",
42            i = self.base.indent
43        )?;
44        self.base.indent += 4;
45
46        // Use dot layout for better edge routing between subgraphs
47        writeln!(
48            self.base.write,
49            "{b:i$}layout=dot;",
50            b = "",
51            i = self.base.indent
52        )?;
53        writeln!(
54            self.base.write,
55            "{b:i$}compound=true;",
56            b = "",
57            i = self.base.indent
58        )?;
59        writeln!(
60            self.base.write,
61            "{b:i$}concentrate=true;",
62            b = "",
63            i = self.base.indent
64        )?;
65
66        const FONTS: &str = "\"Monaco,Menlo,Consolas,&quot;Droid Sans Mono&quot;,Inconsolata,&quot;Courier New&quot;,monospace\"";
67        writeln!(
68            self.base.write,
69            "{b:i$}node [fontname={}, style=filled];",
70            FONTS,
71            b = "",
72            i = self.base.indent
73        )?;
74        writeln!(
75            self.base.write,
76            "{b:i$}edge [fontname={}];",
77            FONTS,
78            b = "",
79            i = self.base.indent
80        )?;
81        Ok(())
82    }
83
84    fn write_node_definition(
85        &mut self,
86        node_id: VizNodeKey,
87        node_label: &super::render::NodeLabel,
88        node_type: HydroNodeType,
89        _location_id: Option<usize>,
90        _location_type: Option<&str>,
91        _backtrace: Option<&crate::compile::ir::backtrace::Backtrace>,
92    ) -> Result<(), Self::Err> {
93        // Create the full label string using DebugExpr::Display for expressions
94        let full_label = match node_label {
95            super::render::NodeLabel::Static(s) => s.clone(),
96            super::render::NodeLabel::WithExprs { op_name, exprs } => {
97                if exprs.is_empty() {
98                    format!("{}()", op_name)
99                } else {
100                    // This is where DebugExpr::Display gets called with q! macro cleanup
101                    let expr_strs: Vec<String> = exprs.iter().map(|e| e.to_string()).collect();
102                    format!("{}({})", op_name, expr_strs.join(", "))
103                }
104            }
105        };
106
107        // Determine what label to display based on config
108        let display_label = if self.base.config.use_short_labels {
109            super::render::extract_short_label(&full_label)
110        } else {
111            full_label
112        };
113
114        let escaped_label = escape_dot(&display_label, "\\l");
115        let label = format!("n{}", node_id);
116
117        let (shape_str, color_str) = match node_type {
118            // ColorBrewer Set3 palette colors (matching Mermaid and Hydroscope)
119            HydroNodeType::Source => ("ellipse", "\"#8dd3c7\""), // Light teal
120            HydroNodeType::Transform => ("box", "\"#ffffb3\""),  // Light yellow
121            HydroNodeType::Join => ("diamond", "\"#bebada\""),   // Light purple
122            HydroNodeType::Aggregation => ("house", "\"#fb8072\""), // Light red/salmon
123            HydroNodeType::Network => ("doubleoctagon", "\"#80b1d3\""), // Light blue
124            HydroNodeType::Sink => ("invhouse", "\"#fdb462\""),  // Light orange
125            HydroNodeType::Tee => ("terminator", "\"#b3de69\""), // Light green
126            HydroNodeType::NonDeterministic => ("hexagon", "\"#fccde5\""), // Light pink/magenta
127        };
128
129        write!(
130            self.base.write,
131            "{b:i$}{label} [label=\"({node_id}) {escaped_label}{}\"",
132            if escaped_label.contains("\\l") {
133                "\\l"
134            } else {
135                ""
136            },
137            b = "",
138            i = self.base.indent,
139        )?;
140        write!(
141            self.base.write,
142            ", shape={shape_str}, fillcolor={color_str}"
143        )?;
144        writeln!(self.base.write, "]")?;
145        Ok(())
146    }
147
148    fn write_edge(
149        &mut self,
150        src_id: VizNodeKey,
151        dst_id: VizNodeKey,
152        edge_properties: &std::collections::HashSet<HydroEdgeProp>,
153        label: Option<&str>,
154    ) -> Result<(), Self::Err> {
155        let mut properties = Vec::<Cow<'static, str>>::new();
156
157        if let Some(label) = label {
158            properties.push(format!("label=\"{}\"", escape_dot(label, "\\n")).into());
159        }
160
161        let style = super::render::get_unified_edge_style(edge_properties, None, None);
162
163        properties.push(format!("color=\"{}\"", style.color).into());
164
165        if style.line_width > 1 {
166            properties.push("style=\"bold\"".into());
167        }
168
169        match style.line_pattern {
170            super::render::LinePattern::Dotted => {
171                properties.push("style=\"dotted\"".into());
172            }
173            super::render::LinePattern::Dashed => {
174                properties.push("style=\"dashed\"".into());
175            }
176            _ => {}
177        }
178
179        write!(
180            self.base.write,
181            "{b:i$}n{} -> n{}",
182            src_id,
183            dst_id,
184            b = "",
185            i = self.base.indent,
186        )?;
187
188        if !properties.is_empty() {
189            write!(self.base.write, " [")?;
190            for prop in itertools::Itertools::intersperse(properties.into_iter(), ", ".into()) {
191                write!(self.base.write, "{}", prop)?;
192            }
193            write!(self.base.write, "]")?;
194        }
195        writeln!(self.base.write)?;
196        Ok(())
197    }
198
199    fn write_location_start(
200        &mut self,
201        location_id: usize,
202        location_type: &str,
203    ) -> Result<(), Self::Err> {
204        writeln!(
205            self.base.write,
206            "{b:i$}subgraph cluster_loc_{id} {{",
207            id = location_id,
208            b = "",
209            i = self.base.indent,
210        )?;
211        self.base.indent += 4;
212
213        // Use dot layout for interior nodes within containers
214        writeln!(
215            self.base.write,
216            "{b:i$}layout=dot;",
217            b = "",
218            i = self.base.indent
219        )?;
220        writeln!(
221            self.base.write,
222            "{b:i$}label = \"{location_type} {id}\"",
223            id = location_id,
224            b = "",
225            i = self.base.indent
226        )?;
227        writeln!(
228            self.base.write,
229            "{b:i$}style=filled",
230            b = "",
231            i = self.base.indent
232        )?;
233        writeln!(
234            self.base.write,
235            "{b:i$}fillcolor=\"#fafafa\"",
236            b = "",
237            i = self.base.indent
238        )?;
239        writeln!(
240            self.base.write,
241            "{b:i$}color=\"#e0e0e0\"",
242            b = "",
243            i = self.base.indent
244        )?;
245        Ok(())
246    }
247
248    fn write_node(&mut self, node_id: VizNodeKey) -> Result<(), Self::Err> {
249        writeln!(
250            self.base.write,
251            "{b:i$}n{node_id}",
252            b = "",
253            i = self.base.indent
254        )
255    }
256
257    fn write_location_end(&mut self) -> Result<(), Self::Err> {
258        self.base.indent -= 4;
259        writeln!(self.base.write, "{b:i$}}}", b = "", i = self.base.indent)
260    }
261
262    fn write_epilogue(&mut self) -> Result<(), Self::Err> {
263        self.base.indent -= 4;
264        writeln!(self.base.write, "{b:i$}}}", b = "", i = self.base.indent)
265    }
266}
267
268/// Open DOT/Graphviz visualization in browser for a BuiltFlow
269#[cfg(feature = "build")]
270pub fn open_browser(
271    built_flow: &crate::compile::built::BuiltFlow,
272) -> Result<(), Box<dyn std::error::Error>> {
273    let config = super::render::HydroWriteConfig {
274        show_metadata: false,
275        show_location_groups: true,
276        use_short_labels: true, // Default to short labels
277        process_id_name: built_flow.process_id_name().clone(),
278        cluster_id_name: built_flow.cluster_id_name().clone(),
279        external_id_name: built_flow.external_id_name().clone(),
280    };
281
282    // Use the existing debug function
283    crate::viz::debug::open_dot(built_flow.ir(), Some(config))?;
284
285    Ok(())
286}