Skip to content
Open
10 changes: 9 additions & 1 deletion hugr-core/src/hugr/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::extension::ExtensionRegistry;
use crate::hugr::internal::{DefaultPGNodeMap, PortgraphNodeMap};
use crate::hugr::views::syn_edge::SynEdgeWrapper;
use crate::metadata::{Metadata, MetadataError, RawMetadataValue};
use crate::ops::{OpParent, OpTag, OpTrait, OpType, handle::NodeHandle};
use crate::ops::{OpParent, OpTag, OpTrait, OpType, RenderStringConfig, handle::NodeHandle};
use crate::types::{EdgeKind, PolyFuncType, Signature, Type};
use crate::{Direction, IncomingPort, OutgoingPort, Port};

Expand Down Expand Up @@ -430,6 +430,14 @@ pub trait HugrView: HugrInternals {
fn mermaid_string(&self) -> String {
self.mermaid_string_with_formatter(self.mermaid_format())
}
/// Return the mermaid representation of the underlying hierarchical graph
/// using the provided rendering configuration.
///
/// This method allows customizing the appearance of node labels and other
/// elements in the mermaid diagram.
fn mermaid_string_with_config(&self, config: RenderStringConfig) -> String {
self.mermaid_string_with_formatter(self.mermaid_format().with_render_string_config(config))
}
Comment on lines +433 to +440

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is dot_string() used for real in rust?


Comment on lines +433 to 441

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if a method like this may be useful

/// Return the mermaid representation of the underlying hierarchical graph
/// according to the provided [`MermaidFormatter`] formatting options.
Expand Down
132 changes: 105 additions & 27 deletions hugr-core/src/hugr/views/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use portgraph::{LinkView, MultiPortGraph, NodeIndex, PortIndex, PortView};

use crate::core::HugrNode;
use crate::hugr::internal::HugrInternals;
use crate::ops::{NamedOp, OpType};
use crate::ops::{OpTrait, RenderStringConfig};
use crate::types::EdgeKind;
use crate::{Hugr, HugrView, Node};

Expand All @@ -26,6 +26,8 @@ pub struct MermaidFormatter<'h, H: HugrInternals + ?Sized = Hugr> {
type_labels_in_edges: bool,
/// A node to highlight as the graph entrypoint.
entrypoint: Option<H::Node>,
/// How operation names are rendered in node labels.
render_string_config: RenderStringConfig,
}

impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
Expand All @@ -37,6 +39,7 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
port_offsets_in_edges: true,
type_labels_in_edges: true,
entrypoint: None,
render_string_config: RenderStringConfig::default(),
}
}

Expand All @@ -60,6 +63,11 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
self.type_labels_in_edges
}

/// The configuration used to render operation names in node labels.
pub fn render_string_config(&self) -> RenderStringConfig {
self.render_string_config
}

/// Set the node labels style.
pub fn with_node_labels(mut self, node_labels: NodeLabel<H::Node>) -> Self {
self.node_labels = node_labels;
Expand All @@ -78,6 +86,12 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
self
}

/// Set how operation names are rendered in node labels.
pub fn with_render_string_config(mut self, config: RenderStringConfig) -> Self {
self.render_string_config = config;
self
}

/// Set the entrypoint node to highlight.
pub fn with_entrypoint(mut self, entrypoint: impl Into<Option<H::Node>>) -> Self {
self.entrypoint = entrypoint.into();
Expand All @@ -102,13 +116,15 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> {
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
} = self;
MermaidFormatter {
hugr,
node_labels,
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
}
}
}
Expand All @@ -132,13 +148,15 @@ macro_rules! impl_mermaid_formatter_from {
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
} = value;
MermaidFormatter {
hugr,
node_labels,
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
}
}
}
Expand All @@ -161,13 +179,15 @@ impl<'h, H: HugrView + ToOwned> From<MermaidFormatter<'h, std::borrow::Cow<'_, H
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
} = value;
MermaidFormatter {
hugr,
node_labels,
port_offsets_in_edges,
type_labels_in_edges,
entrypoint,
render_string_config,
}
}
}
Expand Down Expand Up @@ -195,41 +215,51 @@ pub(in crate::hugr) fn node_style<'a>(
h: &'a Hugr,
formatter: MermaidFormatter<'a>,
) -> Box<dyn FnMut(NodeIndex) -> NodeStyle + 'a> {
fn node_name(h: &Hugr, n: NodeIndex) -> String {
match h.get_optype(n.into()) {
OpType::FuncDecl(f) => format!("FuncDecl: \"{}\"", f.func_name()),
OpType::FuncDefn(f) => format!("FuncDefn: \"{}\"", f.func_name()),
op => op.name().to_string(),
}
}

