hugr_core/hugr/views/
render.rs

1//! Helper methods to compute the node/edge/port style when rendering a HUGR
2//! into dot or mermaid format.
3
4use std::collections::HashMap;
5
6use portgraph::render::{EdgeStyle, NodeStyle, PortStyle, PresentationStyle};
7use portgraph::{LinkView, MultiPortGraph, NodeIndex, PortIndex, PortView};
8
9use crate::core::HugrNode;
10use crate::hugr::internal::HugrInternals;
11use crate::ops::{NamedOp, OpType};
12use crate::types::EdgeKind;
13use crate::{Hugr, HugrView, Node};
14
15/// Reduced configuration for rendering a HUGR graph.
16///
17/// Additional options are available in the [`MermaidFormatter`] struct.
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20#[deprecated(note = "Use `MermaidFormatter` instead")]
21pub struct RenderConfig<N = Node> {
22    /// Show the node index in the graph nodes.
23    pub node_indices: bool,
24    /// Show port offsets in the graph edges.
25    pub port_offsets_in_edges: bool,
26    /// Show type labels on edges.
27    pub type_labels_in_edges: bool,
28    /// A node to highlight as the graph entrypoint.
29    pub entrypoint: Option<N>,
30}
31
32/// Configuration for rendering a HUGR graph.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct MermaidFormatter<'h, H: HugrInternals + ?Sized = Hugr> {
35    /// The HUGR to render.
36    hugr: &'h H,
37    /// How to display the node indices.
38    node_labels: NodeLabel<H::Node>,
39    /// Show port offsets in the graph edges.
40    port_offsets_in_edges: bool,
41    /// Show type labels on edges.
42    type_labels_in_edges: bool,
43    /// A node to highlight as the graph entrypoint.
44    entrypoint: Option<H::Node>,
45}
46
47impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
48    /// Create a new [`MermaidFormatter`] from a [`RenderConfig`].
49    #[allow(deprecated)]
50    pub fn from_render_config(config: RenderConfig<H::Node>, hugr: &'h H) -> Self {
51        let node_labels = if config.node_indices {
52            NodeLabel::Numeric
53        } else {
54            NodeLabel::None
55        };
56        Self {
57            hugr,
58            node_labels,
59            port_offsets_in_edges: config.port_offsets_in_edges,
60            type_labels_in_edges: config.type_labels_in_edges,
61            entrypoint: config.entrypoint,
62        }
63    }
64
65    /// Create a new [`MermaidFormatter`] for the given [`Hugr`].
66    pub fn new(hugr: &'h H) -> Self {
67        Self {
68            hugr,
69            node_labels: NodeLabel::Numeric,
70            port_offsets_in_edges: true,
71            type_labels_in_edges: true,
72            entrypoint: None,
73        }
74    }
75
76    /// The entrypoint to highlight in the rendered graph.
77    pub fn entrypoint(&self) -> Option<H::Node> {
78        self.entrypoint
79    }
80
81    /// The rendering style of the node labels.
82    pub fn node_labels(&self) -> &NodeLabel<H::Node> {
83        &self.node_labels
84    }
85
86    /// Whether to show port offsets on edges.
87    pub fn port_offsets(&self) -> bool {
88        self.port_offsets_in_edges
89    }
90
91    /// Whether to show type labels on edges.
92    pub fn type_labels(&self) -> bool {
93        self.type_labels_in_edges
94    }
95
96    /// Set the node labels style.
97    pub fn with_node_labels(mut self, node_labels: NodeLabel<H::Node>) -> Self {
98        self.node_labels = node_labels;
99        self
100    }
101
102    /// Set whether to show port offsets in edges.
103    pub fn with_port_offsets(mut self, show: bool) -> Self {
104        self.port_offsets_in_edges = show;
105        self
106    }
107
108    /// Set whether to show type labels in edges.
109    pub fn with_type_labels(mut self, show: bool) -> Self {
110        self.type_labels_in_edges = show;
111        self
112    }
113
114    /// Set the entrypoint node to highlight.
115    pub fn with_entrypoint(mut self, entrypoint: impl Into<Option<H::Node>>) -> Self {
116        self.entrypoint = entrypoint.into();
117        self
118    }
119
120    /// Render the graph into a Mermaid string.
121    pub fn finish(self) -> String
122    where
123        H: HugrView,
124    {
125        self.hugr.mermaid_string_with_formatter(self)
126    }
127
128    pub(crate) fn with_hugr<NewH: HugrInternals<Node = H::Node>>(
129        self,
130        hugr: &NewH,
131    ) -> MermaidFormatter<'_, NewH> {
132        let MermaidFormatter {
133            hugr: _,
134            node_labels,
135            port_offsets_in_edges,
136            type_labels_in_edges,
137            entrypoint,
138        } = self;
139        MermaidFormatter {
140            hugr,
141            node_labels,
142            port_offsets_in_edges,
143            type_labels_in_edges,
144            entrypoint,
145        }
146    }
147}
148
149/// An error that occurs when trying to convert a `FullRenderConfig` into a
150/// `RenderConfig`.
151#[derive(Debug, thiserror::Error)]
152pub enum UnsupportedRenderConfig {
153    /// Custom node labels are not supported in the `RenderConfig` struct.
154    #[error("Custom node labels are not supported in the `RenderConfig` struct")]
155    CustomNodeLabels,
156}
157
158#[allow(deprecated)]
159impl<'h, H: HugrInternals + ?Sized> TryFrom<MermaidFormatter<'h, H>> for RenderConfig<H::Node> {
160    type Error = UnsupportedRenderConfig;
161
162    fn try_from(value: MermaidFormatter<'h, H>) -> Result<Self, Self::Error> {
163        if matches!(value.node_labels, NodeLabel::Custom(_)) {
164            return Err(UnsupportedRenderConfig::CustomNodeLabels);
165        }
166        let node_indices = matches!(value.node_labels, NodeLabel::Numeric);
167        Ok(Self {
168            node_indices,
169            port_offsets_in_edges: value.port_offsets_in_edges,
170            type_labels_in_edges: value.type_labels_in_edges,
171            entrypoint: value.entrypoint,
172        })
173    }
174}
175
176macro_rules! impl_mermaid_formatter_from {
177    ($t:ty, $($lifetime:tt)?) => {
178        impl<'h, $($lifetime,)? H: HugrView> From<MermaidFormatter<'h, $t>> for MermaidFormatter<'h, H> {
179            fn from(value: MermaidFormatter<'h, $t>) -> Self {
180                let MermaidFormatter {
181                    hugr,
182                    node_labels,
183                    port_offsets_in_edges,
184                    type_labels_in_edges,
185                    entrypoint,
186                } = value;
187                MermaidFormatter {
188                    hugr,
189                    node_labels,
190                    port_offsets_in_edges,
191                    type_labels_in_edges,
192                    entrypoint,
193                }
194            }
195        }
196    };
197}
198
199impl_mermaid_formatter_from!(&'hh H, 'hh);
200impl_mermaid_formatter_from!(&'hh mut H, 'hh);
201impl_mermaid_formatter_from!(std::rc::Rc<H>,);
202impl_mermaid_formatter_from!(std::sync::Arc<H>,);
203impl_mermaid_formatter_from!(Box<H>,);
204
205impl<'h, H: HugrView + ToOwned> From<MermaidFormatter<'h, std::borrow::Cow<'_, H>>>
206    for MermaidFormatter<'h, H>
207{
208    fn from(value: MermaidFormatter<'h, std::borrow::Cow<'_, H>>) -> Self {
209        let MermaidFormatter {
210            hugr,
211            node_labels,
212            port_offsets_in_edges,
213            type_labels_in_edges,
214            entrypoint,
215        } = value;
216        MermaidFormatter {
217            hugr,
218            node_labels,
219            port_offsets_in_edges,
220            type_labels_in_edges,
221            entrypoint,
222        }
223    }
224}
225
226/// How to display the node indices.
227#[derive(Default, Clone, Debug, PartialEq, Eq)]
228pub enum NodeLabel<N: HugrNode = Node> {
229    /// Do not display the node index.
230    None,
231    /// Display the node index as a number.
232    #[default]
233    Numeric,
234    /// Display the labels corresponding to the node indices.
235    Custom(HashMap<N, String>),
236}
237
238#[allow(deprecated)]
239impl<N> Default for RenderConfig<N> {
240    fn default() -> Self {
241        Self {
242            node_indices: true,
243            port_offsets_in_edges: true,
244            type_labels_in_edges: true,
245            entrypoint: None,
246        }
247    }
248}
249
250/// Formatter method to compute a node style.
251pub(in crate::hugr) fn node_style<'a>(
252    h: &'a Hugr,
253    formatter: MermaidFormatter<'a>,
254) -> Box<dyn FnMut(NodeIndex) -> NodeStyle + 'a> {
255    fn node_name(h: &Hugr, n: NodeIndex) -> String {
256        match h.get_optype(n.into()) {
257            OpType::FuncDecl(f) => format!("FuncDecl: \"{}\"", f.func_name()),
258            OpType::FuncDefn(f) => format!("FuncDefn: \"{}\"", f.func_name()),
259            op => op.name().to_string(),
260        }
261    }
262
263    let mut entrypoint_style = PresentationStyle::default();
264    entrypoint_style.stroke = Some("#832561".to_string());
265    entrypoint_style.stroke_width = Some("3px".to_string());
266    let entrypoint = formatter.entrypoint.map(Node::into_portgraph);
267
268    match formatter.node_labels {
269        NodeLabel::Numeric => Box::new(move |n| {
270            if Some(n) == entrypoint {
271                NodeStyle::boxed(format!(
272                    "({ni}) [**{name}**]",
273                    ni = n.index(),
274                    name = node_name(h, n)
275                ))
276                .with_attrs(entrypoint_style.clone())
277            } else {
278                NodeStyle::boxed(format!(
279                    "({ni}) {name}",
280                    ni = n.index(),
281                    name = node_name(h, n)
282                ))
283            }
284        }),
285        NodeLabel::None => Box::new(move |n| {
286            if Some(n) == entrypoint {
287                NodeStyle::boxed(format!("[**{name}**]", name = node_name(h, n)))
288                    .with_attrs(entrypoint_style.clone())
289            } else {
290                NodeStyle::boxed(node_name(h, n))
291            }
292        }),
293        NodeLabel::Custom(labels) => Box::new(move |n| {
294            if Some(n) == entrypoint {
295                NodeStyle::boxed(format!(
296                    "({label}) [**{name}**]",
297                    label = labels.get(&n.into()).unwrap_or(&n.index().to_string()),
298                    name = node_name(h, n)
299                ))
300                .with_attrs(entrypoint_style.clone())
301            } else {
302                NodeStyle::boxed(format!(
303                    "({label}) {name}",
304                    label = labels.get(&n.into()).unwrap_or(&n.index().to_string()),
305                    name = node_name(h, n)
306                ))
307            }
308        }),
309    }
310}
311
312/// Formatter method to compute a port style.
313pub(in crate::hugr) fn port_style(h: &Hugr) -> Box<dyn FnMut(PortIndex) -> PortStyle + '_> {
314    let graph = &h.graph;
315    Box::new(move |port| {
316        let node = graph.port_node(port).unwrap();
317        let optype = h.get_optype(node.into());
318        let offset = graph.port_offset(port).unwrap();
319        match optype.port_kind(offset).unwrap() {
320            EdgeKind::Function(pf) => PortStyle::new(html_escape::encode_text(&format!("{pf}"))),
321            EdgeKind::Const(ty) | EdgeKind::Value(ty) => {
322                PortStyle::new(html_escape::encode_text(&format!("{ty}")))
323            }
324            EdgeKind::StateOrder => {
325                if graph.port_links(port).count() > 0 {
326                    PortStyle::text("", false)
327                } else {
328                    PortStyle::Hidden
329                }
330            }
331            _ => PortStyle::text("", true),
332        }
333    })
334}
335
336/// Formatter method to compute an edge style.
337#[allow(clippy::type_complexity)]
338pub(in crate::hugr) fn edge_style<'a>(
339    h: &'a Hugr,
340    config: MermaidFormatter<'_>,
341) -> Box<
342    dyn FnMut(
343            <MultiPortGraph<u32, u32, u32> as LinkView>::LinkEndpoint,
344            <MultiPortGraph<u32, u32, u32> as LinkView>::LinkEndpoint,
345        ) -> EdgeStyle
346        + 'a,
347> {
348    let graph = &h.graph;
349    Box::new(move |src, tgt| {
350        let src_node = graph.port_node(src).unwrap();
351        let src_optype = h.get_optype(src_node.into());
352        let src_offset = graph.port_offset(src).unwrap();
353        let tgt_offset = graph.port_offset(tgt).unwrap();
354
355        let port_kind = src_optype.port_kind(src_offset).unwrap();
356
357        // StateOrder edges: Dotted line.
358        // Control flow edges: Dashed line.
359        // Static and Value edges: Solid line with label.
360        let style = match port_kind {
361            EdgeKind::StateOrder => EdgeStyle::Dotted,
362            EdgeKind::ControlFlow => EdgeStyle::Dashed,
363            EdgeKind::Const(_) | EdgeKind::Function(_) | EdgeKind::Value(_) => EdgeStyle::Solid,
364        };
365
366        // Compute the label for the edge, given the setting flags.
367        fn type_label(e: EdgeKind) -> Option<String> {
368            match e {
369                EdgeKind::Const(ty) | EdgeKind::Value(ty) => Some(format!("{ty}")),
370                EdgeKind::Function(pf) => Some(format!("{pf}")),
371                _ => None,
372            }
373        }
374        //
375        // Only static and value edges have types to display.
376        let label = match (
377            config.port_offsets_in_edges,
378            type_label(port_kind).filter(|_| config.type_labels_in_edges),
379        ) {
380            (true, Some(ty)) => {
381                format!("{}:{}\n{ty}", src_offset.index(), tgt_offset.index())
382            }
383            (true, _) => format!("{}:{}", src_offset.index(), tgt_offset.index()),
384            (false, Some(ty)) => ty.to_string(),
385            _ => return style,
386        };
387        style.with_label(label)
388    })
389}
390
391#[cfg(test)]
392mod tests {
393    use crate::{NodeIndex, builder::test::simple_dfg_hugr};
394
395    use super::*;
396
397    #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
398    #[test]
399    fn test_custom_node_labels() {
400        let h = simple_dfg_hugr();
401        let node_labels = h
402            .nodes()
403            .map(|n| (n, format!("node_{}", n.index())))
404            .collect();
405        let config = h
406            .mermaid_format()
407            .with_node_labels(NodeLabel::Custom(node_labels));
408        insta::assert_snapshot!(h.mermaid_string_with_formatter(config));
409    }
410
411    #[test]
412    fn convert_full_render_config_to_render_config() {
413        let h = simple_dfg_hugr();
414        let config: MermaidFormatter =
415            MermaidFormatter::new(&h).with_node_labels(NodeLabel::Custom(HashMap::new()));
416        #[allow(deprecated)]
417        {
418            assert!(RenderConfig::try_from(config).is_err());
419        }
420    }
421}