diff --git a/hugr-core/src/hugr/views.rs b/hugr-core/src/hugr/views.rs index c64ef4fb80..5f5fa2f3fe 100644 --- a/hugr-core/src/hugr/views.rs +++ b/hugr-core/src/hugr/views.rs @@ -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}; @@ -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)) + } /// Return the mermaid representation of the underlying hierarchical graph /// according to the provided [`MermaidFormatter`] formatting options. diff --git a/hugr-core/src/hugr/views/render.rs b/hugr-core/src/hugr/views/render.rs index 1241a84073..d431df1475 100644 --- a/hugr-core/src/hugr/views/render.rs +++ b/hugr-core/src/hugr/views/render.rs @@ -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}; @@ -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, + /// How operation names are rendered in node labels. + render_string_config: RenderStringConfig, } impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { @@ -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(), } } @@ -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) -> Self { self.node_labels = node_labels; @@ -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>) -> Self { self.entrypoint = entrypoint.into(); @@ -102,6 +116,7 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { port_offsets_in_edges, type_labels_in_edges, entrypoint, + render_string_config, } = self; MermaidFormatter { hugr, @@ -109,6 +124,7 @@ impl<'h, H: HugrInternals + ?Sized> MermaidFormatter<'h, H> { port_offsets_in_edges, type_labels_in_edges, entrypoint, + render_string_config, } } } @@ -132,6 +148,7 @@ macro_rules! impl_mermaid_formatter_from { port_offsets_in_edges, type_labels_in_edges, entrypoint, + render_string_config, } = value; MermaidFormatter { hugr, @@ -139,6 +156,7 @@ macro_rules! impl_mermaid_formatter_from { port_offsets_in_edges, type_labels_in_edges, entrypoint, + render_string_config, } } } @@ -161,6 +179,7 @@ impl<'h, H: HugrView + ToOwned> From From( h: &'a Hugr, formatter: MermaidFormatter<'a>, ) -> Box 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) + ) } } @@ -215,21 +240,26 @@ pub(in crate::hugr) fn node_style<'a>( 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| { @@ -252,10 +282,16 @@ 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| { @@ -263,14 +299,14 @@ pub(in crate::hugr) fn node_style<'a>( 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) )) } }), @@ -332,10 +368,10 @@ pub(in crate::hugr) fn edge_style<'a>( }; // Compute the label for the edge, given the setting flags. - fn type_label(e: EdgeKind) -> Option { + fn type_label(e: EdgeKind, config: RenderStringConfig) -> Option { 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)), + EdgeKind::Function(pf) => Some(pf.render_str(config)), _ => None, } } @@ -343,7 +379,8 @@ pub(in crate::hugr) fn edge_style<'a>( // 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()) @@ -358,7 +395,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::*; @@ -375,4 +418,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("
arithmetic.int.types.int<5>@0.1.0")); + assert!(!unqualified.contains("
arithmetic.int.types.int")); + assert!(unqualified.contains("
int")); + } } diff --git a/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cx_gate.snap b/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cx_gate.snap index ed5fd60879..96a4a17d03 100644 --- a/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cx_gate.snap +++ b/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cx_gate.snap @@ -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
qubit"-->7 5--"1:0
qubit"-->7 7--"0:0
qubit"-->6 diff --git a/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cycle_3qb.snap b/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cycle_3qb.snap index 9dd0ad783c..f67ae9f362 100644 --- a/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cycle_3qb.snap +++ b/hugr-core/src/hugr/views/root_checked/snapshots/hugr_core__hugr__views__root_checked__dfg__test__map_io_cycle_3qb.snap @@ -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
qubit"-->7 5--"1:2
qubit"-->6 5--"2:0
qubit"-->7 diff --git a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__dot_dfg.snap b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__dot_dfg.snap index 574235de42..3125e04dea 100644 --- a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__dot_dfg.snap +++ b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__dot_dfg.snap @@ -16,11 +16,11 @@ digraph { 5:out0 -> 7:in0 [style=""] 5:out1 -> 7:in1 [style=""] 6 [shape=plain label=<
0: qubit1: qubit
(6) Output
>] -7 [shape=plain label=<
0: qubit1: qubit
(7) test.quantum.CX
0: qubit1: qubit
>] +7 [shape=plain label=<
0: qubit1: qubit
(7) CX
0: qubit1: qubit
>] 7:out0 -> 8:in1 [style=""] 7:out1 -> 8:in0 [style=""] 7:out2 -> 8:in2 [style="dotted"] -8 [shape=plain label=<
0: qubit1: qubit
(8) test.quantum.CX
0: qubit1: qubit
>] +8 [shape=plain label=<
0: qubit1: qubit
(8) CX
0: qubit1: qubit
>] 8:out0 -> 6:in0 [style=""] 8:out1 -> 6:in1 [style=""] hier0 [shape=plain label="0"] diff --git a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__mmd_dfg.snap b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__mmd_dfg.snap index fcbbbafe3e..e105065f81 100644 --- a/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__mmd_dfg.snap +++ b/hugr-core/src/hugr/views/snapshots/hugr_core__hugr__views__tests__mmd_dfg.snap @@ -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
qubit"-->7 5--"1:1
qubit"-->7 7--"0:1
qubit"-->8 diff --git a/hugr-core/src/ops.rs b/hugr-core/src/ops.rs index fb8148ce61..a5e7002a79 100644 --- a/hugr-core/src/ops.rs +++ b/hugr-core/src/ops.rs @@ -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; diff --git a/hugr-core/src/ops/constant.rs b/hugr-core/src/ops/constant.rs index 0dfd32e266..555ccec92d 100644 --- a/hugr-core/src/ops/constant.rs +++ b/hugr-core/src/ops/constant.rs @@ -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; @@ -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 { ::TAG } diff --git a/hugr-core/src/ops/controlflow.rs b/hugr-core/src/ops/controlflow.rs index 5838495d60..b57ba3cd4a 100644 --- a/hugr-core/src/ops/controlflow.rs +++ b/hugr-core/src/ops/controlflow.rs @@ -7,7 +7,7 @@ use crate::types::{EdgeKind, Signature, Type, TypeRow, TypeRowLike}; use super::OpTag; use super::dataflow::{DataflowOpTrait, DataflowParent}; -use super::{OpTrait, StaticTag, impl_op_name}; +use super::{NamedOp, OpTrait, RenderStringConfig, StaticTag, impl_op_name}; /// Tail-controlled loop. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -30,6 +30,10 @@ impl DataflowOpTrait for TailLoop { "A tail-controlled loop" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { // TODO: Store a cached signature let [inputs, outputs] = @@ -105,6 +109,10 @@ impl DataflowOpTrait for Conditional { "HUGR conditional operation" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { // TODO: Store a cached signature let mut inputs = self.other_inputs.clone(); @@ -147,6 +155,10 @@ impl DataflowOpTrait for CFG { "A dataflow node defined by a child CFG" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { Cow::Borrowed(&self.signature) } @@ -206,6 +218,11 @@ impl OpTrait for DataflowBlock { fn description(&self) -> &'static str { "A CFG basic block node" } + + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + /// Tag identifying the operation. fn tag(&self) -> OpTag { Self::TAG @@ -239,6 +256,11 @@ impl OpTrait for ExitBlock { fn description(&self) -> &'static str { "A CFG exit block node" } + + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + /// Tag identifying the operation. fn tag(&self) -> OpTag { Self::TAG @@ -321,6 +343,10 @@ impl OpTrait for Case { "A case node inside a conditional" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn tag(&self) -> OpTag { ::TAG } diff --git a/hugr-core/src/ops/custom.rs b/hugr-core/src/ops/custom.rs index bbd3b54a10..ead3fcfdc2 100644 --- a/hugr-core/src/ops/custom.rs +++ b/hugr-core/src/ops/custom.rs @@ -19,7 +19,7 @@ use crate::{IncomingPort, ops}; use super::dataflow::DataflowOpTrait; use super::tag::OpTag; -use super::{NamedOp, OpName, OpNameRef}; +use super::{NamedOp, OpName, OpNameRef, RenderStringConfig}; /// An operation defined by an [`OpDef`] from a loaded [Extension]. /// @@ -228,6 +228,20 @@ impl DataflowOpTrait for ExtensionOp { self.def().description() } + fn render_str(&self, config: RenderStringConfig) -> String { + let name = render_name_with_args( + &self.qualified_id(), + self.unqualified_id(), + self.args(), + config, + ); + if config.extension_version { + format!("{}@{}", name, self.extension_version()) + } else { + name + } + } + fn signature(&self) -> Cow<'_, Signature> { Cow::Borrowed(&self.signature) } @@ -286,6 +300,29 @@ pub(crate) fn qualify_name(res_id: &ExtensionId, name: &OpNameRef) -> OpName { format!("{res_id}.{name}").into() } +/// Renders the operation name (optionally qualified) with its type arguments per `config`. +/// The extension version suffix is handled separately by each caller. +fn render_name_with_args( + qualified_id: &OpNameRef, + unqualified_id: &OpNameRef, + args: &[TypeArg], + config: RenderStringConfig, +) -> String { + let mut name = if config.qualify_name { + qualified_id.to_string() + } else { + unqualified_id.to_string() + }; + if config.print_type_args && !args.is_empty() { + name = format!( + "{}<{}>", + name, + args.iter().map(|arg| arg.render_str(config)).join(", ") + ); + } + name +} + impl OpaqueOp { /// Creates a new `OpaqueOp` from all the fields we'd expect to serialize. pub fn new( @@ -383,6 +420,21 @@ impl DataflowOpTrait for OpaqueOp { "Opaque operation" } + fn render_str(&self, config: RenderStringConfig) -> String { + let name = render_name_with_args( + &self.qualified_id(), + self.unqualified_id(), + self.args(), + config, + ); + if config.extension_version + && let Some(version) = self.extension_version() + { + return format!("{}@{}", name, version); + } + name + } + fn signature(&self) -> Cow<'_, Signature> { Cow::Borrowed(&self.signature) } @@ -447,12 +499,14 @@ pub enum OpaqueOpError { #[cfg(test)] mod test { - use ops::OpType; + use ops::{OpTrait, OpType}; use crate::extension::ExtensionRegistry; use crate::extension::resolution::resolve_op_extensions; + use crate::extension::simple_op::MakeRegisteredOp; use crate::std_extensions::STD_REG; use crate::std_extensions::arithmetic::conversions::{self}; + use crate::std_extensions::arithmetic::int_ops::IntOpDef; use crate::types::Type; use crate::{ Extension, @@ -483,6 +537,20 @@ mod test { sig.clone(), ); assert_eq!(op.name(), "OpaqueOp:res.op"); + assert_eq!( + OpTrait::render_str(&op, RenderStringConfig::default()), + "op" + ); + assert_eq!( + OpTrait::render_str( + &op, + RenderStringConfig { + qualify_name: true, + ..Default::default() + } + ), + "res.op" + ); assert_eq!(op.args(), &[usize_t().into()]); assert_eq!(op.signature().as_ref(), &sig); @@ -536,9 +604,93 @@ mod test { }, ); let ext_op = ext.instantiate_extension_op("op", []).unwrap(); + assert_eq!( + OpTrait::render_str(&ext_op, RenderStringConfig::default()), + "op" + ); + assert_eq!( + OpTrait::render_str( + &ext_op, + RenderStringConfig { + qualify_name: true, + ..Default::default() + } + ), + "ext.op" + ); assert_eq!(ext_op.make_opaque().extension_version(), Some(&version)); } + #[test] + fn render_extension_version() { + let ext = Extension::new_arc( + "ext".try_into().unwrap(), + Version::new(1, 2, 3), + |ext, extension_ref| { + ext.add_op( + "op".into(), + String::new(), + SignatureFunc::PolyFuncType( + FuncValueType::from(Signature::new_endo([bool_t()])).into(), + ), + extension_ref, + ) + .unwrap(); + }, + ); + let ext_op = ext.instantiate_extension_op("op", []).unwrap(); + let config = RenderStringConfig { + extension_version: true, + ..Default::default() + }; + + assert_eq!(OpTrait::render_str(&ext_op, config), "op@1.2.3"); + assert_eq!( + OpTrait::render_str( + &ext_op, + RenderStringConfig { + qualify_name: true, + ..config + } + ), + "ext.op@1.2.3" + ); + + let mut opaque = ext_op.make_opaque(); + assert_eq!(OpTrait::render_str(&opaque, config), "op@1.2.3"); + opaque.set_extension_version(None); + assert_eq!(OpTrait::render_str(&opaque, config), "op"); + } + + #[test] + fn render_type_args() { + let ext_op = IntOpDef::ieq.with_log_width(5).to_extension_op().unwrap(); + let config = RenderStringConfig { + extension_version: true, + print_type_args: true, + qualify_name: true, + }; + + assert_eq!( + OpTrait::render_str(&ext_op, config), + "arithmetic.int.ieq<5>@0.1.1" + ); + assert_eq!( + OpTrait::render_str(&ext_op.make_opaque(), config), + "arithmetic.int.ieq<5>@0.1.1" + ); + assert_eq!( + OpTrait::render_str( + &ext_op, + RenderStringConfig { + print_type_args: false, + ..config + } + ), + "arithmetic.int.ieq@0.1.1" + ); + } + #[test] fn resolve_opaque_op() { let registry = &STD_REG; diff --git a/hugr-core/src/ops/dataflow.rs b/hugr-core/src/ops/dataflow.rs index e370fb2936..b4f58e1482 100644 --- a/hugr-core/src/ops/dataflow.rs +++ b/hugr-core/src/ops/dataflow.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; -use super::{OpTag, OpTrait, impl_op_name}; +use super::{NamedOp, OpTag, OpTrait, RenderStringConfig, impl_op_name}; use crate::extension::SignatureError; use crate::ops::StaticTag; @@ -22,6 +22,9 @@ pub trait DataflowOpTrait: Sized { /// A human-readable description of the operation. fn description(&self) -> &str; + /// Returns a string representation of the operation. + fn render_str(&self, config: RenderStringConfig) -> String; + /// The signature of the operation. fn signature(&self) -> Cow<'_, Signature>; @@ -109,6 +112,10 @@ impl DataflowOpTrait for Input { "The input node for this dataflow subgraph" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn other_input(&self) -> Option { None } @@ -131,6 +138,10 @@ impl DataflowOpTrait for Output { "The output node for this dataflow subgraph" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + // Note: We know what the input extensions should be, so we *could* give an // instantiated Signature instead fn signature(&self) -> Cow<'_, Signature> { @@ -154,6 +165,10 @@ impl OpTrait for T { DataflowOpTrait::description(self) } + fn render_str(&self, config: RenderStringConfig) -> String { + DataflowOpTrait::render_str(self, config) + } + fn tag(&self) -> OpTag { T::TAG } @@ -207,6 +222,10 @@ impl DataflowOpTrait for Call { "Call a function directly" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { Cow::Borrowed(&self.instantiation) } @@ -311,6 +330,10 @@ impl DataflowOpTrait for CallIndirect { "Call a function indirectly" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { // TODO: Store a cached signature let mut s = self.signature.clone(); @@ -342,6 +365,10 @@ impl DataflowOpTrait for LoadConstant { "Load a static constant in to the local dataflow graph" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { // TODO: Store a cached signature Cow::Owned(Signature::new(TypeRow::new(), vec![self.datatype.clone()])) @@ -407,6 +434,10 @@ impl DataflowOpTrait for LoadFunction { "Load a static function in to the local dataflow graph" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { Cow::Owned(Signature::new( type_row![], @@ -517,6 +548,10 @@ impl DataflowOpTrait for DFG { "A simply nested dataflow graph" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn signature(&self) -> Cow<'_, Signature> { self.inner_signature() } diff --git a/hugr-core/src/ops/module.rs b/hugr-core/src/ops/module.rs index eda121f235..db032d2710 100644 --- a/hugr-core/src/ops/module.rs +++ b/hugr-core/src/ops/module.rs @@ -13,7 +13,7 @@ use crate::Visibility; use crate::types::{EdgeKind, PolyFuncType, Signature, Type, TypeBound}; use super::dataflow::DataflowParent; -use super::{OpTag, OpTrait, StaticTag, impl_op_name}; +use super::{NamedOp, OpTag, OpTrait, RenderStringConfig, StaticTag, impl_op_name}; /// The root of a module, parent of all other `OpType`s. #[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] @@ -42,6 +42,10 @@ impl OpTrait for Module { "The root of a module, parent of all other `OpType`s" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + fn tag(&self) -> super::OpTag { ::TAG } @@ -131,6 +135,10 @@ impl OpTrait for FuncDefn { "A function definition" } + fn render_str(&self, _config: RenderStringConfig) -> String { + format!("FuncDefn: \"{}\"", self.func_name()) + } + fn tag(&self) -> OpTag { ::TAG } @@ -219,6 +227,10 @@ impl OpTrait for FuncDecl { "External function declaration, linked at runtime" } + fn render_str(&self, _config: RenderStringConfig) -> String { + format!("FuncDecl: \"{}\"", self.func_name()) + } + fn tag(&self) -> OpTag { ::TAG } @@ -249,6 +261,10 @@ impl OpTrait for AliasDefn { "A type alias definition" } + fn render_str(&self, _config: RenderStringConfig) -> String { + NamedOp::name(self).to_string() + } + fn tag(&self) -> OpTag { ::TAG } @@ -293,6 +309,10 @@ impl OpTrait for AliasDecl { "A type alias declaration" } + fn render_str(&self, _config: RenderStringConfig) -> String { + NamedOp::name(self).to_string() + } + fn tag(&self) -> OpTag { ::TAG } diff --git a/hugr-core/src/ops/sum.rs b/hugr-core/src/ops/sum.rs index 3ceb88d9a0..bb6a977aff 100644 --- a/hugr-core/src/ops/sum.rs +++ b/hugr-core/src/ops/sum.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use super::dataflow::DataflowOpTrait; -use super::{OpTag, impl_op_name}; +use super::{NamedOp, OpTag, RenderStringConfig, impl_op_name}; use crate::types::{EdgeKind, Signature, Type, TypeRow, TypeRowLike}; /// An operation that creates a tagged sum value from one of its variants. @@ -37,6 +37,10 @@ impl DataflowOpTrait for Tag { "Tag Sum operation" } + fn render_str(&self, _config: RenderStringConfig) -> String { + self.name().to_string() + } + /// The signature of the operation. fn signature(&self) -> Cow<'_, Signature> { // TODO: Store a cached signature diff --git a/hugr-core/src/types.rs b/hugr-core/src/types.rs index e35af19c26..f30a470fde 100644 --- a/hugr-core/src/types.rs +++ b/hugr-core/src/types.rs @@ -505,6 +505,11 @@ impl Type { Err(ExtensionCollectionError::dropped_type(self, missing)) } } + + /// Render the type as a string using the supplied configuration. + pub fn render_str(&self, config: crate::ops::RenderStringConfig) -> String { + self.0.render_str(config) + } } impl Transformable for Type { diff --git a/hugr-core/src/types/custom.rs b/hugr-core/src/types/custom.rs index a18c54f5aa..c269b4a6f0 100644 --- a/hugr-core/src/types/custom.rs +++ b/hugr-core/src/types/custom.rs @@ -181,9 +181,9 @@ impl Display for CustomType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.id)?; if !self.args.is_empty() { - write!(f, "(")?; + write!(f, "<")?; crate::utils::display_list(&self.args, f)?; - write!(f, ")")?; + write!(f, ">")?; } Ok(()) } diff --git a/hugr-core/src/types/poly_func.rs b/hugr-core/src/types/poly_func.rs index 3b2c0af568..f8bf113fd9 100644 --- a/hugr-core/src/types/poly_func.rs +++ b/hugr-core/src/types/poly_func.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use crate::{ extension::SignatureError, + ops::RenderStringConfig, types::{TypeRow, TypeRowLike, TypeRowRV}, }; @@ -102,6 +103,28 @@ impl PolyFuncTypeBase { } impl PolyFuncTypeBase { + /// Render the polymorphic function type using the supplied configuration. + pub fn render_str(&self, config: RenderStringConfig) -> String { + let params = if self.params.is_empty() { + Cow::Borrowed("") + } else { + Cow::Owned(format!( + "∀ {}. ", + self.params + .iter() + .enumerate() + .map(|(i, param)| format!("(#{i} : {})", param.render_str(config))) + .join(" ") + )) + }; + + format!( + "{params}{} -> {}", + self.body.input().render_str(config), + self.body.output().render_str(config) + ) + } + /// The type parameters, aka binders, over which this type is polymorphic pub fn params(&self) -> &[TypeParam] { &self.params @@ -212,6 +235,26 @@ pub(crate) mod test { } } + #[test] + fn render_str_propagates_config() { + use crate::ops::RenderStringConfig; + use crate::std_extensions::arithmetic::int_types::int_type; + + let poly_func: crate::types::PolyFuncType = PolyFuncTypeBase::new( + [TypeBound::Linear.into()], + Signature::new([Type::new_var_use(0, TypeBound::Linear)], [int_type(5)]), + ); + + assert_eq!( + poly_func.render_str(RenderStringConfig { + extension_version: true, + print_type_args: true, + qualify_name: true, + }), + "∀ (#0 : Type). [#0] -> [arithmetic.int.types.int<5>@0.1.0]" + ); + } + #[test] fn test_opaque() -> Result<(), SignatureError> { let list_def = list::EXTENSION.get_type(&list::LIST_TYPENAME).unwrap(); diff --git a/hugr-core/src/types/type_param.rs b/hugr-core/src/types/type_param.rs index e5c07fafb5..1250a572cb 100644 --- a/hugr-core/src/types/type_param.rs +++ b/hugr-core/src/types/type_param.rs @@ -17,6 +17,7 @@ use tracing::warn; use super::{Substitution, Transformable, Type, TypeBound, TypeRowLike, TypeTransformer}; use crate::extension::SignatureError; +use crate::ops::RenderStringConfig; use crate::types::{CustomType, FuncValueType, SumType}; /// The upper non-inclusive bound of a [`TypeParam::BoundedNat`] @@ -177,6 +178,87 @@ impl Term { /// An empty list of Terms. pub const EMPTY_LIST: Self = Self::List(vec![]); + /// Returns a string representation of this term. + /// + /// Composite terms recursively render each of their nested terms. + pub fn render_str(&self, config: RenderStringConfig) -> String { + match self { + Self::ListKind(term) => format!("List[{}]", term.render_str(config)), + Self::TupleKind(term) => format!("Tuple[{}]", term.render_str(config)), + Self::ExtensionType(custom_type) => { + let mut name = if config.qualify_name { + format!("{}.{}", custom_type.extension(), custom_type.name()) + } else { + custom_type.name().to_string() + }; + if config.print_type_args && !custom_type.args().is_empty() { + name = format!( + "{}<{}>", + name, + custom_type + .args() + .iter() + .map(|arg| arg.render_str(config)) + .join(", ") + ); + } + if config.extension_version + && let Some(version) = custom_type.extension_version() + { + name = format!("{name}@{version}"); + } + name + } + Self::FunctionType(function_type) => format!( + "{} -> {}", + function_type.input().render_str(config), + function_type.output().render_str(config) + ), + Self::SumType(sum_type) => { + if sum_type.num_variants() == 0 { + return "⊥".to_string(); + } + + match sum_type { + SumType::Unit { size: 1 } => "Unit".to_string(), + SumType::Unit { size: 2 } => "Bool".to_string(), + SumType::Unit { size } => itertools::repeat_n("[]", *size as usize).join("+"), + SumType::General(sum) => match sum.rows() { + [row] if row.is_empty() => "Unit".to_string(), + [left, right] if left.is_empty() && right.is_empty() => "Bool".to_string(), + rows => rows.iter().map(|row| row.render_str(config)).join("+"), + }, + } + } + Self::List(terms) => format!( + "[{}]", + terms.iter().map(|term| term.render_str(config)).join(", ") + ), + Self::ListConcat(terms) => format!( + "[{}]", + terms + .iter() + .map(|term| format!("... {}", term.render_str(config))) + .join(",") + ), + Self::Tuple(terms) => { + format!( + "({})", + terms.iter().map(|term| term.render_str(config)).join(",") + ) + } + Self::TupleConcat(terms) => format!( + "({})", + terms + .iter() + .map(|term| format!("... {}", term.render_str(config))) + .join(",") + ), + Self::ConstKind(ty) => ty.render_str(config), + _ => self.to_string(), + } + } + /// Creates a [`Term::BoundedNatKind`] with the maximum bound (`u64::MAX` + 1). #[must_use] pub const fn max_nat_kind() -> Self { @@ -947,6 +1029,132 @@ mod test { use crate::types::type_param::SeqPart; use crate::types::{Term, Type, TypeBound, TypeRow, type_param::TermKindError}; + #[test] + fn render_nested_term() { + let term = Term::Tuple(vec![ + Term::List(vec![ + Term::BoundedNat(1), + Term::Tuple(vec![Term::String("inner".into()), Term::BoundedNat(2)]), + ]), + Term::new_list_kind(Term::Tuple(vec![Term::BoundedNat(3), Term::BoundedNat(4)])), + ]); + + assert_eq!( + term.render_str(crate::ops::RenderStringConfig::default()), + r#"([1, ("inner",2)],List[(3,4)])"# + ); + } + + #[test] + fn render_composite_term_cases() { + use crate::ops::RenderStringConfig; + use crate::std_extensions::arithmetic::int_types::int_type; + + let config = RenderStringConfig::default(); + assert_eq!( + Term::new_tuple_kind(Term::new_tuple([Term::from(1_u64), Term::from(2_u64),])) + .render_str(config), + "Tuple[(1,2)]" + ); + assert_eq!( + Term::ListConcat(vec![Term::new_list([1_u64]), Term::new_list([2_u64])]) + .render_str(config), + "[... [1],... [2]]" + ); + assert_eq!( + Term::new_tuple_concat([ + Term::new_tuple([Term::from(1_u64)]), + Term::new_tuple([Term::from(2_u64)]), + ]) + .render_str(config), + "(... (1),... (2))" + ); + assert_eq!(Term::new_const(int_type(5)).render_str(config), "int"); + } + + #[test] + fn render_extension_type_config_cases() { + use crate::ops::RenderStringConfig; + use crate::std_extensions::arithmetic::int_types::int_type; + + let term = Term::from(int_type(5)); + assert_eq!(term.render_str(RenderStringConfig::default()), "int"); + assert_eq!( + term.render_str(RenderStringConfig { + extension_version: true, + print_type_args: true, + qualify_name: true, + }), + "arithmetic.int.types.int<5>@0.1.0" + ); + } + + #[test] + fn render_function_type_propagates_config() { + use crate::ops::RenderStringConfig; + use crate::std_extensions::arithmetic::int_types::int_type; + use crate::types::FuncValueType; + + let term = Term::FunctionType(Box::new(FuncValueType::new([int_type(5)], [int_type(6)]))); + let config = RenderStringConfig { + extension_version: true, + print_type_args: true, + qualify_name: true, + }; + + assert_eq!( + term.render_str(config), + "[arithmetic.int.types.int<5>@0.1.0] -> [arithmetic.int.types.int<6>@0.1.0]" + ); + } + + #[test] + fn render_sum_type_propagates_config() { + use crate::ops::RenderStringConfig; + use crate::std_extensions::arithmetic::int_types::int_type; + use crate::types::{GeneralSum, SumType, TypeRowRV}; + + let config = RenderStringConfig::default(); + assert_eq!(Term::SumType(SumType::new_unary(0)).render_str(config), "⊥"); + assert_eq!( + Term::SumType(SumType::new_unary(1)).render_str(config), + "Unit" + ); + assert_eq!( + Term::SumType(SumType::new_unary(2)).render_str(config), + "Bool" + ); + assert_eq!( + Term::SumType(SumType::new_unary(3)).render_str(config), + "[]+[]+[]" + ); + assert_eq!( + Term::SumType(SumType::General(GeneralSum::new(vec![TypeRowRV::new()]))) + .render_str(config), + "Unit" + ); + assert_eq!( + Term::SumType(SumType::General(GeneralSum::new(vec![ + TypeRowRV::new(), + TypeRowRV::new(), + ]))) + .render_str(config), + "Bool" + ); + + let term = Term::SumType(SumType::new([[int_type(5)], [int_type(6)]])); + let config = RenderStringConfig { + extension_version: true, + print_type_args: true, + qualify_name: true, + }; + + assert_eq!( + term.render_str(config), + "[arithmetic.int.types.int<5>@0.1.0]+[arithmetic.int.types.int<6>@0.1.0]" + ); + } + #[test] fn new_list_from_parts_items() { let a = TypeArg::new_string("a"); diff --git a/hugr-core/src/types/type_row.rs b/hugr-core/src/types/type_row.rs index eb23e3f76c..32c69a1ed2 100644 --- a/hugr-core/src/types/type_row.rs +++ b/hugr-core/src/types/type_row.rs @@ -82,6 +82,7 @@ impl TypeRow { /// warning when the trait is used as a type bound on a public struct. mod internal { use super::{SignatureError, Substitution, Transformable, TypeParam}; + use crate::ops::RenderStringConfig; /// Sub-trait of [`Transformable`] implemented by things that represent /// rows of types (fixed-length [`TypeRow`] or variable-length [`TypeRowRV`]). @@ -89,6 +90,9 @@ mod internal { /// [`TypeRow`]: super::TypeRow /// [`TypeRowRV`]: super::TypeRowRV pub trait TypeRowLike: Transformable { + /// Render this row using the supplied configuration. + fn render_str(&self, config: RenderStringConfig) -> String; + /// Checks all variables used in `self` are in the provided list of bound /// variables, and that for each [`CustomType`] the corresponding [`TypeDef`] /// is in the [`ExtensionRegistry`] and the type arguments validate (recursively) @@ -113,6 +117,13 @@ mod internal { pub(crate) use internal::TypeRowLike; impl TypeRowLike for TypeRow { + fn render_str(&self, config: crate::ops::RenderStringConfig) -> String { + format!( + "[{}]", + self.iter().map(|ty| ty.render_str(config)).join(", ") + ) + } + fn validate(&self, var_decls: &[TypeParam]) -> Result<(), SignatureError> { self.iter().try_for_each(|t| t.validate(var_decls)) } @@ -287,6 +298,10 @@ impl TypeRowRV { } impl TypeRowLike for TypeRowRV { + fn render_str(&self, config: crate::ops::RenderStringConfig) -> String { + self.0.render_str(config) + } + /// Checks that this is indeed a list of runtime types; /// and that all variables are as declared in the supplied list of params. fn validate(&self, vars: &[TypeParam]) -> Result<(), SignatureError> { diff --git a/hugr-persistent/src/trait_impls.rs b/hugr-persistent/src/trait_impls.rs index 1cbd6860f9..8cfff79328 100644 --- a/hugr-persistent/src/trait_impls.rs +++ b/hugr-persistent/src/trait_impls.rs @@ -292,7 +292,8 @@ impl HugrView for PersistentHugr { .with_entrypoint(entrypoint) .with_node_labels(node_labels) .with_port_offsets(formatter.port_offsets()) - .with_type_labels(formatter.type_labels()); + .with_type_labels(formatter.type_labels()) + .with_render_string_config(formatter.render_string_config()); config.finish() }