fn numeric_label(h: &Hugr, n: NodeIndex, is_entry: bool) -> String {
fn numeric_label(
h: &Hugr,
n: NodeIndex,
is_entry: bool,
inner_label_config: RenderStringConfig,
) -> String {
if is_entry {
format!("({}) [**{}**]", n.index(), node_name(h, n))
format!(
"({}) [**{}**]",
n.index(),
h.get_optype(n.into()).render_str(inner_label_config)
)
} else {
format!("({}) {}", n.index(), node_name(h, n))
format!(
"({}) {}",
n.index(),
h.get_optype(n.into()).render_str(inner_label_config)
)
}
}

let mut entrypoint_style = PresentationStyle::default();
entrypoint_style.stroke = Some("#832561".to_string());
entrypoint_style.stroke_width = Some("3px".to_string());
let entrypoint = formatter.entrypoint.map(Node::into_portgraph);
let render_label_config = formatter.render_string_config();

match formatter.node_labels {
NodeLabel::Numeric => Box::new(move |n| {
if Some(n) == entrypoint {
NodeStyle::boxed(numeric_label(h, n, true)).with_attrs(entrypoint_style.clone())
NodeStyle::boxed(numeric_label(h, n, true, render_label_config))
.with_attrs(entrypoint_style.clone())
} else {
NodeStyle::boxed(numeric_label(h, n, false))
NodeStyle::boxed(numeric_label(h, n, false, render_label_config))
}
}),
NodeLabel::None => Box::new(move |n| {
if Some(n) == entrypoint {
NodeStyle::boxed(format!("[**{name}**]", name = node_name(h, n)))
.with_attrs(entrypoint_style.clone())
NodeStyle::boxed(format!(
"[**{name}**]",
name = h.get_optype(n.into()).render_str(render_label_config)
))
.with_attrs(entrypoint_style.clone())
} else {
NodeStyle::boxed(node_name(h, n))
NodeStyle::boxed(h.get_optype(n.into()).render_str(render_label_config))
}
}),
NodeLabel::MetadataValues { print_keys } => Box::new(move |n| {
Expand All @@ -252,25 +282,31 @@ pub(in crate::hugr) fn node_style<'a>(
.join("; ");

if Some(n) == entrypoint {
NodeStyle::boxed(format!("{}; {kv_str}", numeric_label(h, n, true)))
.with_attrs(entrypoint_style.clone())
NodeStyle::boxed(format!(
"{}; {kv_str}",
numeric_label(h, n, true, render_label_config)
))
.with_attrs(entrypoint_style.clone())
} else {
NodeStyle::boxed(format!("{}; {kv_str}", numeric_label(h, n, false)))
NodeStyle::boxed(format!(
"{}; {kv_str}",
numeric_label(h, n, false, render_label_config)
))
}
}),
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)
name = h.get_optype(n.into()).render_str(render_label_config)
))
.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)
name = h.get_optype(n.into()).render_str(render_label_config)
))
}
}),
Expand Down Expand Up @@ -332,18 +368,20 @@ pub(in crate::hugr) fn edge_style<'a>(
};

