From 4e9fa2a0cebd3f57245a88bcbe975eb2f92b9230 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Thu, 29 May 2025 15:43:39 +0200 Subject: [PATCH 1/7] feat: Add custom node labels in mermaid_string --- hugr-core/src/hugr/persistent/trait_impls.rs | 50 +++---- hugr-core/src/hugr/views.rs | 39 +++++- hugr-core/src/hugr/views/impls.rs | 1 + hugr-core/src/hugr/views/render.rs | 129 ++++++++++++++++--- 4 files changed, 167 insertions(+), 52 deletions(-) diff --git a/hugr-core/src/hugr/persistent/trait_impls.rs b/hugr-core/src/hugr/persistent/trait_impls.rs index c11949db9a..202391175c 100644 --- a/hugr-core/src/hugr/persistent/trait_impls.rs +++ b/hugr-core/src/hugr/persistent/trait_impls.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use itertools::{Either, Itertools}; -use portgraph::render::{DotFormat, MermaidFormat}; +use portgraph::render::MermaidFormat; use crate::{ Direction, Hugr, HugrView, Node, Port, @@ -10,7 +10,7 @@ use crate::{ internal::HugrInternals, views::{ ExtractionResult, - render::{self, RenderConfig}, + render::{self, FullRenderConfig, NodeLabel, RenderConfig}, }, }, }; @@ -255,22 +255,31 @@ impl HugrView for PersistentHugr { // Extract a concrete HUGR for displaying let (hugr, node_map) = self.apply_all(); + // Render the extracted HUGR but map the node indices back to the + // original patch node IDs + let entrypoint = config.entrypoint.map(|n| node_map[&n]); + let node_labels = if config.node_indices { + let node_labels_map: HashMap<_, _> = node_map + .into_iter() + .map(|(k, v)| (v, format!("{:?}", k))) + .collect(); + NodeLabel::Custom(node_labels_map) + } else { + NodeLabel::None + }; + // Map config accordingly - let config = RenderConfig { - entrypoint: config.entrypoint.map(|n| node_map[&n]), - node_indices: config.node_indices, + let config = FullRenderConfig { + entrypoint, + node_labels, port_offsets_in_edges: config.port_offsets_in_edges, type_labels_in_edges: config.type_labels_in_edges, }; - // Render the extracted HUGR but map the node indices back to the - // original patch node IDs - let inv_node_map: HashMap<_, _> = node_map.into_iter().map(|(k, v)| (v, k)).collect(); - let fmt_node_index = |n: portgraph::NodeIndex| format!("{:?}", inv_node_map[&n.into()]); hugr.graph .mermaid_format() .with_hierarchy(&hugr.hierarchy) - .with_node_style(render::node_style(&hugr, config, fmt_node_index)) + .with_node_style(render::node_style(&hugr, config.clone())) .with_edge_style(render::edge_style(&hugr, config)) .finish() } @@ -279,26 +288,7 @@ impl HugrView for PersistentHugr { where Self: Sized, { - // Extract a concrete HUGR for displaying - let (hugr, node_map) = self.apply_all(); - - // Map config accordingly - let config = RenderConfig { - entrypoint: Some(node_map[&self.entrypoint()]), - ..RenderConfig::default() - }; - - // Render the extracted HUGR but map the node indices back to the - // original patch node IDs - let inv_node_map: HashMap<_, _> = node_map.into_iter().map(|(k, v)| (v, k)).collect(); - let fmt_node_index = |n: portgraph::NodeIndex| format!("{:?}", inv_node_map[&n.into()]); - hugr.graph - .dot_format() - .with_hierarchy(&hugr.hierarchy) - .with_node_style(render::node_style(&hugr, config, fmt_node_index)) - .with_port_style(render::port_style(&hugr, config)) - .with_edge_style(render::edge_style(&hugr, config)) - .finish() + unimplemented!("use mermaid_string instead") } fn extensions(&self) -> &crate::extension::ExtensionRegistry { diff --git a/hugr-core/src/hugr/views.rs b/hugr-core/src/hugr/views.rs index e86b024eb3..1c68452341 100644 --- a/hugr-core/src/hugr/views.rs +++ b/hugr-core/src/hugr/views.rs @@ -14,7 +14,7 @@ use std::borrow::Cow; use std::collections::HashMap; pub use self::petgraph::PetgraphWrapper; -use self::render::RenderConfig; +use self::render::{FullRenderConfig, RenderConfig}; pub use rerooted::Rerooted; pub use root_checked::{InvalidSignature, RootCheckable, RootChecked, check_tag}; pub use sibling_subgraph::SiblingSubgraph; @@ -396,6 +396,28 @@ pub trait HugrView: HugrInternals { /// format instead. fn mermaid_string_with_config(&self, config: RenderConfig) -> String; + /// Return the mermaid representation of the underlying hierarchical graph, + /// accepting the full set of configuration options. + /// + /// By default, the configuration will be converted to a [`RenderConfig`] + /// struct and may panic if the configuration is not supported. Where + /// possible, users are encouraged to provide an implementation for this + /// method instead and forward calls to [HugrView::mermaid_string_with_config] + /// to this method. + /// + /// The hierarchy is represented using subgraphs. Edges are labelled with + /// their source and target ports. + /// + /// For a more detailed representation, use the [`HugrView::dot_string`] + /// format instead. + fn mermaid_string_with_full_config( + &self, + config: impl Into>, + ) -> String { + let config: RenderConfig<_> = config.into().try_into().expect("unsupported config"); + self.mermaid_string_with_config(config) + } + /// Return the graphviz representation of the underlying graph and hierarchy side by side. /// /// For a simpler representation, use the [`HugrView::mermaid_string`] format instead. @@ -639,10 +661,15 @@ impl HugrView for Hugr { } fn mermaid_string_with_config(&self, config: RenderConfig) -> String { + self.mermaid_string_with_full_config(config) + } + + fn mermaid_string_with_full_config(&self, config: impl Into) -> String { + let config = config.into(); self.graph .mermaid_format() .with_hierarchy(&self.hierarchy) - .with_node_style(render::node_style(self, config, |n| n.index().to_string())) + .with_node_style(render::node_style(self, config.clone())) .with_edge_style(render::edge_style(self, config)) .finish() } @@ -651,15 +678,15 @@ impl HugrView for Hugr { where Self: Sized, { - let config = RenderConfig { + let config = FullRenderConfig { entrypoint: Some(self.entrypoint()), - ..RenderConfig::default() + ..FullRenderConfig::default() }; self.graph .dot_format() .with_hierarchy(&self.hierarchy) - .with_node_style(render::node_style(self, config, |n| n.index().to_string())) - .with_port_style(render::port_style(self, config)) + .with_node_style(render::node_style(self, config.clone())) + .with_port_style(render::port_style(self)) .with_edge_style(render::edge_style(self, config)) .finish() } diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index 09c4475be8..a99b76c65d 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -59,6 +59,7 @@ macro_rules! hugr_view_methods { fn all_neighbours(&self, node: Self::Node) -> impl Iterator + Clone; fn mermaid_string(&self) -> String; fn mermaid_string_with_config(&self, config: crate::hugr::views::render::RenderConfig) -> String; + fn mermaid_string_with_full_config(&self, config: impl Into>) -> String; fn dot_string(&self) -> String; fn static_source(&self, node: Self::Node) -> Option; fn static_targets(&self, node: Self::Node) -> Option>; diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index 9d597f1c04..24cbb8e850 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -1,14 +1,19 @@ //! Helper methods to compute the node/edge/port style when rendering a HUGR //! into dot or mermaid format. +use std::collections::HashMap; + use portgraph::render::{EdgeStyle, NodeStyle, PortStyle, PresentationStyle}; use portgraph::{LinkView, MultiPortGraph, NodeIndex, PortIndex, PortView}; +use crate::core::HugrNode; use crate::ops::{NamedOp, OpType}; use crate::types::EdgeKind; use crate::{Hugr, HugrView, Node}; -/// Configuration for rendering a HUGR graph. +/// Reduced configuration for rendering a HUGR graph. +/// +/// Additional options are available in the [`FullRenderConfig`] struct. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct RenderConfig { @@ -22,6 +27,74 @@ pub struct RenderConfig { pub entrypoint: Option, } +/// Configuration for rendering a HUGR graph. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct FullRenderConfig { + /// How to display the node indices. + pub node_labels: NodeLabel, + /// Show port offsets in the graph edges. + pub port_offsets_in_edges: bool, + /// Show type labels on edges. + pub type_labels_in_edges: bool, + /// A node to highlight as the graph entrypoint. + pub entrypoint: Option, +} + +impl From> for FullRenderConfig { + fn from(config: RenderConfig) -> Self { + let node_labels = if config.node_indices { + NodeLabel::Numeric + } else { + NodeLabel::None + }; + Self { + node_labels, + port_offsets_in_edges: config.port_offsets_in_edges, + type_labels_in_edges: config.type_labels_in_edges, + entrypoint: config.entrypoint, + } + } +} + +/// An error that occurs when trying to convert a `FullRenderConfig` into a +/// `RenderConfig`. +#[derive(Debug, thiserror::Error)] +pub enum UnsupportedRenderConfig { + /// Custom node labels are not supported in the `RenderConfig` struct. + #[error("Custom node labels are not supported in the `RenderConfig` struct")] + CustomNodeLabels, +} + +impl TryFrom> for RenderConfig { + type Error = UnsupportedRenderConfig; + + fn try_from(value: FullRenderConfig) -> Result { + if matches!(value.node_labels, NodeLabel::Custom(_)) { + return Err(UnsupportedRenderConfig::CustomNodeLabels); + } + let node_indices = matches!(value.node_labels, NodeLabel::Numeric); + Ok(Self { + node_indices, + port_offsets_in_edges: value.port_offsets_in_edges, + type_labels_in_edges: value.type_labels_in_edges, + entrypoint: value.entrypoint, + }) + } +} + +/// How to display the node indices. +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub enum NodeLabel { + /// Do not display the node index. + None, + /// Display the node index as a number. + #[default] + Numeric, + /// Display the labels corresponding to the node indices. + Custom(HashMap), +} + impl Default for RenderConfig { fn default() -> Self { Self { @@ -33,12 +106,23 @@ impl Default for RenderConfig { } } +impl Default for FullRenderConfig { + fn default() -> Self { + Self { + node_labels: NodeLabel::Numeric, + port_offsets_in_edges: true, + type_labels_in_edges: true, + entrypoint: None, + } + } +} + /// Formatter method to compute a node style. pub(in crate::hugr) fn node_style<'a>( h: &'a Hugr, - config: RenderConfig, - fmt_node_index: impl Fn(NodeIndex) -> String + 'a, + config: impl Into, ) -> Box NodeStyle + 'a> { + let config = config.into(); fn node_name(h: &Hugr, n: NodeIndex) -> String { match h.get_optype(n.into()) { OpType::FuncDecl(f) => format!("FuncDecl: \"{}\"", f.func_name()), @@ -52,40 +136,52 @@ pub(in crate::hugr) fn node_style<'a>( entrypoint_style.stroke_width = Some("3px".to_string()); let entrypoint = config.entrypoint.map(Node::into_portgraph); - if config.node_indices { - Box::new(move |n| { + match config.node_labels { + NodeLabel::Numeric => Box::new(move |n| { if Some(n) == entrypoint { NodeStyle::boxed(format!( "({ni}) [**{name}**]", - ni = fmt_node_index(n), + ni = n.index(), name = node_name(h, n) )) .with_attrs(entrypoint_style.clone()) } else { NodeStyle::boxed(format!( "({ni}) {name}", - ni = fmt_node_index(n), + ni = n.index(), name = node_name(h, n) )) } - }) - } else { - Box::new(move |n| { + }), + NodeLabel::None => Box::new(move |n| { if Some(n) == entrypoint { NodeStyle::boxed(format!("[**{name}**]", name = node_name(h, n))) .with_attrs(entrypoint_style.clone()) } else { NodeStyle::boxed(node_name(h, n)) } - }) + }), + NodeLabel::Custom(labels) => Box::new(move |n| { + if Some(n) == entrypoint { + NodeStyle::boxed(format!( + "({label}) [**{name}**]", + label = labels.get(&n.into()).unwrap_or(&n.index().to_string()), + name = node_name(h, n) + )) + .with_attrs(entrypoint_style.clone()) + } else { + NodeStyle::boxed(format!( + "({label}) {name}", + label = labels.get(&n.into()).unwrap_or(&n.index().to_string()), + name = node_name(h, n) + )) + } + }), } } /// Formatter method to compute a port style. -pub(in crate::hugr) fn port_style( - h: &Hugr, - _config: RenderConfig, -) -> Box PortStyle + '_> { +pub(in crate::hugr) fn port_style(h: &Hugr) -> Box PortStyle + '_> { let graph = &h.graph; Box::new(move |port| { let node = graph.port_node(port).unwrap(); @@ -112,7 +208,7 @@ pub(in crate::hugr) fn port_style( #[allow(clippy::type_complexity)] pub(in crate::hugr) fn edge_style( h: &Hugr, - config: RenderConfig, + config: impl Into, ) -> Box< dyn FnMut( ::LinkEndpoint, @@ -121,6 +217,7 @@ pub(in crate::hugr) fn edge_style( + '_, > { let graph = &h.graph; + let config = config.into(); Box::new(move |src, tgt| { let src_node = graph.port_node(src).unwrap(); let src_optype = h.get_optype(src_node.into()); From b5553252e61781023b3ad778f9a4f8875fba4f6e Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Thu, 29 May 2025 16:07:04 +0200 Subject: [PATCH 2/7] add tests --- hugr-core/src/hugr/views/render.rs | 31 +++++++++++++++++++ ...ws__render__tests__custom_node_labels.snap | 21 +++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index 24cbb8e850..fa1fb15be5 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -259,3 +259,34 @@ pub(in crate::hugr) fn edge_style( style.with_label(label) }) } + +#[cfg(test)] +mod tests { + use crate::{NodeIndex, builder::test::simple_dfg_hugr}; + + use super::*; + + #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri + #[test] + fn test_custom_node_labels() { + let h = simple_dfg_hugr(); + let node_labels = h + .nodes() + .map(|n| (n, format!("node_{}", n.index()))) + .collect(); + let config = FullRenderConfig { + node_labels: NodeLabel::Custom(node_labels), + ..Default::default() + }; + insta::assert_snapshot!(h.mermaid_string_with_full_config(config)); + } + + #[test] + fn convert_full_render_config_to_render_config() { + let config: FullRenderConfig = FullRenderConfig { + node_labels: NodeLabel::Custom(HashMap::new()), + ..Default::default() + }; + assert!(RenderConfig::try_from(config).is_err()); + } +} diff --git a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap new file mode 100644 index 0000000000..76a401a416 --- /dev/null +++ b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap @@ -0,0 +1,21 @@ +--- +source: hugr-core/src/hugr/views/render.rs +expression: h.mermaid_string_with_full_config(config) +--- +graph LR + subgraph 0 ["(node_0) Module"] + direction LR + subgraph 1 ["(node_1) FuncDefn: #quot;main#quot;"] + direction LR + 2["(node_2) Input"] + 3["(node_3) Output"] + subgraph 4 ["(node_4) DFG"] + direction LR + 5["(node_5) Input"] + 6["(node_6) Output"] + 5--"0:0
Bool"-->6 + end + 2--"0:0
Bool"-->4 + 4--"0:0
Bool"-->3 + end + end From 4403a75b4cc2a1e850e84d2bb822365cbb627872 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Thu, 29 May 2025 16:10:16 +0200 Subject: [PATCH 3/7] one more delegate impl --- hugr-core/src/hugr/views/rerooted.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index ea849372db..12d241b3aa 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -104,6 +104,7 @@ impl HugrView for Rerooted { fn neighbours(&self, node: Self::Node, dir: crate::Direction) -> impl Iterator + Clone; fn all_neighbours(&self, node: Self::Node) -> impl Iterator + Clone; fn mermaid_string_with_config(&self, config: crate::hugr::views::render::RenderConfig) -> String; + fn mermaid_string_with_full_config(&self, config: impl Into>) -> String; fn dot_string(&self) -> String; fn static_source(&self, node: Self::Node) -> Option; fn static_targets(&self, node: Self::Node) -> Option>; From 9d5c00b65c1a395c89f5933f8ef0c0ab89bfa20d Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Tue, 3 Jun 2025 14:52:00 +0100 Subject: [PATCH 4/7] implement MermaidFormatter --- hugr-core/src/hugr/persistent/parents_view.rs | 5 +- hugr-core/src/hugr/persistent/trait_impls.rs | 71 +++---- hugr-core/src/hugr/views.rs | 83 ++++---- hugr-core/src/hugr/views/impls.rs | 3 +- hugr-core/src/hugr/views/render.rs | 189 ++++++++++++++---- hugr-core/src/hugr/views/rerooted.rs | 17 +- 6 files changed, 251 insertions(+), 117 deletions(-) diff --git a/hugr-core/src/hugr/persistent/parents_view.rs b/hugr-core/src/hugr/persistent/parents_view.rs index 639acefccc..b4aa076060 100644 --- a/hugr-core/src/hugr/persistent/parents_view.rs +++ b/hugr-core/src/hugr/persistent/parents_view.rs @@ -5,7 +5,7 @@ use crate::{ extension::ExtensionRegistry, hugr::{ internal::HugrInternals, - views::{ExtractionResult, render::RenderConfig}, + views::{ExtractionResult, render}, }, ops::OpType, }; @@ -196,7 +196,8 @@ impl HugrView for ParentsView<'_> { unimplemented!() } - fn mermaid_string_with_config(&self, _config: RenderConfig) -> String { + #[allow(deprecated)] + fn mermaid_string_with_config(&self, _config: render::RenderConfig) -> String { unimplemented!() } diff --git a/hugr-core/src/hugr/persistent/trait_impls.rs b/hugr-core/src/hugr/persistent/trait_impls.rs index 202391175c..8cfe18e9f9 100644 --- a/hugr-core/src/hugr/persistent/trait_impls.rs +++ b/hugr-core/src/hugr/persistent/trait_impls.rs @@ -10,7 +10,7 @@ use crate::{ internal::HugrInternals, views::{ ExtractionResult, - render::{self, FullRenderConfig, NodeLabel, RenderConfig}, + render::{self, MermaidFormatter, NodeLabel}, }, }, }; @@ -242,39 +242,44 @@ impl HugrView for PersistentHugr { .flat_map(move |port| self.linked_ports(node, port).map(|(opp_node, _)| opp_node)) } - fn mermaid_string(&self) -> String { - self.mermaid_string_with_config(RenderConfig { - node_indices: true, - port_offsets_in_edges: true, - type_labels_in_edges: true, - entrypoint: Some(self.entrypoint()), - }) + #[allow(deprecated)] + fn mermaid_string_with_config(&self, config: render::RenderConfig) -> String { + self.mermaid_string_with_formatter(MermaidFormatter::from_render_config(config, self)) } - fn mermaid_string_with_config(&self, config: RenderConfig) -> String { + fn mermaid_string_with_formatter(&self, formatter: MermaidFormatter) -> String { // Extract a concrete HUGR for displaying let (hugr, node_map) = self.apply_all(); // Render the extracted HUGR but map the node indices back to the // original patch node IDs - let entrypoint = config.entrypoint.map(|n| node_map[&n]); - let node_labels = if config.node_indices { - let node_labels_map: HashMap<_, _> = node_map - .into_iter() - .map(|(k, v)| (v, format!("{:?}", k))) - .collect(); - NodeLabel::Custom(node_labels_map) - } else { - NodeLabel::None + let entrypoint = formatter.entrypoint().map(|n| node_map[&n]); + let node_labels = match formatter.node_labels() { + NodeLabel::None => NodeLabel::None, + NodeLabel::Numeric => { + // replace node labels with patch node IDs + let node_labels_map: HashMap<_, _> = node_map + .into_iter() + .map(|(k, v)| (v, format!("{:?}", k))) + .collect(); + NodeLabel::Custom(node_labels_map) + } + NodeLabel::Custom(labels) => { + // rekey labels to the extracted HUGR node IDs + let labels = labels + .iter() + .map(|(k, v)| (node_map[&k], v.clone())) + .collect(); + NodeLabel::Custom(labels) + } }; // Map config accordingly - let config = FullRenderConfig { - entrypoint, - node_labels, - port_offsets_in_edges: config.port_offsets_in_edges, - type_labels_in_edges: config.type_labels_in_edges, - }; + let config = MermaidFormatter::new(&hugr) + .with_entrypoint(entrypoint) + .with_node_labels(node_labels) + .with_port_offsets(formatter.port_offsets()) + .with_type_labels(formatter.type_labels()); hugr.graph .mermaid_format() @@ -341,19 +346,15 @@ mod tests { .try_extract_hugr([commit1, commit2, commit4]) .unwrap(); - let mermaid_str = hugr.mermaid_string_with_config(RenderConfig { - node_indices: false, - entrypoint: Some(hugr.entrypoint()), - ..Default::default() - }); + let mermaid_str = hugr + .mermaid_format() + .with_node_labels(NodeLabel::None) + .finish(); let extracted_hugr = hugr.to_hugr(); let exp_str = extracted_hugr - .mermaid_string_with_config(RenderConfig { - node_indices: false, - entrypoint: Some(extracted_hugr.entrypoint()), - ..Default::default() - }) - .to_string(); + .mermaid_format() + .with_node_labels(NodeLabel::None) + .finish(); assert_eq!(mermaid_str, exp_str); } diff --git a/hugr-core/src/hugr/views.rs b/hugr-core/src/hugr/views.rs index 1c68452341..c742493976 100644 --- a/hugr-core/src/hugr/views.rs +++ b/hugr-core/src/hugr/views.rs @@ -14,7 +14,9 @@ use std::borrow::Cow; use std::collections::HashMap; pub use self::petgraph::PetgraphWrapper; -use self::render::{FullRenderConfig, RenderConfig}; +#[allow(deprecated)] +use self::render::{MermaidFormatter, RenderConfig}; +use proptest::test_runner::Config; pub use rerooted::Rerooted; pub use root_checked::{InvalidSignature, RootCheckable, RootChecked, check_tag}; pub use sibling_subgraph::SiblingSubgraph; @@ -385,7 +387,9 @@ pub trait HugrView: HugrInternals { /// /// For a more detailed representation, use the [`HugrView::dot_string`] /// format instead. - fn mermaid_string(&self) -> String; + fn mermaid_string(&self) -> String { + self.mermaid_string_with_formatter(self.mermaid_format()) + } /// Return the mermaid representation of the underlying hierarchical graph. /// @@ -394,30 +398,51 @@ pub trait HugrView: HugrInternals { /// /// For a more detailed representation, use the [`HugrView::dot_string`] /// format instead. + #[deprecated(note = "Use `mermaid_format` instead")] + #[allow(deprecated)] fn mermaid_string_with_config(&self, config: RenderConfig) -> String; - /// Return the mermaid representation of the underlying hierarchical graph, - /// accepting the full set of configuration options. - /// - /// By default, the configuration will be converted to a [`RenderConfig`] - /// struct and may panic if the configuration is not supported. Where - /// possible, users are encouraged to provide an implementation for this - /// method instead and forward calls to [HugrView::mermaid_string_with_config] - /// to this method. + /// Return the mermaid representation of the underlying hierarchical graph + /// according to the provided [`MermaidFormatter`] formatting options. /// /// The hierarchy is represented using subgraphs. Edges are labelled with /// their source and target ports. /// /// For a more detailed representation, use the [`HugrView::dot_string`] /// format instead. - fn mermaid_string_with_full_config( - &self, - config: impl Into>, - ) -> String { - let config: RenderConfig<_> = config.into().try_into().expect("unsupported config"); + /// + /// ## Deprecation of [`RenderConfig`] + /// While the deprecated [HugrView::mermaid_string_with_config] exists, this + /// will by default try to convert the formatter options to a [`RenderConfig`], + /// but this may panic if the configuration is not supported. Users are + /// encouraged to provide an implementation of this method overriding the default + /// and no longer rely on [HugrView::mermaid_string_with_config]. + fn mermaid_string_with_formatter(&self, formatter: MermaidFormatter) -> String { + #[allow(deprecated)] + let config = match RenderConfig::try_from(formatter) { + Ok(config) => config, + Err(e) => { + panic!("Unsupported format option: {}", e); + } + }; + #[allow(deprecated)] self.mermaid_string_with_config(config) } + /// Construct a mermaid representation of the underlying hierarchical graph. + /// + /// Options can be set on the returned [`MermaidFormatter`] struct, before + /// generating the String with [`MermaidFormatter::finish`]. + /// + /// The hierarchy is represented using subgraphs. Edges are labelled with + /// their source and target ports. + /// + /// For a more detailed representation, use the [`HugrView::dot_string`] + /// format instead. + fn mermaid_format(&self) -> MermaidFormatter { + MermaidFormatter::new(self).with_entrypoint(self.entrypoint()) + } + /// Return the graphviz representation of the underlying graph and hierarchy side by side. /// /// For a simpler representation, use the [`HugrView::mermaid_string`] format instead. @@ -651,26 +676,17 @@ impl HugrView for Hugr { self.graph.all_neighbours(node.into_portgraph()).map_into() } - fn mermaid_string(&self) -> String { - self.mermaid_string_with_config(RenderConfig { - node_indices: true, - port_offsets_in_edges: true, - type_labels_in_edges: true, - entrypoint: Some(self.entrypoint()), - }) - } - + #[allow(deprecated)] fn mermaid_string_with_config(&self, config: RenderConfig) -> String { - self.mermaid_string_with_full_config(config) + self.mermaid_string_with_formatter(MermaidFormatter::from_render_config(config, self)) } - fn mermaid_string_with_full_config(&self, config: impl Into) -> String { - let config = config.into(); + fn mermaid_string_with_formatter(&self, formatter: MermaidFormatter) -> String { self.graph .mermaid_format() .with_hierarchy(&self.hierarchy) - .with_node_style(render::node_style(self, config.clone())) - .with_edge_style(render::edge_style(self, config)) + .with_node_style(render::node_style(self, formatter.clone())) + .with_edge_style(render::edge_style(self, formatter)) .finish() } @@ -678,16 +694,13 @@ impl HugrView for Hugr { where Self: Sized, { - let config = FullRenderConfig { - entrypoint: Some(self.entrypoint()), - ..FullRenderConfig::default() - }; + let formatter = MermaidFormatter::new(self).with_entrypoint(self.entrypoint()); self.graph .dot_format() .with_hierarchy(&self.hierarchy) - .with_node_style(render::node_style(self, config.clone())) + .with_node_style(render::node_style(self, formatter.clone())) .with_port_style(render::port_style(self)) - .with_edge_style(render::edge_style(self, config)) + .with_edge_style(render::edge_style(self, formatter)) .finish() } diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index a99b76c65d..75311f2f73 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -58,8 +58,9 @@ macro_rules! hugr_view_methods { fn neighbours(&self, node: Self::Node, dir: crate::Direction) -> impl Iterator + Clone; fn all_neighbours(&self, node: Self::Node) -> impl Iterator + Clone; fn mermaid_string(&self) -> String; + #[allow(deprecated)] fn mermaid_string_with_config(&self, config: crate::hugr::views::render::RenderConfig) -> String; - fn mermaid_string_with_full_config(&self, config: impl Into>) -> String; + fn mermaid_string_with_formatter(&self, #[into] formatter: crate::hugr::views::render::MermaidFormatter) -> String; fn dot_string(&self) -> String; fn static_source(&self, node: Self::Node) -> Option; fn static_targets(&self, node: Self::Node) -> Option>; diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index fa1fb15be5..897eb60550 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -7,15 +7,17 @@ use portgraph::render::{EdgeStyle, NodeStyle, PortStyle, PresentationStyle}; use portgraph::{LinkView, MultiPortGraph, NodeIndex, PortIndex, PortView}; use crate::core::HugrNode; +use crate::hugr::internal::HugrInternals; use crate::ops::{NamedOp, OpType}; use crate::types::EdgeKind; use crate::{Hugr, HugrView, Node}; /// Reduced configuration for rendering a HUGR graph. /// -/// Additional options are available in the [`FullRenderConfig`] struct. +/// Additional options are available in the [`MermaidFormatter`] struct. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] +#[deprecated(note = "Use `MermaidFormatter` instead")] pub struct RenderConfig { /// Show the node index in the graph nodes. pub node_indices: bool, @@ -29,32 +31,112 @@ pub struct RenderConfig { /// Configuration for rendering a HUGR graph. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub struct FullRenderConfig { +pub struct MermaidFormatter<'h, H: HugrInternals + ?Sized = Hugr> { + /// The HUGR to render. + hugr: &'h H, /// How to display the node indices. - pub node_labels: NodeLabel, + node_labels: NodeLabel, /// Show port offsets in the graph edges. - pub port_offsets_in_edges: bool, + port_offsets_in_edges: bool, /// Show type labels on edges. - pub type_labels_in_edges: bool, + type_labels_in_edges: bool, /// A node to highlight as the graph entrypoint. - pub entrypoint: Option, + entrypoint: Option, } -impl From> for FullRenderConfig { - fn from(config: RenderConfig) -> Self { +impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { + #[allow(deprecated)] + pub fn from_render_config(config: RenderConfig, hugr: &'h H) -> Self { let node_labels = if config.node_indices { NodeLabel::Numeric } else { NodeLabel::None }; Self { + hugr, node_labels, port_offsets_in_edges: config.port_offsets_in_edges, type_labels_in_edges: config.type_labels_in_edges, entrypoint: config.entrypoint, } } + + pub fn new(hugr: &'h H) -> Self { + Self { + hugr, + node_labels: NodeLabel::Numeric, + port_offsets_in_edges: true, + type_labels_in_edges: true, + entrypoint: None, + } + } + + pub fn entrypoint(&self) -> Option { + self.entrypoint + } + + pub fn node_labels(&self) -> &NodeLabel { + &self.node_labels + } + + pub fn port_offsets(&self) -> bool { + self.port_offsets_in_edges + } + + pub fn type_labels(&self) -> bool { + self.type_labels_in_edges + } + + /// Set the node labels style. + pub fn with_node_labels(mut self, node_labels: NodeLabel) -> Self { + self.node_labels = node_labels; + self + } + + /// Set whether to show port offsets in edges. + pub fn with_port_offsets(mut self, show: bool) -> Self { + self.port_offsets_in_edges = show; + self + } + + /// Set whether to show type labels in edges. + pub fn with_type_labels(mut self, show: bool) -> Self { + self.type_labels_in_edges = show; + self + } + + /// Set the entrypoint node to highlight. + pub fn with_entrypoint(mut self, entrypoint: impl Into>) -> Self { + self.entrypoint = entrypoint.into(); + self + } + + pub fn finish(self) -> String + where + H: HugrView, + { + self.hugr.mermaid_string_with_formatter(self) + } + + pub(crate) fn with_hugr<'a, NewH: HugrInternals>( + self, + hugr: &'a NewH, + ) -> MermaidFormatter<'a, NewH> { + let MermaidFormatter { + hugr: _, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } = self; + MermaidFormatter { + hugr, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } + } } /// An error that occurs when trying to convert a `FullRenderConfig` into a @@ -66,10 +148,11 @@ pub enum UnsupportedRenderConfig { CustomNodeLabels, } -impl TryFrom> for RenderConfig { +#[allow(deprecated)] +impl<'h, H: HugrInternals + ?Sized> TryFrom> for RenderConfig { type Error = UnsupportedRenderConfig; - fn try_from(value: FullRenderConfig) -> Result { + fn try_from(value: MermaidFormatter<'h, H>) -> Result { if matches!(value.node_labels, NodeLabel::Custom(_)) { return Err(UnsupportedRenderConfig::CustomNodeLabels); } @@ -83,6 +166,56 @@ impl TryFrom> for RenderConfig { } } +macro_rules! impl_mermaid_formatter_from { + ($t:ty, $($lifetime:tt)?) => { + impl<'h, $($lifetime,)? H: HugrView> From> for MermaidFormatter<'h, H> { + fn from(value: MermaidFormatter<'h, $t>) -> Self { + let MermaidFormatter { + hugr, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } = value; + MermaidFormatter { + hugr, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } + } + } + }; +} + +impl_mermaid_formatter_from!(&'hh H, 'hh); +impl_mermaid_formatter_from!(&'hh mut H, 'hh); +impl_mermaid_formatter_from!(std::rc::Rc,); +impl_mermaid_formatter_from!(std::sync::Arc,); +impl_mermaid_formatter_from!(Box,); + +impl<'h, H: HugrView + ToOwned> From>> + for MermaidFormatter<'h, H> +{ + fn from(value: MermaidFormatter<'h, std::borrow::Cow<'_, H>>) -> Self { + let MermaidFormatter { + hugr, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } = value; + MermaidFormatter { + hugr, + node_labels, + port_offsets_in_edges, + type_labels_in_edges, + entrypoint, + } + } +} + /// How to display the node indices. #[derive(Default, Clone, Debug, PartialEq, Eq)] pub enum NodeLabel { @@ -95,6 +228,7 @@ pub enum NodeLabel { Custom(HashMap), } +#[allow(deprecated)] impl Default for RenderConfig { fn default() -> Self { Self { @@ -106,23 +240,11 @@ impl Default for RenderConfig { } } -impl Default for FullRenderConfig { - fn default() -> Self { - Self { - node_labels: NodeLabel::Numeric, - port_offsets_in_edges: true, - type_labels_in_edges: true, - entrypoint: None, - } - } -} - /// Formatter method to compute a node style. pub(in crate::hugr) fn node_style<'a>( h: &'a Hugr, - config: impl Into, + formatter: MermaidFormatter<'a>, ) -> Box NodeStyle + 'a> { - let config = config.into(); fn node_name(h: &Hugr, n: NodeIndex) -> String { match h.get_optype(n.into()) { OpType::FuncDecl(f) => format!("FuncDecl: \"{}\"", f.func_name()), @@ -134,9 +256,9 @@ pub(in crate::hugr) fn node_style<'a>( let mut entrypoint_style = PresentationStyle::default(); entrypoint_style.stroke = Some("#832561".to_string()); entrypoint_style.stroke_width = Some("3px".to_string()); - let entrypoint = config.entrypoint.map(Node::into_portgraph); + let entrypoint = formatter.entrypoint.map(Node::into_portgraph); - match config.node_labels { + match formatter.node_labels { NodeLabel::Numeric => Box::new(move |n| { if Some(n) == entrypoint { NodeStyle::boxed(format!( @@ -206,18 +328,17 @@ pub(in crate::hugr) fn port_style(h: &Hugr) -> Box PortS /// Formatter method to compute an edge style. #[allow(clippy::type_complexity)] -pub(in crate::hugr) fn edge_style( - h: &Hugr, - config: impl Into, +pub(in crate::hugr) fn edge_style<'a>( + h: &'a Hugr, + config: MermaidFormatter<'_>, ) -> Box< dyn FnMut( ::LinkEndpoint, ::LinkEndpoint, ) -> EdgeStyle - + '_, + + 'a, > { let graph = &h.graph; - let config = config.into(); Box::new(move |src, tgt| { let src_node = graph.port_node(src).unwrap(); let src_optype = h.get_optype(src_node.into()); @@ -274,16 +395,16 @@ mod tests { .nodes() .map(|n| (n, format!("node_{}", n.index()))) .collect(); - let config = FullRenderConfig { + let config = MermaidFormatter { node_labels: NodeLabel::Custom(node_labels), ..Default::default() }; - insta::assert_snapshot!(h.mermaid_string_with_full_config(config)); + insta::assert_snapshot!(h.mermaid_string_with_formatter(config)); } #[test] fn convert_full_render_config_to_render_config() { - let config: FullRenderConfig = FullRenderConfig { + let config: MermaidFormatter = MermaidFormatter { node_labels: NodeLabel::Custom(HashMap::new()), ..Default::default() }; diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 12d241b3aa..21216c6e27 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -4,7 +4,6 @@ use crate::hugr::HugrMut; use crate::hugr::internal::{HugrInternals, HugrMutInternals}; -use super::render::RenderConfig; use super::{HugrView, panic_invalid_node}; /// A HUGR wrapper with a modified entrypoint node. @@ -60,14 +59,12 @@ impl HugrView for Rerooted { self.hugr.get_optype(self.entrypoint) } - #[inline] - fn mermaid_string(&self) -> String { - self.mermaid_string_with_config(RenderConfig { - node_indices: true, - port_offsets_in_edges: true, - type_labels_in_edges: true, - entrypoint: Some(self.entrypoint()), - }) + fn mermaid_string_with_formatter( + &self, + formatter: crate::hugr::views::render::MermaidFormatter, + ) -> String { + self.hugr + .mermaid_string_with_formatter(formatter.with_hugr(&self.hugr)) } delegate::delegate! { @@ -103,8 +100,8 @@ impl HugrView for Rerooted { fn first_child(&self, node: Self::Node) -> Option; fn neighbours(&self, node: Self::Node, dir: crate::Direction) -> impl Iterator + Clone; fn all_neighbours(&self, node: Self::Node) -> impl Iterator + Clone; + #[allow(deprecated)] fn mermaid_string_with_config(&self, config: crate::hugr::views::render::RenderConfig) -> String; - fn mermaid_string_with_full_config(&self, config: impl Into>) -> String; fn dot_string(&self) -> String; fn static_source(&self, node: Self::Node) -> Option; fn static_targets(&self, node: Self::Node) -> Option>; From 773cb4c6ebbb4126e92045b76880aee21badffa8 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Tue, 3 Jun 2025 20:26:19 +0200 Subject: [PATCH 5/7] add docs --- hugr-core/src/hugr/persistent/trait_impls.rs | 2 +- hugr-core/src/hugr/views.rs | 1 - hugr-core/src/hugr/views/render.rs | 32 ++++++++++++-------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hugr-core/src/hugr/persistent/trait_impls.rs b/hugr-core/src/hugr/persistent/trait_impls.rs index 8cfe18e9f9..6c68762029 100644 --- a/hugr-core/src/hugr/persistent/trait_impls.rs +++ b/hugr-core/src/hugr/persistent/trait_impls.rs @@ -268,7 +268,7 @@ impl HugrView for PersistentHugr { // rekey labels to the extracted HUGR node IDs let labels = labels .iter() - .map(|(k, v)| (node_map[&k], v.clone())) + .map(|(k, v)| (node_map[k], v.clone())) .collect(); NodeLabel::Custom(labels) } diff --git a/hugr-core/src/hugr/views.rs b/hugr-core/src/hugr/views.rs index c742493976..1704d79f65 100644 --- a/hugr-core/src/hugr/views.rs +++ b/hugr-core/src/hugr/views.rs @@ -16,7 +16,6 @@ use std::collections::HashMap; pub use self::petgraph::PetgraphWrapper; #[allow(deprecated)] use self::render::{MermaidFormatter, RenderConfig}; -use proptest::test_runner::Config; pub use rerooted::Rerooted; pub use root_checked::{InvalidSignature, RootCheckable, RootChecked, check_tag}; pub use sibling_subgraph::SiblingSubgraph; diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index 897eb60550..d29b23edfc 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -45,6 +45,7 @@ pub struct MermaidFormatter<'h, H: HugrInternals + ?Sized = Hugr> { } impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { + /// Create a new [`MermaidFormatter`] from a [`RenderConfig`]. #[allow(deprecated)] pub fn from_render_config(config: RenderConfig, hugr: &'h H) -> Self { let node_labels = if config.node_indices { @@ -61,6 +62,7 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { } } + /// Create a new [`MermaidFormatter`] for the given [`Hugr`]. pub fn new(hugr: &'h H) -> Self { Self { hugr, @@ -71,18 +73,22 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { } } + /// The entrypoint to highlight in the rendered graph. pub fn entrypoint(&self) -> Option { self.entrypoint } + /// The rendering style of the node labels. pub fn node_labels(&self) -> &NodeLabel { &self.node_labels } + /// Whether to show port offsets on edges. pub fn port_offsets(&self) -> bool { self.port_offsets_in_edges } + /// Whether to show type labels on edges. pub fn type_labels(&self) -> bool { self.type_labels_in_edges } @@ -111,6 +117,7 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { self } + /// Render the graph into a Mermaid string. pub fn finish(self) -> String where H: HugrView, @@ -118,10 +125,10 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { self.hugr.mermaid_string_with_formatter(self) } - pub(crate) fn with_hugr<'a, NewH: HugrInternals>( + pub(crate) fn with_hugr>( self, - hugr: &'a NewH, - ) -> MermaidFormatter<'a, NewH> { + hugr: &NewH, + ) -> MermaidFormatter<'_, NewH> { let MermaidFormatter { hugr: _, node_labels, @@ -395,19 +402,20 @@ mod tests { .nodes() .map(|n| (n, format!("node_{}", n.index()))) .collect(); - let config = MermaidFormatter { - node_labels: NodeLabel::Custom(node_labels), - ..Default::default() - }; + let config = h + .mermaid_format() + .with_node_labels(NodeLabel::Custom(node_labels)); insta::assert_snapshot!(h.mermaid_string_with_formatter(config)); } #[test] fn convert_full_render_config_to_render_config() { - let config: MermaidFormatter = MermaidFormatter { - node_labels: NodeLabel::Custom(HashMap::new()), - ..Default::default() - }; - assert!(RenderConfig::try_from(config).is_err()); + let h = simple_dfg_hugr(); + let config: MermaidFormatter = + MermaidFormatter::new(&h).with_node_labels(NodeLabel::Custom(HashMap::new())); + #[allow(deprecated)] + { + assert!(RenderConfig::try_from(config).is_err()); + } } } From 26e334455a31795eeeeb370220bb4e91349b9bb0 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Tue, 3 Jun 2025 20:31:05 +0200 Subject: [PATCH 6/7] fix snapshot --- ...core__hugr__views__render__tests__custom_node_labels.snap | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap index 76a401a416..22b2ed8a51 100644 --- a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap +++ b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__render__tests__custom_node_labels.snap @@ -1,6 +1,6 @@ --- source: hugr-core/src/hugr/views/render.rs -expression: h.mermaid_string_with_full_config(config) +expression: h.mermaid_string_with_formatter(config) --- graph LR subgraph 0 ["(node_0) Module"] @@ -9,8 +9,9 @@ graph LR direction LR 2["(node_2) Input"] 3["(node_3) Output"] - subgraph 4 ["(node_4) DFG"] + subgraph 4 ["(node_4) [**DFG**]"] direction LR + style 4 stroke:#832561,stroke-width:3px 5["(node_5) Input"] 6["(node_6) Output"] 5--"0:0
Bool"-->6 From 3c92e648b1f6f39bccc62a9301850a809e12bf34 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Tue, 3 Jun 2025 20:51:40 +0200 Subject: [PATCH 7/7] add mermaid tests --- hugr-core/src/hugr/views/render.rs | 10 ++++++++++ hugr-core/src/hugr/views/rerooted.rs | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index d29b23edfc..b787a9e383 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -417,5 +417,15 @@ mod tests { { assert!(RenderConfig::try_from(config).is_err()); } + + #[allow(deprecated)] + let config = RenderConfig { + entrypoint: Some(h.entrypoint()), + ..Default::default() + }; + assert_eq!( + MermaidFormatter::from_render_config(config, &h), + h.mermaid_format() + ) } } diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 21216c6e27..8c84abdc71 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -233,4 +233,12 @@ mod test { ); assert!(extracted_hugr.get_optype(extracted_bb).is_dataflow_block()); } + + #[test] + fn mermaid_format() { + let h = simple_cfg_hugr(); + let rerooted = h.with_entrypoint(h.entrypoint()); + let mermaid_str = rerooted.mermaid_format().finish(); + assert_eq!(mermaid_str, h.mermaid_format().finish()); + } }