Skip to main content

hydro_lang/viz/
graphviz.rs

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