// Compute the label for the edge, given the setting flags.
fn type_label(e: EdgeKind) -> Option<String> {
fn type_label(e: EdgeKind, config: RenderStringConfig) -> Option<String> {
match e {
EdgeKind::Const(ty) | EdgeKind::Value(ty) => Some(format!("{ty}")),
EdgeKind::Function(pf) => Some(format!("{pf}")),
EdgeKind::Const(ty) | EdgeKind::Value(ty) => Some(ty.render_str(config)),
// todo: use the render_str method for function types
EdgeKind::Function(pf) => Some(pf.render_str(config)),
_ => None,
Comment thread
nicolaassolini-qntm marked this conversation as resolved.
}
}
//
// Only static and value edges have types to display.
let label = match (
config.port_offsets_in_edges,
type_label(port_kind).filter(|_| config.type_labels_in_edges),
type_label(port_kind, config.render_string_config)
.filter(|_| config.type_labels_in_edges),
) {
(true, Some(ty)) => {
format!("{}:{}\n{ty}", src_offset.index(), tgt_offset.index())
Expand All @@ -358,7 +396,13 @@ pub(in crate::hugr) fn edge_style<'a>(

#[cfg(test)]
mod tests {
use crate::{NodeIndex, builder::test::simple_dfg_hugr};
use crate::{
NodeIndex,
builder::{DFGBuilder, Dataflow, DataflowHugr, test::simple_dfg_hugr},
extension::prelude::bool_t,
std_extensions::arithmetic::{int_ops::IntOpDef, int_types::int_type},
types::Signature,
};

use super::*;

Expand All @@ -375,4 +419,38 @@ mod tests {
.with_node_labels(NodeLabel::Custom(node_labels));
insta::assert_snapshot!(h.mermaid_string_with_formatter(config));
}

#[test]
fn render_string_config_is_applied_to_node_labels() {
let int_type = int_type(5);
let mut builder =
DFGBuilder::new(Signature::new([int_type.clone(), int_type], [bool_t()])).unwrap();
let [lhs, rhs] = builder.input_wires_arr();
let output = builder
.add_dataflow_op(IntOpDef::ieq.with_log_width(5), [lhs, rhs])
.unwrap()
.out_wire(0);
let h = builder.finish_hugr_with_outputs([output]).unwrap();

let options_on = h
.mermaid_format()
.with_render_string_config(RenderStringConfig {
extension_version: true,
print_type_args: true,
qualify_name: true,
})
.finish();
let unqualified = h
.mermaid_format()
.with_render_string_config(RenderStringConfig::default())
.finish();

assert!(options_on.contains("arithmetic.int.ieq<5>@0.1.1"));
assert!(!unqualified.contains("arithmetic.int.ieq"));
assert!(unqualified.contains("ieq"));

assert!(options_on.contains("<br>arithmetic.int.types.int<5>@0.1.0"));
assert!(!unqualified.contains("<br>arithmetic.int.types.int"));
assert!(unqualified.contains("<br>int"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ graph LR
style 4 stroke:#832561,stroke-width:3px
5["(5) Input"]
6["(6) Output"]
7["(7) test.quantum.CX"]
7["(7) CX"]
5--"0:1<br>qubit"-->7
5--"1:0<br>qubit"-->7
7--"0:0<br>qubit"-->6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ graph LR
style 4 stroke:#832561,stroke-width:3px
5["(5) Input"]
6["(6) Output"]
7["(7) test.quantum.CX"]
7["(7) CX"]
5--"0:1<br>qubit"-->7
5--"1:2<br>qubit"-->6
5--"2:0<br>qubit"-->7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ digraph {
5:out0 -> 7:in0 [style=""]
5:out1 -> 7:in1 [style=""]
6 [shape=plain label=<<table border="1"><tr><td port="in0" align="text" colspan="1" cellpadding="1" >0: qubit</td><td port="in1" align="text" colspan="1" cellpadding="1" >1: qubit</td></tr><tr><td align="text" border="0" colspan="2">(6) Output</td></tr></table>>]
7 [shape=plain label=<<table border="1"><tr><td port="in0" align="text" colspan="3" cellpadding="1" >0: qubit</td><td port="in1" align="text" colspan="3" cellpadding="1" >1: qubit</td></tr><tr><td align="text" border="0" colspan="6">(7) test.quantum.CX</td></tr><tr><td port="out0" align="text" colspan="2" cellpadding="1" >0: qubit</td><td port="out1" align="text" colspan="2" cellpadding="1" >1: qubit</td><td port="out2" align="text" colspan="2" cellpadding="1" border="0"></td></tr></table>>]
7 [shape=plain label=<<table border="1"><tr><td port="in0" align="text" colspan="3" cellpadding="1" >0: qubit</td><td port="in1" align="text" colspan="3" cellpadding="1" >1: qubit</td></tr><tr><td align="text" border="0" colspan="6">(7) CX</td></tr><tr><td port="out0" align="text" colspan="2" cellpadding="1" >0: qubit</td><td port="out1" align="text" colspan="2" cellpadding="1" >1: qubit</td><td port="out2" align="text" colspan="2" cellpadding="1" border="0"></td></tr></table>>]
7:out0 -> 8:in1 [style=""]
7:out1 -> 8:in0 [style=""]
7:out2 -> 8:in2 [style="dotted"]
8 [shape=plain label=<<table border="1"><tr><td port="in0" align="text" colspan="2" cellpadding="1" >0: qubit</td><td port="in1" align="text" colspan="2" cellpadding="1" >1: qubit</td><td port="in2" align="text" colspan="2" cellpadding="1" border="0"></td></tr><tr><td align="text" border="0" colspan="6">(8) test.quantum.CX</td></tr><tr><td port="out0" align="text" colspan="3" cellpadding="1" >0: qubit</td><td port="out1" align="text" colspan="3" cellpadding="1" >1: qubit</td></tr></table>>]
8 [shape=plain label=<<table border="1"><tr><td port="in0" align="text" colspan="2" cellpadding="1" >0: qubit</td><td port="in1" align="text" colspan="2" cellpadding="1" >1: qubit</td><td port="in2" align="text" colspan="2" cellpadding="1" border="0"></td></tr><tr><td align="text" border="0" colspan="6">(8) CX</td></tr><tr><td port="out0" align="text" colspan="3" cellpadding="1" >0: qubit</td><td port="out1" align="text" colspan="3" cellpadding="1" >1: qubit</td></tr></table>>]
8:out0 -> 6:in0 [style=""]
8:out1 -> 6:in1 [style=""]
hier0 [shape=plain label="0"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ graph LR
style 4 stroke:#832561,stroke-width:3px
5["(5) Input"]
6["(6) Output"]
7["(7) test.quantum.CX"]
8["(8) test.quantum.CX"]
7["(7) CX"]
8["(8) CX"]
5--"0:0<br>qubit"-->7
5--"1:1<br>qubit"-->7
7--"0:1<br>qubit"-->8
Expand Down
14 changes: 14 additions & 0 deletions hugr-core/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,12 +548,26 @@ pub trait StaticTag {
const TAG: OpTag;
}

/// Configuration for rendering an operation as a string.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderStringConfig {
/// Include the version of the extension defining the operation.
pub extension_version: bool,
/// Include the operation's type arguments.
pub print_type_args: bool,
/// Qualify operation name with their extension identifier.
pub qualify_name: bool,
}

#[enum_dispatch]
/// Trait implemented by all `OpType` variants.
pub trait OpTrait: Sized + Clone {
/// A human-readable description of the operation.
fn description(&self) -> &str;

/// Returns an exhaustive string representation of the operation.
fn render_str(&self, config: RenderStringConfig) -> String;

/// Tag identifying the operation.
fn tag(&self) -> OpTag;

Expand Down
6 changes: 5 additions & 1 deletion hugr-core/src/ops/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod serialize;
use std::collections::hash_map::DefaultHasher; // Moves into std::hash in Rust 1.76.
use std::hash::{Hash, Hasher};

use super::{NamedOp, OpName, OpTrait, StaticTag};
use super::{NamedOp, OpName, OpTrait, RenderStringConfig, StaticTag};
use super::{OpTag, OpType};
use crate::types::{CustomType, EdgeKind, SumType, SumTypeError, Type, TypeRow};
use serialize::SerialSum;
Expand Down Expand Up @@ -83,6 +83,10 @@ impl OpTrait for Const {
"Constant value"
}

fn render_str(&self, _config: RenderStringConfig) -> String {
self.name().to_string()
}

fn tag(&self) -> OpTag {
<Self as StaticTag>::TAG
}
Expand Down
Loading
Loading