1use std::borrow::Cow;
2use std::fmt::Write;
3
4use super::render::{HydroEdgeProp, HydroGraphWrite, HydroNodeType, IndentedGraphWriter};
5use crate::viz::render::VizNodeKey;
6
7pub fn escape_dot(string: &str, newline: &str) -> String {
9 string.replace('"', "\\\"").replace('\n', newline)
10}
11
12pub 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 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,"Droid Sans Mono",Inconsolata,"Courier New",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 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 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 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 HydroNodeType::Source => ("ellipse", "\"#8dd3c7\""), HydroNodeType::Transform => ("box", "\"#ffffb3\""), HydroNodeType::Join => ("diamond", "\"#bebada\""), HydroNodeType::Aggregation => ("house", "\"#fb8072\""), HydroNodeType::Network => ("doubleoctagon", "\"#80b1d3\""), HydroNodeType::Sink => ("invhouse", "\"#fdb462\""), HydroNodeType::Tee => ("terminator", "\"#b3de69\""), HydroNodeType::NonDeterministic => ("hexagon", "\"#fccde5\""), };
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 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#[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, 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 crate::viz::debug::open_dot(built_flow.ir(), Some(config))?;
284
285 Ok(())
286}