From 1723b489b2f82e644a1ada8ddac9aa3c55532c48 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 Aug 2025 13:28:55 +0100 Subject: [PATCH 01/70] Add HugrMut::insert_forest / insert_view_forest, implement insert_hugr/from_view --- hugr-core/src/hugr/hugrmut.rs | 103 +++++++++++++++++++-------- hugr-core/src/hugr/views/impls.rs | 7 +- hugr-core/src/hugr/views/rerooted.rs | 7 +- 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 74a6d1461d..d34179c42d 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -188,7 +188,9 @@ pub trait HugrMut: HugrMutInternals { Self::insert_region(self, root, other, region) } - /// Insert a sub-region of another hugr into this one, under a given parent node. + /// Insert a subtree of another hugr into this one, under a given parent node. + /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected + /// in `self`. /// /// # Panics /// @@ -199,9 +201,17 @@ pub trait HugrMut: HugrMutInternals { root: Self::Node, other: Hugr, region: Node, - ) -> InsertionResult; + ) -> InsertionResult { + let node_map = self.insert_forest(other, HashMap::from([(region, root)])); + InsertionResult { + inserted_entrypoint: node_map[®ion], + node_map, + } + } - /// Copy another hugr into this one, under a given parent node. + /// Copy the entrypoint subtree of another hugr into this one, under a given parent node. + /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected + /// in `self`. /// /// # Panics /// @@ -210,7 +220,15 @@ pub trait HugrMut: HugrMutInternals { &mut self, root: Self::Node, other: &H, - ) -> InsertionResult; + ) -> InsertionResult { + let ep = other.entrypoint(); + let node_map = + self.insert_view_forest(other, other.descendants(ep), HashMap::from([(ep, root)])); + InsertionResult { + inserted_entrypoint: node_map[&ep], + node_map, + } + } /// Copy a subgraph from another hugr into this one, under a given parent node. /// @@ -233,6 +251,41 @@ pub trait HugrMut: HugrMutInternals { subgraph: &SiblingSubgraph, ) -> HashMap; + /// Insert a forest of nodes from another Hugr into this one. + /// `root_parents` maps from roots of regions in the other Hugr to insert, + /// to the node in this Hugr that shall be parent for that region. + /// + /// # Panics + /// + /// If any of the values in `roots` are not nodes in `self`. + // TODO: Error for doubly-included subtrees + fn insert_forest( + &mut self, + other: Hugr, + root_parents: HashMap, + ) -> HashMap; + + /// Copy a forest of nodes from a view into this one. + /// `nodes` enumerates all nodes to copy; `root_parents` identifies + /// those nodes from `other` which should be placed under the given + /// parent nodes in `self`. + /// + /// Note that item in `nodes` must *either* be present in `root_parents`, + /// or follow its parent (in `other`) in the iteration order. + /// + /// # Panics + /// + /// If any of the values in `roots` are not nodes in `self`. + /// ?? If `nodes` contains any duplicate elements, or does not adhere to the ordering + /// requirement above + // TODO: turn double-inclusion into error? + fn insert_view_forest( + &mut self, + other: &H, + nodes: impl IntoIterator, + roots: HashMap, + ) -> HashMap; + /// Applies a patch to the graph. fn apply_patch(&mut self, rw: impl Patch) -> Result where @@ -412,15 +465,17 @@ impl HugrMut for Hugr { (src_port, dst_port) } - fn insert_region( + fn insert_forest( &mut self, - root: Self::Node, mut other: Hugr, - region: Node, - ) -> InsertionResult { - let node_map = insert_hugr_internal(self, &other, other.descendants(region), |&n| { - if n == region { Some(root) } else { None } - }); + roots: HashMap, + ) -> HashMap { + let node_map = insert_hugr_internal( + self, + &other, + roots.keys().flat_map(|n| other.descendants(*n)), + |k| roots.get(k).cloned(), + ); // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, taking them from the other graph. @@ -434,24 +489,17 @@ impl HugrMut for Hugr { let meta = other.metadata.take(node_pg); self.metadata.set(new_node_pg, meta); } - InsertionResult { - inserted_entrypoint: node_map[®ion], - node_map, - } + node_map } - fn insert_from_view( + fn insert_view_forest( &mut self, - root: Self::Node, other: &H, - ) -> InsertionResult { - let node_map = insert_hugr_internal(self, other, other.entry_descendants(), |&n| { - if n == other.entrypoint() { - Some(root) - } else { - None - } - }); + nodes: impl IntoIterator, + roots: HashMap, + ) -> HashMap { + let node_map = + insert_hugr_internal(self, other, nodes.into_iter(), |k| roots.get(k).cloned()); // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. @@ -467,10 +515,7 @@ impl HugrMut for Hugr { .set(new_node.into_portgraph(), Some(meta.clone())); } } - InsertionResult { - inserted_entrypoint: node_map[&other.entrypoint()], - node_map, - } + node_map } fn insert_subgraph( diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index dbc9ad7b8f..befaa0c72e 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -1,6 +1,6 @@ //! Implementation of the core hugr traits for different wrappers of a `Hugr`. -use std::{borrow::Cow, rc::Rc, sync::Arc}; +use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc}; use super::HugrView; use crate::hugr::HugrMut; @@ -114,9 +114,8 @@ macro_rules! hugr_mut_methods { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_hugr(&mut self, root: Self::Node, other: crate::Hugr) -> crate::hugr::hugrmut::InsertionResult; - fn insert_region(&mut self, root: Self::Node, other: crate::Hugr, region: crate::Node) -> crate::hugr::hugrmut::InsertionResult; - fn insert_from_view(&mut self, root: Self::Node, other: &Other) -> crate::hugr::hugrmut::InsertionResult; + fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; + fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; fn insert_subgraph(&mut self, root: Self::Node, other: &Other, subgraph: &crate::hugr::views::SiblingSubgraph) -> std::collections::HashMap; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 18821cfe29..2b4a241b0b 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -1,6 +1,8 @@ //! A HUGR wrapper with a modified entrypoint node, returned by //! [`HugrView::with_entrypoint`] and [`HugrMut::with_entrypoint_mut`]. +use std::collections::HashMap; + use crate::hugr::HugrMut; use crate::hugr::internal::{HugrInternals, HugrMutInternals}; @@ -137,9 +139,8 @@ impl HugrMut for Rerooted { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_hugr(&mut self, root: Self::Node, other: crate::Hugr) -> crate::hugr::hugrmut::InsertionResult; - fn insert_region(&mut self, root: Self::Node, other: crate::Hugr, region: crate::Node) -> crate::hugr::hugrmut::InsertionResult; - fn insert_from_view(&mut self, root: Self::Node, other: &Other) -> crate::hugr::hugrmut::InsertionResult; + fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; + fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; fn insert_subgraph(&mut self, root: Self::Node, other: &Other, subgraph: &crate::hugr::views::SiblingSubgraph) -> std::collections::HashMap; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; From 2e665426a7a954caba3800b61832df359a900d1b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 Aug 2025 17:19:54 +0100 Subject: [PATCH 02/70] insert_subgraph delegates to insert_forest (copying nodes list) --- hugr-core/src/hugr/hugrmut.rs | 35 ++++++---------------------- hugr-core/src/hugr/views/impls.rs | 1 - hugr-core/src/hugr/views/rerooted.rs | 1 - 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index d34179c42d..b0f46da518 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -249,7 +249,13 @@ pub trait HugrMut: HugrMutInternals { root: Self::Node, other: &H, subgraph: &SiblingSubgraph, - ) -> HashMap; + ) -> HashMap { + self.insert_view_forest( + other, + subgraph.nodes().iter().cloned(), + subgraph.nodes().iter().map(|n| (*n, root)).collect(), + ) + } /// Insert a forest of nodes from another Hugr into this one. /// `root_parents` maps from roots of regions in the other Hugr to insert, @@ -518,33 +524,6 @@ impl HugrMut for Hugr { node_map } - fn insert_subgraph( - &mut self, - root: Self::Node, - other: &H, - subgraph: &SiblingSubgraph, - ) -> HashMap { - let node_map = insert_hugr_internal(self, other, subgraph.nodes().iter().copied(), |_| { - Some(root) - }); - // Update the optypes and metadata, copying them from the other graph. - for (&node, &new_node) in &node_map { - let nodetype = other.get_optype(node); - self.op_types - .set(new_node.into_portgraph(), nodetype.clone()); - let meta = other.node_metadata_map(node); - if !meta.is_empty() { - self.metadata - .set(new_node.into_portgraph(), Some(meta.clone())); - } - // Add the required extensions to the registry. - if let Ok(exts) = nodetype.used_extensions() { - self.use_extensions(exts); - } - } - node_map - } - fn copy_descendants( &mut self, root: Self::Node, diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index befaa0c72e..3c17100c5c 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -116,7 +116,6 @@ macro_rules! hugr_mut_methods { fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; - fn insert_subgraph(&mut self, root: Self::Node, other: &Other, subgraph: &crate::hugr::views::SiblingSubgraph) -> std::collections::HashMap; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 2b4a241b0b..ef29e8ea2c 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -141,7 +141,6 @@ impl HugrMut for Rerooted { fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; - fn insert_subgraph(&mut self, root: Self::Node, other: &Other, subgraph: &crate::hugr::views::SiblingSubgraph) -> std::collections::HashMap; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } From 02fea7312bc734270f7916c8f6cc416f8c7f2187 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 11 Aug 2025 17:56:48 +0100 Subject: [PATCH 03/70] errors; docs --- hugr-core/src/hugr/hugrmut.rs | 93 +++++++++++++++++++++------- hugr-core/src/hugr/views/impls.rs | 6 +- hugr-core/src/hugr/views/rerooted.rs | 6 +- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index b0f46da518..8ce30f20f6 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -202,7 +202,9 @@ pub trait HugrMut: HugrMutInternals { other: Hugr, region: Node, ) -> InsertionResult { - let node_map = self.insert_forest(other, HashMap::from([(region, root)])); + let node_map = self + .insert_forest(other, HashMap::from([(region, root)])) + .expect("No errors possible for single subtree"); InsertionResult { inserted_entrypoint: node_map[®ion], node_map, @@ -222,8 +224,9 @@ pub trait HugrMut: HugrMutInternals { other: &H, ) -> InsertionResult { let ep = other.entrypoint(); - let node_map = - self.insert_view_forest(other, other.descendants(ep), HashMap::from([(ep, root)])); + let node_map = self + .insert_view_forest(other, other.descendants(ep), HashMap::from([(ep, root)])) + .expect("No errors possible for single subtree"); InsertionResult { inserted_entrypoint: node_map[&ep], node_map, @@ -255,42 +258,58 @@ pub trait HugrMut: HugrMutInternals { subgraph.nodes().iter().cloned(), subgraph.nodes().iter().map(|n| (*n, root)).collect(), ) + .expect("SiblingSubgraph nodes are a set") } /// Insert a forest of nodes from another Hugr into this one. /// `root_parents` maps from roots of regions in the other Hugr to insert, /// to the node in this Hugr that shall be parent for that region. /// + /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` + /// and where each value is the corresponding (new) node in `self`. + /// + /// # Errors + /// + /// [InsertForestError::DoubleCopy] if the subtrees of the keys of `root_parents` + /// are not disjoint (the error indicates the root of the _inner_ subtree). + /// /// # Panics /// /// If any of the values in `roots` are not nodes in `self`. - // TODO: Error for doubly-included subtrees fn insert_forest( &mut self, other: Hugr, root_parents: HashMap, - ) -> HashMap; + ) -> InsertForestResult; /// Copy a forest of nodes from a view into this one. - /// `nodes` enumerates all nodes to copy; `root_parents` identifies - /// those nodes from `other` which should be placed under the given - /// parent nodes in `self`. /// - /// Note that item in `nodes` must *either* be present in `root_parents`, - /// or follow its parent (in `other`) in the iteration order. + /// `nodes` enumerates all nodes in `other` to copy; every item must *either* be + /// present in `root_parents`, or follow its parent (in `other`) in the iteration order. + /// + /// `root_parents` identifies those nodes in `nodes` which should be placed under + /// the given parent nodes in `self`. Note that unlike [Self::insert_forest] this + /// allows inserting most of a subtree in one location but with subparts of that + /// subtree placed elsewhere. + /// + /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` + /// and where each value is the corresponding (new) node in `self`. + /// + /// # Errors + /// + /// [InsertForestError::DoubleCopy] if any node appears in `nodes` more than once /// /// # Panics /// /// If any of the values in `roots` are not nodes in `self`. - /// ?? If `nodes` contains any duplicate elements, or does not adhere to the ordering - /// requirement above - // TODO: turn double-inclusion into error? + /// + /// If `nodes` does not adhere to the ordering requirement above fn insert_view_forest( &mut self, other: &H, nodes: impl IntoIterator, roots: HashMap, - ) -> HashMap; + ) -> InsertForestResult; /// Applies a patch to the graph. fn apply_patch(&mut self, rw: impl Patch) -> Result @@ -322,6 +341,22 @@ pub trait HugrMut: HugrMutInternals { ExtensionRegistry: Extend; } +/// Result of inserting a forest from a hugr of `SN` nodes, into a hugr with +/// `TN` nodes. +/// +/// On success, a map giving the new indices; or an error in the request. +/// Used by [HugrMut::insert_forest] and [HugrMut::insert_view_forest]. +pub type InsertForestResult = Result, InsertForestError>; + +/// An error from [HugrMut::insert_forest] or [HugrMut::insert_view_forest] +#[derive(Clone, Debug, derive_more::Display, derive_more::Error, PartialEq)] +#[non_exhaustive] +pub enum InsertForestError { + /// The specified source node would be copied twice into the target + #[display("Node/subtree {_0} would be copied twice")] + DoubleCopy(N), +} + /// Records the result of inserting a Hugr or view via [`HugrMut::insert_hugr`], /// [`HugrMut::insert_from_view`], or [`HugrMut::insert_region`]. /// @@ -475,13 +510,23 @@ impl HugrMut for Hugr { &mut self, mut other: Hugr, roots: HashMap, - ) -> HashMap { + ) -> Result, InsertForestError> { + for &r in roots.keys() { + let mut n = r; + while let Some(p) = other.get_parent(n) { + if roots.contains_key(&p) { + return Err(InsertForestError::DoubleCopy(r)); + } + n = p; + } + } let node_map = insert_hugr_internal( self, &other, roots.keys().flat_map(|n| other.descendants(*n)), |k| roots.get(k).cloned(), - ); + ) + .expect("Trees disjoint so no repeated nodes"); // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, taking them from the other graph. @@ -495,7 +540,7 @@ impl HugrMut for Hugr { let meta = other.metadata.take(node_pg); self.metadata.set(new_node_pg, meta); } - node_map + Ok(node_map) } fn insert_view_forest( @@ -503,9 +548,9 @@ impl HugrMut for Hugr { other: &H, nodes: impl IntoIterator, roots: HashMap, - ) -> HashMap { + ) -> Result, InsertForestError> { let node_map = - insert_hugr_internal(self, other, nodes.into_iter(), |k| roots.get(k).cloned()); + insert_hugr_internal(self, other, nodes.into_iter(), |k| roots.get(k).cloned())?; // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. @@ -521,7 +566,7 @@ impl HugrMut for Hugr { .set(new_node.into_portgraph(), Some(meta.clone())); } } - node_map + Ok(node_map) } fn copy_descendants( @@ -597,7 +642,7 @@ fn insert_hugr_internal( other: &H, other_nodes: impl Iterator, reroot: impl Fn(&H::Node) -> Option, -) -> HashMap { +) -> Result, InsertForestError> { let new_node_count_hint = other_nodes.size_hint().1.unwrap_or_default(); // Insert the nodes from the other graph into this one. @@ -609,7 +654,9 @@ fn insert_hugr_internal( // correct optype, avoiding cloning if possible. let op = OpType::default(); let new = hugr.add_node(op); - node_map.insert(old, new); + if node_map.insert(old, new).is_some() { + return Err(InsertForestError::DoubleCopy(old)); + } hugr.set_num_ports(new, other.num_inputs(old), other.num_outputs(old)); @@ -644,7 +691,7 @@ fn insert_hugr_internal( } } } - node_map + Ok(node_map) } #[cfg(test)] diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index 3c17100c5c..3fcaea0f8b 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -3,8 +3,8 @@ use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc}; use super::HugrView; -use crate::hugr::HugrMut; use crate::hugr::internal::{HugrInternals, HugrMutInternals}; +use crate::hugr::{HugrMut, hugrmut::InsertForestError}; macro_rules! hugr_internal_methods { // The extra ident here is because invocations of the macro cannot pass `self` as argument @@ -114,8 +114,8 @@ macro_rules! hugr_mut_methods { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; - fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; + fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> Result, InsertForestError>; + fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> Result, InsertForestError>; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index ef29e8ea2c..61e39a3316 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; -use crate::hugr::HugrMut; use crate::hugr::internal::{HugrInternals, HugrMutInternals}; +use crate::hugr::{HugrMut, hugrmut::InsertForestError}; use super::{HugrView, panic_invalid_node}; @@ -139,8 +139,8 @@ impl HugrMut for Rerooted { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> HashMap; - fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> HashMap; + fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> Result, InsertForestError>; + fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> Result, InsertForestError>; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } From fdee9444e2fd36aac4e7ae3fd2a534bd3f4492e5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 09:43:42 +0100 Subject: [PATCH 04/70] Tests --- hugr-core/src/hugr/hugrmut.rs | 180 +++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 8ce30f20f6..23dd5498ce 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -696,12 +696,17 @@ fn insert_hugr_internal( #[cfg(test)] mod test { + use cool_asserts::assert_matches; + use itertools::Itertools; + + use crate::builder::test::simple_dfg_hugr; + use crate::builder::{DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder}; use crate::extension::PRELUDE; - use crate::{ - extension::prelude::{Noop, usize_t}, - ops::{self, FuncDefn, Input, Output, dataflow::IOTrait}, - types::Signature, - }; + use crate::extension::prelude::{Noop, bool_t, usize_t}; + use crate::hugr::ValidationError; + use crate::ops::handle::{FuncID, NodeHandle}; + use crate::ops::{self, FuncDefn, Input, Output, dataflow::IOTrait}; + use crate::types::Signature; use super::*; @@ -780,4 +785,169 @@ mod test { hugr.validate().unwrap(); assert_eq!(hugr.num_nodes(), 1); } + + /// A DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, + /// to a FuncDefn and a FuncDecl each bool_t->bool_t, and their handles. + pub(crate) fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { + let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); + let new_defn = { + let mut mb = dfb.module_root_builder(); + let fb = mb + .define_function("helper_id", Signature::new_endo(bool_t())) + .unwrap(); + let [f_inp] = fb.input_wires_arr(); + fb.finish_with_outputs([f_inp]).unwrap() + }; + let new_decl = dfb + .module_root_builder() + .declare("helper2", Signature::new_endo(bool_t()).into()) + .unwrap(); + let cst = dfb.add_load_value(ops::Value::true_val()); + let [c1] = dfb + .call(new_defn.handle(), &[], [cst]) + .unwrap() + .outputs_arr(); + let [c2] = dfb.call(&new_decl, &[], [c1]).unwrap().outputs_arr(); + ( + dfb.finish_hugr_with_outputs([c2]).unwrap(), + *new_defn.handle(), + new_decl, + ) + } + + #[test] + fn test_insert_forest() { + // Specify which decls to transfer + for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { + let mut h = simple_dfg_hugr(); + let (insert, defn, decl) = dfg_calling_defn_decl(); + let roots = HashMap::from_iter( + std::iter::once((insert.entrypoint(), h.entrypoint())) + .chain(call1.then_some((defn.node(), h.module_root())).into_iter()) + .chain(call2.then_some((decl.node(), h.module_root())).into_iter()), + ); + h.insert_forest(insert, roots).unwrap(); + if call1 && call2 { + h.validate().unwrap(); + } else { + assert!(matches!( + h.validate(), + Err(ValidationError::UnconnectedPort { .. }) + )); + } + assert_eq!( + h.children(h.module_root()).count(), + 1 + (call1 as usize) + (call2 as usize) + ); + let [calln1, calln2] = h + .nodes() + .filter(|n| h.get_optype(*n).is_call()) + .collect_array() + .unwrap(); + + let tgt1 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_defn() + .is_some_and(|fd| fd.func_name() == "helper_id") + }); + assert_eq!(tgt1.is_some(), call1); + assert_eq!(h.static_source(calln1), tgt1); + + let tgt2 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_decl() + .is_some_and(|fd| fd.func_name() == "helper2") + }); + assert_eq!(tgt2.is_some(), call2); + assert_eq!(h.static_source(calln2), tgt2); + } + } + + #[test] + fn test_insert_view_forest() { + let (insert, defn, decl) = dfg_calling_defn_decl(); + let mut h = simple_dfg_hugr(); + + let mut roots = HashMap::from([ + (insert.entrypoint(), h.entrypoint()), + (defn.node(), h.module_root()), + (decl.node(), h.module_root()), + ]); + + // Straightforward case: three complete subtrees + h.insert_view_forest( + &insert, + insert + .entry_descendants() + .chain(insert.descendants(defn.node())) + .chain(std::iter::once(decl.node())), + roots.clone(), + ) + .unwrap(); + h.validate().unwrap(); + + // Copy the FuncDefn node but not its children + let mut h = simple_dfg_hugr(); + let node_map = h + .insert_view_forest( + &insert, + insert.entry_descendants().chain([defn.node(), decl.node()]), + roots.clone(), + ) + .unwrap(); + assert_matches!(h.validate(), + Err(ValidationError::ContainerWithoutChildren { node, optype: _ }) => assert_eq!(node, node_map[&defn.node()])); + + // Copy the FuncDefn *containing* the entrypoint but transplant the entrypoint + let func_containing_entry = insert.get_parent(insert.entrypoint()).unwrap(); + assert!(matches!( + insert.get_optype(func_containing_entry), + OpType::FuncDefn(_) + )); + roots.insert(func_containing_entry, h.module_root()); + let mut h = simple_dfg_hugr(); + let node_map = h + .insert_view_forest(&insert, insert.nodes().skip(1), roots) + .unwrap(); + assert!(matches!( + h.validate(), + Err(ValidationError::InterGraphEdgeError(_)) + )); + for c in h.nodes().filter(|n| h.get_optype(*n).is_call()) { + assert!(h.static_source(c).is_some()); + } + let new_defn = node_map[&func_containing_entry]; + // The DFG (entrypoint) has been moved elsewhere + let [inp, outp] = h.get_io(new_defn).unwrap(); + assert_eq!(h.children(new_defn).count(), 2); + assert!(matches!(h.get_optype(inp), OpType::Input(_))); + assert!(matches!(h.get_optype(outp), OpType::Output(_))); + let inserted_ep = node_map[&insert.entrypoint()]; + assert_eq!(h.output_neighbours(inp).next().unwrap(), inserted_ep); + assert_eq!(h.get_parent(inserted_ep), Some(h.entrypoint())); + } + + #[test] + fn bad_insert_forest() { + let backup = simple_dfg_hugr(); + let mut h = backup.clone(); + + let (insert, _, _) = dfg_calling_defn_decl(); + let ep = insert.entrypoint(); + let epp = insert.get_parent(ep).unwrap(); + let roots = HashMap::from([(epp, h.module_root()), (ep, h.entrypoint())]); + let r = h.insert_view_forest( + &insert, + insert.descendants(epp).chain(insert.descendants(ep)), + roots.clone(), + ); + assert_eq!(r, Err(InsertForestError::DoubleCopy(ep))); + assert!(h.validate().is_err()); + + let mut h = backup.clone(); + let r = h.insert_forest(insert, roots); + assert_eq!(r, Err(InsertForestError::DoubleCopy(ep))); + // Here the error is detected in building `nodes` from `roots` so before any mutation + assert_eq!(h, backup); + } } From 0eaf3eb4c368192e8f223de16e277e17fb44d759 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 14:08:43 +0100 Subject: [PATCH 05/70] fix test - edges removed rather than delocalized --- hugr-core/src/hugr/hugrmut.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 23dd5498ce..d85e30ff1a 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -916,15 +916,18 @@ mod test { for c in h.nodes().filter(|n| h.get_optype(*n).is_call()) { assert!(h.static_source(c).is_some()); } + // The DFG (entrypoint) has been moved: + let inserted_ep = node_map[&insert.entrypoint()]; + assert_eq!(h.get_parent(inserted_ep), Some(h.entrypoint())); let new_defn = node_map[&func_containing_entry]; - // The DFG (entrypoint) has been moved elsewhere - let [inp, outp] = h.get_io(new_defn).unwrap(); assert_eq!(h.children(new_defn).count(), 2); + + let [inp, outp] = h.get_io(new_defn).unwrap(); assert!(matches!(h.get_optype(inp), OpType::Input(_))); assert!(matches!(h.get_optype(outp), OpType::Output(_))); - let inserted_ep = node_map[&insert.entrypoint()]; - assert_eq!(h.output_neighbours(inp).next().unwrap(), inserted_ep); - assert_eq!(h.get_parent(inserted_ep), Some(h.entrypoint())); + // It seems the edge from Input is disconnected, but the edge to Output preserved + assert_eq!(h.all_neighbours(inp).next(), None); + assert_eq!(h.input_neighbours(outp).next(), Some(inserted_ep)); } #[test] From f55c288ca3e1d0daecd5144291af8c8909a62fa1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 12:04:28 +0100 Subject: [PATCH 06/70] More docs/links --- hugr-core/src/hugr/hugrmut.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index d85e30ff1a..f18d2dce4e 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -178,7 +178,9 @@ pub trait HugrMut: HugrMutInternals { /// If the node is not in the graph, or if the port is invalid. fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (OutgoingPort, IncomingPort); - /// Insert another hugr into this one, under a given parent node. + /// Insert another hugr into this one, under a given parent node. Edges into the + /// inserted subtree (i.e. nonlocal or static) will be disconnected in `self`. + /// (See [Self::insert_forest] for a way to insert sources of such edges as well.) /// /// # Panics /// @@ -190,7 +192,8 @@ pub trait HugrMut: HugrMutInternals { /// Insert a subtree of another hugr into this one, under a given parent node. /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected - /// in `self`. + /// in `self`. (See [Self::insert_forest] for a way to preserve such edges by + /// inserting their sources as well.) /// /// # Panics /// @@ -213,7 +216,8 @@ pub trait HugrMut: HugrMutInternals { /// Copy the entrypoint subtree of another hugr into this one, under a given parent node. /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected - /// in `self`. + /// in `self`. (See [Self::insert_view_forest] for a way to insert sources of such edges + /// as well.) /// /// # Panics /// @@ -262,8 +266,10 @@ pub trait HugrMut: HugrMutInternals { } /// Insert a forest of nodes from another Hugr into this one. + /// /// `root_parents` maps from roots of regions in the other Hugr to insert, /// to the node in this Hugr that shall be parent for that region. + /// If `root_parents` is empty, nothing is inserted. /// /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` /// and where each value is the corresponding (new) node in `self`. From 3621a7f5e298e70bfcb8c62a37b4c890eb976ff6 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 16:35:13 +0100 Subject: [PATCH 07/70] WIP --- hugr-core/src/builder.rs | 40 +++ hugr-core/src/builder/build_traits.rs | 73 ++++- hugr-core/src/builder/dataflow.rs | 66 +++- hugr-core/src/hugr.rs | 2 +- hugr-core/src/hugr/hugrmut.rs | 2 +- hugr-core/src/hugr/linking.rs | 435 ++++++++++++++++++++++++++ 6 files changed, 596 insertions(+), 22 deletions(-) create mode 100644 hugr-core/src/hugr/linking.rs diff --git a/hugr-core/src/builder.rs b/hugr-core/src/builder.rs index ee5046dd5c..ca7135005e 100644 --- a/hugr-core/src/builder.rs +++ b/hugr-core/src/builder.rs @@ -90,6 +90,7 @@ use thiserror::Error; use crate::extension::SignatureError; use crate::extension::simple_op::OpLoadError; use crate::hugr::ValidationError; +use crate::hugr::linking::NodeLinkingError; use crate::ops::handle::{BasicBlockID, CfgID, ConditionalID, DfgID, FuncID, TailLoopID}; use crate::ops::{NamedOp, OpType}; use crate::types::Type; @@ -177,6 +178,16 @@ pub enum BuildError { node: Node, }, + /// From [Dataflow::add_hugr_with_wires_link_nodes] + #[error{"In inserting Hugr: {0}"}] + HugrInsertionError(#[from] NodeLinkingError), + + /// From [Dataflow::add_hugr_view_with_wires_link_nodes]. + /// Note that because the type of node in the [NodeLinkingError] depends + /// upon the view being inserted, we convert the error to a string here. + #[error("In inserting HugrView: {0}")] + HugrViewInsertionError(String), + /// Wire not found in Hugr #[error("Wire not found in Hugr: {0}.")] WireNotFound(Wire), @@ -352,4 +363,33 @@ pub(crate) mod test { ); hugr } + + /// A DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, + /// to a FuncDefn and a FuncDecl each bool_t->bool_t, and their handles. + pub(crate) fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { + let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); + let new_defn = { + let mut mb = dfb.module_root_builder(); + let fb = mb + .define_function("helper_id", Signature::new_endo(bool_t())) + .unwrap(); + let [f_inp] = fb.input_wires_arr(); + fb.finish_with_outputs([f_inp]).unwrap() + }; + let new_decl = dfb + .module_root_builder() + .declare("helper2", Signature::new_endo(bool_t()).into()) + .unwrap(); + let cst = dfb.add_load_value(ops::Value::true_val()); + let [c1] = dfb + .call(new_defn.handle(), &[], [cst]) + .unwrap() + .outputs_arr(); + let [c2] = dfb.call(&new_decl, &[], [c1]).unwrap().outputs_arr(); + ( + dfb.finish_hugr_with_outputs([c2]).unwrap(), + *new_defn.handle(), + new_decl, + ) + } } diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index eb3cf2730c..fb601bca1b 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -1,11 +1,13 @@ use crate::extension::prelude::MakeTuple; use crate::hugr::hugrmut::InsertionResult; +use crate::hugr::linking::{LinkHugr, NodeLinkingDirective}; use crate::hugr::views::HugrView; use crate::hugr::{NodeMetadata, ValidationError}; use crate::ops::{self, OpTag, OpTrait, OpType, Tag, TailLoop}; use crate::utils::collect_array; use crate::{Extension, IncomingPort, Node, OutgoingPort}; +use std::collections::HashMap; use std::iter; use std::sync::Arc; @@ -99,6 +101,8 @@ pub trait Container { } /// Insert a copy of a HUGR as a child of the container. + /// (Only the portion below the entrypoint will be inserted, with any incoming + /// edges broken; see [Dataflow::add_hugr_view_with_wires_link_nodes]) fn add_hugr_view(&mut self, child: &H) -> InsertionResult { let parent = self.container_node(); self.hugr_mut().insert_from_view(parent, child) @@ -243,20 +247,33 @@ pub trait Dataflow: Container { region: Node, input_wires: impl IntoIterator, ) -> Result, BuildError> { - let optype = hugr.get_optype(region).clone(); - let num_outputs = optype.value_output_count(); let node = self.add_hugr_region(hugr, region).inserted_entrypoint; - wire_up_inputs(input_wires, node, self).map_err(|error| BuildError::OperationWiring { - op: Box::new(optype), - error, - })?; + wire_ins_return_outs(input_wires, node, self) + } - Ok((node, num_outputs).into()) + /// Insert a hugr, adding its entrypoint to the sibling graph and wiring up the + /// `input_wires` to the incoming ports of the resulting root node. `defns` may + /// contain other children of the module root of `hugr`, which will be added to + /// the module root being built. + fn add_hugr_with_wires_link_nodes( + &mut self, + hugr: Hugr, + input_wires: impl IntoIterator, + defns: HashMap, + ) -> Result, BuildError> { + let parent = Some(self.container_node()); + let ep = hugr.entrypoint(); + let node = self + .hugr_mut() + .insert_hugr_link_nodes(parent, hugr, defns)?[&ep]; + wire_ins_return_outs(input_wires, node, self) } /// Copy a hugr-defined op into the sibling graph, wiring up the - /// `input_wires` to the incoming ports of the resulting root node. + /// `input_wires` to the incoming ports of the node that was the entrypoint. + /// (Any part of `hugr` outside the entrypoint is not copied; see + /// [Self::add_hugr_view_with_wires_link_nodes]) /// /// # Errors /// @@ -268,15 +285,25 @@ pub trait Dataflow: Container { input_wires: impl IntoIterator, ) -> Result, BuildError> { let node = self.add_hugr_view(hugr).inserted_entrypoint; - let optype = hugr.get_optype(hugr.entrypoint()).clone(); - let num_outputs = optype.value_output_count(); - - wire_up_inputs(input_wires, node, self).map_err(|error| BuildError::OperationWiring { - op: Box::new(optype), - error, - })?; + wire_ins_return_outs(input_wires, node, self) + } - Ok((node, num_outputs).into()) + /// Copy a Hugr, adding its entrypoint into the sibling graph and wiring up the + /// `input_wires` to the incoming ports. `defns` may contain other children of + /// the module root of `hugr`, which will be added to the module root being built. + fn add_hugr_view_with_wires_link_nodes( + &mut self, + hugr: &H, + input_wires: impl IntoIterator, + defns: HashMap, + ) -> Result, BuildError> { + let parent = Some(self.container_node()); + let node = self + .hugr_mut() + .insert_from_view_link_nodes(parent, hugr, defns) + .map_err(|ins_err| BuildError::HugrViewInsertionError(ins_err.to_string()))? + [&hugr.entrypoint()]; + wire_ins_return_outs(input_wires, node, self) } /// Wire up the `output_wires` to the input ports of the Output node. @@ -726,6 +753,20 @@ fn wire_up_inputs( Ok(()) } +fn wire_ins_return_outs( + inputs: impl IntoIterator, + node: Node, + data_builder: &mut T, +) -> Result, BuildError> { + let op = data_builder.hugr().get_optype(node).clone(); + let num_outputs = op.value_output_count(); + wire_up_inputs(inputs, node, data_builder).map_err(|error| BuildError::OperationWiring { + op: Box::new(op), + error, + })?; + Ok((node, num_outputs).into()) +} + /// Add edge from src to dst. /// /// # Errors diff --git a/hugr-core/src/builder/dataflow.rs b/hugr-core/src/builder/dataflow.rs index d1131116f3..3861ebea62 100644 --- a/hugr-core/src/builder/dataflow.rs +++ b/hugr-core/src/builder/dataflow.rs @@ -339,20 +339,21 @@ impl HugrBuilder for DFGWrapper { #[cfg(test)] pub(crate) mod test { use cool_asserts::assert_matches; - use ops::OpParent; use rstest::rstest; use serde_json::json; + use std::collections::HashMap; use crate::builder::build_traits::DataflowHugr; + use crate::builder::test::dfg_calling_defn_decl; use crate::builder::{ BuilderWiringError, DataflowSubContainer, ModuleBuilder, endo_sig, inout_sig, }; use crate::extension::SignatureError; use crate::extension::prelude::Noop; use crate::extension::prelude::{bool_t, qb_t, usize_t}; + use crate::hugr::linking::NodeLinkingDirective; use crate::hugr::validate::InterGraphEdgeError; - use crate::ops::{OpTag, handle::NodeHandle}; - use crate::ops::{OpTrait, Value}; + use crate::ops::{FuncDecl, FuncDefn, OpParent, OpTag, OpTrait, Value, handle::NodeHandle}; use crate::std_extensions::logic::test::and_op; use crate::types::type_param::TypeParam; @@ -545,7 +546,7 @@ pub(crate) mod test { } #[test] - fn insert_hugr() -> Result<(), BuildError> { + fn add_hugr() -> Result<(), BuildError> { // Create a simple DFG let mut dfg_builder = DFGBuilder::new(Signature::new(vec![bool_t()], vec![bool_t()]))?; let [i1] = dfg_builder.input_wires_arr(); @@ -576,6 +577,63 @@ pub(crate) mod test { Ok(()) } + #[rstest] + fn add_hugr_link_nodes( + #[values(false, true)] replace: bool, + #[values(true, false)] view: bool, + ) { + let mut fb = FunctionBuilder::new("main", Signature::new_endo(bool_t())).unwrap(); + let my_decl = fb + .module_root_builder() + .declare("func1", Signature::new_endo(bool_t()).into()) + .unwrap(); + let (insert, ins_defn, ins_decl) = dfg_calling_defn_decl(); + let ins_defn_name = insert + .get_optype(ins_defn.node()) + .as_func_defn() + .unwrap() + .func_name() + .clone(); + let ins_decl_name = insert + .get_optype(ins_decl.node()) + .as_func_decl() + .unwrap() + .func_name() + .clone(); + let decl_mode = if replace { + NodeLinkingDirective::UseExisting(my_decl.node()) + } else { + NodeLinkingDirective::add() + }; + let link_spec = HashMap::from([ + (ins_defn.node(), NodeLinkingDirective::add()), + (ins_decl.node(), decl_mode), + ]); + let inserted = if view { + fb.add_hugr_view_with_wires_link_nodes(&insert, [], link_spec) + .unwrap() + } else { + fb.add_hugr_with_wires_link_nodes(insert, [], link_spec) + .unwrap() + }; + let h = fb.finish_hugr_with_outputs(inserted.outputs()).unwrap(); + let defn_names = h + .nodes() + .filter_map(|n| h.get_optype(n).as_func_defn().map(FuncDefn::func_name)) + .collect_vec(); + assert_eq!(defn_names, [&"main".to_string(), &ins_defn_name]); + let decl_names = h + .nodes() + .filter_map(|n| h.get_optype(n).as_func_decl().map(FuncDecl::func_name)) + .cloned() + .collect_vec(); + let mut expected_decl_names = vec!["func1".to_string()]; + if !replace { + expected_decl_names.push(ins_decl_name) + } + assert_eq!(decl_names, expected_decl_names); + } + #[test] fn barrier_node() -> Result<(), BuildError> { let mut parent = DFGBuilder::new(endo_sig(bool_t()))?; diff --git a/hugr-core/src/hugr.rs b/hugr-core/src/hugr.rs index 78bdd88390..46584ef939 100644 --- a/hugr-core/src/hugr.rs +++ b/hugr-core/src/hugr.rs @@ -1,9 +1,9 @@ //! The Hugr data structure, and its basic component handles. pub mod hugrmut; - pub(crate) mod ident; pub mod internal; +pub mod linking; pub mod patch; pub mod serialize; pub mod validate; diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index f18d2dce4e..c5cea1fcac 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -643,7 +643,7 @@ impl HugrMut for Hugr { /// - `reroot`: A function that returns the new parent for each inserted node. /// If `None`, the parent is set to the original parent after it has been inserted into `hugr`. /// If that is the case, the parent must come before the child in the `other_nodes` iterator. -fn insert_hugr_internal( +pub(super) fn insert_hugr_internal( hugr: &mut Hugr, other: &H, other_nodes: impl Iterator, diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs new file mode 100644 index 0000000000..79847f1cef --- /dev/null +++ b/hugr-core/src/hugr/linking.rs @@ -0,0 +1,435 @@ +//! Directives and errors relating to linking Hugrs. + +use std::{collections::HashMap, fmt::Display}; + +use itertools::Either; + +use crate::{ + Hugr, HugrView, Node, + hugr::{HugrMut, hugrmut::insert_hugr_internal}, +}; + +pub trait LinkHugr: HugrMut { + /// Copy nodes from another Hugr into this one, with linking directives specified by Node. + /// + /// If `parent` is non-None, then `other`'s entrypoint-subtree is copied under it. + /// `children` of the Module root of `other` may also be inserted with their + /// subtrees or linked according to their [NodeLinkingDirective]. + /// + /// # Errors + /// + /// * If `children` are not `children` of the root of `other` + /// * If `parent` is Some, and `other.entrypoint()` is either + /// * among `children`, or + /// * descends from an element of `children` with [NodeLinkingDirective::Add] + /// + /// # Panics + /// + /// If `parent` is `Some` but not in the graph. + #[allow(clippy::type_complexity)] + fn insert_from_view_link_nodes( + &mut self, + parent: Option, + other: &H, + children: NodeLinkingDirectives, + ) -> Result, NodeLinkingError> { + todo!() + } + + /// Insert another Hugr into this one, with linking directives specified by Node. + /// + /// If `parent` is non-None, then `other`'s entrypoint-subtree is placed under it. + /// `children` of the Module root of `other` may also be inserted with their + /// subtrees or linked according to their [NodeLinkingDirective]. + /// + /// # Errors + /// + /// * If `children` are not `children` of the root of `other` + /// * If `other`s entrypoint is among `children`, or descends from an element + /// of `children` with [NodeLinkingDirective::Add] + /// + /// # Panics + /// + /// If `parent` is not in this graph. + fn insert_hugr_link_nodes( + &mut self, + parent: Option, + other: Hugr, + children: NodeLinkingDirectives, + ) -> Result, NodeLinkingError> { + todo!() + } +} + +impl LinkHugr for T {} + +/// An error resulting from an [NodeLinkingDirective] passed to [insert_hugr_link_nodes] +/// or [insert_from_view_link_nodes]. +/// +/// [insert_hugr_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_hugr_link_nodes +/// [insert_from_view_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_from_view_link_nodes +#[derive(Clone, Debug, PartialEq, thiserror::Error)] +#[non_exhaustive] +pub enum NodeLinkingError { + /// Inserting the whole Hugr, yet also asked to insert some of its children + /// (so the inserted Hugr's entrypoint was its module-root). + #[error( + "Cannot insert children (e.g. {_0}) when already inserting whole Hugr (entrypoint == module_root)" + )] + ChildOfEntrypoint(N), + /// A module-child requested contained (or was) the entrypoint + #[error("Requested to insert module-child {_0} but this contains the entrypoint")] + ChildContainsEntrypoint(N), + /// A module-child requested was not a child of the module root + #[error("{_0} was not a child of the module root")] + NotChildOfRoot(N), +} + +/// Directive for how to treat a particular FuncDefn/FuncDecl in the source Hugr. +/// (TN is a node in the target Hugr.) +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub enum NodeLinkingDirective { + /// Insert the FuncDecl, or the FuncDefn and its subtree, into the target Hugr. + Add { + // TODO If non-None, change the name of the inserted function + //rename: Option, + /// Existing/old nodes in the target which will be removed (with their subtrees), + /// and any ([EdgeKind::Function]) edges from them changed to leave the newly-inserted + /// node instead. (Typically, this `Vec` would contain at most one `FuncDefn`, + /// or perhaps-multiple, aliased, `FuncDecl`s.) + /// + /// [EdgeKind::Function]: crate::types::EdgeKind::Function + replace: Vec, + }, + /// Do not insert the node/subtree from the source, but for any static edge from it + /// to an inserted node, instead add an edge from the specified node already existing + /// in the target Hugr. (Static edges are [EdgeKind::Function] and [EdgeKind::Const].) + /// + /// [EdgeKind::Const]: crate::types::EdgeKind::Const + /// [EdgeKind::Function]: crate::types::EdgeKind::Function + UseExisting(TN), +} + +impl NodeLinkingDirective { + /// Just add the node (and any subtree) into the target. + /// (Could lead to an invalid Hugr if the target Hugr + /// already has another with the same name and both are [Public]) + /// + /// [Public]: crate::Visibility::Public + pub const fn add() -> Self { + Self::Add { replace: vec![] } + } +} + +/// Details, node-by-node, how module-children of a source Hugr should be inserted into a +/// target Hugr (beneath the module root). For use with [insert_hugr_link_nodes] and +/// [insert_from_view_link_nodes]. +/// +/// [insert_hugr_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_hugr_link_nodes +/// [insert_from_view_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_from_view_link_nodes +pub type NodeLinkingDirectives = HashMap>; + +fn insert_link_by_node( + hugr: &mut Hugr, + parent: Option, + other: &H, + children: HashMap, +) -> Result, NodeLinkingError> { + if parent.is_some() { + if other.entrypoint() == other.module_root() { + if let Some(c) = children.keys().next() { + return Err(NodeLinkingError::ChildOfEntrypoint(*c)); + } + } else { + let mut n = other.entrypoint(); + if children.contains_key(&n) { + // If parent == hugr.module_root() and the directive is to Add, we could + // allow that - it amounts to two instructions to do the same thing. + // (If the directive is to UseExisting, then we'd have nothing to add + // beneath parent! And if parent != hugr.module_root(), then not only + // would we have to double-copy the entrypoint-subtree, but also + // (unless n is a Const!) we would be creating an illegal Hugr.) + return Err(NodeLinkingError::ChildContainsEntrypoint(n)); + } + while let Some(p) = other.get_parent(n) { + if matches!(children.get(&p), Some(NodeLinkingDirective::Add { .. })) { + return Err(NodeLinkingError::ChildContainsEntrypoint(p)); + } + n = p + } + } + } + for &c in children.keys() { + if other.get_parent(c) != Some(other.module_root()) { + return Err(NodeLinkingError::NotChildOfRoot(c)); + } + } + // In fact we'll copy all `children`, but only including subtrees + // for children that should be `Add`ed. This ensures we copy + // edges from any of those children to any other copied nodes. + let nodes = children + .iter() + .flat_map(|(&ch, m)| match m { + NodeLinkingDirective::Add { .. } => Either::Left(other.descendants(ch)), + NodeLinkingDirective::UseExisting(_) => Either::Right(std::iter::once(ch)), + }) + .chain(parent.iter().flat_map(|_| other.entry_descendants())); + let hugr_root = hugr.module_root(); + let mut node_map = insert_hugr_internal(hugr, &other, nodes, |&n| { + if n == other.entrypoint() { + parent // If parent is None, quite possible this case will not be used + } else { + children.contains_key(&n).then_some(hugr_root) + } + }); + // Now enact any `Add`s with replaces, and `UseExisting`s, removing the copied children + for (ch, m) in children { + match m { + NodeLinkingDirective::UseExisting(replace_with) => { + let copy = node_map.remove(&ch).unwrap(); + // Because of `UseExisting` we avoided adding `ch`s descendants above + debug_assert_eq!(hugr.children(copy).next(), None); + replace_static_src(hugr, copy, replace_with); + } + NodeLinkingDirective::Add { replace } => { + let new_node = *node_map.get(&ch).unwrap(); + for replace in replace { + replace_static_src(hugr, replace, new_node); + } + } + } + } + Ok(node_map) +} + +fn replace_static_src(hugr: &mut Hugr, old_src: Node, new_src: Node) { + let targets = hugr.all_linked_inputs(old_src).collect::>(); + for (target, inport) in targets { + let (src_node, outport) = hugr.single_linked_output(target, inport).unwrap(); + debug_assert_eq!(src_node, old_src); + hugr.disconnect(target, inport); + hugr.connect(new_src, outport, target, inport); + } + hugr.remove_subtree(old_src); +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use itertools::Itertools; + + use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; + use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; + use crate::hugr::{HugrMut, ValidationError}; + use crate::ops::{OpTag, OpTrait, handle::NodeHandle}; + use crate::{Hugr, HugrView}; + + #[test] + fn test_insert_link_nodes_add() { + let mut h = simple_dfg_hugr(); + let (insert, _, _) = dfg_calling_defn_decl(); + + // Defaults + h.insert_from_view(h.entrypoint(), &insert); + check_insertion(h, false, false); + + let mut h = simple_dfg_hugr(); + h.insert_hugr(h.entrypoint(), insert); + check_insertion(h, true, true); + + // Specify which decls to transfer + for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { + let (insert, defn, decl) = dfg_calling_defn_decl(); + let mod_children = HashMap::from_iter( + call1 + .then_some((defn.node(), NodeLinkingDirective::add())) + .into_iter() + .chain(call2.then_some((decl.node(), NodeLinkingDirective::add()))), + ); + + let mut h = simple_dfg_hugr(); + h.insert_from_view_link_nodes(Some(h.entrypoint()), &insert, mod_children.clone()) + .unwrap(); + check_insertion(h, call1, call2); + + let mut h = simple_dfg_hugr(); + h.insert_hugr_link_nodes(Some(h.entrypoint()), insert, mod_children) + .unwrap(); + check_insertion(h, call1, call2); + } + } + + fn check_insertion(h: Hugr, call1_ok: bool, call2_ok: bool) { + if call1_ok && call2_ok { + h.validate().unwrap(); + } else { + assert!(matches!( + h.validate(), + Err(ValidationError::UnconnectedPort { .. }) + )); + } + assert_eq!( + h.children(h.module_root()).count(), + 1 + (call1_ok as usize) + (call2_ok as usize) + ); + let [call1, call2] = h + .nodes() + .filter(|n| h.get_optype(*n).is_call()) + .collect_array() + .unwrap(); + + let tgt1 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_defn() + .is_some_and(|fd| fd.func_name() == "helper_id") + }); + assert_eq!(tgt1.is_some(), call1_ok); + assert_eq!(h.static_source(call1), tgt1); + + let tgt2 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_decl() + .is_some_and(|fd| fd.func_name() == "helper2") + }); + assert_eq!(tgt2.is_some(), call2_ok); + assert_eq!(h.static_source(call2), tgt2); + } + + #[test] + fn insert_link_nodes_replace() { + let (mut host, defn, decl) = dfg_calling_defn_decl(); + assert_eq!( + host.children(host.module_root()) + .map(|n| host.get_optype(n).tag()) + .collect_vec(), + vec![OpTag::FuncDefn, OpTag::FuncDefn, OpTag::Function] + ); + let insert = simple_dfg_hugr(); + let pol = HashMap::from([( + insert + .children(insert.module_root()) + .exactly_one() + .ok() + .unwrap(), + NodeLinkingDirective::Add { + replace: vec![defn.node(), decl.node()], + }, + )]); + host.insert_hugr_link_nodes(None, insert, pol).unwrap(); + host.validate().unwrap(); + assert_eq!( + host.children(host.module_root()) + .map(|n| host.get_optype(n).tag()) + .collect_vec(), + vec![OpTag::FuncDefn; 2] + ); + } + + #[test] + fn insert_link_nodes_use_existing() { + let (insert, defn, decl) = dfg_calling_defn_decl(); + let mut chmap = + HashMap::from([defn.node(), decl.node()].map(|n| (n, NodeLinkingDirective::add()))); + let (h, node_map) = { + let mut h = simple_dfg_hugr(); + let res = h + .insert_from_view_link_nodes(Some(h.entrypoint()), &insert, chmap.clone()) + .unwrap(); + (h, res) + }; + h.validate().unwrap(); + let num_nodes = h.num_nodes(); + let num_ep_nodes = h.descendants(node_map[&insert.entrypoint()]).count(); + let [inserted_defn, inserted_decl] = [defn.node(), decl.node()].map(|n| node_map[&n]); + + // No reason we can't add the decl again, or replace the defn with the decl, + // but here we'll limit to the "interesting" (likely) cases + for decl_replacement in [inserted_defn, inserted_decl] { + let decl_mode = NodeLinkingDirective::UseExisting(decl_replacement); + chmap.insert(decl.node(), decl_mode); + for defn_mode in [ + NodeLinkingDirective::add(), + NodeLinkingDirective::UseExisting(inserted_defn), + ] { + chmap.insert(defn.node(), defn_mode.clone()); + let mut h = h.clone(); + h.insert_hugr_link_nodes(Some(h.entrypoint()), insert.clone(), chmap.clone()) + .unwrap(); + h.validate().unwrap(); + if defn_mode != NodeLinkingDirective::add() { + assert_eq!(h.num_nodes(), num_nodes + num_ep_nodes); + } + assert_eq!( + h.children(h.module_root()).count(), + 3 + (defn_mode == NodeLinkingDirective::add()) as usize + ); + let expected_defn_uses = 1 + + (defn_mode == NodeLinkingDirective::UseExisting(inserted_defn)) as usize + + (decl_replacement == inserted_defn) as usize; + assert_eq!( + h.static_targets(inserted_defn).unwrap().count(), + expected_defn_uses + ); + assert_eq!( + h.static_targets(inserted_decl).unwrap().count(), + 1 + (decl_replacement == inserted_decl) as usize + ); + } + } + } + + #[test] + fn bad_insert_link_nodes() { + let backup = simple_dfg_hugr(); + let mut h = backup.clone(); + + let (insert, defn, decl) = dfg_calling_defn_decl(); + let (defn, decl) = (defn.node(), decl.node()); + + let epp = insert.get_parent(insert.entrypoint()).unwrap(); + let r = h.insert_from_view_link_nodes( + Some(h.entrypoint()), + &insert, + HashMap::from([(epp, NodeLinkingDirective::add())]), + ); + assert_eq!( + r.err().unwrap(), + NodeLinkingError::ChildContainsEntrypoint(epp) + ); + assert_eq!(h, backup); + + let [inp, _] = insert.get_io(defn).unwrap(); + let r = h.insert_from_view_link_nodes( + Some(h.entrypoint()), + &insert, + HashMap::from([(inp, NodeLinkingDirective::add())]), + ); + assert_eq!(r.err().unwrap(), NodeLinkingError::NotChildOfRoot(inp)); + assert_eq!(h, backup); + + let mut insert = insert; + insert.set_entrypoint(defn); + let r = h.insert_from_view_link_nodes( + Some(h.module_root()), + &insert, + HashMap::from([( + defn, + NodeLinkingDirective::UseExisting(h.get_parent(h.entrypoint()).unwrap()), + )]), + ); + assert_eq!( + r.err().unwrap(), + NodeLinkingError::ChildContainsEntrypoint(defn) + ); + + assert_eq!(h, backup); + insert.set_entrypoint(insert.module_root()); + let r = h.insert_hugr_link_nodes( + Some(h.module_root()), + insert, + HashMap::from([(decl, NodeLinkingDirective::add())]), + ); + assert_eq!(r.err().unwrap(), NodeLinkingError::ChildOfEntrypoint(decl)); + } +} From 0f3883b70efc0763d1894269a4cc1134e26b298f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 17:00:00 +0100 Subject: [PATCH 08/70] Dedup dfg_calling_defn_decl, start --- hugr-core/src/hugr/hugrmut.rs | 31 +------------------------------ hugr-core/src/hugr/linking.rs | 10 +++++++--- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index c5cea1fcac..812465eb68 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -705,7 +705,7 @@ mod test { use cool_asserts::assert_matches; use itertools::Itertools; - use crate::builder::test::simple_dfg_hugr; + use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; use crate::builder::{DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder}; use crate::extension::PRELUDE; use crate::extension::prelude::{Noop, bool_t, usize_t}; @@ -792,35 +792,6 @@ mod test { assert_eq!(hugr.num_nodes(), 1); } - /// A DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, - /// to a FuncDefn and a FuncDecl each bool_t->bool_t, and their handles. - pub(crate) fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { - let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); - let new_defn = { - let mut mb = dfb.module_root_builder(); - let fb = mb - .define_function("helper_id", Signature::new_endo(bool_t())) - .unwrap(); - let [f_inp] = fb.input_wires_arr(); - fb.finish_with_outputs([f_inp]).unwrap() - }; - let new_decl = dfb - .module_root_builder() - .declare("helper2", Signature::new_endo(bool_t()).into()) - .unwrap(); - let cst = dfb.add_load_value(ops::Value::true_val()); - let [c1] = dfb - .call(new_defn.handle(), &[], [cst]) - .unwrap() - .outputs_arr(); - let [c2] = dfb.call(&new_decl, &[], [c1]).unwrap().outputs_arr(); - ( - dfb.finish_hugr_with_outputs([c2]).unwrap(), - *new_defn.handle(), - new_decl, - ) - } - #[test] fn test_insert_forest() { // Specify which decls to transfer diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 79847f1cef..8b3c469944 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -5,8 +5,7 @@ use std::{collections::HashMap, fmt::Display}; use itertools::Either; use crate::{ - Hugr, HugrView, Node, - hugr::{HugrMut, hugrmut::insert_hugr_internal}, + core::HugrNode, hugr::{hugrmut::insert_hugr_internal, HugrMut}, Hugr, HugrView, Node }; pub trait LinkHugr: HugrMut { @@ -182,6 +181,12 @@ fn insert_link_by_node( children.contains_key(&n).then_some(hugr_root) } }); + Ok(node_map) +} + +fn link_by_node(hugr: &TGT, + children: NodeLinkingDirectives, + node_map: &mut HashMap) { // Now enact any `Add`s with replaces, and `UseExisting`s, removing the copied children for (ch, m) in children { match m { @@ -199,7 +204,6 @@ fn insert_link_by_node( } } } - Ok(node_map) } fn replace_static_src(hugr: &mut Hugr, old_src: Node, new_src: Node) { From 865fdb81b05ef3eb6d503ddc31c411f2727ef2f5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 17:24:55 +0100 Subject: [PATCH 09/70] Implement LinkHugr, split old insert_link_nodes -> check_directives+link_nodes --- hugr-core/src/hugr/hugrmut.rs | 8 ++-- hugr-core/src/hugr/linking.rs | 90 ++++++++++++++++++++++------------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 812465eb68..7499fc6a35 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -643,7 +643,7 @@ impl HugrMut for Hugr { /// - `reroot`: A function that returns the new parent for each inserted node. /// If `None`, the parent is set to the original parent after it has been inserted into `hugr`. /// If that is the case, the parent must come before the child in the `other_nodes` iterator. -pub(super) fn insert_hugr_internal( +fn insert_hugr_internal( hugr: &mut Hugr, other: &H, other_nodes: impl Iterator, @@ -706,11 +706,11 @@ mod test { use itertools::Itertools; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; - use crate::builder::{DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder}; + use crate::extension::PRELUDE; - use crate::extension::prelude::{Noop, bool_t, usize_t}; + use crate::extension::prelude::{Noop, usize_t}; use crate::hugr::ValidationError; - use crate::ops::handle::{FuncID, NodeHandle}; + use crate::ops::handle::NodeHandle; use crate::ops::{self, FuncDefn, Input, Output, dataflow::IOTrait}; use crate::types::Signature; diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 8b3c469944..54d4be4b7f 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -5,9 +5,12 @@ use std::{collections::HashMap, fmt::Display}; use itertools::Either; use crate::{ - core::HugrNode, hugr::{hugrmut::insert_hugr_internal, HugrMut}, Hugr, HugrView, Node + Hugr, HugrView, Node, + core::HugrNode, + hugr::{HugrMut, internal::HugrMutInternals}, }; +/// Methods for linking Hugrs, i.e. merging the Hugrs and adding edges between old and inserted nodes. pub trait LinkHugr: HugrMut { /// Copy nodes from another Hugr into this one, with linking directives specified by Node. /// @@ -32,7 +35,27 @@ pub trait LinkHugr: HugrMut { other: &H, children: NodeLinkingDirectives, ) -> Result, NodeLinkingError> { - todo!() + check_directives(other, parent, &children)?; + let nodes = + parent + .iter() + .flat_map(|_| other.entry_descendants()) + .chain(children.iter().flat_map(|(&ch, dirv)| match dirv { + NodeLinkingDirective::Add { .. } => Either::Left(other.descendants(ch)), + NodeLinkingDirective::UseExisting(_) => Either::Right(std::iter::once(ch)), + })); + let mut roots = HashMap::new(); + if let Some(parent) = parent { + roots.insert(other.entrypoint(), parent); + } + for ch in children.keys() { + roots.insert(*ch, self.module_root()); + } + let mut node_map = self + .insert_view_forest(other, nodes, roots) + .expect("NodeLinkingDirectives were checked for disjointness"); + link_by_node(self, children, &mut node_map); + Ok(node_map) } /// Insert another Hugr into this one, with linking directives specified by Node. @@ -53,10 +76,30 @@ pub trait LinkHugr: HugrMut { fn insert_hugr_link_nodes( &mut self, parent: Option, - other: Hugr, + mut other: Hugr, children: NodeLinkingDirectives, ) -> Result, NodeLinkingError> { - todo!() + check_directives(&other, parent, &children)?; + let mut roots = HashMap::new(); + if let Some(parent) = parent { + roots.insert(other.entrypoint(), parent); + other.set_parent(other.entrypoint(), other.module_root()); + }; + for (ch, dirv) in children.iter() { + roots.insert(*ch, self.module_root()); + if matches!(dirv, NodeLinkingDirective::UseExisting(_)) { + // We do not need to copy the children of ch + while let Some(gch) = other.first_child(*ch) { + // No point in deleting subtree, we won't copy disconnected nodes + other.remove_node(gch); + } + } + } + let mut node_map = self + .insert_forest(other, roots) + .expect("NodeLinkingDirectives were checked for disjointness"); + link_by_node(self, children, &mut node_map); + Ok(node_map) } } @@ -128,12 +171,11 @@ impl NodeLinkingDirective { /// [insert_from_view_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_from_view_link_nodes pub type NodeLinkingDirectives = HashMap>; -fn insert_link_by_node( - hugr: &mut Hugr, - parent: Option, - other: &H, - children: HashMap, -) -> Result, NodeLinkingError> { +fn check_directives( + other: &SRC, + parent: Option, + children: &HashMap>, +) -> Result<(), NodeLinkingError> { if parent.is_some() { if other.entrypoint() == other.module_root() { if let Some(c) = children.keys().next() { @@ -163,30 +205,14 @@ fn insert_link_by_node( return Err(NodeLinkingError::NotChildOfRoot(c)); } } - // In fact we'll copy all `children`, but only including subtrees - // for children that should be `Add`ed. This ensures we copy - // edges from any of those children to any other copied nodes. - let nodes = children - .iter() - .flat_map(|(&ch, m)| match m { - NodeLinkingDirective::Add { .. } => Either::Left(other.descendants(ch)), - NodeLinkingDirective::UseExisting(_) => Either::Right(std::iter::once(ch)), - }) - .chain(parent.iter().flat_map(|_| other.entry_descendants())); - let hugr_root = hugr.module_root(); - let mut node_map = insert_hugr_internal(hugr, &other, nodes, |&n| { - if n == other.entrypoint() { - parent // If parent is None, quite possible this case will not be used - } else { - children.contains_key(&n).then_some(hugr_root) - } - }); - Ok(node_map) + Ok(()) } -fn link_by_node(hugr: &TGT, +fn link_by_node( + hugr: &mut TGT, children: NodeLinkingDirectives, - node_map: &mut HashMap) { + node_map: &mut HashMap, +) { // Now enact any `Add`s with replaces, and `UseExisting`s, removing the copied children for (ch, m) in children { match m { @@ -206,7 +232,7 @@ fn link_by_node(hugr: &TGT, } } -fn replace_static_src(hugr: &mut Hugr, old_src: Node, new_src: Node) { +fn replace_static_src(hugr: &mut H, old_src: H::Node, new_src: H::Node) { let targets = hugr.all_linked_inputs(old_src).collect::>(); for (target, inport) in targets { let (src_node, outport) = hugr.single_linked_output(target, inport).unwrap(); From 54e87a0eb91f2e1c03d05c4f712802dc388237f5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 17:33:48 +0100 Subject: [PATCH 10/70] Fix test, and note duplication --- hugr-core/src/hugr/linking.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 54d4be4b7f..ec2f9f166f 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -257,16 +257,17 @@ mod test { #[test] fn test_insert_link_nodes_add() { - let mut h = simple_dfg_hugr(); + // ALAN TODO these just duplicate tests of insert_forest.. let (insert, _, _) = dfg_calling_defn_decl(); // Defaults + let mut h = simple_dfg_hugr(); h.insert_from_view(h.entrypoint(), &insert); check_insertion(h, false, false); let mut h = simple_dfg_hugr(); h.insert_hugr(h.entrypoint(), insert); - check_insertion(h, true, true); + check_insertion(h, false, false); // Specify which decls to transfer for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { From b522ff1265ef8086a5d4369789eee7aaef03e1be Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 18:32:42 +0100 Subject: [PATCH 11/70] Common up insertion-checking in tests; expand doc --- hugr-core/src/hugr/hugrmut.rs | 72 ++++++++++++++++++----------------- hugr-core/src/hugr/linking.rs | 61 +++++++---------------------- 2 files changed, 52 insertions(+), 81 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 7499fc6a35..709826a71b 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -701,7 +701,7 @@ fn insert_hugr_internal( } #[cfg(test)] -mod test { +pub(super) mod test { use cool_asserts::assert_matches; use itertools::Itertools; @@ -792,6 +792,42 @@ mod test { assert_eq!(hugr.num_nodes(), 1); } + pub(in crate::hugr) fn check_calls_defn_decl(h: &Hugr, call1_defn: bool, call2_decl: bool) { + if call1_defn && call2_decl { + h.validate().unwrap(); + } else { + assert!(matches!( + h.validate(), + Err(ValidationError::UnconnectedPort { .. }) + )); + } + assert_eq!( + h.children(h.module_root()).count(), + 1 + (call1_defn as usize) + (call2_decl as usize) + ); + let [call1, call2] = h + .nodes() + .filter(|n| h.get_optype(*n).is_call()) + .collect_array() + .unwrap(); + + let tgt1 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_defn() + .is_some_and(|fd| fd.func_name() == "helper_id") + }); + assert_eq!(tgt1.is_some(), call1_defn); + assert_eq!(h.static_source(call1), tgt1); + + let tgt2 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_decl() + .is_some_and(|fd| fd.func_name() == "helper2") + }); + assert_eq!(tgt2.is_some(), call2_decl); + assert_eq!(h.static_source(call2), tgt2); + } + #[test] fn test_insert_forest() { // Specify which decls to transfer @@ -804,39 +840,7 @@ mod test { .chain(call2.then_some((decl.node(), h.module_root())).into_iter()), ); h.insert_forest(insert, roots).unwrap(); - if call1 && call2 { - h.validate().unwrap(); - } else { - assert!(matches!( - h.validate(), - Err(ValidationError::UnconnectedPort { .. }) - )); - } - assert_eq!( - h.children(h.module_root()).count(), - 1 + (call1 as usize) + (call2 as usize) - ); - let [calln1, calln2] = h - .nodes() - .filter(|n| h.get_optype(*n).is_call()) - .collect_array() - .unwrap(); - - let tgt1 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_defn() - .is_some_and(|fd| fd.func_name() == "helper_id") - }); - assert_eq!(tgt1.is_some(), call1); - assert_eq!(h.static_source(calln1), tgt1); - - let tgt2 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_decl() - .is_some_and(|fd| fd.func_name() == "helper2") - }); - assert_eq!(tgt2.is_some(), call2); - assert_eq!(h.static_source(calln2), tgt2); + check_calls_defn_decl(&h, call1, call2); } } diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index ec2f9f166f..b7c8e44126 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -11,6 +11,10 @@ use crate::{ }; /// Methods for linking Hugrs, i.e. merging the Hugrs and adding edges between old and inserted nodes. +/// +/// This is done by module-children from the inserted (source) Hugr replacing, or being replaced by, +/// module-children already in the target Hugr; static edges from the replaced node, +/// are transferred to come from the replacing node, and the replaced node(/subtree) then deleted. pub trait LinkHugr: HugrMut { /// Copy nodes from another Hugr into this one, with linking directives specified by Node. /// @@ -251,25 +255,24 @@ mod test { use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; - use crate::hugr::{HugrMut, ValidationError}; + use crate::hugr::hugrmut::test::check_calls_defn_decl; use crate::ops::{OpTag, OpTrait, handle::NodeHandle}; - use crate::{Hugr, HugrView}; + use crate::{HugrView, hugr::HugrMut}; #[test] fn test_insert_link_nodes_add() { - // ALAN TODO these just duplicate tests of insert_forest.. + // Default (non-linking) methods...just for comparison let (insert, _, _) = dfg_calling_defn_decl(); - // Defaults let mut h = simple_dfg_hugr(); h.insert_from_view(h.entrypoint(), &insert); - check_insertion(h, false, false); + check_calls_defn_decl(&h, false, false); let mut h = simple_dfg_hugr(); h.insert_hugr(h.entrypoint(), insert); - check_insertion(h, false, false); + check_calls_defn_decl(&h, false, false); - // Specify which decls to transfer + // Specify which decls to transfer. No real "linking" here though. for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { let (insert, defn, decl) = dfg_calling_defn_decl(); let mod_children = HashMap::from_iter( @@ -282,49 +285,13 @@ mod test { let mut h = simple_dfg_hugr(); h.insert_from_view_link_nodes(Some(h.entrypoint()), &insert, mod_children.clone()) .unwrap(); - check_insertion(h, call1, call2); + check_calls_defn_decl(&h, call1, call2); let mut h = simple_dfg_hugr(); h.insert_hugr_link_nodes(Some(h.entrypoint()), insert, mod_children) .unwrap(); - check_insertion(h, call1, call2); - } - } - - fn check_insertion(h: Hugr, call1_ok: bool, call2_ok: bool) { - if call1_ok && call2_ok { - h.validate().unwrap(); - } else { - assert!(matches!( - h.validate(), - Err(ValidationError::UnconnectedPort { .. }) - )); + check_calls_defn_decl(&h, call1, call2); } - assert_eq!( - h.children(h.module_root()).count(), - 1 + (call1_ok as usize) + (call2_ok as usize) - ); - let [call1, call2] = h - .nodes() - .filter(|n| h.get_optype(*n).is_call()) - .collect_array() - .unwrap(); - - let tgt1 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_defn() - .is_some_and(|fd| fd.func_name() == "helper_id") - }); - assert_eq!(tgt1.is_some(), call1_ok); - assert_eq!(h.static_source(call1), tgt1); - - let tgt2 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_decl() - .is_some_and(|fd| fd.func_name() == "helper2") - }); - assert_eq!(tgt2.is_some(), call2_ok); - assert_eq!(h.static_source(call2), tgt2); } #[test] @@ -337,7 +304,7 @@ mod test { vec![OpTag::FuncDefn, OpTag::FuncDefn, OpTag::Function] ); let insert = simple_dfg_hugr(); - let pol = HashMap::from([( + let dirvs = HashMap::from([( insert .children(insert.module_root()) .exactly_one() @@ -347,7 +314,7 @@ mod test { replace: vec![defn.node(), decl.node()], }, )]); - host.insert_hugr_link_nodes(None, insert, pol).unwrap(); + host.insert_hugr_link_nodes(None, insert, dirvs).unwrap(); host.validate().unwrap(); assert_eq!( host.children(host.module_root()) From 5774665da7c2048e3cc46f97d9d4a66378c9dfe1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 12:08:39 +0100 Subject: [PATCH 12/70] fix doclinks --- hugr-core/src/hugr/linking.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index b7c8e44126..2a6ca84ff1 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -109,11 +109,8 @@ pub trait LinkHugr: HugrMut { impl LinkHugr for T {} -/// An error resulting from an [NodeLinkingDirective] passed to [insert_hugr_link_nodes] -/// or [insert_from_view_link_nodes]. -/// -/// [insert_hugr_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_hugr_link_nodes -/// [insert_from_view_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_from_view_link_nodes +/// An error resulting from an [NodeLinkingDirective] passed to [LinkHugr::insert_hugr_link_nodes] +/// or [LinkHugr::insert_from_view_link_nodes]. #[derive(Clone, Debug, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum NodeLinkingError { @@ -168,11 +165,9 @@ impl NodeLinkingDirective { } /// Details, node-by-node, how module-children of a source Hugr should be inserted into a -/// target Hugr (beneath the module root). For use with [insert_hugr_link_nodes] and -/// [insert_from_view_link_nodes]. +/// target Hugr. /// -/// [insert_hugr_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_hugr_link_nodes -/// [insert_from_view_link_nodes]: crate::hugr::hugrmut::HugrMut::insert_from_view_link_nodes +/// For use with [LinkHugr::insert_hugr_link_nodes] and [LinkHugr::insert_from_view_link_nodes]. pub type NodeLinkingDirectives = HashMap>; fn check_directives( From 7c41f73ad5a839ee00e5370d2a82249f0498a38b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 14:18:02 +0100 Subject: [PATCH 13/70] Error on overlapping Add::replace; handle Add overlapping w/ UseExisting NOTESTS --- hugr-core/src/builder.rs | 2 +- hugr-core/src/hugr/linking.rs | 88 ++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/hugr-core/src/builder.rs b/hugr-core/src/builder.rs index ca7135005e..405e3c67ac 100644 --- a/hugr-core/src/builder.rs +++ b/hugr-core/src/builder.rs @@ -180,7 +180,7 @@ pub enum BuildError { /// From [Dataflow::add_hugr_with_wires_link_nodes] #[error{"In inserting Hugr: {0}"}] - HugrInsertionError(#[from] NodeLinkingError), + HugrInsertionError(#[from] NodeLinkingError), /// From [Dataflow::add_hugr_view_with_wires_link_nodes]. /// Note that because the type of node in the [NodeLinkingError] depends diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 2a6ca84ff1..446cd6a0c8 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -38,8 +38,8 @@ pub trait LinkHugr: HugrMut { parent: Option, other: &H, children: NodeLinkingDirectives, - ) -> Result, NodeLinkingError> { - check_directives(other, parent, &children)?; + ) -> Result, NodeLinkingError> { + let transfers = check_directives(other, parent, &children)?; let nodes = parent .iter() @@ -58,7 +58,7 @@ pub trait LinkHugr: HugrMut { let mut node_map = self .insert_view_forest(other, nodes, roots) .expect("NodeLinkingDirectives were checked for disjointness"); - link_by_node(self, children, &mut node_map); + link_by_node(self, transfers, &mut node_map); Ok(node_map) } @@ -82,8 +82,8 @@ pub trait LinkHugr: HugrMut { parent: Option, mut other: Hugr, children: NodeLinkingDirectives, - ) -> Result, NodeLinkingError> { - check_directives(&other, parent, &children)?; + ) -> Result, NodeLinkingError> { + let transfers = check_directives(&other, parent, &children)?; let mut roots = HashMap::new(); if let Some(parent) = parent { roots.insert(other.entrypoint(), parent); @@ -102,7 +102,7 @@ pub trait LinkHugr: HugrMut { let mut node_map = self .insert_forest(other, roots) .expect("NodeLinkingDirectives were checked for disjointness"); - link_by_node(self, children, &mut node_map); + link_by_node(self, transfers, &mut node_map); Ok(node_map) } } @@ -111,21 +111,27 @@ impl LinkHugr for T {} /// An error resulting from an [NodeLinkingDirective] passed to [LinkHugr::insert_hugr_link_nodes] /// or [LinkHugr::insert_from_view_link_nodes]. +/// +/// `SN` is the type of nodes in the source (inserted) Hugr; `TN` similarly for the target Hugr. #[derive(Clone, Debug, PartialEq, thiserror::Error)] #[non_exhaustive] -pub enum NodeLinkingError { +pub enum NodeLinkingError { /// Inserting the whole Hugr, yet also asked to insert some of its children /// (so the inserted Hugr's entrypoint was its module-root). #[error( "Cannot insert children (e.g. {_0}) when already inserting whole Hugr (entrypoint == module_root)" )] - ChildOfEntrypoint(N), + ChildOfEntrypoint(SN), /// A module-child requested contained (or was) the entrypoint #[error("Requested to insert module-child {_0} but this contains the entrypoint")] - ChildContainsEntrypoint(N), + ChildContainsEntrypoint(SN), /// A module-child requested was not a child of the module root #[error("{_0} was not a child of the module root")] - NotChildOfRoot(N), + NotChildOfRoot(SN), + /// A node in the target Hugr was in a [NodeLinkingDirective::Add::replace] for multiple + /// inserted nodes (it is not clear to which we should transfer edges). + #[error("Target node {_0} is to be replaced by two source nodes {_1} and {_2}")] + NodeMultiplyReplaced(TN, SN, SN), } /// Directive for how to treat a particular FuncDefn/FuncDecl in the source Hugr. @@ -170,11 +176,18 @@ impl NodeLinkingDirective { /// For use with [LinkHugr::insert_hugr_link_nodes] and [LinkHugr::insert_from_view_link_nodes]. pub type NodeLinkingDirectives = HashMap>; +/// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) +/// TargetNodes can be (in RHS of multiple directives) +struct Transfers { + use_existing: HashMap, + replace: HashMap, +} + fn check_directives( other: &SRC, parent: Option, children: &HashMap>, -) -> Result<(), NodeLinkingError> { +) -> Result, NodeLinkingError> { if parent.is_some() { if other.entrypoint() == other.module_root() { if let Some(c) = children.keys().next() { @@ -199,35 +212,46 @@ fn check_directives( } } } - for &c in children.keys() { - if other.get_parent(c) != Some(other.module_root()) { - return Err(NodeLinkingError::NotChildOfRoot(c)); + let mut trns = Transfers { + replace: HashMap::default(), + use_existing: HashMap::default(), + }; + for (&sn, dirv) in children { + if other.get_parent(sn) != Some(other.module_root()) { + return Err(NodeLinkingError::NotChildOfRoot(sn)); + } + match dirv { + NodeLinkingDirective::Add { replace } => { + for &r in replace { + if let Some(old_sn) = trns.replace.insert(r, sn) { + return Err(NodeLinkingError::NodeMultiplyReplaced(r, sn, old_sn)); + } + } + } + NodeLinkingDirective::UseExisting(tn) => { + trns.use_existing.insert(sn, *tn); + } } } - Ok(()) + Ok(trns) } fn link_by_node( hugr: &mut TGT, - children: NodeLinkingDirectives, + transfers: Transfers, node_map: &mut HashMap, ) { - // Now enact any `Add`s with replaces, and `UseExisting`s, removing the copied children - for (ch, m) in children { - match m { - NodeLinkingDirective::UseExisting(replace_with) => { - let copy = node_map.remove(&ch).unwrap(); - // Because of `UseExisting` we avoided adding `ch`s descendants above - debug_assert_eq!(hugr.children(copy).next(), None); - replace_static_src(hugr, copy, replace_with); - } - NodeLinkingDirective::Add { replace } => { - let new_node = *node_map.get(&ch).unwrap(); - for replace in replace { - replace_static_src(hugr, replace, new_node); - } - } - } + // Resolve `use_existing` first in case the existing node is also replaced by + // a new node (which we know will not be in RHS of any entry in `replace`). + for (sn, tn) in transfers.use_existing { + let copy = node_map.remove(&sn).unwrap(); + // Because of `UseExisting` we avoided adding `sn`s descendants + debug_assert_eq!(hugr.children(copy).next(), None); + replace_static_src(hugr, copy, tn); + } + for (tn, sn) in transfers.replace { + let new_node = *node_map.get(&sn).unwrap(); + replace_static_src(hugr, tn, new_node); } } From e70015f9285bb70066031b94b24cc180b87d4456 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 16:17:13 +0100 Subject: [PATCH 14/70] test multi-replace / replace+add --- hugr-core/src/hugr/linking.rs | 72 +++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 446cd6a0c8..230981584a 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -168,6 +168,16 @@ impl NodeLinkingDirective { pub const fn add() -> Self { Self::Add { replace: vec![] } } + + /// Replace the specified node in the target. + + /// (Could lead to an invalid Hugr if the replaced node has a different type, + /// or if the target already has another function with the same name and both are public.) + pub fn replace(replaced: TN) -> Self { + Self::Add { + replace: vec![replaced], + } + } } /// Details, node-by-node, how module-children of a source Hugr should be inserted into a @@ -224,7 +234,7 @@ fn check_directives( NodeLinkingDirective::Add { replace } => { for &r in replace { if let Some(old_sn) = trns.replace.insert(r, sn) { - return Err(NodeLinkingError::NodeMultiplyReplaced(r, sn, old_sn)); + return Err(NodeLinkingError::NodeMultiplyReplaced(r, old_sn, sn)); } } } @@ -275,8 +285,8 @@ mod test { use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; use crate::hugr::hugrmut::test::check_calls_defn_decl; - use crate::ops::{OpTag, OpTrait, handle::NodeHandle}; - use crate::{HugrView, hugr::HugrMut}; + use crate::ops::{FuncDecl, OpTag, OpTrait, handle::NodeHandle}; + use crate::{HugrView, hugr::HugrMut, types::Signature}; #[test] fn test_insert_link_nodes_add() { @@ -439,8 +449,8 @@ mod test { r.err().unwrap(), NodeLinkingError::ChildContainsEntrypoint(defn) ); - assert_eq!(h, backup); + insert.set_entrypoint(insert.module_root()); let r = h.insert_hugr_link_nodes( Some(h.module_root()), @@ -448,5 +458,59 @@ mod test { HashMap::from([(decl, NodeLinkingDirective::add())]), ); assert_eq!(r.err().unwrap(), NodeLinkingError::ChildOfEntrypoint(decl)); + assert_eq!(h, backup); + + let (insert, defn, decl) = dfg_calling_defn_decl(); + let sig = insert + .get_optype(defn.node()) + .as_func_defn() + .unwrap() + .signature() + .clone(); + let tmp = h.add_node_with_parent(h.module_root(), FuncDecl::new("replaced", sig)); + let r = h.insert_hugr_link_nodes( + Some(h.entrypoint()), + insert, + HashMap::from([ + (decl.node(), NodeLinkingDirective::replace(tmp)), + (defn.node(), NodeLinkingDirective::replace(tmp)), + ]), + ); + assert_eq!( + r.err().unwrap(), + NodeLinkingError::NodeMultiplyReplaced(tmp, decl.node(), defn.node()) + ); + } + + #[test] + fn test_replace_used() { + let mut h = simple_dfg_hugr(); + let temp = h.add_node_with_parent( + h.module_root(), + FuncDecl::new("temp", Signature::new_endo(vec![])), + ); + + let (insert, defn, decl) = dfg_calling_defn_decl(); + let node_map = h + .insert_hugr_link_nodes( + Some(h.entrypoint()), + insert, + HashMap::from([ + (defn.node(), NodeLinkingDirective::replace(temp)), + (decl.node(), NodeLinkingDirective::UseExisting(temp)), + ]), + ) + .unwrap(); + let defn = node_map[&defn.node()]; + assert_eq!(node_map.get(&decl.node()), None); + assert_eq!(h.contains_node(temp), false); + + assert!( + h.children(h.module_root()) + .all(|n| h.get_optype(n).is_func_defn()) + ); + for call in h.nodes().filter(|n| h.get_optype(*n).is_call()) { + assert_eq!(h.static_source(call), Some(defn)); + } } } From 7001f9cfe9d02b0cefcef2d46e4484062f980898 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 16:20:06 +0100 Subject: [PATCH 15/70] fix doc --- hugr-core/src/hugr/linking.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 230981584a..6f6498ea78 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -169,8 +169,8 @@ impl NodeLinkingDirective { Self::Add { replace: vec![] } } - /// Replace the specified node in the target. - + /// The new node should replace the specified node in the target. + /// /// (Could lead to an invalid Hugr if the replaced node has a different type, /// or if the target already has another function with the same name and both are public.) pub fn replace(replaced: TN) -> Self { From 698283c26ec1bae28e2c2d0c10ac975e1e1fcf8f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 16:26:18 +0100 Subject: [PATCH 16/70] NodeLinkingDirective::replace takes impl IntoIterator --- hugr-core/src/hugr/linking.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 6f6498ea78..4e2e38ce6f 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -169,13 +169,14 @@ impl NodeLinkingDirective { Self::Add { replace: vec![] } } - /// The new node should replace the specified node in the target. + /// The new node should replace the specified node(s) already existing + /// in the target. /// - /// (Could lead to an invalid Hugr if the replaced node has a different type, + /// (Could lead to an invalid Hugr if they have different signatures, /// or if the target already has another function with the same name and both are public.) - pub fn replace(replaced: TN) -> Self { + pub fn replace(nodes: impl IntoIterator) -> Self { Self::Add { - replace: vec![replaced], + replace: nodes.into_iter().collect(), } } } @@ -472,8 +473,8 @@ mod test { Some(h.entrypoint()), insert, HashMap::from([ - (decl.node(), NodeLinkingDirective::replace(tmp)), - (defn.node(), NodeLinkingDirective::replace(tmp)), + (decl.node(), NodeLinkingDirective::replace([tmp])), + (defn.node(), NodeLinkingDirective::replace([tmp])), ]), ); assert_eq!( @@ -496,7 +497,7 @@ mod test { Some(h.entrypoint()), insert, HashMap::from([ - (defn.node(), NodeLinkingDirective::replace(temp)), + (defn.node(), NodeLinkingDirective::replace([temp])), (decl.node(), NodeLinkingDirective::UseExisting(temp)), ]), ) From 18e1cf775f12ad14d4715b05359def3e15fab8cc Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 16:37:14 +0100 Subject: [PATCH 17/70] Avoid test nondeterminism; clippy --- hugr-core/src/hugr/linking.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 4e2e38ce6f..a026aa89e2 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -281,6 +281,7 @@ fn replace_static_src(hugr: &mut H, old_src: H::Node, new_s mod test { use std::collections::HashMap; + use cool_asserts::assert_matches; use itertools::Itertools; use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; @@ -477,10 +478,12 @@ mod test { (defn.node(), NodeLinkingDirective::replace([tmp])), ]), ); - assert_eq!( + assert_matches!( r.err().unwrap(), - NodeLinkingError::NodeMultiplyReplaced(tmp, decl.node(), defn.node()) - ); + NodeLinkingError::NodeMultiplyReplaced(tn, sn1, sn2) => { + assert_eq!(tmp, tn); + assert_eq!([sn1,sn2].into_iter().sorted().collect_vec(), [defn.node(), decl.node()]); + }); } #[test] @@ -504,7 +507,7 @@ mod test { .unwrap(); let defn = node_map[&defn.node()]; assert_eq!(node_map.get(&decl.node()), None); - assert_eq!(h.contains_node(temp), false); + assert!(!h.contains_node(temp)); assert!( h.children(h.module_root()) From 5e0cf3368d8527fd20eb291b555ec556e463721e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 16:39:39 +0100 Subject: [PATCH 18/70] Add NameLinkingPolicy+friends, and LinkHugr::link_hugr --- hugr-core/src/core.rs | 7 + hugr-core/src/hugr/linking.rs | 465 +++++++++++++++++++++++++++++++++- 2 files changed, 468 insertions(+), 4 deletions(-) diff --git a/hugr-core/src/core.rs b/hugr-core/src/core.rs index 4578aa5357..d7edbcf2ab 100644 --- a/hugr-core/src/core.rs +++ b/hugr-core/src/core.rs @@ -298,6 +298,13 @@ pub enum Visibility { Private, } +impl Visibility { + /// Returns `true` iff this is [Self::Public] + pub fn is_public(&self) -> bool { + self == &Self::Public + } +} + impl From for Visibility { fn from(value: hugr_model::v0::Visibility) -> Self { match value { diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index a026aa89e2..0887200164 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -2,12 +2,14 @@ use std::{collections::HashMap, fmt::Display}; -use itertools::Either; +use itertools::{Either, Itertools}; use crate::{ - Hugr, HugrView, Node, + Hugr, HugrView, Node, Visibility, core::HugrNode, hugr::{HugrMut, internal::HugrMutInternals}, + ops::OpType, + types::PolyFuncType, }; /// Methods for linking Hugrs, i.e. merging the Hugrs and adding edges between old and inserted nodes. @@ -105,6 +107,35 @@ pub trait LinkHugr: HugrMut { link_by_node(self, transfers, &mut node_map); Ok(node_map) } + + /// Copy module-children from another Hugr into this one according to a [NameLinkingPolicy]. + /// + /// All [Visibility::Public] module-children are copied, or linked, according to the + /// specified policy; private children will also be copied, at least including all those + /// used by the copied public children. + // Yes at present we copy all private children, i.e. a safe over-approximation! + /// + /// # Errors + /// + /// * If [NameLinkingPolicy::error_on_conflicting_sig] is true and there are public + /// functions with the same name but different signatures + /// + /// * If [MultipleImplHandling::ErrorDontInsert] is used + /// and both `self` and `other` have public [FuncDefn]s with the same name and signature + /// + /// [Visibility::Public]: crate::Visibility::Public + /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert + #[allow(clippy::type_complexity)] + fn link_hugr( + &mut self, + other: Hugr, + policy: NameLinkingPolicy, + ) -> Result, NameLinkingError> { + let per_node = policy.to_node_linking(self, &other)?; + Ok(self + .insert_hugr_link_nodes(None, other, per_node) + .expect("NodeLinkingPolicy was constructed to avoid any error")) + } } impl LinkHugr for T {} @@ -181,6 +212,200 @@ impl NodeLinkingDirective { } } +/// Describes ways to link a "Source" Hugr being inserted into a target Hugr. +/// ALAN TODO accessor methods! +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NameLinkingPolicy { + // TODO: consider pub-funcs-to-add? (With others, optionally filtered by callgraph, made private) + /// If true, all private functions from the source hugr are inserted into the target. + /// (Since these are private, name conflicts do not make the Hugr invalid.) + /// If false, instead edges from said private functions to any inserted parts + /// of the source Hugr will be broken, making the target Hugr invalid. + /// ALAN TODO: drop this; reintroduce "copy_only_required_privates" in next PR. + copy_private_funcs: bool, + /// How to handle cases where the same (public) name is present in both + /// inserted and target Hugr but with different signatures. + /// `true` means an error is raised and nothing is added to the target Hugr. + /// `false` means the new function will be added alongside the existing one + /// - this will give an invalid Hugr (duplicate names). + // NOTE there are other possible handling schemes, both where we don't insert the new function, both leading to an invalid Hugr: + // * don't insert but break edges --> Unconnected ports (or, replace and break existing edges) + // * use (or replace) the existing function --> incompatible ports + // but given you'll need to patch the Hugr up afterwards, you can get there just + // by setting this to `false` (and maybe removing one FuncDefn), or via explicit node linking. + error_on_conflicting_sig: bool, + /// How to handle cases where both target and inserted Hugr have a FuncDefn + /// with the same name and signature. + // TODO consider Set of names where to prefer new? Or optional map from name? + multi_impls: MultipleImplHandling, + // TODO Renames to apply to public functions in the inserted Hugr. These take effect + // before [error_on_conflicting_sig] or [take_existing_and_new_impls]. + // rename_map: HashMap +} + +/// What to do when both target and inserted Hugr +/// have a [Visibility::Public] FuncDefn with the same name and signature. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub enum MultipleImplHandling { + /// Do not perform insertion; raise an error instead + ErrorDontInsert, + /// Keep the implementation already in the target Hugr. (Edges in the source + /// Hugr will be redirected to use the function from the target.) + UseExisting, + /// Keep the implementation in the source Hugr. (Edges in the target Hugr + /// will be redirected to use the function from the source; the previously-existing + /// function in the target Hugr will be removed.) + UseNew, + /// Add the new function alongside the existing one in the target Hugr, + /// preserving (separately) uses of both. (The Hugr will be invalid because + /// of duplicate names.) + UseBoth, +} + +/// An error in using names to determine how to link functions in source and target Hugrs. +/// (SN = Source Node, TN = Target Node) +#[derive(Clone, Debug, thiserror::Error, PartialEq)] +pub enum NameLinkingError { + /// Both source and target contained a [FuncDefn] (public and with same name + /// and signature). + /// + /// [FuncDefn]: crate::ops::FuncDefn + #[error("Source ({_1}) and target ({_2}) both contained FuncDefn with same public name {_0}")] + MultipleImpls(String, SN, TN), + /// Source and target containing public functions with conflicting signatures + // TODO ALAN Should we indicate which were decls or defns? via an extra enum? + #[error( + "Conflicting signatures for name {name} - Source ({src_node}) has {src_sig}, Target ({tgt_node}) has ({tgt_sig})" + )] + #[allow(missing_docs)] + Signatures { + name: String, + src_node: SN, + src_sig: Box, + tgt_node: TN, + tgt_sig: Box, + }, + /// A [Visibility::Public] function in the source, whose body is being added + /// to the target, contained the entrypoint (which needs to be added + /// in a different place). + #[error("The entrypoint is contained within function {_0} which will be added as {_1:?}")] + AddFunctionContainingEntrypoint(SN, NodeLinkingDirective), +} + +impl NameLinkingPolicy { + /// Builds an explicit map of [NodeLinkingDirective]s that implements this policy for a given + /// source (inserted) and target (inserted-into) Hugr. + /// The map should be such that no [NodeLinkingError] will occur. + #[allow(clippy::type_complexity)] + pub fn to_node_linking( + &self, + target: &T, + source: &S, + ) -> Result, NameLinkingError> { + let existing = gather_existing(target); + let mut res = NodeLinkingDirectives::new(); + + let NameLinkingPolicy { + copy_private_funcs, + error_on_conflicting_sig, + multi_impls, + } = self; + for n in source.children(source.module_root()) { + if let Some((name, is_defn, vis, sig)) = link_sig(source, n) { + let mut dirv = NodeLinkingDirective::add(); + if !vis.is_public() { + if !copy_private_funcs { + continue; + }; + } else if let Some((ex_ns, ex_sig)) = existing.get(name) { + if sig == *ex_sig { + dirv = directive(name, n, is_defn, ex_ns, multi_impls)? + } else if *error_on_conflicting_sig { + return Err(NameLinkingError::Signatures { + name: name.clone(), + src_node: n, + src_sig: Box::new(sig.clone()), + tgt_node: *ex_ns.as_ref().left_or_else(|(n, _)| n), + tgt_sig: Box::new((*ex_sig).clone()), + }); + } + }; + res.insert(n, dirv); + } + } + + Ok(res) + } +} + +fn directive( + name: &str, + new_n: SN, + new_defn: bool, + ex_ns: &Either)>, + multi_impls: &MultipleImplHandling, +) -> Result, NameLinkingError> { + Ok(match (new_defn, ex_ns) { + (false, Either::Right(_)) => NodeLinkingDirective::add(), // another alias + (false, Either::Left(defn)) => NodeLinkingDirective::UseExisting(*defn), // resolve decl + (true, Either::Right((decl, decls))) => { + NodeLinkingDirective::replace(std::iter::once(decl).chain(decls).cloned()) + } + (true, &Either::Left(defn)) => match multi_impls { + MultipleImplHandling::UseExisting => NodeLinkingDirective::UseExisting(defn), + MultipleImplHandling::UseNew => NodeLinkingDirective::replace([defn]), + MultipleImplHandling::ErrorDontInsert => { + return Err(NameLinkingError::MultipleImpls( + name.to_owned(), + new_n, + defn, + )); + } + MultipleImplHandling::UseBoth => NodeLinkingDirective::add(), + }, + }) +} + +type PubFuncs<'a, N> = (Either)>, &'a PolyFuncType); + +fn link_sig( + h: &H, + n: H::Node, +) -> Option<(&String, bool, &Visibility, &PolyFuncType)> { + match h.get_optype(n) { + OpType::FuncDecl(fd) => Some((fd.func_name(), false, fd.visibility(), fd.signature())), + OpType::FuncDefn(fd) => Some((fd.func_name(), true, fd.visibility(), fd.signature())), + _ => None, + } +} + +fn gather_existing(h: &H) -> HashMap<&String, PubFuncs> { + let left_if = |b| if b { Either::Left } else { Either::Right }; + h.children(h.module_root()) + .filter_map(|n| { + link_sig(h, n).and_then(|(fname, is_defn, vis, sig)| { + vis.is_public() + .then_some((fname, (left_if(is_defn)(n), sig))) + }) + }) + .into_grouping_map() + .aggregate(|acc: Option>, name, (new, sig2)| { + let Some((mut acc, sig1)) = acc else { + return Some((new.map_right(|n| (n, vec![])), sig2)); + }; + assert_eq!(sig1, sig2, "Invalid Hugr: different signatures for {name}"); + let (Either::Right((_, decls)), Either::Right(ndecl)) = (&mut acc, &new) else { + let err = match acc.is_left() && new.is_left() { + true => "Multiple FuncDefns", + false => "FuncDefn and FuncDecl(s)", + }; + panic!("Invalid Hugr: {err} for {name}"); + }; + decls.push(*ndecl); + Some((acc, sig2)) + }) +} + /// Details, node-by-node, how module-children of a source Hugr should be inserted into a /// target Hugr. /// @@ -283,12 +508,22 @@ mod test { use cool_asserts::assert_matches; use itertools::Itertools; + use rstest::rstest; use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; + use crate::builder::{ + DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder, + ModuleBuilder, + }; + use crate::extension::prelude::{ConstUsize, usize_t}; use crate::hugr::hugrmut::test::check_calls_defn_decl; - use crate::ops::{FuncDecl, OpTag, OpTrait, handle::NodeHandle}; - use crate::{HugrView, hugr::HugrMut, types::Signature}; + use crate::hugr::linking::{MultipleImplHandling, NameLinkingError, NameLinkingPolicy}; + use crate::hugr::{ValidationError, hugrmut::HugrMut}; + use crate::ops::{FuncDecl, OpTag, OpTrait, OpType, handle::NodeHandle}; + use crate::std_extensions::arithmetic::int_ops::IntOpDef; + use crate::std_extensions::arithmetic::int_types::{ConstInt, INT_TYPES}; + use crate::{Hugr, HugrView, Visibility, types::Signature}; #[test] fn test_insert_link_nodes_add() { @@ -517,4 +752,226 @@ mod test { assert_eq!(h.static_source(call), Some(defn)); } } + + fn list_decls_defns(h: &H) -> (HashMap, HashMap) { + let mut decls = HashMap::new(); + let mut defns = HashMap::new(); + for n in h.children(h.module_root()) { + match h.get_optype(n) { + OpType::FuncDecl(fd) => decls.insert(n, fd.func_name().as_str()), + OpType::FuncDefn(fd) => defns.insert(n, fd.func_name().as_str()), + _ => None, + }; + } + (decls, defns) + } + + fn call_targets(h: &H) -> HashMap { + h.nodes() + .filter(|n| h.get_optype(*n).is_call()) + .map(|n| { + let tgt = h.static_source(n).expect(format!("For node {n}").as_str()); + (n, tgt) + }) + .collect() + } + + #[test] + fn combines_decls_defn() { + let i64_t = || INT_TYPES[6].to_owned(); + let foo_sig = Signature::new_endo(i64_t()); + let bar_sig = Signature::new(vec![i64_t(); 2], i64_t()); + let orig_target = { + let mut fb = + FunctionBuilder::new_vis("foo", foo_sig.clone(), Visibility::Public).unwrap(); + let mut mb = fb.module_root_builder(); + let bar1 = mb.declare("bar", bar_sig.clone().into()).unwrap(); + let bar2 = mb.declare("bar", bar_sig.clone().into()).unwrap(); // alias + let [i] = fb.input_wires_arr(); + let [c] = fb.call(&bar1, &[], [i, i]).unwrap().outputs_arr(); + let r = fb.call(&bar2, &[], [i, c]).unwrap(); + let h = fb.finish_hugr_with_outputs(r.outputs()).unwrap(); + assert_eq!( + list_decls_defns(&h), + ( + HashMap::from([(bar1.node(), "bar"), (bar2.node(), "bar")]), + HashMap::from([(h.entrypoint(), "foo")]) + ) + ); + h + }; + + let inserted = { + let mut dfb = DFGBuilder::new(Signature::new(vec![], i64_t())).unwrap(); + let mut mb = dfb.module_root_builder(); + let foo1 = mb.declare("foo", foo_sig.clone().into()).unwrap(); + let foo2 = mb.declare("foo", foo_sig.clone().into()).unwrap(); + let mut bar = mb + .define_function_vis("bar", bar_sig.clone(), Visibility::Public) + .unwrap(); + let res = bar + .add_dataflow_op(IntOpDef::iadd.with_log_width(6), bar.input_wires()) + .unwrap(); + let bar = bar.finish_with_outputs(res.outputs()).unwrap(); + let i = dfb.add_load_value(ConstInt::new_u(6, 257).unwrap()); + let c = dfb.call(&foo1, &[], [i]).unwrap(); + let r = dfb.call(&foo2, &[], c.outputs()).unwrap(); + let h = dfb.finish_hugr_with_outputs(r.outputs()).unwrap(); + assert_eq!( + list_decls_defns(&h), + ( + HashMap::from([(foo1.node(), "foo"), (foo2.node(), "foo")]), + HashMap::from([ + (h.get_parent(h.entrypoint()).unwrap(), "main"), + (bar.node(), "bar") + ]) + ) + ); + h + }; + + // Linking by name...neither of the looped-over params should make any difference: + for error_on_conflicting_sig in [false, true] { + for multi_impls in [ + MultipleImplHandling::ErrorDontInsert, + MultipleImplHandling::UseNew, + MultipleImplHandling::UseExisting, + MultipleImplHandling::UseBoth, + ] { + let mut pol = NameLinkingPolicy { + copy_private_funcs: true, + error_on_conflicting_sig, + multi_impls, + }; + let mut target = orig_target.clone(); + + // ALAN this won't work, test expects entrypoint-subtree to be copied + pol.copy_private_funcs = false; + target.link_hugr(inserted.clone(), pol).unwrap(); + target.validate().unwrap(); + let (decls, defns) = list_decls_defns(&target); + assert_eq!(decls, HashMap::new()); + assert_eq!( + defns.values().copied().sorted().collect_vec(), + ["bar", "foo"] + ); + let call_tgts = call_targets(&target); + for defn in defns.keys() { + // Defns now have two calls each (was one to each alias) + assert_eq!(call_tgts.values().filter(|tgt| *tgt == defn).count(), 2); + } + } + } + } + + // TODO test copy_private_funcs actually copying; presence/absence of parent when inserting (subtree of) public func + #[rstest] + fn sig_conflict( + #[values(false, true)] host_defn: bool, + #[values(false, true)] inserted_defn: bool, + ) { + let mk_def_or_decl = |n, sig: Signature, defn| { + let mut mb = ModuleBuilder::new(); + let node = if defn { + let fb = mb.define_function_vis(n, sig, Visibility::Public).unwrap(); + let ins = fb.input_wires(); + fb.finish_with_outputs(ins).unwrap().node() + } else { + mb.declare(n, sig.into()).unwrap().node() + }; + (mb.finish_hugr().unwrap(), node) + }; + + let old_sig = Signature::new_endo(usize_t()); + let (orig_host, orig_fn) = mk_def_or_decl("foo", old_sig.clone(), host_defn); + let new_sig = Signature::new_endo(INT_TYPES[3].clone()); + let (inserted, inserted_fn) = mk_def_or_decl("foo", new_sig.clone(), inserted_defn); + + let mut pol = NameLinkingPolicy { + copy_private_funcs: true, + error_on_conflicting_sig: true, + multi_impls: MultipleImplHandling::ErrorDontInsert, + }; + let mut host = orig_host.clone(); + let res = host.link_hugr(inserted.clone(), pol.clone()); + assert_eq!(host, orig_host); // Did nothing + assert_eq!( + res, + Err(NameLinkingError::Signatures { + name: "foo".to_string(), + src_node: inserted_fn, + src_sig: Box::new(new_sig.into()), + tgt_node: orig_fn, + tgt_sig: Box::new(old_sig.into()) + }) + ); + + pol.error_on_conflicting_sig = false; + let node_map = host.link_hugr(inserted, pol).unwrap(); + assert_eq!( + host.validate(), + Err(ValidationError::DuplicateExport { + link_name: "foo".to_string(), + children: [orig_fn, node_map[&inserted_fn]] + }) + ); + } + + #[rstest] + #[case(MultipleImplHandling::UseNew, vec![11])] + #[case(MultipleImplHandling::UseExisting, vec![5])] + #[case(MultipleImplHandling::UseBoth, vec![5, 11])] + #[case(MultipleImplHandling::ErrorDontInsert, vec![])] + fn impl_conflict(#[case] multi_impls: MultipleImplHandling, #[case] expected: Vec) { + fn build_hugr(cst: u64) -> Hugr { + let mut mb = ModuleBuilder::new(); + let mut fb = mb + .define_function_vis("foo", Signature::new(vec![], usize_t()), Visibility::Public) + .unwrap(); + let c = fb.add_load_value(ConstUsize::new(cst)); + fb.finish_with_outputs([c]).unwrap(); + mb.finish_hugr().unwrap() + } + let backup = build_hugr(5); + let mut host = backup.clone(); + let inserted = build_hugr(11); + + let res = host.link_hugr( + inserted, + NameLinkingPolicy { + copy_private_funcs: true, + error_on_conflicting_sig: false, + multi_impls, + }, + ); + if multi_impls == MultipleImplHandling::ErrorDontInsert { + assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); + assert_eq!(host, backup); + return; + } + res.unwrap(); + let res = host.validate(); + if multi_impls == MultipleImplHandling::UseBoth { + assert!( + matches!(res, Err(ValidationError::DuplicateExport { link_name, .. }) if link_name == "foo") + ); + } else { + res.unwrap(); + } + let func_consts = host + .children(host.module_root()) + .filter(|n| host.get_optype(*n).is_func_defn()) + .map(|n| { + host.children(n) + .filter_map(|ch| host.get_optype(ch).as_const()) + .exactly_one() + .ok() + .unwrap() + .get_custom_value::() + .unwrap() + .value() + }) + .collect_vec(); + assert_eq!(func_consts, expected); + } } From c210f283865885aac36c8002f499d368b1cf0ca0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 18:12:37 +0100 Subject: [PATCH 19/70] Remove copy_private_funcs; fix test to expect main --- hugr-core/src/hugr/linking.rs | 75 ++++++++++++++--------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 0887200164..2704e1ab39 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -217,12 +217,7 @@ impl NodeLinkingDirective { #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameLinkingPolicy { // TODO: consider pub-funcs-to-add? (With others, optionally filtered by callgraph, made private) - /// If true, all private functions from the source hugr are inserted into the target. - /// (Since these are private, name conflicts do not make the Hugr invalid.) - /// If false, instead edges from said private functions to any inserted parts - /// of the source Hugr will be broken, making the target Hugr invalid. - /// ALAN TODO: drop this; reintroduce "copy_only_required_privates" in next PR. - copy_private_funcs: bool, + // copy_private_funcs: bool, // TODO: allow filtering private funcs to only those reachable in callgraph /// How to handle cases where the same (public) name is present in both /// inserted and target Hugr but with different signatures. /// `true` means an error is raised and nothing is added to the target Hugr. @@ -306,30 +301,27 @@ impl NameLinkingPolicy { let mut res = NodeLinkingDirectives::new(); let NameLinkingPolicy { - copy_private_funcs, error_on_conflicting_sig, multi_impls, } = self; for n in source.children(source.module_root()) { if let Some((name, is_defn, vis, sig)) = link_sig(source, n) { let mut dirv = NodeLinkingDirective::add(); - if !vis.is_public() { - if !copy_private_funcs { - continue; - }; - } else if let Some((ex_ns, ex_sig)) = existing.get(name) { - if sig == *ex_sig { - dirv = directive(name, n, is_defn, ex_ns, multi_impls)? - } else if *error_on_conflicting_sig { - return Err(NameLinkingError::Signatures { - name: name.clone(), - src_node: n, - src_sig: Box::new(sig.clone()), - tgt_node: *ex_ns.as_ref().left_or_else(|(n, _)| n), - tgt_sig: Box::new((*ex_sig).clone()), - }); + if vis.is_public() { + if let Some((ex_ns, ex_sig)) = existing.get(name) { + if sig == *ex_sig { + dirv = directive(name, n, is_defn, ex_ns, multi_impls)? + } else if *error_on_conflicting_sig { + return Err(NameLinkingError::Signatures { + name: name.clone(), + src_node: n, + src_sig: Box::new(sig.clone()), + tgt_node: *ex_ns.as_ref().left_or_else(|(n, _)| n), + tgt_sig: Box::new((*ex_sig).clone()), + }); + } } - }; + } res.insert(n, dirv); } } @@ -513,8 +505,7 @@ mod test { use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; use crate::builder::{ - DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder, - ModuleBuilder, + Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder, ModuleBuilder, }; use crate::extension::prelude::{ConstUsize, usize_t}; use crate::hugr::hugrmut::test::check_calls_defn_decl; @@ -802,8 +793,8 @@ mod test { }; let inserted = { - let mut dfb = DFGBuilder::new(Signature::new(vec![], i64_t())).unwrap(); - let mut mb = dfb.module_root_builder(); + let mut main_b = FunctionBuilder::new("main", Signature::new(vec![], i64_t())).unwrap(); + let mut mb = main_b.module_root_builder(); let foo1 = mb.declare("foo", foo_sig.clone().into()).unwrap(); let foo2 = mb.declare("foo", foo_sig.clone().into()).unwrap(); let mut bar = mb @@ -813,18 +804,15 @@ mod test { .add_dataflow_op(IntOpDef::iadd.with_log_width(6), bar.input_wires()) .unwrap(); let bar = bar.finish_with_outputs(res.outputs()).unwrap(); - let i = dfb.add_load_value(ConstInt::new_u(6, 257).unwrap()); - let c = dfb.call(&foo1, &[], [i]).unwrap(); - let r = dfb.call(&foo2, &[], c.outputs()).unwrap(); - let h = dfb.finish_hugr_with_outputs(r.outputs()).unwrap(); + let i = main_b.add_load_value(ConstInt::new_u(6, 257).unwrap()); + let c = main_b.call(&foo1, &[], [i]).unwrap(); + let r = main_b.call(&foo2, &[], c.outputs()).unwrap(); + let h = main_b.finish_hugr_with_outputs(r.outputs()).unwrap(); assert_eq!( list_decls_defns(&h), ( HashMap::from([(foo1.node(), "foo"), (foo2.node(), "foo")]), - HashMap::from([ - (h.get_parent(h.entrypoint()).unwrap(), "main"), - (bar.node(), "bar") - ]) + HashMap::from([(h.entrypoint(), "main"), (bar.node(), "bar")]) ) ); h @@ -838,27 +826,26 @@ mod test { MultipleImplHandling::UseExisting, MultipleImplHandling::UseBoth, ] { - let mut pol = NameLinkingPolicy { - copy_private_funcs: true, + let pol = NameLinkingPolicy { error_on_conflicting_sig, multi_impls, }; let mut target = orig_target.clone(); - // ALAN this won't work, test expects entrypoint-subtree to be copied - pol.copy_private_funcs = false; target.link_hugr(inserted.clone(), pol).unwrap(); target.validate().unwrap(); let (decls, defns) = list_decls_defns(&target); assert_eq!(decls, HashMap::new()); assert_eq!( defns.values().copied().sorted().collect_vec(), - ["bar", "foo"] + ["bar", "foo", "main"] ); let call_tgts = call_targets(&target); - for defn in defns.keys() { - // Defns now have two calls each (was one to each alias) - assert_eq!(call_tgts.values().filter(|tgt| *tgt == defn).count(), 2); + for (defn, name) in defns { + if name != "main" { + // Defns now have two calls each (was one to each alias) + assert_eq!(call_tgts.values().filter(|tgt| **tgt == defn).count(), 2); + } } } } @@ -888,7 +875,6 @@ mod test { let (inserted, inserted_fn) = mk_def_or_decl("foo", new_sig.clone(), inserted_defn); let mut pol = NameLinkingPolicy { - copy_private_funcs: true, error_on_conflicting_sig: true, multi_impls: MultipleImplHandling::ErrorDontInsert, }; @@ -939,7 +925,6 @@ mod test { let res = host.link_hugr( inserted, NameLinkingPolicy { - copy_private_funcs: true, error_on_conflicting_sig: false, multi_impls, }, From 68a1ca6631b21a3a4f738d19fc24d04295d3a518 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 13 Aug 2025 23:14:36 +0100 Subject: [PATCH 20/70] Accessor/factory methods (need docs), enum SignatureConflictHandling --- hugr-core/src/hugr/linking.rs | 114 ++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 2704e1ab39..6ae2fdb154 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -213,22 +213,13 @@ impl NodeLinkingDirective { } /// Describes ways to link a "Source" Hugr being inserted into a target Hugr. -/// ALAN TODO accessor methods! #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameLinkingPolicy { // TODO: consider pub-funcs-to-add? (With others, optionally filtered by callgraph, made private) // copy_private_funcs: bool, // TODO: allow filtering private funcs to only those reachable in callgraph /// How to handle cases where the same (public) name is present in both /// inserted and target Hugr but with different signatures. - /// `true` means an error is raised and nothing is added to the target Hugr. - /// `false` means the new function will be added alongside the existing one - /// - this will give an invalid Hugr (duplicate names). - // NOTE there are other possible handling schemes, both where we don't insert the new function, both leading to an invalid Hugr: - // * don't insert but break edges --> Unconnected ports (or, replace and break existing edges) - // * use (or replace) the existing function --> incompatible ports - // but given you'll need to patch the Hugr up afterwards, you can get there just - // by setting this to `false` (and maybe removing one FuncDefn), or via explicit node linking. - error_on_conflicting_sig: bool, + sig_conflict: SignatureConflictHandling, /// How to handle cases where both target and inserted Hugr have a FuncDefn /// with the same name and signature. // TODO consider Set of names where to prefer new? Or optional map from name? @@ -238,11 +229,28 @@ pub struct NameLinkingPolicy { // rename_map: HashMap } +/// What to do when both target and inserted Hugr have a +/// [Visibility::Public] function with the same name but different signatures. +// ALAN Note: we *could* combine with MultipleImplHandling; the UseExisting/UseNew variants +// would lead to invalid edges (between ports of different EdgeKind). + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[non_exhaustive] // could consider e.g. disconnections +pub enum SignatureConflictHandling { + /// Do not link the Hugrs together; raise a [NameLinkingError::SignatureConflict] instead. + ErrorDontInsert, + /// Add the new function alongside the existing one in the target Hugr, + /// preserving (separately) uses of both. (The Hugr will be invalid because + /// of duplicate names.) + UseBoth, +} + /// What to do when both target and inserted Hugr /// have a [Visibility::Public] FuncDefn with the same name and signature. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[non_exhaustive] // could consider e.g. disconnections pub enum MultipleImplHandling { - /// Do not perform insertion; raise an error instead + /// Do not link the Hugrs together; raise a [NameLinkingError::MultipleImpls] instead ErrorDontInsert, /// Keep the implementation already in the target Hugr. (Edges in the source /// Hugr will be redirected to use the function from the target.) @@ -257,6 +265,36 @@ pub enum MultipleImplHandling { UseBoth, } +impl NameLinkingPolicy { + pub fn err_on_conflict(multi_impls: MultipleImplHandling) -> Self { + Self { + multi_impls, + sig_conflict: SignatureConflictHandling::ErrorDontInsert, + } + } + + pub fn keep_both_invalid() -> Self { + Self { + multi_impls: MultipleImplHandling::UseBoth, + sig_conflict: SignatureConflictHandling::UseBoth, + } + } + + pub fn on_signature_conflict(&mut self, s: SignatureConflictHandling) { + self.sig_conflict = s; + } + + pub fn on_multiple_impls(&mut self, mih: MultipleImplHandling) { + self.multi_impls = mih; + } +} + +impl Default for NameLinkingPolicy { + fn default() -> Self { + Self::err_on_conflict(MultipleImplHandling::ErrorDontInsert) + } +} + /// An error in using names to determine how to link functions in source and target Hugrs. /// (SN = Source Node, TN = Target Node) #[derive(Clone, Debug, thiserror::Error, PartialEq)] @@ -273,7 +311,7 @@ pub enum NameLinkingError { "Conflicting signatures for name {name} - Source ({src_node}) has {src_sig}, Target ({tgt_node}) has ({tgt_sig})" )] #[allow(missing_docs)] - Signatures { + SignatureConflict { name: String, src_node: SN, src_sig: Box, @@ -301,18 +339,18 @@ impl NameLinkingPolicy { let mut res = NodeLinkingDirectives::new(); let NameLinkingPolicy { - error_on_conflicting_sig, + sig_conflict, multi_impls, } = self; for n in source.children(source.module_root()) { if let Some((name, is_defn, vis, sig)) = link_sig(source, n) { - let mut dirv = NodeLinkingDirective::add(); - if vis.is_public() { - if let Some((ex_ns, ex_sig)) = existing.get(name) { - if sig == *ex_sig { - dirv = directive(name, n, is_defn, ex_ns, multi_impls)? - } else if *error_on_conflicting_sig { - return Err(NameLinkingError::Signatures { + let dirv = if let Some((ex_ns, ex_sig)) = + vis.is_public().then(|| existing.get(name)).flatten() + { + match *sig_conflict { + _ if sig == *ex_sig => directive(name, n, is_defn, ex_ns, multi_impls)?, + SignatureConflictHandling::ErrorDontInsert => { + return Err(NameLinkingError::SignatureConflict { name: name.clone(), src_node: n, src_sig: Box::new(sig.clone()), @@ -320,8 +358,11 @@ impl NameLinkingPolicy { tgt_sig: Box::new((*ex_sig).clone()), }); } + SignatureConflictHandling::UseBoth => NodeLinkingDirective::add(), } - } + } else { + NodeLinkingDirective::add() + }; res.insert(n, dirv); } } @@ -509,7 +550,9 @@ mod test { }; use crate::extension::prelude::{ConstUsize, usize_t}; use crate::hugr::hugrmut::test::check_calls_defn_decl; - use crate::hugr::linking::{MultipleImplHandling, NameLinkingError, NameLinkingPolicy}; + use crate::hugr::linking::{ + MultipleImplHandling, NameLinkingError, NameLinkingPolicy, SignatureConflictHandling, + }; use crate::hugr::{ValidationError, hugrmut::HugrMut}; use crate::ops::{FuncDecl, OpTag, OpTrait, OpType, handle::NodeHandle}; use crate::std_extensions::arithmetic::int_ops::IntOpDef; @@ -819,7 +862,10 @@ mod test { }; // Linking by name...neither of the looped-over params should make any difference: - for error_on_conflicting_sig in [false, true] { + for sig_conflict in [ + SignatureConflictHandling::ErrorDontInsert, + SignatureConflictHandling::UseBoth, + ] { for multi_impls in [ MultipleImplHandling::ErrorDontInsert, MultipleImplHandling::UseNew, @@ -827,7 +873,7 @@ mod test { MultipleImplHandling::UseBoth, ] { let pol = NameLinkingPolicy { - error_on_conflicting_sig, + sig_conflict, multi_impls, }; let mut target = orig_target.clone(); @@ -851,7 +897,6 @@ mod test { } } - // TODO test copy_private_funcs actually copying; presence/absence of parent when inserting (subtree of) public func #[rstest] fn sig_conflict( #[values(false, true)] host_defn: bool, @@ -874,16 +919,13 @@ mod test { let new_sig = Signature::new_endo(INT_TYPES[3].clone()); let (inserted, inserted_fn) = mk_def_or_decl("foo", new_sig.clone(), inserted_defn); - let mut pol = NameLinkingPolicy { - error_on_conflicting_sig: true, - multi_impls: MultipleImplHandling::ErrorDontInsert, - }; + let mut pol = NameLinkingPolicy::err_on_conflict(MultipleImplHandling::ErrorDontInsert); let mut host = orig_host.clone(); let res = host.link_hugr(inserted.clone(), pol.clone()); assert_eq!(host, orig_host); // Did nothing assert_eq!( res, - Err(NameLinkingError::Signatures { + Err(NameLinkingError::SignatureConflict { name: "foo".to_string(), src_node: inserted_fn, src_sig: Box::new(new_sig.into()), @@ -892,7 +934,7 @@ mod test { }) ); - pol.error_on_conflicting_sig = false; + pol.on_signature_conflict(SignatureConflictHandling::UseBoth); let node_map = host.link_hugr(inserted, pol).unwrap(); assert_eq!( host.validate(), @@ -922,13 +964,9 @@ mod test { let mut host = backup.clone(); let inserted = build_hugr(11); - let res = host.link_hugr( - inserted, - NameLinkingPolicy { - error_on_conflicting_sig: false, - multi_impls, - }, - ); + let mut pol = NameLinkingPolicy::keep_both_invalid(); + pol.on_multiple_impls(multi_impls); + let res = host.link_hugr(inserted, pol); if multi_impls == MultipleImplHandling::ErrorDontInsert { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); assert_eq!(host, backup); From 10778cef26aba75fd762e239ffd74049b40158d0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 12 Aug 2025 15:45:40 +0100 Subject: [PATCH 21/70] Optimize by turning HashMap into impl IntoIterator, avoid size_hint --- hugr-core/src/hugr/hugrmut.rs | 114 +++++++++++++++------------ hugr-core/src/hugr/views/impls.rs | 4 +- hugr-core/src/hugr/views/rerooted.rs | 4 +- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index f18d2dce4e..440def21ac 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -206,7 +206,7 @@ pub trait HugrMut: HugrMutInternals { region: Node, ) -> InsertionResult { let node_map = self - .insert_forest(other, HashMap::from([(region, root)])) + .insert_forest(other, [(region, root)]) .expect("No errors possible for single subtree"); InsertionResult { inserted_entrypoint: node_map[®ion], @@ -229,7 +229,7 @@ pub trait HugrMut: HugrMutInternals { ) -> InsertionResult { let ep = other.entrypoint(); let node_map = self - .insert_view_forest(other, other.descendants(ep), HashMap::from([(ep, root)])) + .insert_view_forest(other, other.descendants(ep), [(ep, root)]) .expect("No errors possible for single subtree"); InsertionResult { inserted_entrypoint: node_map[&ep], @@ -260,7 +260,7 @@ pub trait HugrMut: HugrMutInternals { self.insert_view_forest( other, subgraph.nodes().iter().cloned(), - subgraph.nodes().iter().map(|n| (*n, root)).collect(), + subgraph.nodes().iter().map(|n| (*n, root)), ) .expect("SiblingSubgraph nodes are a set") } @@ -281,17 +281,16 @@ pub trait HugrMut: HugrMutInternals { /// /// # Panics /// - /// If any of the values in `roots` are not nodes in `self`. + /// If any of the keys in `roots` are not nodes in `other`, or any of the values not in `self`. fn insert_forest( &mut self, other: Hugr, - root_parents: HashMap, + root_parents: impl IntoIterator, ) -> InsertForestResult; /// Copy a forest of nodes from a view into this one. /// - /// `nodes` enumerates all nodes in `other` to copy; every item must *either* be - /// present in `root_parents`, or follow its parent (in `other`) in the iteration order. + /// `nodes` enumerates all nodes in `other` to copy. /// /// `root_parents` identifies those nodes in `nodes` which should be placed under /// the given parent nodes in `self`. Note that unlike [Self::insert_forest] this @@ -303,18 +302,16 @@ pub trait HugrMut: HugrMutInternals { /// /// # Errors /// - /// [InsertForestError::DoubleCopy] if any node appears in `nodes` more than once + /// [InsertForestError::DoubleCopy] if any node appears in `nodes` more than once. /// /// # Panics /// - /// If any of the values in `roots` are not nodes in `self`. - /// - /// If `nodes` does not adhere to the ordering requirement above + /// If any of the keys in `roots` are not in `nodes`, or any of the values not nodes in `self`. fn insert_view_forest( &mut self, other: &H, - nodes: impl IntoIterator, - roots: HashMap, + nodes: impl Iterator + Clone, + roots: impl IntoIterator, ) -> InsertForestResult; /// Applies a patch to the graph. @@ -515,23 +512,42 @@ impl HugrMut for Hugr { fn insert_forest( &mut self, mut other: Hugr, - roots: HashMap, + roots: impl IntoIterator, ) -> Result, InsertForestError> { - for &r in roots.keys() { - let mut n = r; - while let Some(p) = other.get_parent(n) { - if roots.contains_key(&p) { - return Err(InsertForestError::DoubleCopy(r)); + let mut roots = roots.into_iter(); + let Some(fst_root) = roots.next() else { + return Ok(HashMap::new()); + }; + let node_map = match roots.next() { + None => { + // Skip DoubleCopy check and avoid allocating singleton HashMap + insert_hugr_internal( + self, + &other, + other.descendants(fst_root.0), + std::iter::once(fst_root), + ) + } + Some(snd_root) => { + let roots: HashMap = + [fst_root, snd_root].into_iter().chain(roots).collect(); + for &r in roots.keys() { + let mut n = r; + while let Some(p) = other.get_parent(n) { + if roots.contains_key(&p) { + return Err(InsertForestError::DoubleCopy(r)); + } + n = p; + } } - n = p; + insert_hugr_internal( + self, + &other, + roots.keys().flat_map(|n| other.descendants(*n)), + roots.iter().map(|(r, p)| (*r, *p)), + ) } } - let node_map = insert_hugr_internal( - self, - &other, - roots.keys().flat_map(|n| other.descendants(*n)), - |k| roots.get(k).cloned(), - ) .expect("Trees disjoint so no repeated nodes"); // Merge the extension sets. self.extensions.extend(other.extensions()); @@ -552,11 +568,10 @@ impl HugrMut for Hugr { fn insert_view_forest( &mut self, other: &H, - nodes: impl IntoIterator, - roots: HashMap, + nodes: impl Iterator + Clone, + roots: impl IntoIterator, ) -> Result, InsertForestError> { - let node_map = - insert_hugr_internal(self, other, nodes.into_iter(), |k| roots.get(k).cloned())?; + let node_map = insert_hugr_internal(self, other, nodes, roots)?; // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. @@ -646,8 +661,8 @@ impl HugrMut for Hugr { fn insert_hugr_internal( hugr: &mut Hugr, other: &H, - other_nodes: impl Iterator, - reroot: impl Fn(&H::Node) -> Option, + other_nodes: impl Iterator + Clone, + root_parents: impl IntoIterator, ) -> Result, InsertForestError> { let new_node_count_hint = other_nodes.size_hint().1.unwrap_or_default(); @@ -655,7 +670,7 @@ fn insert_hugr_internal( let mut node_map = HashMap::with_capacity(new_node_count_hint); hugr.reserve(new_node_count_hint, 0); - for old in other_nodes { + for old in other_nodes.clone() { // We use a dummy optype here. The callers take care of updating the // correct optype, avoiding cloning if possible. let op = OpType::default(); @@ -666,16 +681,6 @@ fn insert_hugr_internal( hugr.set_num_ports(new, other.num_inputs(old), other.num_outputs(old)); - let new_parent = if let Some(new_parent) = reroot(&old) { - new_parent - } else { - let old_parent = other.get_parent(old).unwrap(); - *node_map - .get(&old_parent) - .expect("Child node came before parent in `other_nodes` iterator") - }; - hugr.set_parent(new, new_parent); - // Reconnect the edges to the new node. for tgt in other.node_inputs(old) { for (neigh, src) in other.linked_outputs(old, tgt) { @@ -697,6 +702,17 @@ fn insert_hugr_internal( } } } + for (r, p) in root_parents { + hugr.set_parent(node_map[&r], p); + } + for old in other_nodes { + let new = node_map[&old]; + if hugr.get_parent(new).is_none() { + let old_parent = other.get_parent(old).unwrap(); + let new_parent = node_map[&old_parent]; + hugr.set_parent(new, new_parent); + } + } Ok(node_map) } @@ -827,11 +843,9 @@ mod test { for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { let mut h = simple_dfg_hugr(); let (insert, defn, decl) = dfg_calling_defn_decl(); - let roots = HashMap::from_iter( - std::iter::once((insert.entrypoint(), h.entrypoint())) - .chain(call1.then_some((defn.node(), h.module_root())).into_iter()) - .chain(call2.then_some((decl.node(), h.module_root())).into_iter()), - ); + let roots = std::iter::once((insert.entrypoint(), h.entrypoint())) + .chain(call1.then_some((defn.node(), h.module_root())).into_iter()) + .chain(call2.then_some((decl.node(), h.module_root())).into_iter()); h.insert_forest(insert, roots).unwrap(); if call1 && call2 { h.validate().unwrap(); @@ -944,11 +958,11 @@ mod test { let (insert, _, _) = dfg_calling_defn_decl(); let ep = insert.entrypoint(); let epp = insert.get_parent(ep).unwrap(); - let roots = HashMap::from([(epp, h.module_root()), (ep, h.entrypoint())]); + let roots = [(epp, h.module_root()), (ep, h.entrypoint())]; let r = h.insert_view_forest( &insert, insert.descendants(epp).chain(insert.descendants(ep)), - roots.clone(), + roots, ); assert_eq!(r, Err(InsertForestError::DoubleCopy(ep))); assert!(h.validate().is_err()); diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index 3fcaea0f8b..f5cf9dfc40 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -114,8 +114,8 @@ macro_rules! hugr_mut_methods { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> Result, InsertForestError>; - fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> Result, InsertForestError>; + fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> Result, InsertForestError>; + fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> Result, InsertForestError>; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 61e39a3316..33a3d0c49b 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -139,8 +139,8 @@ impl HugrMut for Rerooted { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: HashMap) -> Result, InsertForestError>; - fn insert_view_forest(&mut self, other: &Other, nodes: impl IntoIterator, roots: HashMap) -> Result, InsertForestError>; + fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> Result, InsertForestError>; + fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> Result, InsertForestError>; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } From 826990dd8fb30b6ed076edada9ebd64f78b5095d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 15 Aug 2025 17:12:31 +0100 Subject: [PATCH 22/70] struct InsertedForest; use InsertForestResult alias more widely --- hugr-core/src/hugr/hugrmut.rs | 55 +++++++++++++++++++--------- hugr-core/src/hugr/views/impls.rs | 8 ++-- hugr-core/src/hugr/views/rerooted.rs | 8 ++-- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 440def21ac..94a9f65b92 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -207,7 +207,8 @@ pub trait HugrMut: HugrMutInternals { ) -> InsertionResult { let node_map = self .insert_forest(other, [(region, root)]) - .expect("No errors possible for single subtree"); + .expect("No errors possible for single subtree") + .node_map; InsertionResult { inserted_entrypoint: node_map[®ion], node_map, @@ -230,7 +231,8 @@ pub trait HugrMut: HugrMutInternals { let ep = other.entrypoint(); let node_map = self .insert_view_forest(other, other.descendants(ep), [(ep, root)]) - .expect("No errors possible for single subtree"); + .expect("No errors possible for single subtree") + .node_map; InsertionResult { inserted_entrypoint: node_map[&ep], node_map, @@ -263,6 +265,7 @@ pub trait HugrMut: HugrMutInternals { subgraph.nodes().iter().map(|n| (*n, root)), ) .expect("SiblingSubgraph nodes are a set") + .node_map } /// Insert a forest of nodes from another Hugr into this one. @@ -349,7 +352,7 @@ pub trait HugrMut: HugrMutInternals { /// /// On success, a map giving the new indices; or an error in the request. /// Used by [HugrMut::insert_forest] and [HugrMut::insert_view_forest]. -pub type InsertForestResult = Result, InsertForestError>; +pub type InsertForestResult = Result, InsertForestError>; /// An error from [HugrMut::insert_forest] or [HugrMut::insert_view_forest] #[derive(Clone, Debug, derive_more::Display, derive_more::Error, PartialEq)] @@ -377,6 +380,18 @@ pub struct InsertionResult { pub node_map: HashMap, } +/// Records the result of inserting a Hugr or view via [`HugrMut::insert_forest`] +/// or [`HugrMut::insert_view_forest`]. +/// +/// Contains a map from the nodes in the source HUGR to the nodes in the target +/// HUGR, using their respective `Node` types. +#[derive(Clone, Debug, Default)] +pub struct InsertedForest { + /// Map from the nodes from the source Hugr/view that were inserted, + /// to the corresponding nodes in the Hugr into which said was inserted. + pub node_map: HashMap, +} + /// Translate a portgraph node index map into a map from nodes in the source /// HUGR to nodes in the target HUGR. /// @@ -513,12 +528,14 @@ impl HugrMut for Hugr { &mut self, mut other: Hugr, roots: impl IntoIterator, - ) -> Result, InsertForestError> { + ) -> InsertForestResult { let mut roots = roots.into_iter(); let Some(fst_root) = roots.next() else { - return Ok(HashMap::new()); + return Ok(InsertedForest { + node_map: HashMap::new(), + }); }; - let node_map = match roots.next() { + let inserted = match roots.next() { None => { // Skip DoubleCopy check and avoid allocating singleton HashMap insert_hugr_internal( @@ -554,7 +571,7 @@ impl HugrMut for Hugr { // Update the optypes and metadata, taking them from the other graph. // // No need to compute each node's extensions here, as we merge `other.extensions` directly. - for (&node, &new_node) in &node_map { + for (&node, &new_node) in &inserted.node_map { let node_pg = node.into_portgraph(); let new_node_pg = new_node.into_portgraph(); let optype = other.op_types.take(node_pg); @@ -562,7 +579,7 @@ impl HugrMut for Hugr { let meta = other.metadata.take(node_pg); self.metadata.set(new_node_pg, meta); } - Ok(node_map) + Ok(inserted) } fn insert_view_forest( @@ -570,14 +587,14 @@ impl HugrMut for Hugr { other: &H, nodes: impl Iterator + Clone, roots: impl IntoIterator, - ) -> Result, InsertForestError> { - let node_map = insert_hugr_internal(self, other, nodes, roots)?; + ) -> InsertForestResult { + let inserted = insert_hugr_internal(self, other, nodes, roots)?; // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. // // No need to compute each node's extensions here, as we merge `other.extensions` directly. - for (&node, &new_node) in &node_map { + for (&node, &new_node) in &inserted.node_map { let nodetype = other.get_optype(node); self.op_types .set(new_node.into_portgraph(), nodetype.clone()); @@ -587,7 +604,7 @@ impl HugrMut for Hugr { .set(new_node.into_portgraph(), Some(meta.clone())); } } - Ok(node_map) + Ok(inserted) } fn copy_descendants( @@ -663,7 +680,7 @@ fn insert_hugr_internal( other: &H, other_nodes: impl Iterator + Clone, root_parents: impl IntoIterator, -) -> Result, InsertForestError> { +) -> InsertForestResult { let new_node_count_hint = other_nodes.size_hint().1.unwrap_or_default(); // Insert the nodes from the other graph into this one. @@ -713,7 +730,7 @@ fn insert_hugr_internal( hugr.set_parent(new, new_parent); } } - Ok(node_map) + Ok(InsertedForest { node_map }) } #[cfg(test)] @@ -914,7 +931,8 @@ mod test { insert.entry_descendants().chain([defn.node(), decl.node()]), roots.clone(), ) - .unwrap(); + .unwrap() + .node_map; assert_matches!(h.validate(), Err(ValidationError::ContainerWithoutChildren { node, optype: _ }) => assert_eq!(node, node_map[&defn.node()])); @@ -928,7 +946,8 @@ mod test { let mut h = simple_dfg_hugr(); let node_map = h .insert_view_forest(&insert, insert.nodes().skip(1), roots) - .unwrap(); + .unwrap() + .node_map; assert!(matches!( h.validate(), Err(ValidationError::InterGraphEdgeError(_)) @@ -964,12 +983,12 @@ mod test { insert.descendants(epp).chain(insert.descendants(ep)), roots, ); - assert_eq!(r, Err(InsertForestError::DoubleCopy(ep))); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); assert!(h.validate().is_err()); let mut h = backup.clone(); let r = h.insert_forest(insert, roots); - assert_eq!(r, Err(InsertForestError::DoubleCopy(ep))); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); // Here the error is detected in building `nodes` from `roots` so before any mutation assert_eq!(h, backup); } diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index f5cf9dfc40..3be4e76beb 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -1,10 +1,10 @@ //! Implementation of the core hugr traits for different wrappers of a `Hugr`. -use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc}; +use std::{borrow::Cow, rc::Rc, sync::Arc}; use super::HugrView; use crate::hugr::internal::{HugrInternals, HugrMutInternals}; -use crate::hugr::{HugrMut, hugrmut::InsertForestError}; +use crate::hugr::{HugrMut, hugrmut::InsertForestResult}; macro_rules! hugr_internal_methods { // The extra ident here is because invocations of the macro cannot pass `self` as argument @@ -114,8 +114,8 @@ macro_rules! hugr_mut_methods { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> Result, InsertForestError>; - fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> Result, InsertForestError>; + fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> InsertForestResult; + fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> InsertForestResult; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } diff --git a/hugr-core/src/hugr/views/rerooted.rs b/hugr-core/src/hugr/views/rerooted.rs index 33a3d0c49b..73fbe0cba9 100644 --- a/hugr-core/src/hugr/views/rerooted.rs +++ b/hugr-core/src/hugr/views/rerooted.rs @@ -1,10 +1,8 @@ //! A HUGR wrapper with a modified entrypoint node, returned by //! [`HugrView::with_entrypoint`] and [`HugrMut::with_entrypoint_mut`]. -use std::collections::HashMap; - use crate::hugr::internal::{HugrInternals, HugrMutInternals}; -use crate::hugr::{HugrMut, hugrmut::InsertForestError}; +use crate::hugr::{HugrMut, hugrmut::InsertForestResult}; use super::{HugrView, panic_invalid_node}; @@ -139,8 +137,8 @@ impl HugrMut for Rerooted { fn connect(&mut self, src: Self::Node, src_port: impl Into, dst: Self::Node, dst_port: impl Into); fn disconnect(&mut self, node: Self::Node, port: impl Into); fn add_other_edge(&mut self, src: Self::Node, dst: Self::Node) -> (crate::OutgoingPort, crate::IncomingPort); - fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> Result, InsertForestError>; - fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> Result, InsertForestError>; + fn insert_forest(&mut self, other: crate::Hugr, roots: impl IntoIterator) -> InsertForestResult; + fn insert_view_forest(&mut self, other: &Other, nodes: impl Iterator + Clone, roots: impl IntoIterator) -> InsertForestResult; fn use_extension(&mut self, extension: impl Into>); fn use_extensions(&mut self, registry: impl IntoIterator) where crate::extension::ExtensionRegistry: Extend; } From f9eee4423d5dbe3653496bfddbbefe9cf019d0df Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 15 Aug 2025 17:34:08 +0100 Subject: [PATCH 23/70] N -> SN --- hugr-core/src/hugr/hugrmut.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 94a9f65b92..cd32063807 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -354,13 +354,15 @@ pub trait HugrMut: HugrMutInternals { /// Used by [HugrMut::insert_forest] and [HugrMut::insert_view_forest]. pub type InsertForestResult = Result, InsertForestError>; -/// An error from [HugrMut::insert_forest] or [HugrMut::insert_view_forest] +/// An error from [HugrMut::insert_forest] or [HugrMut::insert_view_forest]. +/// +/// `SN` is the type of nodes in the source Hugr #[derive(Clone, Debug, derive_more::Display, derive_more::Error, PartialEq)] #[non_exhaustive] -pub enum InsertForestError { +pub enum InsertForestError { /// The specified source node would be copied twice into the target #[display("Node/subtree {_0} would be copied twice")] - DoubleCopy(N), + DoubleCopy(SN), } /// Records the result of inserting a Hugr or view via [`HugrMut::insert_hugr`], From caecc43f50cb3372bbff122434aaf53f1542e882 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 15 Aug 2025 17:35:04 +0100 Subject: [PATCH 24/70] Make DoubleCopy contain a named field --- hugr-core/src/hugr/hugrmut.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index cd32063807..efef819c55 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -361,8 +361,11 @@ pub type InsertForestResult = Result, InsertFores #[non_exhaustive] pub enum InsertForestError { /// The specified source node would be copied twice into the target - #[display("Node/subtree {_0} would be copied twice")] - DoubleCopy(SN), + #[display("Node/subtree {node} would be copied twice")] + DoubleCopy { + /// The node or root of subtree that would be copied twice + node: SN, + }, } /// Records the result of inserting a Hugr or view via [`HugrMut::insert_hugr`], @@ -550,13 +553,13 @@ impl HugrMut for Hugr { Some(snd_root) => { let roots: HashMap = [fst_root, snd_root].into_iter().chain(roots).collect(); - for &r in roots.keys() { - let mut n = r; - while let Some(p) = other.get_parent(n) { + for &node in roots.keys() { + let mut anc = node; + while let Some(p) = other.get_parent(anc) { if roots.contains_key(&p) { - return Err(InsertForestError::DoubleCopy(r)); + return Err(InsertForestError::DoubleCopy { node }); } - n = p; + anc = p; } } insert_hugr_internal( @@ -695,7 +698,7 @@ fn insert_hugr_internal( let op = OpType::default(); let new = hugr.add_node(op); if node_map.insert(old, new).is_some() { - return Err(InsertForestError::DoubleCopy(old)); + return Err(InsertForestError::DoubleCopy { node: old }); } hugr.set_num_ports(new, other.num_inputs(old), other.num_outputs(old)); @@ -985,12 +988,12 @@ mod test { insert.descendants(epp).chain(insert.descendants(ep)), roots, ); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy { node: ep })); assert!(h.validate().is_err()); let mut h = backup.clone(); let r = h.insert_forest(insert, roots); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy { node: ep })); // Here the error is detected in building `nodes` from `roots` so before any mutation assert_eq!(h, backup); } From bd12196eaa941bac27f7a974e04823d490d4fad2 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 16 Aug 2025 09:46:28 +0100 Subject: [PATCH 25/70] Revert "Make DoubleCopy contain a named field" This reverts commit caecc43f50cb3372bbff122434aaf53f1542e882. --- hugr-core/src/hugr/hugrmut.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index efef819c55..cd32063807 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -361,11 +361,8 @@ pub type InsertForestResult = Result, InsertFores #[non_exhaustive] pub enum InsertForestError { /// The specified source node would be copied twice into the target - #[display("Node/subtree {node} would be copied twice")] - DoubleCopy { - /// The node or root of subtree that would be copied twice - node: SN, - }, + #[display("Node/subtree {_0} would be copied twice")] + DoubleCopy(SN), } /// Records the result of inserting a Hugr or view via [`HugrMut::insert_hugr`], @@ -553,13 +550,13 @@ impl HugrMut for Hugr { Some(snd_root) => { let roots: HashMap = [fst_root, snd_root].into_iter().chain(roots).collect(); - for &node in roots.keys() { - let mut anc = node; - while let Some(p) = other.get_parent(anc) { + for &r in roots.keys() { + let mut n = r; + while let Some(p) = other.get_parent(n) { if roots.contains_key(&p) { - return Err(InsertForestError::DoubleCopy { node }); + return Err(InsertForestError::DoubleCopy(r)); } - anc = p; + n = p; } } insert_hugr_internal( @@ -698,7 +695,7 @@ fn insert_hugr_internal( let op = OpType::default(); let new = hugr.add_node(op); if node_map.insert(old, new).is_some() { - return Err(InsertForestError::DoubleCopy { node: old }); + return Err(InsertForestError::DoubleCopy(old)); } hugr.set_num_ports(new, other.num_inputs(old), other.num_outputs(old)); @@ -988,12 +985,12 @@ mod test { insert.descendants(epp).chain(insert.descendants(ep)), roots, ); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy { node: ep })); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); assert!(h.validate().is_err()); let mut h = backup.clone(); let r = h.insert_forest(insert, roots); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy { node: ep })); + assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); // Here the error is detected in building `nodes` from `roots` so before any mutation assert_eq!(h, backup); } From bdc0ff1402c075c2a87c8ac5d0bb79f7bf675d3f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 16 Aug 2025 10:03:30 +0100 Subject: [PATCH 26/70] Use rstest::fixture --- hugr-core/src/hugr/hugrmut.rs | 99 ++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index cd32063807..51cee318dd 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -739,6 +739,7 @@ fn insert_hugr_internal( mod test { use cool_asserts::assert_matches; use itertools::Itertools; + use rstest::{fixture, rstest}; use crate::builder::test::simple_dfg_hugr; use crate::builder::{DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder}; @@ -829,6 +830,7 @@ mod test { /// A DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, /// to a FuncDefn and a FuncDecl each bool_t->bool_t, and their handles. + #[fixture] pub(crate) fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); let new_defn = { @@ -856,55 +858,56 @@ mod test { ) } - #[test] - fn test_insert_forest() { - // Specify which decls to transfer - for (call1, call2) in [(false, false), (false, true), (true, false), (true, true)] { - let mut h = simple_dfg_hugr(); - let (insert, defn, decl) = dfg_calling_defn_decl(); - let roots = std::iter::once((insert.entrypoint(), h.entrypoint())) - .chain(call1.then_some((defn.node(), h.module_root())).into_iter()) - .chain(call2.then_some((decl.node(), h.module_root())).into_iter()); - h.insert_forest(insert, roots).unwrap(); - if call1 && call2 { - h.validate().unwrap(); - } else { - assert!(matches!( - h.validate(), - Err(ValidationError::UnconnectedPort { .. }) - )); - } - assert_eq!( - h.children(h.module_root()).count(), - 1 + (call1 as usize) + (call2 as usize) - ); - let [calln1, calln2] = h - .nodes() - .filter(|n| h.get_optype(*n).is_call()) - .collect_array() - .unwrap(); + #[rstest] + fn test_insert_forest( + dfg_calling_defn_decl: (Hugr, FuncID, FuncID), + #[values(false, true)] copy_defn: bool, + #[values(false, true)] copy_decl: bool, + ) { + let (insert, defn, decl) = dfg_calling_defn_decl; + let mut h = simple_dfg_hugr(); + let roots = std::iter::once((insert.entrypoint(), h.entrypoint())) + .chain(copy_defn.then_some((defn.node(), h.module_root()))) + .chain(copy_decl.then_some((decl.node(), h.module_root()))); + h.insert_forest(insert, roots).unwrap(); + if copy_defn && copy_decl { + h.validate().unwrap(); + } else { + assert!(matches!( + h.validate(), + Err(ValidationError::UnconnectedPort { .. }) + )); + } + assert_eq!( + h.children(h.module_root()).count(), + 1 + (copy_defn as usize) + (copy_decl as usize) + ); + let [calln1, calln2] = h + .nodes() + .filter(|n| h.get_optype(*n).is_call()) + .collect_array() + .unwrap(); - let tgt1 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_defn() - .is_some_and(|fd| fd.func_name() == "helper_id") - }); - assert_eq!(tgt1.is_some(), call1); - assert_eq!(h.static_source(calln1), tgt1); + let tgt1 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_defn() + .is_some_and(|fd| fd.func_name() == "helper_id") + }); + assert_eq!(tgt1.is_some(), copy_defn); + assert_eq!(h.static_source(calln1), tgt1); - let tgt2 = h.nodes().find(|n| { - h.get_optype(*n) - .as_func_decl() - .is_some_and(|fd| fd.func_name() == "helper2") - }); - assert_eq!(tgt2.is_some(), call2); - assert_eq!(h.static_source(calln2), tgt2); - } + let tgt2 = h.nodes().find(|n| { + h.get_optype(*n) + .as_func_decl() + .is_some_and(|fd| fd.func_name() == "helper2") + }); + assert_eq!(tgt2.is_some(), copy_decl); + assert_eq!(h.static_source(calln2), tgt2); } - #[test] - fn test_insert_view_forest() { - let (insert, defn, decl) = dfg_calling_defn_decl(); + #[rstest] + fn test_insert_view_forest(dfg_calling_defn_decl: (Hugr, FuncID, FuncID)) { + let (insert, defn, decl) = dfg_calling_defn_decl; let mut h = simple_dfg_hugr(); let mut roots = HashMap::from([ @@ -971,12 +974,12 @@ mod test { assert_eq!(h.input_neighbours(outp).next(), Some(inserted_ep)); } - #[test] - fn bad_insert_forest() { + #[rstest] + fn bad_insert_forest(dfg_calling_defn_decl: (Hugr, FuncID, FuncID)) { let backup = simple_dfg_hugr(); let mut h = backup.clone(); - let (insert, _, _) = dfg_calling_defn_decl(); + let (insert, _, _) = dfg_calling_defn_decl; let ep = insert.entrypoint(); let epp = insert.get_parent(ep).unwrap(); let roots = [(epp, h.module_root()), (ep, h.entrypoint())]; From 58dccdd383e52a10b3a54a9523f694020fdb42ae Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 16 Aug 2025 10:07:11 +0100 Subject: [PATCH 27/70] test tidy --- hugr-core/src/hugr/hugrmut.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 51cee318dd..db64c39c4b 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -878,10 +878,8 @@ mod test { Err(ValidationError::UnconnectedPort { .. }) )); } - assert_eq!( - h.children(h.module_root()).count(), - 1 + (copy_defn as usize) + (copy_decl as usize) - ); + let expected_mod_children = 1 + (copy_defn as usize) + (copy_decl as usize); + assert_eq!(h.children(h.module_root()).count(), expected_mod_children); let [calln1, calln2] = h .nodes() .filter(|n| h.get_optype(*n).is_call()) From b1c0d2a2df6abdb06135f5b1215616690678568d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 17 Aug 2025 10:38:20 +0100 Subject: [PATCH 28/70] add benchmark --- hugr/benches/benchmarks/hugr.rs | 46 ++++++++++++++++++++++-- hugr/benches/benchmarks/hugr/examples.rs | 29 ++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index e5f2d4de2b..31a5bfca00 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -3,13 +3,17 @@ pub mod examples; use criterion::{AxisScale, BenchmarkId, Criterion, PlotConfiguration, criterion_group}; -use hugr::Hugr; use hugr::envelope::{EnvelopeConfig, EnvelopeFormat}; +use hugr::hugr::hugrmut::HugrMut; +use hugr::ops::handle::NodeHandle; #[allow(unused)] use hugr::std_extensions::STD_REG; +use hugr::{Hugr, HugrView}; use std::hint::black_box; -pub use examples::{BENCH_EXTENSIONS, circuit, simple_cfg_hugr, simple_dfg_hugr}; +pub use examples::{ + BENCH_EXTENSIONS, circuit, dfg_calling_defn_decl, simple_cfg_hugr, simple_dfg_hugr, +}; trait Serializer { fn serialize(&self, hugr: &Hugr) -> Vec; @@ -60,6 +64,44 @@ fn bench_builder(c: &mut Criterion) { group.finish(); } +fn bench_insertion(c: &mut Criterion) { + let mut group = c.benchmark_group("insertion"); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + group.bench_function("insert_from_view", |b| { + let mut h1 = simple_dfg_hugr(); + let h2 = simple_cfg_hugr(); + b.iter(|| black_box(h1.insert_from_view(h1.entrypoint(), &h2))) + }); + group.bench_function("insert_hugr_view", |b| { + let mut h = simple_dfg_hugr(); + // Note it's possible that creation of simple_dfg_hugr may dominate the cost of insertion! + b.iter(|| black_box(h.insert_hugr(h.entrypoint(), simple_dfg_hugr()))) + }); + group.bench_function("insert_view_forest", |b| { + let mut h = simple_dfg_hugr(); + let (insert, decl, defn) = dfg_calling_defn_decl(); + let nodes = insert.entry_descendants().chain([defn.node(), decl.node()]); + let roots = [ + (insert.entrypoint(), h.entrypoint()), + (defn.node(), h.module_root()), + (decl.node(), h.module_root()), + ]; + b.iter(|| black_box(h.insert_view_forest(&insert, nodes.clone(), roots.iter().cloned()))) + }); + group.bench_function("insert_forest", |b| { + let mut h = simple_dfg_hugr(); + b.iter(|| { + let (insert, decl, defn) = dfg_calling_defn_decl(); + let roots = [ + (insert.entrypoint(), h.entrypoint()), + (defn.node(), h.module_root()), + (decl.node(), h.module_root()), + ]; + black_box(h.insert_forest(insert, roots.into_iter())) + }) + }); +} + fn bench_serialization(c: &mut Criterion) { c.bench_function("simple_cfg_serialize/json", |b| { let h = simple_cfg_hugr(); diff --git a/hugr/benches/benchmarks/hugr/examples.rs b/hugr/benches/benchmarks/hugr/examples.rs index 8e274ac8ff..bad373cb98 100644 --- a/hugr/benches/benchmarks/hugr/examples.rs +++ b/hugr/benches/benchmarks/hugr/examples.rs @@ -8,7 +8,7 @@ use hugr::builder::{ }; use hugr::extension::ExtensionRegistry; use hugr::extension::prelude::{bool_t, qb_t, usize_t}; -use hugr::ops::OpName; +use hugr::ops::{OpName, Value, handle::FuncID}; use hugr::std_extensions::STD_REG; use hugr::std_extensions::arithmetic::float_types::{ConstF64, float64_type}; use hugr::types::Signature; @@ -52,6 +52,33 @@ pub fn simple_cfg_hugr() -> Hugr { cfg_builder.finish_hugr().unwrap() } +pub fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { + let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); + let new_defn = { + let mut mb = dfb.module_root_builder(); + let fb = mb + .define_function("helper_id", Signature::new_endo(bool_t())) + .unwrap(); + let [f_inp] = fb.input_wires_arr(); + fb.finish_with_outputs([f_inp]).unwrap() + }; + let new_decl = dfb + .module_root_builder() + .declare("helper2", Signature::new_endo(bool_t()).into()) + .unwrap(); + let cst = dfb.add_load_value(Value::true_val()); + let [c1] = dfb + .call(new_defn.handle(), &[], [cst]) + .unwrap() + .outputs_arr(); + let [c2] = dfb.call(&new_decl, &[], [c1]).unwrap().outputs_arr(); + ( + dfb.finish_hugr_with_outputs([c2]).unwrap(), + *new_defn.handle(), + new_decl, + ) +} + pub static QUANTUM_EXT: LazyLock> = LazyLock::new(|| { Extension::new_arc( "bench.quantum".try_into().unwrap(), From 88a4987295f22a8ff1c5dc1132f5d0b631d7fbf3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 17 Aug 2025 20:53:21 +0100 Subject: [PATCH 29/70] lint benchmark and add to criterion group --- hugr/benches/benchmarks/hugr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index 31a5bfca00..5a55c16426 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -97,7 +97,7 @@ fn bench_insertion(c: &mut Criterion) { (defn.node(), h.module_root()), (decl.node(), h.module_root()), ]; - black_box(h.insert_forest(insert, roots.into_iter())) + black_box(h.insert_forest(insert, roots)) }) }); } @@ -151,5 +151,5 @@ criterion_group! { name = benches; config = Criterion::default(); targets = - bench_builder, bench_serialization + bench_builder, bench_insertion, bench_serialization } From cb92e0c01341df67a05dcb501861026fc838d831 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 17 Aug 2025 21:37:54 +0100 Subject: [PATCH 30/70] comments --- hugr-core/src/hugr/hugrmut.rs | 27 ++++++++++++++++----------- hugr/benches/benchmarks/hugr.rs | 1 + 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index db64c39c4b..d055892553 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -270,8 +270,11 @@ pub trait HugrMut: HugrMutInternals { /// Insert a forest of nodes from another Hugr into this one. /// - /// `root_parents` maps from roots of regions in the other Hugr to insert, - /// to the node in this Hugr that shall be parent for that region. + /// `root_parents` contains pairs of + /// * the root of a region in `other` to insert, + /// * the node in `self` that shall be parent for that region. + /// + /// Later entries for the same region override earlier ones. /// If `root_parents` is empty, nothing is inserted. /// /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` @@ -279,8 +282,8 @@ pub trait HugrMut: HugrMutInternals { /// /// # Errors /// - /// [InsertForestError::DoubleCopy] if the subtrees of the keys of `root_parents` - /// are not disjoint (the error indicates the root of the _inner_ subtree). + /// [InsertForestError::DoubleCopy] if the regions in `root_parents` are not disjount + /// (the error indicates the root of the _inner_ subtree). /// /// # Panics /// @@ -295,13 +298,15 @@ pub trait HugrMut: HugrMutInternals { /// /// `nodes` enumerates all nodes in `other` to copy. /// - /// `root_parents` identifies those nodes in `nodes` which should be placed under - /// the given parent nodes in `self`. Note that unlike [Self::insert_forest] this - /// allows inserting most of a subtree in one location but with subparts of that - /// subtree placed elsewhere. + /// `root_parents` contains pairs of a node in `nodes` and the parent in `self` under which + /// it should be to placed. Later entries (for the same node) override earlier ones. + /// Note that unlike [Self::insert_forest] this allows inserting most of a subtree in one + /// location but with subparts of that subtree placed elsewhere. /// - /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` - /// and where each value is the corresponding (new) node in `self`. + /// Nodes in `nodes` which are not mentioned in `root_parents` and whose parent in `other` + /// is not in `nodes`, will have no parent in `self`. + /// + /// Returns a [`HashMap`] from each node in `nodes` to the corresponding (new) node in `self`. /// /// # Errors /// @@ -309,7 +314,7 @@ pub trait HugrMut: HugrMutInternals { /// /// # Panics /// - /// If any of the keys in `roots` are not in `nodes`, or any of the values not nodes in `self`. + /// If any of the keys in `root_parents` are not in `nodes`, or any of the values not nodes in `self`. fn insert_view_forest( &mut self, other: &H, diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index 5a55c16426..75ab7780a8 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -91,6 +91,7 @@ fn bench_insertion(c: &mut Criterion) { group.bench_function("insert_forest", |b| { let mut h = simple_dfg_hugr(); b.iter(|| { + // Note the cost of constructing `insert`` here may dominate the cost of insertion! let (insert, decl, defn) = dfg_calling_defn_decl(); let roots = [ (insert.entrypoint(), h.entrypoint()), From a73ce4f23e103353f018374e84b6c51c04dff654 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 19 Aug 2025 12:40:13 +0100 Subject: [PATCH 31/70] Better benchmarks via iter_batched --- hugr/benches/benchmarks/hugr.rs | 38 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index 75ab7780a8..bc1f609b7b 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -2,7 +2,7 @@ pub mod examples; -use criterion::{AxisScale, BenchmarkId, Criterion, PlotConfiguration, criterion_group}; +use criterion::{AxisScale, BatchSize, BenchmarkId, Criterion, PlotConfiguration, criterion_group}; use hugr::envelope::{EnvelopeConfig, EnvelopeFormat}; use hugr::hugr::hugrmut::HugrMut; use hugr::ops::handle::NodeHandle; @@ -72,14 +72,17 @@ fn bench_insertion(c: &mut Criterion) { let h2 = simple_cfg_hugr(); b.iter(|| black_box(h1.insert_from_view(h1.entrypoint(), &h2))) }); - group.bench_function("insert_hugr_view", |b| { - let mut h = simple_dfg_hugr(); - // Note it's possible that creation of simple_dfg_hugr may dominate the cost of insertion! - b.iter(|| black_box(h.insert_hugr(h.entrypoint(), simple_dfg_hugr()))) + group.bench_function("insert_hugr", |b| { + b.iter_batched( + || (simple_dfg_hugr(), simple_cfg_hugr()), + |(mut h, insert)| black_box(h.insert_hugr(h.entrypoint(), insert)), + BatchSize::SmallInput, + ) }); group.bench_function("insert_view_forest", |b| { let mut h = simple_dfg_hugr(); let (insert, decl, defn) = dfg_calling_defn_decl(); + // Note it would be better to use iter_batched to avoid cloning nodes/roots. let nodes = insert.entry_descendants().chain([defn.node(), decl.node()]); let roots = [ (insert.entrypoint(), h.entrypoint()), @@ -89,17 +92,20 @@ fn bench_insertion(c: &mut Criterion) { b.iter(|| black_box(h.insert_view_forest(&insert, nodes.clone(), roots.iter().cloned()))) }); group.bench_function("insert_forest", |b| { - let mut h = simple_dfg_hugr(); - b.iter(|| { - // Note the cost of constructing `insert`` here may dominate the cost of insertion! - let (insert, decl, defn) = dfg_calling_defn_decl(); - let roots = [ - (insert.entrypoint(), h.entrypoint()), - (defn.node(), h.module_root()), - (decl.node(), h.module_root()), - ]; - black_box(h.insert_forest(insert, roots)) - }) + b.iter_batched( + || { + let h = simple_dfg_hugr(); + let (insert, decl, defn) = dfg_calling_defn_decl(); + let roots = [ + (insert.entrypoint(), h.entrypoint()), + (defn.node(), h.module_root()), + (decl.node(), h.module_root()), + ]; + (h, insert, roots) + }, + |(mut h, insert, roots)| black_box(h.insert_forest(insert, roots)), + BatchSize::SmallInput, + ) }); } From 481648ec69ed6c5f07fa5a9e019691f044bb9f4d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 19 Aug 2025 15:04:03 +0100 Subject: [PATCH 32/70] Always convert to HashMap; update insert_forest_internal; roots->root_parents --- hugr-core/src/hugr/hugrmut.rs | 61 ++++++++++++----------------------- 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index d055892553..1c8c314e69 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -319,7 +319,7 @@ pub trait HugrMut: HugrMutInternals { &mut self, other: &H, nodes: impl Iterator + Clone, - roots: impl IntoIterator, + root_parents: impl IntoIterator, ) -> InsertForestResult; /// Applies a patch to the graph. @@ -534,44 +534,24 @@ impl HugrMut for Hugr { fn insert_forest( &mut self, mut other: Hugr, - roots: impl IntoIterator, + root_parents: impl IntoIterator, ) -> InsertForestResult { - let mut roots = roots.into_iter(); - let Some(fst_root) = roots.next() else { - return Ok(InsertedForest { - node_map: HashMap::new(), - }); - }; - let inserted = match roots.next() { - None => { - // Skip DoubleCopy check and avoid allocating singleton HashMap - insert_hugr_internal( - self, - &other, - other.descendants(fst_root.0), - std::iter::once(fst_root), - ) - } - Some(snd_root) => { - let roots: HashMap = - [fst_root, snd_root].into_iter().chain(roots).collect(); - for &r in roots.keys() { - let mut n = r; - while let Some(p) = other.get_parent(n) { - if roots.contains_key(&p) { - return Err(InsertForestError::DoubleCopy(r)); - } - n = p; - } + let roots: HashMap<_, _> = root_parents.into_iter().collect(); + for &r in roots.keys() { + let mut n = r; + while let Some(p) = other.get_parent(n) { + if roots.contains_key(&p) { + return Err(InsertForestError::DoubleCopy(r)); } - insert_hugr_internal( - self, - &other, - roots.keys().flat_map(|n| other.descendants(*n)), - roots.iter().map(|(r, p)| (*r, *p)), - ) + n = p; } } + let inserted = insert_forest_internal( + self, + &other, + roots.keys().flat_map(|n| other.descendants(*n)), + &roots, + ) .expect("Trees disjoint so no repeated nodes"); // Merge the extension sets. self.extensions.extend(other.extensions()); @@ -593,9 +573,10 @@ impl HugrMut for Hugr { &mut self, other: &H, nodes: impl Iterator + Clone, - roots: impl IntoIterator, + root_parents: impl IntoIterator, ) -> InsertForestResult { - let inserted = insert_hugr_internal(self, other, nodes, roots)?; + let inserted = + insert_forest_internal(self, other, nodes, &root_parents.into_iter().collect())?; // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. @@ -682,11 +663,11 @@ impl HugrMut for Hugr { /// - `reroot`: A function that returns the new parent for each inserted node. /// If `None`, the parent is set to the original parent after it has been inserted into `hugr`. /// If that is the case, the parent must come before the child in the `other_nodes` iterator. -fn insert_hugr_internal( +fn insert_forest_internal( hugr: &mut Hugr, other: &H, other_nodes: impl Iterator + Clone, - root_parents: impl IntoIterator, + root_parents: &HashMap, ) -> InsertForestResult { let new_node_count_hint = other_nodes.size_hint().1.unwrap_or_default(); @@ -727,7 +708,7 @@ fn insert_hugr_internal( } } for (r, p) in root_parents { - hugr.set_parent(node_map[&r], p); + hugr.set_parent(node_map[r], *p); } for old in other_nodes { let new = node_map[&old]; From 9a9c32528202fade55cf960eed79362bda5bddbf Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 19 Aug 2025 15:26:14 +0100 Subject: [PATCH 33/70] Do not require HashMap, so insert_view_forest can avoid building one --- hugr-core/src/hugr/hugrmut.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 1c8c314e69..1a1da8116a 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -550,7 +550,7 @@ impl HugrMut for Hugr { self, &other, roots.keys().flat_map(|n| other.descendants(*n)), - &roots, + roots.iter().map(|(r, p)| (*r, *p)), ) .expect("Trees disjoint so no repeated nodes"); // Merge the extension sets. @@ -575,8 +575,7 @@ impl HugrMut for Hugr { nodes: impl Iterator + Clone, root_parents: impl IntoIterator, ) -> InsertForestResult { - let inserted = - insert_forest_internal(self, other, nodes, &root_parents.into_iter().collect())?; + let inserted = insert_forest_internal(self, other, nodes, root_parents.into_iter())?; // Merge the extension sets. self.extensions.extend(other.extensions()); // Update the optypes and metadata, copying them from the other graph. @@ -667,7 +666,7 @@ fn insert_forest_internal( hugr: &mut Hugr, other: &H, other_nodes: impl Iterator + Clone, - root_parents: &HashMap, + root_parents: impl Iterator, ) -> InsertForestResult { let new_node_count_hint = other_nodes.size_hint().1.unwrap_or_default(); @@ -708,7 +707,7 @@ fn insert_forest_internal( } } for (r, p) in root_parents { - hugr.set_parent(node_map[r], *p); + hugr.set_parent(node_map[&r], p); } for old in other_nodes { let new = node_map[&old]; From ec01c365389497775eebf1986834416d3586d6cc Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 19 Aug 2025 15:47:52 +0100 Subject: [PATCH 34/70] Separate errors: DoubleCopy => DuplicateNode + SubtreeAlreadyCopied --- hugr-core/src/hugr/hugrmut.rs | 46 +++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 1a1da8116a..1e049c8c6b 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -282,8 +282,7 @@ pub trait HugrMut: HugrMutInternals { /// /// # Errors /// - /// [InsertForestError::DoubleCopy] if the regions in `root_parents` are not disjount - /// (the error indicates the root of the _inner_ subtree). + /// [InsertForestError::SubtreeAlreadyCopied] if the regions in `root_parents` are not disjount /// /// # Panics /// @@ -310,7 +309,7 @@ pub trait HugrMut: HugrMutInternals { /// /// # Errors /// - /// [InsertForestError::DoubleCopy] if any node appears in `nodes` more than once. + /// [InsertForestError::DuplicateNode] if any node appears in `nodes` more than once. /// /// # Panics /// @@ -365,9 +364,20 @@ pub type InsertForestResult = Result, InsertFores #[derive(Clone, Debug, derive_more::Display, derive_more::Error, PartialEq)] #[non_exhaustive] pub enum InsertForestError { - /// The specified source node would be copied twice into the target - #[display("Node/subtree {_0} would be copied twice")] - DoubleCopy(SN), + /// A source node was specified twice in a call to [HugrMut::insert_view_forest] + #[display("Node {_0} would be copied twice")] + DuplicateNode(SN), + /// A subtree would be copied twice (i.e. it is contained in another) in a call to + /// [HugrMut::insert_forest] + #[display( + "Subtree rooted at {subtree} is already being copied as part of that rooted at {parent}" + )] + SubtreeAlreadyCopied { + /// Root of the inner subtree + subtree: SN, + /// Root of the outer subtree that also contains the inner + parent: SN, + }, } /// Records the result of inserting a Hugr or view via [`HugrMut::insert_hugr`], @@ -537,13 +547,13 @@ impl HugrMut for Hugr { root_parents: impl IntoIterator, ) -> InsertForestResult { let roots: HashMap<_, _> = root_parents.into_iter().collect(); - for &r in roots.keys() { - let mut n = r; - while let Some(p) = other.get_parent(n) { - if roots.contains_key(&p) { - return Err(InsertForestError::DoubleCopy(r)); + for &subtree in roots.keys() { + let mut n = subtree; + while let Some(parent) = other.get_parent(n) { + if roots.contains_key(&parent) { + return Err(InsertForestError::SubtreeAlreadyCopied { subtree, parent }); } - n = p; + n = parent; } } let inserted = insert_forest_internal( @@ -680,7 +690,7 @@ fn insert_forest_internal( let op = OpType::default(); let new = hugr.add_node(op); if node_map.insert(old, new).is_some() { - return Err(InsertForestError::DoubleCopy(old)); + return Err(InsertForestError::DuplicateNode(old)); } hugr.set_num_ports(new, other.num_inputs(old), other.num_outputs(old)); @@ -971,12 +981,18 @@ mod test { insert.descendants(epp).chain(insert.descendants(ep)), roots, ); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); + assert_eq!(r.err(), Some(InsertForestError::DuplicateNode(ep))); assert!(h.validate().is_err()); let mut h = backup.clone(); let r = h.insert_forest(insert, roots); - assert_eq!(r.err(), Some(InsertForestError::DoubleCopy(ep))); + assert_eq!( + r.err(), + Some(InsertForestError::SubtreeAlreadyCopied { + subtree: ep, + parent: epp + }) + ); // Here the error is detected in building `nodes` from `roots` so before any mutation assert_eq!(h, backup); } From 84264987a6531051f33ee27bbe064fb5cb33d319 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 20 Aug 2025 09:46:14 +0100 Subject: [PATCH 35/70] docs --- hugr-core/src/hugr/hugrmut.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 1e049c8c6b..791ac2959d 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -277,16 +277,14 @@ pub trait HugrMut: HugrMutInternals { /// Later entries for the same region override earlier ones. /// If `root_parents` is empty, nothing is inserted. /// - /// Returns a [`HashMap`] whose keys are all the inserted nodes of `other` - /// and where each value is the corresponding (new) node in `self`. - /// /// # Errors /// - /// [InsertForestError::SubtreeAlreadyCopied] if the regions in `root_parents` are not disjount + /// [InsertForestError::SubtreeAlreadyCopied] if the regions in `root_parents` are not disjoint /// /// # Panics /// - /// If any of the keys in `roots` are not nodes in `other`, or any of the values not in `self`. + /// If any of the keys in `root_parents` are not nodes in `other`, + /// or any of the values not in `self`. fn insert_forest( &mut self, other: Hugr, @@ -305,8 +303,6 @@ pub trait HugrMut: HugrMutInternals { /// Nodes in `nodes` which are not mentioned in `root_parents` and whose parent in `other` /// is not in `nodes`, will have no parent in `self`. /// - /// Returns a [`HashMap`] from each node in `nodes` to the corresponding (new) node in `self`. - /// /// # Errors /// /// [InsertForestError::DuplicateNode] if any node appears in `nodes` more than once. @@ -400,8 +396,8 @@ pub struct InsertionResult { /// Records the result of inserting a Hugr or view via [`HugrMut::insert_forest`] /// or [`HugrMut::insert_view_forest`]. /// -/// Contains a map from the nodes in the source HUGR to the nodes in the target -/// HUGR, using their respective `Node` types. +/// Contains a map from the nodes in the source HUGR that were copied, to the +/// corresponding nodes in the target HUGR, using the respective `Node` types. #[derive(Clone, Debug, Default)] pub struct InsertedForest { /// Map from the nodes from the source Hugr/view that were inserted, @@ -669,9 +665,7 @@ impl HugrMut for Hugr { /// - `hugr`: The hugr to insert into. /// - `other`: The other graph to insert from. /// - `other_nodes`: The nodes in the other graph to insert. -/// - `reroot`: A function that returns the new parent for each inserted node. -/// If `None`, the parent is set to the original parent after it has been inserted into `hugr`. -/// If that is the case, the parent must come before the child in the `other_nodes` iterator. +/// - `root_parents`: a list of pairs of (node in `other`, parent to assign in `hugr`) fn insert_forest_internal( hugr: &mut Hugr, other: &H, From f8bdaa11eb8bfc26e28fbd8d344caac8459f7946 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 21 Aug 2025 18:14:26 +0100 Subject: [PATCH 36/70] Builder docs for #2496 --- hugr-core/src/builder/build_traits.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index eb3cf2730c..ef5076f2cf 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -99,6 +99,8 @@ pub trait Container { } /// Insert a copy of a HUGR as a child of the container. + /// (Only the portion below the entrypoint will be inserted, with any incoming + /// edges broken; see [Dataflow::add_hugr_view_with_wires_link_nodes]) fn add_hugr_view(&mut self, child: &H) -> InsertionResult { let parent = self.container_node(); self.hugr_mut().insert_from_view(parent, child) @@ -256,7 +258,9 @@ pub trait Dataflow: Container { } /// Copy a hugr-defined op into the sibling graph, wiring up the - /// `input_wires` to the incoming ports of the resulting root node. + /// `input_wires` to the incoming ports of the node that was the entrypoint. + /// (Note, any part of `hugr` outside the entrypoint is not copied; + /// this may lead to new input ports being disconnected.) /// /// # Errors /// From 17a8de4e0e43689d6c2014c64f7bc77cc27298bd Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 21 Aug 2025 21:41:14 +0100 Subject: [PATCH 37/70] remove not-yet-existent doclink --- hugr-core/src/builder/build_traits.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index ef5076f2cf..3421963d87 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -99,8 +99,9 @@ pub trait Container { } /// Insert a copy of a HUGR as a child of the container. - /// (Only the portion below the entrypoint will be inserted, with any incoming - /// edges broken; see [Dataflow::add_hugr_view_with_wires_link_nodes]) + /// + /// Only the portion below the entrypoint will be inserted, with any incoming + /// edges broken. fn add_hugr_view(&mut self, child: &H) -> InsertionResult { let parent = self.container_node(); self.hugr_mut().insert_from_view(parent, child) From 191119f85c3326dfd8d1695dba7a83f0ec124c94 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 22 Aug 2025 17:24:15 +0100 Subject: [PATCH 38/70] Improve doc of dfg_calling_defn_decl --- hugr-core/src/hugr/hugrmut.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 791ac2959d..69434bb71a 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -817,8 +817,9 @@ mod test { assert_eq!(hugr.num_nodes(), 1); } - /// A DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, - /// to a FuncDefn and a FuncDecl each bool_t->bool_t, and their handles. + /// Builds a DFG-entrypoint Hugr (no inputs, one bool_t output) containing two calls, + /// to a FuncDefn and a FuncDecl each bool_t->bool_t. + /// Returns the Hugr and both function handles. #[fixture] pub(crate) fn dfg_calling_defn_decl() -> (Hugr, FuncID, FuncID) { let mut dfb = DFGBuilder::new(Signature::new(vec![], bool_t())).unwrap(); From 351fa782ac19ee0e481ccc6e9a8c9bec1576fc2e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 10:27:32 +0100 Subject: [PATCH 39/70] Rename Builder add_(hugr_=>)view_with_wires_link_nodes --- hugr-core/src/builder.rs | 2 +- hugr-core/src/builder/build_traits.rs | 6 +++--- hugr-core/src/builder/dataflow.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/builder.rs b/hugr-core/src/builder.rs index 676e1069fd..c7607d95b5 100644 --- a/hugr-core/src/builder.rs +++ b/hugr-core/src/builder.rs @@ -182,7 +182,7 @@ pub enum BuildError { #[error{"In inserting Hugr: {0}"}] HugrInsertionError(#[from] NodeLinkingError), - /// From [Dataflow::add_hugr_view_with_wires_link_nodes]. + /// From [Dataflow::add_view_with_wires_link_nodes]. /// Note that because the type of node in the [NodeLinkingError] depends /// upon the view being inserted, we convert the error to a string here. #[error("In inserting HugrView: {0}")] diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index 2153d14f44..da3879b2ca 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -102,7 +102,7 @@ pub trait Container { /// Insert a copy of a HUGR as a child of the container. /// (Only the portion below the entrypoint will be inserted, with any incoming - /// edges broken; see [Dataflow::add_hugr_view_with_wires_link_nodes]) + /// edges broken; see [Dataflow::add_view_with_wires_link_nodes]) fn add_hugr_view(&mut self, child: &H) -> InsertionResult { let parent = self.container_node(); self.hugr_mut().insert_from_view(parent, child) @@ -275,7 +275,7 @@ pub trait Dataflow: Container { /// `input_wires` to the incoming ports of the node that was the entrypoint. /// (Note, any part of `hugr` outside the entrypoint is not copied, /// this may lead to new input ports being disconnected. See - /// [Self::add_hugr_view_with_wires_link_nodes]) + /// [Self::add_view_with_wires_link_nodes]) /// /// # Errors /// @@ -293,7 +293,7 @@ pub trait Dataflow: Container { /// Copy a Hugr, adding its entrypoint into the sibling graph and wiring up the /// `input_wires` to the incoming ports. `defns` may contain other children of /// the module root of `hugr`, which will be added to the module root being built. - fn add_hugr_view_with_wires_link_nodes( + fn add_view_with_wires_link_nodes( &mut self, hugr: &H, input_wires: impl IntoIterator, diff --git a/hugr-core/src/builder/dataflow.rs b/hugr-core/src/builder/dataflow.rs index 3861ebea62..183a556d81 100644 --- a/hugr-core/src/builder/dataflow.rs +++ b/hugr-core/src/builder/dataflow.rs @@ -610,7 +610,7 @@ pub(crate) mod test { (ins_decl.node(), decl_mode), ]); let inserted = if view { - fb.add_hugr_view_with_wires_link_nodes(&insert, [], link_spec) + fb.add_view_with_wires_link_nodes(&insert, [], link_spec) .unwrap() } else { fb.add_hugr_with_wires_link_nodes(insert, [], link_spec) From 871e26e45ddf6f6c1ec3c5353877b83ce752341a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 10:27:56 +0100 Subject: [PATCH 40/70] Rename LinkHugr => HugrLinking --- hugr-core/src/builder/build_traits.rs | 2 +- hugr-core/src/hugr/linking.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index da3879b2ca..91a6877469 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -1,6 +1,6 @@ use crate::extension::prelude::MakeTuple; use crate::hugr::hugrmut::InsertionResult; -use crate::hugr::linking::{LinkHugr, NodeLinkingDirective}; +use crate::hugr::linking::{HugrLinking, NodeLinkingDirective}; use crate::hugr::views::HugrView; use crate::hugr::{NodeMetadata, ValidationError}; use crate::ops::{self, OpTag, OpTrait, OpType, Tag, TailLoop}; diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 9763abaa0e..e225cf2e96 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -15,7 +15,7 @@ use crate::{ /// This is done by module-children from the inserted (source) Hugr replacing, or being replaced by, /// module-children already in the target Hugr; static edges from the replaced node, /// are transferred to come from the replacing node, and the replaced node(/subtree) then deleted. -pub trait LinkHugr: HugrMut { +pub trait HugrLinking: HugrMut { /// Copy nodes from another Hugr into this one, with linking directives specified by Node. /// /// If `parent` is non-None, then `other`'s entrypoint-subtree is copied under it. @@ -107,10 +107,10 @@ pub trait LinkHugr: HugrMut { } } -impl LinkHugr for T {} +impl HugrLinking for T {} -/// An error resulting from an [NodeLinkingDirective] passed to [LinkHugr::insert_hugr_link_nodes] -/// or [LinkHugr::insert_from_view_link_nodes]. +/// An error resulting from an [NodeLinkingDirective] passed to [HugrLinking::insert_hugr_link_nodes] +/// or [HugrLinking::insert_from_view_link_nodes]. /// /// `SN` is the type of nodes in the source (inserted) Hugr; `TN` similarly for the target Hugr. #[derive(Clone, Debug, PartialEq, thiserror::Error)] @@ -184,7 +184,7 @@ impl NodeLinkingDirective { /// Details, node-by-node, how module-children of a source Hugr should be inserted into a /// target Hugr. /// -/// For use with [LinkHugr::insert_hugr_link_nodes] and [LinkHugr::insert_from_view_link_nodes]. +/// For use with [HugrLinking::insert_hugr_link_nodes] and [HugrLinking::insert_from_view_link_nodes]. pub type NodeLinkingDirectives = HashMap>; /// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) @@ -247,7 +247,7 @@ fn check_directives( Ok(trns) } -fn link_by_node( +fn link_by_node( hugr: &mut TGT, transfers: Transfers, node_map: &mut HashMap, @@ -284,7 +284,7 @@ mod test { use cool_asserts::assert_matches; use itertools::Itertools; - use super::{LinkHugr, NodeLinkingDirective, NodeLinkingError}; + use super::{HugrLinking, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; use crate::hugr::hugrmut::test::check_calls_defn_decl; use crate::ops::{FuncDecl, OpTag, OpTrait, handle::NodeHandle}; From ccc57bafb8b617920809f9808986cf43023ff48a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 10:31:22 +0100 Subject: [PATCH 41/70] Rename insert_(hugr/from_view)_link_nodes => add_(hugr/view)_link_nodes --- hugr-core/src/builder/build_traits.rs | 4 ++-- hugr-core/src/hugr/linking.rs | 32 +++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index 91a6877469..0280cae698 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -266,7 +266,7 @@ pub trait Dataflow: Container { let ep = hugr.entrypoint(); let node = self .hugr_mut() - .insert_hugr_link_nodes(parent, hugr, defns)? + .add_hugr_link_nodes(parent, hugr, defns)? .node_map[&ep]; wire_ins_return_outs(input_wires, node, self) } @@ -302,7 +302,7 @@ pub trait Dataflow: Container { let parent = Some(self.container_node()); let node = self .hugr_mut() - .insert_from_view_link_nodes(parent, hugr, defns) + .add_view_link_nodes(parent, hugr, defns) .map_err(|ins_err| BuildError::HugrViewInsertionError(ins_err.to_string()))? .node_map[&hugr.entrypoint()]; wire_ins_return_outs(input_wires, node, self) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index e225cf2e96..f27396ddc9 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -33,7 +33,7 @@ pub trait HugrLinking: HugrMut { /// /// If `parent` is `Some` but not in the graph. #[allow(clippy::type_complexity)] - fn insert_from_view_link_nodes( + fn add_view_link_nodes( &mut self, parent: Option, other: &H, @@ -77,7 +77,7 @@ pub trait HugrLinking: HugrMut { /// # Panics /// /// If `parent` is not in this graph. - fn insert_hugr_link_nodes( + fn add_hugr_link_nodes( &mut self, parent: Option, mut other: Hugr, @@ -109,8 +109,8 @@ pub trait HugrLinking: HugrMut { impl HugrLinking for T {} -/// An error resulting from an [NodeLinkingDirective] passed to [HugrLinking::insert_hugr_link_nodes] -/// or [HugrLinking::insert_from_view_link_nodes]. +/// An error resulting from an [NodeLinkingDirective] passed to [HugrLinking::add_hugr_link_nodes] +/// or [HugrLinking::add_view_link_nodes]. /// /// `SN` is the type of nodes in the source (inserted) Hugr; `TN` similarly for the target Hugr. #[derive(Clone, Debug, PartialEq, thiserror::Error)] @@ -184,7 +184,7 @@ impl NodeLinkingDirective { /// Details, node-by-node, how module-children of a source Hugr should be inserted into a /// target Hugr. /// -/// For use with [HugrLinking::insert_hugr_link_nodes] and [HugrLinking::insert_from_view_link_nodes]. +/// For use with [HugrLinking::add_hugr_link_nodes] and [HugrLinking::add_view_link_nodes]. pub type NodeLinkingDirectives = HashMap>; /// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) @@ -314,12 +314,12 @@ mod test { ); let mut h = simple_dfg_hugr(); - h.insert_from_view_link_nodes(Some(h.entrypoint()), &insert, mod_children.clone()) + h.add_view_link_nodes(Some(h.entrypoint()), &insert, mod_children.clone()) .unwrap(); check_calls_defn_decl(&h, call1, call2); let mut h = simple_dfg_hugr(); - h.insert_hugr_link_nodes(Some(h.entrypoint()), insert, mod_children) + h.add_hugr_link_nodes(Some(h.entrypoint()), insert, mod_children) .unwrap(); check_calls_defn_decl(&h, call1, call2); } @@ -345,7 +345,7 @@ mod test { replace: vec![defn.node(), decl.node()], }, )]); - host.insert_hugr_link_nodes(None, insert, dirvs).unwrap(); + host.add_hugr_link_nodes(None, insert, dirvs).unwrap(); host.validate().unwrap(); assert_eq!( host.children(host.module_root()) @@ -363,7 +363,7 @@ mod test { let (h, node_map) = { let mut h = simple_dfg_hugr(); let res = h - .insert_from_view_link_nodes(Some(h.entrypoint()), &insert, chmap.clone()) + .add_view_link_nodes(Some(h.entrypoint()), &insert, chmap.clone()) .unwrap(); (h, res.node_map) }; @@ -383,7 +383,7 @@ mod test { ] { chmap.insert(defn.node(), defn_mode.clone()); let mut h = h.clone(); - h.insert_hugr_link_nodes(Some(h.entrypoint()), insert.clone(), chmap.clone()) + h.add_hugr_link_nodes(Some(h.entrypoint()), insert.clone(), chmap.clone()) .unwrap(); h.validate().unwrap(); if defn_mode != NodeLinkingDirective::add() { @@ -417,7 +417,7 @@ mod test { let (defn, decl) = (defn.node(), decl.node()); let epp = insert.get_parent(insert.entrypoint()).unwrap(); - let r = h.insert_from_view_link_nodes( + let r = h.add_view_link_nodes( Some(h.entrypoint()), &insert, HashMap::from([(epp, NodeLinkingDirective::add())]), @@ -429,7 +429,7 @@ mod test { assert_eq!(h, backup); let [inp, _] = insert.get_io(defn).unwrap(); - let r = h.insert_from_view_link_nodes( + let r = h.add_view_link_nodes( Some(h.entrypoint()), &insert, HashMap::from([(inp, NodeLinkingDirective::add())]), @@ -439,7 +439,7 @@ mod test { let mut insert = insert; insert.set_entrypoint(defn); - let r = h.insert_from_view_link_nodes( + let r = h.add_view_link_nodes( Some(h.module_root()), &insert, HashMap::from([( @@ -454,7 +454,7 @@ mod test { assert_eq!(h, backup); insert.set_entrypoint(insert.module_root()); - let r = h.insert_hugr_link_nodes( + let r = h.add_hugr_link_nodes( Some(h.module_root()), insert, HashMap::from([(decl, NodeLinkingDirective::add())]), @@ -470,7 +470,7 @@ mod test { .signature() .clone(); let tmp = h.add_node_with_parent(h.module_root(), FuncDecl::new("replaced", sig)); - let r = h.insert_hugr_link_nodes( + let r = h.add_hugr_link_nodes( Some(h.entrypoint()), insert, HashMap::from([ @@ -496,7 +496,7 @@ mod test { let (insert, defn, decl) = dfg_calling_defn_decl(); let node_map = h - .insert_hugr_link_nodes( + .add_hugr_link_nodes( Some(h.entrypoint()), insert, HashMap::from([ From f3d0d84d1f782ae83b7df42dac1794286500010f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 11:03:37 +0100 Subject: [PATCH 42/70] Review suggestions (doc improvements) --- hugr-core/src/builder/build_traits.rs | 7 +++---- hugr-core/src/hugr/hugrmut.rs | 17 ++++++++++++----- hugr-core/src/hugr/linking.rs | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index 0280cae698..ed718b67c0 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -271,11 +271,10 @@ pub trait Dataflow: Container { wire_ins_return_outs(input_wires, node, self) } - /// Copy a hugr-defined op into the sibling graph, wiring up the + /// Copy a hugr's entrypoint-subtree (only) into the sibling graph, wiring up the /// `input_wires` to the incoming ports of the node that was the entrypoint. - /// (Note, any part of `hugr` outside the entrypoint is not copied, - /// this may lead to new input ports being disconnected. See - /// [Self::add_view_with_wires_link_nodes]) + /// (Note that any wires from outside the entrypoint-subtree are disconnected in the copy; + /// see [Self::add_view_with_wires_link_nodes] for an alternative.) /// /// # Errors /// diff --git a/hugr-core/src/hugr/hugrmut.rs b/hugr-core/src/hugr/hugrmut.rs index 2f2f12919b..3a1564cf8e 100644 --- a/hugr-core/src/hugr/hugrmut.rs +++ b/hugr-core/src/hugr/hugrmut.rs @@ -180,11 +180,14 @@ pub trait HugrMut: HugrMutInternals { /// Insert another hugr into this one, under a given parent node. Edges into the /// inserted subtree (i.e. nonlocal or static) will be disconnected in `self`. - /// (See [Self::insert_forest] for a way to insert sources of such edges as well.) + /// (See [Self::insert_forest] or trait [HugrLinking] for methods that can + /// preserve such edges by also inserting their sources.) /// /// # Panics /// /// If the root node is not in the graph. + /// + /// [HugrLinking]: super::linking::HugrLinking fn insert_hugr(&mut self, root: Self::Node, other: Hugr) -> InsertionResult { let region = other.entrypoint(); Self::insert_region(self, root, other, region) @@ -192,13 +195,15 @@ pub trait HugrMut: HugrMutInternals { /// Insert a subtree of another hugr into this one, under a given parent node. /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected - /// in `self`. (See [Self::insert_forest] for a way to preserve such edges by - /// inserting their sources as well.) + /// in `self`. (See [Self::insert_forest] or trait [HugrLinking] for methods that + /// can preserve such edges by also inserting their sources.) /// /// # Panics /// /// - If the root node is not in the graph. /// - If the `region` node is not in `other`. + /// + /// [HugrLinking]: super::linking::HugrLinking fn insert_region( &mut self, root: Self::Node, @@ -217,12 +222,14 @@ pub trait HugrMut: HugrMutInternals { /// Copy the entrypoint subtree of another hugr into this one, under a given parent node. /// Edges into the inserted subtree (i.e. nonlocal or static) will be disconnected - /// in `self`. (See [Self::insert_view_forest] for a way to insert sources of such edges - /// as well.) + /// in `self`. (See [Self::insert_view_forest] or trait [HugrLinking] for methods that + /// can preserve such edges by also copying their sources.) /// /// # Panics /// /// If the root node is not in the graph. + /// + /// [HugrLinking]: super::linking::HugrLinking fn insert_from_view( &mut self, root: Self::Node, diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index f27396ddc9..f3ad3511f0 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -10,7 +10,7 @@ use crate::{ hugr::{HugrMut, hugrmut::InsertedForest, internal::HugrMutInternals}, }; -/// Methods for linking Hugrs, i.e. merging the Hugrs and adding edges between old and inserted nodes. +/// Methods that merge Hugrs, adding edges between old and inserted nodes. /// /// This is done by module-children from the inserted (source) Hugr replacing, or being replaced by, /// module-children already in the target Hugr; static edges from the replaced node, From 3438e5e9f495779dcd3c05691ec5c45475f39aa1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 11:11:32 +0100 Subject: [PATCH 43/70] doc: HugrLinking: add _static_ edges between old and inserted nodes --- hugr-core/src/hugr/linking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index f3ad3511f0..f3654bf206 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -10,7 +10,7 @@ use crate::{ hugr::{HugrMut, hugrmut::InsertedForest, internal::HugrMutInternals}, }; -/// Methods that merge Hugrs, adding edges between old and inserted nodes. +/// Methods that merge Hugrs, adding static edges between old and inserted nodes. /// /// This is done by module-children from the inserted (source) Hugr replacing, or being replaced by, /// module-children already in the target Hugr; static edges from the replaced node, From 3de64e882138efdf89f317181c9b95846c9ccb90 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 11:32:55 +0100 Subject: [PATCH 44/70] Move accessors, document --- hugr-core/src/hugr/linking.rs | 80 ++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 79d69f9cef..c563ff926e 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -117,13 +117,14 @@ pub trait HugrLinking: HugrMut { /// /// # Errors /// - /// * If [NameLinkingPolicy::error_on_conflicting_sig] is true and there are public - /// functions with the same name but different signatures + /// * If [SignatureConflictHandling::ErrorDontInsert] is used and `self` and + /// `other` have public functions with the same name but different signatures /// - /// * If [MultipleImplHandling::ErrorDontInsert] is used - /// and both `self` and `other` have public [FuncDefn]s with the same name and signature + /// * If [MultipleImplHandling::ErrorDontInsert] is used and both `self` + /// and `other` have public [FuncDefn]s with the same name and signature /// /// [Visibility::Public]: crate::Visibility::Public + /// [FuncDefn]: crate::ops::FuncDefn /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert #[allow(clippy::type_complexity)] fn link_hugr( @@ -265,36 +266,6 @@ pub enum MultipleImplHandling { UseBoth, } -impl NameLinkingPolicy { - pub fn err_on_conflict(multi_impls: MultipleImplHandling) -> Self { - Self { - multi_impls, - sig_conflict: SignatureConflictHandling::ErrorDontInsert, - } - } - - pub fn keep_both_invalid() -> Self { - Self { - multi_impls: MultipleImplHandling::UseBoth, - sig_conflict: SignatureConflictHandling::UseBoth, - } - } - - pub fn on_signature_conflict(&mut self, s: SignatureConflictHandling) { - self.sig_conflict = s; - } - - pub fn on_multiple_impls(&mut self, mih: MultipleImplHandling) { - self.multi_impls = mih; - } -} - -impl Default for NameLinkingPolicy { - fn default() -> Self { - Self::err_on_conflict(MultipleImplHandling::ErrorDontInsert) - } -} - /// An error in using names to determine how to link functions in source and target Hugrs. /// (SN = Source Node, TN = Target Node) #[derive(Clone, Debug, thiserror::Error, PartialEq)] @@ -326,6 +297,41 @@ pub enum NameLinkingError { } impl NameLinkingPolicy { + /// Makes a new instance that specifies to + /// [raise an error on conflicting signatures](SignatureConflictHandling::ErrorDontInsert), + /// and to handle multiple [FuncDefn]s according to `multi_impls`. + /// + /// [FuncDefn]: crate::ops::FuncDefn + pub fn err_on_conflict(multi_impls: MultipleImplHandling) -> Self { + Self { + multi_impls, + sig_conflict: SignatureConflictHandling::ErrorDontInsert, + } + } + + /// Makes a new instance that specifies to keep both decls/defns when (for the same name) + /// they have different signatures or when both are defns. Thus, an error is never raised; + /// a (potentially-invalid) Hugr is always produced. + pub fn keep_both_invalid() -> Self { + Self { + multi_impls: MultipleImplHandling::UseBoth, + sig_conflict: SignatureConflictHandling::UseBoth, + } + } + + /// Modifies this instance to handle conflicting signatures in the specified way. + pub fn on_signature_conflict(&mut self, s: SignatureConflictHandling) { + self.sig_conflict = s; + } + + /// Modifies this instance to handle multiple implementations ([FuncDefn]s) in + /// the specified way. + /// + /// [FuncDefn]: crate::ops::FuncDefn + pub fn on_multiple_impls(&mut self, mih: MultipleImplHandling) { + self.multi_impls = mih; + } + /// Builds an explicit map of [NodeLinkingDirective]s that implements this policy for a given /// source (inserted) and target (inserted-into) Hugr. /// The map should be such that no [NodeLinkingError] will occur. @@ -371,6 +377,12 @@ impl NameLinkingPolicy { } } +impl Default for NameLinkingPolicy { + fn default() -> Self { + Self::err_on_conflict(MultipleImplHandling::ErrorDontInsert) + } +} + fn directive( name: &str, new_n: SN, From 2a6f33aa5a792ffffa32a5d38360297b0987bed7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 11:47:46 +0100 Subject: [PATCH 45/70] Improve docs: module-children, not necessarily funcdefn/decl --- hugr-core/src/hugr/linking.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index f3654bf206..ac2e5ba716 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -134,19 +134,22 @@ pub enum NodeLinkingError { NodeMultiplyReplaced(TN, SN, SN), } -/// Directive for how to treat a particular FuncDefn/FuncDecl in the source Hugr. +/// Directive for how to treat a particular module-child in the source Hugr. /// (TN is a node in the target Hugr.) #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum NodeLinkingDirective { - /// Insert the FuncDecl, or the FuncDefn and its subtree, into the target Hugr. + /// Insert the module-child (with subtree if any) into the target Hugr. Add { // TODO If non-None, change the name of the inserted function //rename: Option, /// Existing/old nodes in the target which will be removed (with their subtrees), - /// and any ([EdgeKind::Function]) edges from them changed to leave the newly-inserted - /// node instead. (Typically, this `Vec` would contain at most one `FuncDefn`, - /// or perhaps-multiple, aliased, `FuncDecl`s.) + /// and any static ([EdgeKind::Function]/[EdgeKind::Const]) edges from them changed + /// to leave the newly-inserted node instead. (Typically, this `Vec` would contain + /// at most one [FuncDefn], or perhaps-multiple, aliased, [FuncDecl]s.) /// + /// [FuncDecl]: crate::ops::FuncDecl + /// [FuncDefn]: crate::ops::FuncDefn + /// [EdgeKind::Const]: crate::types::EdgeKind::Const /// [EdgeKind::Function]: crate::types::EdgeKind::Function replace: Vec, }, From 8c16c6686edd0a31a6b4c712512ebb002397cc6c Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 11:55:51 +0100 Subject: [PATCH 46/70] Rename link_hugr to link_module, add link_module_view --- hugr-core/src/hugr/linking.rs | 46 +++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index ee39f1bba0..51db680832 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -108,10 +108,10 @@ pub trait HugrLinking: HugrMut { Ok(inserted) } - /// Copy module-children from another Hugr into this one according to a [NameLinkingPolicy]. + /// Insert module-children from another Hugr into this one according to a [NameLinkingPolicy]. /// - /// All [Visibility::Public] module-children are copied, or linked, according to the - /// specified policy; private children will also be copied, at least including all those + /// All [Visibility::Public] module-children are inserted, or linked, according to the + /// specified policy; private children will also be inserted, at least including all those /// used by the copied public children. // Yes at present we copy all private children, i.e. a safe over-approximation! /// @@ -127,7 +127,7 @@ pub trait HugrLinking: HugrMut { /// [FuncDefn]: crate::ops::FuncDefn /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert #[allow(clippy::type_complexity)] - fn link_hugr( + fn link_module( &mut self, other: Hugr, policy: NameLinkingPolicy, @@ -137,6 +137,36 @@ pub trait HugrLinking: HugrMut { .add_hugr_link_nodes(None, other, per_node) .expect("NodeLinkingPolicy was constructed to avoid any error")) } + + /// Copy module-children from another Hugr into this one according to a [NameLinkingPolicy]. + /// + /// All [Visibility::Public] module-children are copied, or linked, according to the + /// specified policy; private children will also be copied, at least including all those + /// used by the copied public children. + // Yes at present we copy all private children, i.e. a safe over-approximation! + /// + /// # Errors + /// + /// * If [SignatureConflictHandling::ErrorDontInsert] is used and `self` and + /// `other` have public functions with the same name but different signatures + /// + /// * If [MultipleImplHandling::ErrorDontInsert] is used and both `self` + /// and `other` have public [FuncDefn]s with the same name and signature + /// + /// [Visibility::Public]: crate::Visibility::Public + /// [FuncDefn]: crate::ops::FuncDefn + /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert + #[allow(clippy::type_complexity)] + fn link_module_view( + &mut self, + other: &H, + policy: NameLinkingPolicy, + ) -> Result, NameLinkingError> { + let per_node = policy.to_node_linking(self, &other)?; + Ok(self + .add_view_link_nodes(None, other, per_node) + .expect("NodeLinkingPolicy was constructed to avoid any error")) + } } impl HugrLinking for T {} @@ -894,7 +924,7 @@ mod test { }; let mut target = orig_target.clone(); - target.link_hugr(inserted.clone(), pol).unwrap(); + target.link_module(inserted.clone(), pol).unwrap(); target.validate().unwrap(); let (decls, defns) = list_decls_defns(&target); assert_eq!(decls, HashMap::new()); @@ -937,7 +967,7 @@ mod test { let mut pol = NameLinkingPolicy::err_on_conflict(MultipleImplHandling::ErrorDontInsert); let mut host = orig_host.clone(); - let res = host.link_hugr(inserted.clone(), pol.clone()); + let res = host.link_module(inserted.clone(), pol.clone()); assert_eq!(host, orig_host); // Did nothing assert_eq!( res.err(), @@ -951,7 +981,7 @@ mod test { ); pol.on_signature_conflict(SignatureConflictHandling::UseBoth); - let node_map = host.link_hugr(inserted, pol).unwrap().node_map; + let node_map = host.link_module(inserted, pol).unwrap().node_map; assert_eq!( host.validate(), Err(ValidationError::DuplicateExport { @@ -982,7 +1012,7 @@ mod test { let mut pol = NameLinkingPolicy::keep_both_invalid(); pol.on_multiple_impls(multi_impls); - let res = host.link_hugr(inserted, pol); + let res = host.link_module(inserted, pol); if multi_impls == MultipleImplHandling::ErrorDontInsert { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); assert_eq!(host, backup); From e5432cc3445b6dbbf0c9150da12c50f32c39fced Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 12:18:43 +0100 Subject: [PATCH 47/70] link_xyz take &NameLinkingPolicy; refactor tests --- hugr-core/src/hugr/linking.rs | 91 ++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 51db680832..eeb36edbe2 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -130,7 +130,7 @@ pub trait HugrLinking: HugrMut { fn link_module( &mut self, other: Hugr, - policy: NameLinkingPolicy, + policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { let per_node = policy.to_node_linking(self, &other)?; Ok(self @@ -160,7 +160,7 @@ pub trait HugrLinking: HugrMut { fn link_module_view( &mut self, other: &H, - policy: NameLinkingPolicy, + policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { let per_node = policy.to_node_linking(self, &other)?; Ok(self @@ -856,12 +856,25 @@ mod test { .collect() } - #[test] - fn combines_decls_defn() { + #[rstest] + fn combines_decls_defn( + #[values( + SignatureConflictHandling::ErrorDontInsert, + SignatureConflictHandling::UseBoth + )] + sig_conflict: SignatureConflictHandling, + #[values( + MultipleImplHandling::ErrorDontInsert, + MultipleImplHandling::UseNew, + MultipleImplHandling::UseExisting, + MultipleImplHandling::UseBoth + )] + multi_impls: MultipleImplHandling, + ) { let i64_t = || INT_TYPES[6].to_owned(); let foo_sig = Signature::new_endo(i64_t()); let bar_sig = Signature::new(vec![i64_t(); 2], i64_t()); - let orig_target = { + let mut target = { let mut fb = FunctionBuilder::new_vis("foo", foo_sig.clone(), Visibility::Public).unwrap(); let mut mb = fb.module_root_builder(); @@ -907,37 +920,27 @@ mod test { h }; - // Linking by name...neither of the looped-over params should make any difference: - for sig_conflict in [ - SignatureConflictHandling::ErrorDontInsert, - SignatureConflictHandling::UseBoth, - ] { - for multi_impls in [ - MultipleImplHandling::ErrorDontInsert, - MultipleImplHandling::UseNew, - MultipleImplHandling::UseExisting, - MultipleImplHandling::UseBoth, - ] { - let pol = NameLinkingPolicy { - sig_conflict, - multi_impls, - }; - let mut target = orig_target.clone(); - - target.link_module(inserted.clone(), pol).unwrap(); - target.validate().unwrap(); - let (decls, defns) = list_decls_defns(&target); - assert_eq!(decls, HashMap::new()); - assert_eq!( - defns.values().copied().sorted().collect_vec(), - ["bar", "foo", "main"] - ); - let call_tgts = call_targets(&target); - for (defn, name) in defns { - if name != "main" { - // Defns now have two calls each (was one to each alias) - assert_eq!(call_tgts.values().filter(|tgt| **tgt == defn).count(), 2); - } + let pol = NameLinkingPolicy { + sig_conflict, + multi_impls, + }; + let mut target2 = target.clone(); + + target.link_module_view(&inserted, &pol).unwrap(); + target2.link_module(inserted, &pol).unwrap(); + for tgt in [target, target2] { + tgt.validate().unwrap(); + let (decls, defns) = list_decls_defns(&tgt); + assert_eq!(decls, HashMap::new()); + assert_eq!( + defns.values().copied().sorted().collect_vec(), + ["bar", "foo", "main"] + ); + let call_tgts = call_targets(&tgt); + for (defn, name) in defns { + if name != "main" { + // Defns now have two calls each (was one to each alias) + assert_eq!(call_tgts.values().filter(|tgt| **tgt == defn).count(), 2); } } } @@ -967,7 +970,7 @@ mod test { let mut pol = NameLinkingPolicy::err_on_conflict(MultipleImplHandling::ErrorDontInsert); let mut host = orig_host.clone(); - let res = host.link_module(inserted.clone(), pol.clone()); + let res = host.link_module_view(&inserted, &pol); assert_eq!(host, orig_host); // Did nothing assert_eq!( res.err(), @@ -981,7 +984,7 @@ mod test { ); pol.on_signature_conflict(SignatureConflictHandling::UseBoth); - let node_map = host.link_module(inserted, pol).unwrap().node_map; + let node_map = host.link_module(inserted, &pol).unwrap().node_map; assert_eq!( host.validate(), Err(ValidationError::DuplicateExport { @@ -1012,20 +1015,20 @@ mod test { let mut pol = NameLinkingPolicy::keep_both_invalid(); pol.on_multiple_impls(multi_impls); - let res = host.link_module(inserted, pol); + let res = host.link_module(inserted, &pol); if multi_impls == MultipleImplHandling::ErrorDontInsert { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); assert_eq!(host, backup); return; } res.unwrap(); - let res = host.validate(); + let val_res = host.validate(); if multi_impls == MultipleImplHandling::UseBoth { assert!( - matches!(res, Err(ValidationError::DuplicateExport { link_name, .. }) if link_name == "foo") + matches!(val_res, Err(ValidationError::DuplicateExport { link_name, .. }) if link_name == "foo") ); } else { - res.unwrap(); + val_res.unwrap(); } let func_consts = host .children(host.module_root()) @@ -1033,12 +1036,10 @@ mod test { .map(|n| { host.children(n) .filter_map(|ch| host.get_optype(ch).as_const()) + .map(|c| c.get_custom_value::().unwrap().value()) .exactly_one() .ok() .unwrap() - .get_custom_value::() - .unwrap() - .value() }) .collect_vec(); assert_eq!(func_consts, expected); From fd02eab767b2381406ef3acd0251e22d595aaade Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 12:23:32 +0100 Subject: [PATCH 48/70] clippy --- hugr-core/src/hugr/linking.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index eeb36edbe2..a08c424d6f 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -126,7 +126,6 @@ pub trait HugrLinking: HugrMut { /// [Visibility::Public]: crate::Visibility::Public /// [FuncDefn]: crate::ops::FuncDefn /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert - #[allow(clippy::type_complexity)] fn link_module( &mut self, other: Hugr, @@ -833,6 +832,7 @@ mod test { } } + #[allow(clippy::type_complexity)] fn list_decls_defns(h: &H) -> (HashMap, HashMap) { let mut decls = HashMap::new(); let mut defns = HashMap::new(); @@ -849,10 +849,7 @@ mod test { fn call_targets(h: &H) -> HashMap { h.nodes() .filter(|n| h.get_optype(*n).is_call()) - .map(|n| { - let tgt = h.static_source(n).expect(format!("For node {n}").as_str()); - (n, tgt) - }) + .map(|n| (n, h.static_source(n).unwrap())) .collect() } From 347f96d793d6979dccfd63e315827e7fd4bf29e3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 14:38:01 +0100 Subject: [PATCH 49/70] newer clippy --- hugr-core/src/hugr/linking.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index a08c424d6f..5f6b43a89a 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -456,7 +456,9 @@ fn link_sig( } } -fn gather_existing(h: &H) -> HashMap<&String, PubFuncs> { +fn gather_existing<'a, H: HugrView + ?Sized>( + h: &'a H, +) -> HashMap<&'a String, PubFuncs<'a, H::Node>> { let left_if = |b| if b { Either::Left } else { Either::Right }; h.children(h.module_root()) .filter_map(|n| { From ff8d6ea024b995f5e81e7fd795d258231bdf6946 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 25 Aug 2025 15:03:48 +0100 Subject: [PATCH 50/70] Handle Constants, update test --- hugr-core/src/hugr/linking.rs | 99 ++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 5f6b43a89a..d5eec24ba3 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, fmt::Display}; use itertools::{Either, Itertools}; use crate::{ - Hugr, HugrView, Node, Visibility, + Hugr, HugrView, Node, core::HugrNode, hugr::{HugrMut, hugrmut::InsertedForest, internal::HugrMutInternals}, ops::OpType, @@ -381,28 +381,30 @@ impl NameLinkingPolicy { multi_impls, } = self; for n in source.children(source.module_root()) { - if let Some((name, is_defn, vis, sig)) = link_sig(source, n) { - let dirv = if let Some((ex_ns, ex_sig)) = - vis.is_public().then(|| existing.get(name)).flatten() - { - match *sig_conflict { - _ if sig == *ex_sig => directive(name, n, is_defn, ex_ns, multi_impls)?, - SignatureConflictHandling::ErrorDontInsert => { - return Err(NameLinkingError::SignatureConflict { - name: name.clone(), - src_node: n, - src_sig: Box::new(sig.clone()), - tgt_node: *ex_ns.as_ref().left_or_else(|(n, _)| n), - tgt_sig: Box::new((*ex_sig).clone()), - }); + let dirv = match link_sig(source, n) { + None => continue, + Some(LinkSig::Private) => NodeLinkingDirective::add(), + Some(LinkSig::Public { name, is_defn, sig }) => { + if let Some((ex_ns, ex_sig)) = existing.get(name) { + match *sig_conflict { + _ if sig == *ex_sig => directive(name, n, is_defn, ex_ns, multi_impls)?, + SignatureConflictHandling::ErrorDontInsert => { + return Err(NameLinkingError::SignatureConflict { + name: name.clone(), + src_node: n, + src_sig: Box::new(sig.clone()), + tgt_node: *ex_ns.as_ref().left_or_else(|(n, _)| n), + tgt_sig: Box::new((*ex_sig).clone()), + }); + } + SignatureConflictHandling::UseBoth => NodeLinkingDirective::add(), } - SignatureConflictHandling::UseBoth => NodeLinkingDirective::add(), + } else { + NodeLinkingDirective::add() } - } else { - NodeLinkingDirective::add() - }; - res.insert(n, dirv); - } + } + }; + res.insert(n, dirv); } Ok(res) @@ -445,15 +447,27 @@ fn directive( type PubFuncs<'a, N> = (Either)>, &'a PolyFuncType); -fn link_sig( - h: &H, - n: H::Node, -) -> Option<(&String, bool, &Visibility, &PolyFuncType)> { - match h.get_optype(n) { - OpType::FuncDecl(fd) => Some((fd.func_name(), false, fd.visibility(), fd.signature())), - OpType::FuncDefn(fd) => Some((fd.func_name(), true, fd.visibility(), fd.signature())), - _ => None, - } +enum LinkSig<'a> { + Private, + Public { + name: &'a String, + is_defn: bool, + sig: &'a PolyFuncType, + }, +} + +fn link_sig(h: &H, n: H::Node) -> Option> { + let (name, is_defn, vis, sig) = match h.get_optype(n) { + OpType::FuncDecl(fd) => (fd.func_name(), false, fd.visibility(), fd.signature()), + OpType::FuncDefn(fd) => (fd.func_name(), true, fd.visibility(), fd.signature()), + OpType::Const(_) => return Some(LinkSig::Private), + _ => return None, + }; + Some(if vis.is_public() { + LinkSig::Public { name, is_defn, sig } + } else { + LinkSig::Private + }) } fn gather_existing<'a, H: HugrView + ?Sized>( @@ -462,9 +476,9 @@ fn gather_existing<'a, H: HugrView + ?Sized>( let left_if = |b| if b { Either::Left } else { Either::Right }; h.children(h.module_root()) .filter_map(|n| { - link_sig(h, n).and_then(|(fname, is_defn, vis, sig)| { - vis.is_public() - .then_some((fname, (left_if(is_defn)(n), sig))) + link_sig(h, n).and_then(|link_sig| match link_sig { + LinkSig::Public { name, is_defn, sig } => Some((name, (left_if(is_defn)(n), sig))), + LinkSig::Private => None, }) }) .into_grouping_map() @@ -592,7 +606,8 @@ mod test { use super::{HugrLinking, NodeLinkingDirective, NodeLinkingError}; use crate::builder::test::{dfg_calling_defn_decl, simple_dfg_hugr}; use crate::builder::{ - Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder, ModuleBuilder, + Container, Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder, + ModuleBuilder, }; use crate::extension::prelude::{ConstUsize, usize_t}; use crate::hugr::hugrmut::test::check_calls_defn_decl; @@ -600,7 +615,7 @@ mod test { MultipleImplHandling, NameLinkingError, NameLinkingPolicy, SignatureConflictHandling, }; use crate::hugr::{ValidationError, hugrmut::HugrMut}; - use crate::ops::{FuncDecl, OpTag, OpTrait, OpType, handle::NodeHandle}; + use crate::ops::{FuncDecl, OpTag, OpTrait, OpType, Value, handle::NodeHandle}; use crate::std_extensions::arithmetic::int_ops::IntOpDef; use crate::std_extensions::arithmetic::int_types::{ConstInt, INT_TYPES}; use crate::{Hugr, HugrView, Visibility, types::Signature}; @@ -1001,10 +1016,11 @@ mod test { fn impl_conflict(#[case] multi_impls: MultipleImplHandling, #[case] expected: Vec) { fn build_hugr(cst: u64) -> Hugr { let mut mb = ModuleBuilder::new(); + let cst = mb.add_constant(Value::from(ConstUsize::new(cst))); let mut fb = mb .define_function_vis("foo", Signature::new(vec![], usize_t()), Visibility::Public) .unwrap(); - let c = fb.add_load_value(ConstUsize::new(cst)); + let c = fb.load_const(&cst); fb.finish_with_outputs([c]).unwrap(); mb.finish_hugr().unwrap() } @@ -1034,7 +1050,8 @@ mod test { .filter(|n| host.get_optype(*n).is_func_defn()) .map(|n| { host.children(n) - .filter_map(|ch| host.get_optype(ch).as_const()) + .filter_map(|ch| host.static_source(ch)) // LoadConstant's + .map(|c| host.get_optype(c).as_const().unwrap()) .map(|c| c.get_custom_value::().unwrap().value()) .exactly_one() .ok() @@ -1042,5 +1059,13 @@ mod test { }) .collect_vec(); assert_eq!(func_consts, expected); + // At the moment we copy all the constants regardless of whether they are used: + let all_consts: Vec<_> = host + .children(host.module_root()) + .filter_map(|ch| host.get_optype(ch).as_const()) + .map(|c| c.get_custom_value::().unwrap().value()) + .sorted() + .collect(); + assert_eq!(all_consts, [5, 11]); } } From d3f95df66460138e67831c5bdabb2640a42e6d93 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 26 Aug 2025 10:52:25 +0100 Subject: [PATCH 51/70] doclinks --- hugr-core/src/hugr/linking.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index d5eec24ba3..8adb49fa5b 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -264,9 +264,10 @@ pub struct NameLinkingPolicy { /// What to do when both target and inserted Hugr have a /// [Visibility::Public] function with the same name but different signatures. +/// +/// [Visibility::Public]: crate::Visibility::Public // ALAN Note: we *could* combine with MultipleImplHandling; the UseExisting/UseNew variants // would lead to invalid edges (between ports of different EdgeKind). - #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[non_exhaustive] // could consider e.g. disconnections pub enum SignatureConflictHandling { @@ -280,6 +281,8 @@ pub enum SignatureConflictHandling { /// What to do when both target and inserted Hugr /// have a [Visibility::Public] FuncDefn with the same name and signature. +/// +/// [Visibility::Public]: crate::Visibility::Public #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[non_exhaustive] // could consider e.g. disconnections pub enum MultipleImplHandling { @@ -324,6 +327,8 @@ pub enum NameLinkingError { /// A [Visibility::Public] function in the source, whose body is being added /// to the target, contained the entrypoint (which needs to be added /// in a different place). + /// + /// [Visibility::Public]: crate::Visibility::Public #[error("The entrypoint is contained within function {_0} which will be added as {_1:?}")] AddFunctionContainingEntrypoint(SN, NodeLinkingDirective), } From 685e19fc1a168c8886420e24b149bb102eb4690b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 1 Sep 2025 18:13:13 +0100 Subject: [PATCH 52/70] insert_view_forest uses iter_batched and recreates host each time --- hugr/benches/benchmarks/hugr.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index bc1f609b7b..9e42b696eb 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -80,16 +80,21 @@ fn bench_insertion(c: &mut Criterion) { ) }); group.bench_function("insert_view_forest", |b| { - let mut h = simple_dfg_hugr(); let (insert, decl, defn) = dfg_calling_defn_decl(); - // Note it would be better to use iter_batched to avoid cloning nodes/roots. - let nodes = insert.entry_descendants().chain([defn.node(), decl.node()]); - let roots = [ - (insert.entrypoint(), h.entrypoint()), - (defn.node(), h.module_root()), - (decl.node(), h.module_root()), - ]; - b.iter(|| black_box(h.insert_view_forest(&insert, nodes.clone(), roots.iter().cloned()))) + b.iter_batched( + || { + let h = simple_dfg_hugr(); + let nodes = insert.entry_descendants().chain([defn.node(), decl.node()]); + let roots = [ + (insert.entrypoint(), h.entrypoint()), + (defn.node(), h.module_root()), + (decl.node(), h.module_root()), + ]; + (h, insert, nodes, roots) + }, + |(mut h, insert, nodes, roots)| black_box(h.insert_view_forest(&insert, nodes, roots)), + BatchSize::SmallInput, + ) }); group.bench_function("insert_forest", |b| { b.iter_batched( From 57e58e29610165b5b99c96f4e185dfcc2a5fe3aa Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 1 Sep 2025 18:18:20 +0100 Subject: [PATCH 53/70] oops, fix borrow in bench --- hugr/benches/benchmarks/hugr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr/benches/benchmarks/hugr.rs b/hugr/benches/benchmarks/hugr.rs index 9e42b696eb..d434209411 100644 --- a/hugr/benches/benchmarks/hugr.rs +++ b/hugr/benches/benchmarks/hugr.rs @@ -90,9 +90,9 @@ fn bench_insertion(c: &mut Criterion) { (defn.node(), h.module_root()), (decl.node(), h.module_root()), ]; - (h, insert, nodes, roots) + (h, &insert, nodes, roots) }, - |(mut h, insert, nodes, roots)| black_box(h.insert_view_forest(&insert, nodes, roots)), + |(mut h, insert, nodes, roots)| black_box(h.insert_view_forest(insert, nodes, roots)), BatchSize::SmallInput, ) }); From 5979bf6989dc31a6c5403ada3c6d7009bb34e9cf Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 2 Sep 2025 16:34:02 +0100 Subject: [PATCH 54/70] Add ModuleBuilder::add_{hugr,view}_link_nodes --- hugr-core/src/builder/module.rs | 91 ++++++++++++++++++++++++++++++--- hugr-core/src/hugr/linking.rs | 2 +- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/hugr-core/src/builder/module.rs b/hugr-core/src/builder/module.rs index 5499abae92..1376311b56 100644 --- a/hugr-core/src/builder/module.rs +++ b/hugr-core/src/builder/module.rs @@ -4,13 +4,14 @@ use super::{ dataflow::{DFGBuilder, FunctionBuilder}, }; -use crate::hugr::internal::HugrMutInternals; -use crate::hugr::views::HugrView; +use crate::hugr::linking::{HugrLinking, NodeLinkingDirectives, NodeLinkingError}; +use crate::hugr::{ + ValidationError, hugrmut::InsertedForest, internal::HugrMutInternals, views::HugrView, +}; use crate::ops; use crate::ops::handle::{AliasID, FuncID, NodeHandle}; use crate::types::{PolyFuncType, Type, TypeBound}; -use crate::{Hugr, Node, Visibility}; -use crate::{hugr::ValidationError, ops::FuncDefn}; +use crate::{Hugr, Node, Visibility, ops::FuncDefn}; use smol_str::SmolStr; @@ -220,17 +221,46 @@ impl + AsRef> ModuleBuilder { Ok(AliasID::new(node, name, bound)) } + + /// Adds some module-children of another Hugr to this module, with + /// linking directives specified explicitly by [Node]. + /// + /// `children` contains a map from the children of `other` to insert, + /// to how they should be combined with the nodes in `self`. Note if + /// this map is empty, nothing is added. + pub fn add_hugr_link_nodes( + &mut self, + other: Hugr, + children: NodeLinkingDirectives, + ) -> Result { + self.hugr_mut().add_hugr_link_nodes(None, other, children) + } + + /// Copies module-children from a HugrView to this module, with + /// linking directives specified explicitly by [Node]. + /// + /// `children` contains a map from the children of `other` to copy, + /// to how they should be combined with the nodes in `self`. Note if + /// this map is empty, nothing is added. + pub fn add_view_link_nodes( + &mut self, + other: &H, + children: NodeLinkingDirectives, + ) -> Result, NodeLinkingError> { + self.hugr_mut().add_view_link_nodes(None, other, children) + } } #[cfg(test)] mod test { + use std::collections::{HashMap, HashSet}; + use cool_asserts::assert_matches; + use crate::builder::test::dfg_calling_defn_decl; + use crate::builder::{Dataflow, DataflowSubContainer, test::n_identity}; use crate::extension::prelude::usize_t; - use crate::{ - builder::{Dataflow, DataflowSubContainer, test::n_identity}, - types::Signature, - }; + use crate::{hugr::linking::NodeLinkingDirective, ops::OpType, types::Signature}; use super::*; #[test] @@ -289,4 +319,49 @@ mod test { Ok(()) } + + #[test] + fn add_link_nodes() { + let mut mb = ModuleBuilder::new(); + let (dfg, defn, decl) = dfg_calling_defn_decl(); + let added = mb + .add_view_link_nodes( + &dfg, + HashMap::from([ + (defn.node(), NodeLinkingDirective::add()), + (decl.node(), NodeLinkingDirective::add()), + ]), + ) + .unwrap(); + let n_defn = added.node_map[&defn.node()]; + let n_decl = added.node_map[&decl.node()]; + let h = mb.hugr(); + assert_eq!(h.children(h.module_root()).count(), 2); + h.validate().unwrap(); + let old_name = match mb.hugr_mut().optype_mut(n_defn) { + OpType::FuncDefn(fd) => std::mem::replace(fd.func_name_mut(), "new".to_string()), + _ => panic!(), + }; + let main = dfg.get_parent(dfg.entrypoint()).unwrap(); + assert_eq!( + dfg.get_optype(main).as_func_defn().unwrap().func_name(), + "main" + ); + mb.add_hugr_link_nodes( + dfg, + HashMap::from([ + (main, NodeLinkingDirective::add()), + (decl.node(), NodeLinkingDirective::UseExisting(n_defn)), + (defn.node(), NodeLinkingDirective::replace([n_decl])), + ]), + ) + .unwrap(); + let h = mb.finish_hugr().unwrap(); + assert_eq!( + h.children(h.module_root()) + .map(|n| h.get_optype(n).as_func_defn().unwrap().func_name().as_str()) + .collect::>(), + HashSet::from(["main", "new", old_name.as_str()]) + ); + } } diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index ac2e5ba716..9cbe6b4d67 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -115,7 +115,7 @@ impl HugrLinking for T {} /// `SN` is the type of nodes in the source (inserted) Hugr; `TN` similarly for the target Hugr. #[derive(Clone, Debug, PartialEq, thiserror::Error)] #[non_exhaustive] -pub enum NodeLinkingError { +pub enum NodeLinkingError { /// Inserting the whole Hugr, yet also asked to insert some of its children /// (so the inserted Hugr's entrypoint was its module-root). #[error( From 1a3e37a4224baee716c4825e942dc04cda033b94 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Wed, 3 Sep 2025 08:50:42 +0100 Subject: [PATCH 55/70] ModuleBuilder::add_(hugr/view)_link_nodes => link_(hugr/view)_by_node --- hugr-core/src/builder/module.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hugr-core/src/builder/module.rs b/hugr-core/src/builder/module.rs index 1376311b56..59bf8d954d 100644 --- a/hugr-core/src/builder/module.rs +++ b/hugr-core/src/builder/module.rs @@ -228,7 +228,7 @@ impl + AsRef> ModuleBuilder { /// `children` contains a map from the children of `other` to insert, /// to how they should be combined with the nodes in `self`. Note if /// this map is empty, nothing is added. - pub fn add_hugr_link_nodes( + pub fn link_hugr_by_node( &mut self, other: Hugr, children: NodeLinkingDirectives, @@ -242,7 +242,7 @@ impl + AsRef> ModuleBuilder { /// `children` contains a map from the children of `other` to copy, /// to how they should be combined with the nodes in `self`. Note if /// this map is empty, nothing is added. - pub fn add_view_link_nodes( + pub fn link_view_by_node( &mut self, other: &H, children: NodeLinkingDirectives, @@ -321,11 +321,11 @@ mod test { } #[test] - fn add_link_nodes() { + fn link_by_node() { let mut mb = ModuleBuilder::new(); let (dfg, defn, decl) = dfg_calling_defn_decl(); let added = mb - .add_view_link_nodes( + .link_view_by_node( &dfg, HashMap::from([ (defn.node(), NodeLinkingDirective::add()), @@ -347,7 +347,7 @@ mod test { dfg.get_optype(main).as_func_defn().unwrap().func_name(), "main" ); - mb.add_hugr_link_nodes( + mb.link_hugr_by_node( dfg, HashMap::from([ (main, NodeLinkingDirective::add()), From cc861fa4bc00f6bc082f449546606f7744c15c1b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 4 Sep 2025 17:57:46 +0100 Subject: [PATCH 56/70] HugrMut methods are insert_link_(hugr/view)_by_node --- hugr-core/src/builder/build_traits.rs | 4 ++-- hugr-core/src/builder/module.rs | 6 ++++-- hugr-core/src/hugr/linking.rs | 30 +++++++++++++-------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index ed718b67c0..8014e51dc8 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -266,7 +266,7 @@ pub trait Dataflow: Container { let ep = hugr.entrypoint(); let node = self .hugr_mut() - .add_hugr_link_nodes(parent, hugr, defns)? + .insert_link_hugr_by_node(parent, hugr, defns)? .node_map[&ep]; wire_ins_return_outs(input_wires, node, self) } @@ -301,7 +301,7 @@ pub trait Dataflow: Container { let parent = Some(self.container_node()); let node = self .hugr_mut() - .add_view_link_nodes(parent, hugr, defns) + .insert_link_view_by_node(parent, hugr, defns) .map_err(|ins_err| BuildError::HugrViewInsertionError(ins_err.to_string()))? .node_map[&hugr.entrypoint()]; wire_ins_return_outs(input_wires, node, self) diff --git a/hugr-core/src/builder/module.rs b/hugr-core/src/builder/module.rs index 59bf8d954d..ed9e86ab08 100644 --- a/hugr-core/src/builder/module.rs +++ b/hugr-core/src/builder/module.rs @@ -233,7 +233,8 @@ impl + AsRef> ModuleBuilder { other: Hugr, children: NodeLinkingDirectives, ) -> Result { - self.hugr_mut().add_hugr_link_nodes(None, other, children) + self.hugr_mut() + .insert_link_hugr_by_node(None, other, children) } /// Copies module-children from a HugrView to this module, with @@ -247,7 +248,8 @@ impl + AsRef> ModuleBuilder { other: &H, children: NodeLinkingDirectives, ) -> Result, NodeLinkingError> { - self.hugr_mut().add_view_link_nodes(None, other, children) + self.hugr_mut() + .insert_link_view_by_node(None, other, children) } } diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 9cbe6b4d67..b409096a60 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -16,7 +16,7 @@ use crate::{ /// module-children already in the target Hugr; static edges from the replaced node, /// are transferred to come from the replacing node, and the replaced node(/subtree) then deleted. pub trait HugrLinking: HugrMut { - /// Copy nodes from another Hugr into this one, with linking directives specified by Node. + /// Copy and link nodes from another Hugr into this one, with linking specified by Node. /// /// If `parent` is non-None, then `other`'s entrypoint-subtree is copied under it. /// `children` of the Module root of `other` may also be inserted with their @@ -33,7 +33,7 @@ pub trait HugrLinking: HugrMut { /// /// If `parent` is `Some` but not in the graph. #[allow(clippy::type_complexity)] - fn add_view_link_nodes( + fn insert_link_view_by_node( &mut self, parent: Option, other: &H, @@ -62,7 +62,7 @@ pub trait HugrLinking: HugrMut { Ok(inserted) } - /// Insert another Hugr into this one, with linking directives specified by Node. + /// Insert and link another Hugr into this one, with linking specified by Node. /// /// If `parent` is non-None, then `other`'s entrypoint-subtree is placed under it. /// `children` of the Module root of `other` may also be inserted with their @@ -77,7 +77,7 @@ pub trait HugrLinking: HugrMut { /// # Panics /// /// If `parent` is not in this graph. - fn add_hugr_link_nodes( + fn insert_link_hugr_by_node( &mut self, parent: Option, mut other: Hugr, @@ -317,12 +317,12 @@ mod test { ); let mut h = simple_dfg_hugr(); - h.add_view_link_nodes(Some(h.entrypoint()), &insert, mod_children.clone()) + h.insert_link_view_by_node(Some(h.entrypoint()), &insert, mod_children.clone()) .unwrap(); check_calls_defn_decl(&h, call1, call2); let mut h = simple_dfg_hugr(); - h.add_hugr_link_nodes(Some(h.entrypoint()), insert, mod_children) + h.insert_link_hugr_by_node(Some(h.entrypoint()), insert, mod_children) .unwrap(); check_calls_defn_decl(&h, call1, call2); } @@ -348,7 +348,7 @@ mod test { replace: vec![defn.node(), decl.node()], }, )]); - host.add_hugr_link_nodes(None, insert, dirvs).unwrap(); + host.insert_link_hugr_by_node(None, insert, dirvs).unwrap(); host.validate().unwrap(); assert_eq!( host.children(host.module_root()) @@ -366,7 +366,7 @@ mod test { let (h, node_map) = { let mut h = simple_dfg_hugr(); let res = h - .add_view_link_nodes(Some(h.entrypoint()), &insert, chmap.clone()) + .insert_link_view_by_node(Some(h.entrypoint()), &insert, chmap.clone()) .unwrap(); (h, res.node_map) }; @@ -386,7 +386,7 @@ mod test { ] { chmap.insert(defn.node(), defn_mode.clone()); let mut h = h.clone(); - h.add_hugr_link_nodes(Some(h.entrypoint()), insert.clone(), chmap.clone()) + h.insert_link_hugr_by_node(Some(h.entrypoint()), insert.clone(), chmap.clone()) .unwrap(); h.validate().unwrap(); if defn_mode != NodeLinkingDirective::add() { @@ -420,7 +420,7 @@ mod test { let (defn, decl) = (defn.node(), decl.node()); let epp = insert.get_parent(insert.entrypoint()).unwrap(); - let r = h.add_view_link_nodes( + let r = h.insert_link_view_by_node( Some(h.entrypoint()), &insert, HashMap::from([(epp, NodeLinkingDirective::add())]), @@ -432,7 +432,7 @@ mod test { assert_eq!(h, backup); let [inp, _] = insert.get_io(defn).unwrap(); - let r = h.add_view_link_nodes( + let r = h.insert_link_view_by_node( Some(h.entrypoint()), &insert, HashMap::from([(inp, NodeLinkingDirective::add())]), @@ -442,7 +442,7 @@ mod test { let mut insert = insert; insert.set_entrypoint(defn); - let r = h.add_view_link_nodes( + let r = h.insert_link_view_by_node( Some(h.module_root()), &insert, HashMap::from([( @@ -457,7 +457,7 @@ mod test { assert_eq!(h, backup); insert.set_entrypoint(insert.module_root()); - let r = h.add_hugr_link_nodes( + let r = h.insert_link_hugr_by_node( Some(h.module_root()), insert, HashMap::from([(decl, NodeLinkingDirective::add())]), @@ -473,7 +473,7 @@ mod test { .signature() .clone(); let tmp = h.add_node_with_parent(h.module_root(), FuncDecl::new("replaced", sig)); - let r = h.add_hugr_link_nodes( + let r = h.insert_link_hugr_by_node( Some(h.entrypoint()), insert, HashMap::from([ @@ -499,7 +499,7 @@ mod test { let (insert, defn, decl) = dfg_calling_defn_decl(); let node_map = h - .add_hugr_link_nodes( + .insert_link_hugr_by_node( Some(h.entrypoint()), insert, HashMap::from([ From 0de13267a854e66abd541ee61d85a31c74719b77 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 4 Sep 2025 18:02:15 +0100 Subject: [PATCH 57/70] Dataflow builder: add_link_(hugr/view)_by_node_with_wires --- hugr-core/src/builder/build_traits.rs | 4 ++-- hugr-core/src/builder/dataflow.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index 8014e51dc8..6152c19dbd 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -256,7 +256,7 @@ pub trait Dataflow: Container { /// `input_wires` to the incoming ports of the resulting root node. `defns` may /// contain other children of the module root of `hugr`, which will be added to /// the module root being built. - fn add_hugr_with_wires_link_nodes( + fn add_link_hugr_by_node_with_wires( &mut self, hugr: Hugr, input_wires: impl IntoIterator, @@ -292,7 +292,7 @@ pub trait Dataflow: Container { /// Copy a Hugr, adding its entrypoint into the sibling graph and wiring up the /// `input_wires` to the incoming ports. `defns` may contain other children of /// the module root of `hugr`, which will be added to the module root being built. - fn add_view_with_wires_link_nodes( + fn add_link_view_by_node_with_wires( &mut self, hugr: &H, input_wires: impl IntoIterator, diff --git a/hugr-core/src/builder/dataflow.rs b/hugr-core/src/builder/dataflow.rs index 183a556d81..1152e6ce9b 100644 --- a/hugr-core/src/builder/dataflow.rs +++ b/hugr-core/src/builder/dataflow.rs @@ -610,10 +610,10 @@ pub(crate) mod test { (ins_decl.node(), decl_mode), ]); let inserted = if view { - fb.add_view_with_wires_link_nodes(&insert, [], link_spec) + fb.add_link_view_by_node_with_wires(&insert, [], link_spec) .unwrap() } else { - fb.add_hugr_with_wires_link_nodes(insert, [], link_spec) + fb.add_link_hugr_by_node_with_wires(insert, [], link_spec) .unwrap() }; let h = fb.finish_hugr_with_outputs(inserted.outputs()).unwrap(); From 97b3bf1ad0fa2564ba865d9b3d9ffc5ae2c29927 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Thu, 4 Sep 2025 20:50:28 +0100 Subject: [PATCH 58/70] doclinks --- hugr-core/src/builder.rs | 4 ++-- hugr-core/src/builder/build_traits.rs | 4 ++-- hugr-core/src/hugr/linking.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hugr-core/src/builder.rs b/hugr-core/src/builder.rs index c7607d95b5..85b8e37cb0 100644 --- a/hugr-core/src/builder.rs +++ b/hugr-core/src/builder.rs @@ -178,11 +178,11 @@ pub enum BuildError { node: Node, }, - /// From [Dataflow::add_hugr_with_wires_link_nodes] + /// From [Dataflow::add_link_hugr_by_node_with_wires] #[error{"In inserting Hugr: {0}"}] HugrInsertionError(#[from] NodeLinkingError), - /// From [Dataflow::add_view_with_wires_link_nodes]. + /// From [Dataflow::add_link_view_by_node_with_wires]. /// Note that because the type of node in the [NodeLinkingError] depends /// upon the view being inserted, we convert the error to a string here. #[error("In inserting HugrView: {0}")] diff --git a/hugr-core/src/builder/build_traits.rs b/hugr-core/src/builder/build_traits.rs index 6152c19dbd..1936c5410d 100644 --- a/hugr-core/src/builder/build_traits.rs +++ b/hugr-core/src/builder/build_traits.rs @@ -102,7 +102,7 @@ pub trait Container { /// Insert a copy of a HUGR as a child of the container. /// (Only the portion below the entrypoint will be inserted, with any incoming - /// edges broken; see [Dataflow::add_view_with_wires_link_nodes]) + /// edges broken; see [Dataflow::add_link_view_by_node_with_wires]) fn add_hugr_view(&mut self, child: &H) -> InsertionResult { let parent = self.container_node(); self.hugr_mut().insert_from_view(parent, child) @@ -274,7 +274,7 @@ pub trait Dataflow: Container { /// Copy a hugr's entrypoint-subtree (only) into the sibling graph, wiring up the /// `input_wires` to the incoming ports of the node that was the entrypoint. /// (Note that any wires from outside the entrypoint-subtree are disconnected in the copy; - /// see [Self::add_view_with_wires_link_nodes] for an alternative.) + /// see [Self::add_link_view_by_node_with_wires] for an alternative.) /// /// # Errors /// diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index b409096a60..4e0eeba844 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -109,8 +109,8 @@ pub trait HugrLinking: HugrMut { impl HugrLinking for T {} -/// An error resulting from an [NodeLinkingDirective] passed to [HugrLinking::add_hugr_link_nodes] -/// or [HugrLinking::add_view_link_nodes]. +/// An error resulting from an [NodeLinkingDirective] passed to [HugrLinking::insert_link_hugr_by_node] +/// or [HugrLinking::insert_link_view_by_node]. /// /// `SN` is the type of nodes in the source (inserted) Hugr; `TN` similarly for the target Hugr. #[derive(Clone, Debug, PartialEq, thiserror::Error)] @@ -187,7 +187,7 @@ impl NodeLinkingDirective { /// Details, node-by-node, how module-children of a source Hugr should be inserted into a /// target Hugr. /// -/// For use with [HugrLinking::add_hugr_link_nodes] and [HugrLinking::add_view_link_nodes]. +/// For use with [HugrLinking::insert_link_hugr_by_node] and [HugrLinking::insert_link_view_by_node]. pub type NodeLinkingDirectives = HashMap>; /// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) From a1f9a1efc1c28b42febe0397d2e7d8816ce48fa7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Sep 2025 20:32:27 +0100 Subject: [PATCH 59/70] SignatureConflictHandling -> NewFuncHandling, MultipleImplHandling::AsFunc --- hugr-core/src/hugr/linking.rs | 81 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 8adb49fa5b..ca81425dd0 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -252,7 +252,7 @@ pub struct NameLinkingPolicy { // copy_private_funcs: bool, // TODO: allow filtering private funcs to only those reachable in callgraph /// How to handle cases where the same (public) name is present in both /// inserted and target Hugr but with different signatures. - sig_conflict: SignatureConflictHandling, + sig_conflict: NewFuncHandling, /// How to handle cases where both target and inserted Hugr have a FuncDefn /// with the same name and signature. // TODO consider Set of names where to prefer new? Or optional map from name? @@ -262,21 +262,22 @@ pub struct NameLinkingPolicy { // rename_map: HashMap } -/// What to do when both target and inserted Hugr have a -/// [Visibility::Public] function with the same name but different signatures. +/// What to do with a function; used +/// * when both target and inserted Hugr have a [Visibility::Public] function +/// with the same name but different signatures ([NameLinkingPolicy::on_signature_conflict]) +/// * when both target and inserted Hugr have [Visibility::Public] [FuncDefn]s +/// with the same name and signatures - in [MultipleImplHandling::HandleFunc] /// /// [Visibility::Public]: crate::Visibility::Public -// ALAN Note: we *could* combine with MultipleImplHandling; the UseExisting/UseNew variants -// would lead to invalid edges (between ports of different EdgeKind). #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[non_exhaustive] // could consider e.g. disconnections -pub enum SignatureConflictHandling { - /// Do not link the Hugrs together; raise a [NameLinkingError::SignatureConflict] instead. - ErrorDontInsert, +pub enum NewFuncHandling { + /// Do not link the Hugrs together; fail with a [NameLinkingError] instead. + RaiseError, /// Add the new function alongside the existing one in the target Hugr, /// preserving (separately) uses of both. (The Hugr will be invalid because - /// of duplicate names.) - UseBoth, + /// of [duplicate names](crate::hugr::ValidationError::DuplicateExport).) + Add, } /// What to do when both target and inserted Hugr @@ -286,8 +287,6 @@ pub enum SignatureConflictHandling { #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[non_exhaustive] // could consider e.g. disconnections pub enum MultipleImplHandling { - /// Do not link the Hugrs together; raise a [NameLinkingError::MultipleImpls] instead - ErrorDontInsert, /// Keep the implementation already in the target Hugr. (Edges in the source /// Hugr will be redirected to use the function from the target.) UseExisting, @@ -295,10 +294,14 @@ pub enum MultipleImplHandling { /// will be redirected to use the function from the source; the previously-existing /// function in the target Hugr will be removed.) UseNew, - /// Add the new function alongside the existing one in the target Hugr, - /// preserving (separately) uses of both. (The Hugr will be invalid because - /// of duplicate names.) - UseBoth, + /// Proceed as per the specified [NewFuncHandling]. + NewFunc(NewFuncHandling), +} + +impl From for MultipleImplHandling { + fn from(value: NewFuncHandling) -> Self { + Self::NewFunc(value) + } } /// An error in using names to determine how to link functions in source and target Hugrs. @@ -339,10 +342,10 @@ impl NameLinkingPolicy { /// and to handle multiple [FuncDefn]s according to `multi_impls`. /// /// [FuncDefn]: crate::ops::FuncDefn - pub fn err_on_conflict(multi_impls: MultipleImplHandling) -> Self { + pub fn err_on_conflict(multi_impls: impl Into) -> Self { Self { - multi_impls, - sig_conflict: SignatureConflictHandling::ErrorDontInsert, + multi_impls: multi_impls.into(), + sig_conflict: NewFuncHandling::RaiseError, } } @@ -351,13 +354,13 @@ impl NameLinkingPolicy { /// a (potentially-invalid) Hugr is always produced. pub fn keep_both_invalid() -> Self { Self { - multi_impls: MultipleImplHandling::UseBoth, - sig_conflict: SignatureConflictHandling::UseBoth, + multi_impls: MultipleImplHandling::NewFunc(NewFuncHandling::Add), + sig_conflict: NewFuncHandling::Add, } } /// Modifies this instance to handle conflicting signatures in the specified way. - pub fn on_signature_conflict(&mut self, s: SignatureConflictHandling) { + pub fn on_signature_conflict(&mut self, s: NewFuncHandling) { self.sig_conflict = s; } @@ -393,7 +396,7 @@ impl NameLinkingPolicy { if let Some((ex_ns, ex_sig)) = existing.get(name) { match *sig_conflict { _ if sig == *ex_sig => directive(name, n, is_defn, ex_ns, multi_impls)?, - SignatureConflictHandling::ErrorDontInsert => { + NewFuncHandling::RaiseError => { return Err(NameLinkingError::SignatureConflict { name: name.clone(), src_node: n, @@ -402,7 +405,7 @@ impl NameLinkingPolicy { tgt_sig: Box::new((*ex_sig).clone()), }); } - SignatureConflictHandling::UseBoth => NodeLinkingDirective::add(), + NewFuncHandling::Add => NodeLinkingDirective::add(), } } else { NodeLinkingDirective::add() @@ -418,7 +421,7 @@ impl NameLinkingPolicy { impl Default for NameLinkingPolicy { fn default() -> Self { - Self::err_on_conflict(MultipleImplHandling::ErrorDontInsert) + Self::err_on_conflict(NewFuncHandling::RaiseError) } } @@ -438,14 +441,14 @@ fn directive( (true, &Either::Left(defn)) => match multi_impls { MultipleImplHandling::UseExisting => NodeLinkingDirective::UseExisting(defn), MultipleImplHandling::UseNew => NodeLinkingDirective::replace([defn]), - MultipleImplHandling::ErrorDontInsert => { + MultipleImplHandling::NewFunc(NewFuncHandling::RaiseError) => { return Err(NameLinkingError::MultipleImpls( name.to_owned(), new_n, defn, )); } - MultipleImplHandling::UseBoth => NodeLinkingDirective::add(), + MultipleImplHandling::NewFunc(NewFuncHandling::Add) => NodeLinkingDirective::add(), }, }) } @@ -617,7 +620,7 @@ mod test { use crate::extension::prelude::{ConstUsize, usize_t}; use crate::hugr::hugrmut::test::check_calls_defn_decl; use crate::hugr::linking::{ - MultipleImplHandling, NameLinkingError, NameLinkingPolicy, SignatureConflictHandling, + MultipleImplHandling, NameLinkingError, NameLinkingPolicy, NewFuncHandling, }; use crate::hugr::{ValidationError, hugrmut::HugrMut}; use crate::ops::{FuncDecl, OpTag, OpTrait, OpType, Value, handle::NodeHandle}; @@ -877,16 +880,12 @@ mod test { #[rstest] fn combines_decls_defn( + #[values(NewFuncHandling::RaiseError, NewFuncHandling::Add)] sig_conflict: NewFuncHandling, #[values( - SignatureConflictHandling::ErrorDontInsert, - SignatureConflictHandling::UseBoth - )] - sig_conflict: SignatureConflictHandling, - #[values( - MultipleImplHandling::ErrorDontInsert, + NewFuncHandling::RaiseError.into(), MultipleImplHandling::UseNew, MultipleImplHandling::UseExisting, - MultipleImplHandling::UseBoth + NewFuncHandling::Add.into() )] multi_impls: MultipleImplHandling, ) { @@ -987,7 +986,7 @@ mod test { let new_sig = Signature::new_endo(INT_TYPES[3].clone()); let (inserted, inserted_fn) = mk_def_or_decl("foo", new_sig.clone(), inserted_defn); - let mut pol = NameLinkingPolicy::err_on_conflict(MultipleImplHandling::ErrorDontInsert); + let mut pol = NameLinkingPolicy::err_on_conflict(NewFuncHandling::RaiseError); let mut host = orig_host.clone(); let res = host.link_module_view(&inserted, &pol); assert_eq!(host, orig_host); // Did nothing @@ -1002,7 +1001,7 @@ mod test { }) ); - pol.on_signature_conflict(SignatureConflictHandling::UseBoth); + pol.on_signature_conflict(NewFuncHandling::Add); let node_map = host.link_module(inserted, &pol).unwrap().node_map; assert_eq!( host.validate(), @@ -1016,8 +1015,8 @@ mod test { #[rstest] #[case(MultipleImplHandling::UseNew, vec![11])] #[case(MultipleImplHandling::UseExisting, vec![5])] - #[case(MultipleImplHandling::UseBoth, vec![5, 11])] - #[case(MultipleImplHandling::ErrorDontInsert, vec![])] + #[case(NewFuncHandling::Add.into(), vec![5, 11])] + #[case(NewFuncHandling::RaiseError.into(), vec![])] fn impl_conflict(#[case] multi_impls: MultipleImplHandling, #[case] expected: Vec) { fn build_hugr(cst: u64) -> Hugr { let mut mb = ModuleBuilder::new(); @@ -1036,14 +1035,14 @@ mod test { let mut pol = NameLinkingPolicy::keep_both_invalid(); pol.on_multiple_impls(multi_impls); let res = host.link_module(inserted, &pol); - if multi_impls == MultipleImplHandling::ErrorDontInsert { + if multi_impls == NewFuncHandling::RaiseError.into() { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); assert_eq!(host, backup); return; } res.unwrap(); let val_res = host.validate(); - if multi_impls == MultipleImplHandling::UseBoth { + if multi_impls == NewFuncHandling::Add.into() { assert!( matches!(val_res, Err(ValidationError::DuplicateExport { link_name, .. }) if link_name == "foo") ); From 2d11a692d0666bd89f810bf729de30cc671f7aac Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Sep 2025 20:51:00 +0100 Subject: [PATCH 60/70] ...and fix docs --- hugr-core/src/hugr/linking.rs | 48 ++++++++++++++--------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index ca81425dd0..a4bf9132b3 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -117,15 +117,12 @@ pub trait HugrLinking: HugrMut { /// /// # Errors /// - /// * If [SignatureConflictHandling::ErrorDontInsert] is used and `self` and - /// `other` have public functions with the same name but different signatures - /// - /// * If [MultipleImplHandling::ErrorDontInsert] is used and both `self` - /// and `other` have public [FuncDefn]s with the same name and signature + /// If [NameLinkingPolicy::on_signature_conflict] or [NameLinkingPolicy::on_multiple_impls] + /// are set to [NewFuncHandling::RaiseError], and the respective conflict occurs between + /// `self` and `other`. /// /// [Visibility::Public]: crate::Visibility::Public /// [FuncDefn]: crate::ops::FuncDefn - /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert fn link_module( &mut self, other: Hugr, @@ -146,15 +143,12 @@ pub trait HugrLinking: HugrMut { /// /// # Errors /// - /// * If [SignatureConflictHandling::ErrorDontInsert] is used and `self` and - /// `other` have public functions with the same name but different signatures - /// - /// * If [MultipleImplHandling::ErrorDontInsert] is used and both `self` - /// and `other` have public [FuncDefn]s with the same name and signature + /// If [NameLinkingPolicy::on_signature_conflict] or [NameLinkingPolicy::on_multiple_impls] + /// are set to [NewFuncHandling::RaiseError], and the respective conflict occurs between + /// `self` and `other`. /// /// [Visibility::Public]: crate::Visibility::Public /// [FuncDefn]: crate::ops::FuncDefn - /// [MultipleImplHandling::ErrorDontInsert]: crate::hugr::linking::MultipleImplHandling::ErrorDontInsert #[allow(clippy::type_complexity)] fn link_module_view( &mut self, @@ -250,11 +244,7 @@ impl NodeLinkingDirective { pub struct NameLinkingPolicy { // TODO: consider pub-funcs-to-add? (With others, optionally filtered by callgraph, made private) // copy_private_funcs: bool, // TODO: allow filtering private funcs to only those reachable in callgraph - /// How to handle cases where the same (public) name is present in both - /// inserted and target Hugr but with different signatures. sig_conflict: NewFuncHandling, - /// How to handle cases where both target and inserted Hugr have a FuncDefn - /// with the same name and signature. // TODO consider Set of names where to prefer new? Or optional map from name? multi_impls: MultipleImplHandling, // TODO Renames to apply to public functions in the inserted Hugr. These take effect @@ -262,12 +252,11 @@ pub struct NameLinkingPolicy { // rename_map: HashMap } -/// What to do with a function; used -/// * when both target and inserted Hugr have a [Visibility::Public] function -/// with the same name but different signatures ([NameLinkingPolicy::on_signature_conflict]) -/// * when both target and inserted Hugr have [Visibility::Public] [FuncDefn]s -/// with the same name and signatures - in [MultipleImplHandling::HandleFunc] +/// Specifies what to do with a function in some situation - used in +/// * [NameLinkingPolicy::on_signature_conflict] +/// * [MultipleImplHandling::NewFunc] /// +/// [FuncDefn]: crate::ops::FuncDefn /// [Visibility::Public]: crate::Visibility::Public #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[non_exhaustive] // could consider e.g. disconnections @@ -337,9 +326,9 @@ pub enum NameLinkingError { } impl NameLinkingPolicy { - /// Makes a new instance that specifies to - /// [raise an error on conflicting signatures](SignatureConflictHandling::ErrorDontInsert), - /// and to handle multiple [FuncDefn]s according to `multi_impls`. + /// Makes a new instance that specifies to handle + /// [signature conflicts](Self::on_signature_conflict) by failing with an error and + /// multiple [FuncDefn]s according to `multi_impls`. /// /// [FuncDefn]: crate::ops::FuncDefn pub fn err_on_conflict(multi_impls: impl Into) -> Self { @@ -359,15 +348,16 @@ impl NameLinkingPolicy { } } - /// Modifies this instance to handle conflicting signatures in the specified way. + /// Specifies how to behave when both target and inserted Hugr have a + /// [Public] function with the same name but different signatures. + /// + /// [Public]: crate::Visibility::Public pub fn on_signature_conflict(&mut self, s: NewFuncHandling) { self.sig_conflict = s; } - /// Modifies this instance to handle multiple implementations ([FuncDefn]s) in - /// the specified way. - /// - /// [FuncDefn]: crate::ops::FuncDefn + /// Specifies how to behave when both target and inserted Hugr have a + /// [FuncDefn](crate::ops::FuncDefn) with the same name and signature. pub fn on_multiple_impls(&mut self, mih: MultipleImplHandling) { self.multi_impls = mih; } From 2bee21f69f702f299a0492cadbf15ea63bfb1461 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 5 Sep 2025 20:52:48 +0100 Subject: [PATCH 61/70] on_multiple_impls / on_signature_conflict return &mut --- hugr-core/src/hugr/linking.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index a4bf9132b3..df87ae2b65 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -350,16 +350,16 @@ impl NameLinkingPolicy { /// Specifies how to behave when both target and inserted Hugr have a /// [Public] function with the same name but different signatures. - /// + /// /// [Public]: crate::Visibility::Public - pub fn on_signature_conflict(&mut self, s: NewFuncHandling) { - self.sig_conflict = s; + pub fn on_signature_conflict(&mut self) -> &mut NewFuncHandling { + &mut self.sig_conflict } /// Specifies how to behave when both target and inserted Hugr have a /// [FuncDefn](crate::ops::FuncDefn) with the same name and signature. - pub fn on_multiple_impls(&mut self, mih: MultipleImplHandling) { - self.multi_impls = mih; + pub fn on_multiple_impls(&mut self) -> &mut MultipleImplHandling { + &mut self.multi_impls } /// Builds an explicit map of [NodeLinkingDirective]s that implements this policy for a given @@ -991,7 +991,7 @@ mod test { }) ); - pol.on_signature_conflict(NewFuncHandling::Add); + *pol.on_signature_conflict() = NewFuncHandling::Add; let node_map = host.link_module(inserted, &pol).unwrap().node_map; assert_eq!( host.validate(), @@ -1023,7 +1023,7 @@ mod test { let inserted = build_hugr(11); let mut pol = NameLinkingPolicy::keep_both_invalid(); - pol.on_multiple_impls(multi_impls); + *pol.on_multiple_impls() = multi_impls; let res = host.link_module(inserted, &pol); if multi_impls == NewFuncHandling::RaiseError.into() { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); From 158fcfd5acd5f231bbb7c40d406c5296945b292f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 14 Sep 2025 08:54:39 +0100 Subject: [PATCH 62/70] NameLinkingPolicy mutators are self->Self --- hugr-core/src/hugr/linking.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index df87ae2b65..f78377fe18 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -348,18 +348,34 @@ impl NameLinkingPolicy { } } - /// Specifies how to behave when both target and inserted Hugr have a + /// Sets how to behave when both target and inserted Hugr have a /// [Public] function with the same name but different signatures. /// /// [Public]: crate::Visibility::Public - pub fn on_signature_conflict(&mut self) -> &mut NewFuncHandling { - &mut self.sig_conflict + pub fn on_signature_conflict(mut self, sc: NewFuncHandling) -> Self { + self.sig_conflict = sc; + self } - /// Specifies how to behave when both target and inserted Hugr have a + /// Tells how to behave when both target and inserted Hugr have a + /// [Public] function with the same name but different signatures. + /// + /// [Public]: crate::Visibility::Public + pub fn get_signature_conflict(&self) -> NewFuncHandling { + self.sig_conflict + } + + /// Sets how to behave when both target and inserted Hugr have a + /// [FuncDefn](crate::ops::FuncDefn) with the same name and signature. + pub fn on_multiple_impls(mut self, mih: MultipleImplHandling) -> Self { + self.multi_impls = mih; + self + } + + /// Tells how to behave when both target and inserted Hugr have a /// [FuncDefn](crate::ops::FuncDefn) with the same name and signature. - pub fn on_multiple_impls(&mut self) -> &mut MultipleImplHandling { - &mut self.multi_impls + pub fn get_multiple_impls(&self) -> MultipleImplHandling { + self.multi_impls } /// Builds an explicit map of [NodeLinkingDirective]s that implements this policy for a given @@ -976,7 +992,7 @@ mod test { let new_sig = Signature::new_endo(INT_TYPES[3].clone()); let (inserted, inserted_fn) = mk_def_or_decl("foo", new_sig.clone(), inserted_defn); - let mut pol = NameLinkingPolicy::err_on_conflict(NewFuncHandling::RaiseError); + let pol = NameLinkingPolicy::err_on_conflict(NewFuncHandling::RaiseError); let mut host = orig_host.clone(); let res = host.link_module_view(&inserted, &pol); assert_eq!(host, orig_host); // Did nothing @@ -991,7 +1007,7 @@ mod test { }) ); - *pol.on_signature_conflict() = NewFuncHandling::Add; + let pol = pol.on_signature_conflict(NewFuncHandling::Add); let node_map = host.link_module(inserted, &pol).unwrap().node_map; assert_eq!( host.validate(), @@ -1022,8 +1038,7 @@ mod test { let mut host = backup.clone(); let inserted = build_hugr(11); - let mut pol = NameLinkingPolicy::keep_both_invalid(); - *pol.on_multiple_impls() = multi_impls; + let pol = NameLinkingPolicy::keep_both_invalid().on_multiple_impls(multi_impls); let res = host.link_module(inserted, &pol); if multi_impls == NewFuncHandling::RaiseError.into() { assert!(matches!(res, Err(NameLinkingError::MultipleImpls(n, _, _)) if n == "foo")); From 82759b5a4b153ce08855a6a45113a32570859bf3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 14 Sep 2025 19:16:27 +0100 Subject: [PATCH 63/70] NodeLinkingDirective is non-exhaustive --- hugr-core/src/hugr/linking.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 4e0eeba844..f20c006e61 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -137,6 +137,7 @@ pub enum NodeLinkingError { /// Directive for how to treat a particular module-child in the source Hugr. /// (TN is a node in the target Hugr.) #[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[non_exhaustive] pub enum NodeLinkingDirective { /// Insert the module-child (with subtree if any) into the target Hugr. Add { From 0d9db3aefb4806ae1ab557baea0c4e223dc0bf76 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sun, 14 Sep 2025 09:08:14 +0100 Subject: [PATCH 64/70] Add struct LinkerActions --- hugr-core/src/hugr/linking.rs | 40 +++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 3fbfe1bf68..a0d4fd967a 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -128,9 +128,9 @@ pub trait HugrLinking: HugrMut { other: Hugr, policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { - let per_node = policy.to_node_linking(self, &other)?; + let LinkerActions { directives } = policy.to_node_linking(self, &other)?; Ok(self - .insert_link_hugr_by_node(None, other, per_node) + .insert_link_hugr_by_node(None, other, directives) .expect("NodeLinkingPolicy was constructed to avoid any error")) } @@ -155,9 +155,9 @@ pub trait HugrLinking: HugrMut { other: &H, policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { - let per_node = policy.to_node_linking(self, &other)?; + let LinkerActions { directives } = policy.to_node_linking(self, &other)?; Ok(self - .insert_link_view_by_node(None, other, per_node) + .insert_link_view_by_node(None, other, directives) .expect("NodeLinkingPolicy was constructed to avoid any error")) } } @@ -379,15 +379,15 @@ impl NameLinkingPolicy { self.multi_impls } - /// Builds an explicit map of [NodeLinkingDirective]s that implements this policy for a given - /// source (inserted) and target (inserted-into) Hugr. - /// The map should be such that no [NodeLinkingError] will occur. + /// Computes how this policy will act on a specified source (inserted) and target + /// (host) Hugr. + /// The [LinkerActions::directives] should be such that no [NodeLinkingError] will occur. #[allow(clippy::type_complexity)] pub fn to_node_linking( &self, target: &T, source: &S, - ) -> Result, NameLinkingError> { + ) -> Result, NameLinkingError> { let existing = gather_existing(target); let mut res = NodeLinkingDirectives::new(); @@ -422,7 +422,7 @@ impl NameLinkingPolicy { res.insert(n, dirv); } - Ok(res) + Ok(LinkerActions { directives: res }) } } @@ -520,6 +520,28 @@ fn gather_existing<'a, H: HugrView + ?Sized>( /// For use with [HugrLinking::insert_link_hugr_by_node] and [HugrLinking::insert_link_view_by_node]. pub type NodeLinkingDirectives = HashMap>; +/// Details the concrete actions to link a specific source Hugr into a specific target Hugr. +/// +/// Computed from a [NameLinkingPolicy] and contains all actions required to implement +/// that policy (for those specific Hugrs). +// Note: this is an extension point, i.e. to allow a NameLinkingPolicy to in the future +// do things that cannot be done by the low-level insert_link_(hugr/view)_by_node. The alternative +// would be to make NodeLinkingDirective non-exhaustive; that is perhaps a neater interface +// (this is more factored, will probably require multiple maps with overlapping keys) +// but would require insert_link_(hugr/view)_by_node to handle all required actions/new `Directive`s. +// This way keeps the ..._by_node methods simple. +pub struct LinkerActions { + directives: NodeLinkingDirectives, +} + +impl LinkerActions { + /// Gets the directives to pass to [HugrLinking::insert_link_hugr_by_node] + /// or [HugrLinking::insert_link_view_by_node]. + pub fn directives(&self) -> &NodeLinkingDirectives { + &self.directives + } +} + /// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) /// TargetNodes can be (in RHS of multiple directives) struct Transfers { From 0c49003356237772c563da246352f54520dbd4c3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 15 Sep 2025 13:00:34 +0100 Subject: [PATCH 65/70] Revert; add enum LinkAction (and map thereof) --- hugr-core/src/hugr/linking.rs | 53 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index a0d4fd967a..301fb5c2a9 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -128,7 +128,11 @@ pub trait HugrLinking: HugrMut { other: Hugr, policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { - let LinkerActions { directives } = policy.to_node_linking(self, &other)?; + let actions = policy.to_node_linking(self, &other)?; + let directives = actions + .into_iter() + .map(|(k, LinkAction::LinkNode(d))| (k, d)) + .collect(); Ok(self .insert_link_hugr_by_node(None, other, directives) .expect("NodeLinkingPolicy was constructed to avoid any error")) @@ -155,7 +159,11 @@ pub trait HugrLinking: HugrMut { other: &H, policy: &NameLinkingPolicy, ) -> Result, NameLinkingError> { - let LinkerActions { directives } = policy.to_node_linking(self, &other)?; + let actions = policy.to_node_linking(self, &other)?; + let directives = actions + .into_iter() + .map(|(k, LinkAction::LinkNode(d))| (k, d)) + .collect(); Ok(self .insert_link_view_by_node(None, other, directives) .expect("NodeLinkingPolicy was constructed to avoid any error")) @@ -381,15 +389,14 @@ impl NameLinkingPolicy { /// Computes how this policy will act on a specified source (inserted) and target /// (host) Hugr. - /// The [LinkerActions::directives] should be such that no [NodeLinkingError] will occur. #[allow(clippy::type_complexity)] pub fn to_node_linking( &self, target: &T, source: &S, - ) -> Result, NameLinkingError> { + ) -> Result, NameLinkingError> { let existing = gather_existing(target); - let mut res = NodeLinkingDirectives::new(); + let mut res = LinkActions::new(); let NameLinkingPolicy { sig_conflict, @@ -419,10 +426,10 @@ impl NameLinkingPolicy { } } }; - res.insert(n, dirv); + res.insert(n, LinkAction::LinkNode(dirv)); } - Ok(LinkerActions { directives: res }) + Ok(res) } } @@ -520,27 +527,23 @@ fn gather_existing<'a, H: HugrView + ?Sized>( /// For use with [HugrLinking::insert_link_hugr_by_node] and [HugrLinking::insert_link_view_by_node]. pub type NodeLinkingDirectives = HashMap>; -/// Details the concrete actions to link a specific source Hugr into a specific target Hugr. +/// Details a concrete action to link a specific node from source Hugr into a specific target Hugr. /// -/// Computed from a [NameLinkingPolicy] and contains all actions required to implement -/// that policy (for those specific Hugrs). -// Note: this is an extension point, i.e. to allow a NameLinkingPolicy to in the future -// do things that cannot be done by the low-level insert_link_(hugr/view)_by_node. The alternative -// would be to make NodeLinkingDirective non-exhaustive; that is perhaps a neater interface -// (this is more factored, will probably require multiple maps with overlapping keys) -// but would require insert_link_(hugr/view)_by_node to handle all required actions/new `Directive`s. -// This way keeps the ..._by_node methods simple. -pub struct LinkerActions { - directives: NodeLinkingDirectives, +/// A separate enum from [NodeLinkingDirective] to allow [NameLinkingPolicy::to_node_linking] +/// to specify a greater range of actions than that supported by +/// [HugrLinking::insert_link_hugr_by_node] and [HugrLinking::insert_link_view_by_node]. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[non_exhaustive] +pub enum LinkAction { + /// Just apply the specified [NodeLinkingDirective]. + LinkNode(NodeLinkingDirective), } -impl LinkerActions { - /// Gets the directives to pass to [HugrLinking::insert_link_hugr_by_node] - /// or [HugrLinking::insert_link_view_by_node]. - pub fn directives(&self) -> &NodeLinkingDirectives { - &self.directives - } -} +/// Details the concrete actions to implement a specific source Hugr into a specific target Hugr. +/// +/// Computed from a [NameLinkingPolicy] and contains all actions required to implement +/// that policy (for those specific Hugrs). +pub type LinkActions = HashMap>; /// Invariant: no SourceNode can be in both maps (by type of [NodeLinkingDirective]) /// TargetNodes can be (in RHS of multiple directives) From 70b4e3f2229eef67f92cbfc2bde4a300d612dccd Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 16 Sep 2025 10:10:50 +0100 Subject: [PATCH 66/70] Review comment be more imperative --- hugr-core/src/builder/module.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/builder/module.rs b/hugr-core/src/builder/module.rs index ed9e86ab08..f3d8a8b2fd 100644 --- a/hugr-core/src/builder/module.rs +++ b/hugr-core/src/builder/module.rs @@ -222,7 +222,7 @@ impl + AsRef> ModuleBuilder { Ok(AliasID::new(node, name, bound)) } - /// Adds some module-children of another Hugr to this module, with + /// Add some module-children of another Hugr to this module, with /// linking directives specified explicitly by [Node]. /// /// `children` contains a map from the children of `other` to insert, @@ -237,7 +237,7 @@ impl + AsRef> ModuleBuilder { .insert_link_hugr_by_node(None, other, children) } - /// Copies module-children from a HugrView to this module, with + /// Copy module-children from a HugrView into this module, with /// linking directives specified explicitly by [Node]. /// /// `children` contains a map from the children of `other` to copy, From a76c37c1c55ddb8d7e9518addd1962a0f9cd86c0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 16 Sep 2025 12:23:50 +0100 Subject: [PATCH 67/70] Drop Visibility::is_public, single callee and better to inline --- hugr-core/src/core.rs | 7 ------- hugr-core/src/hugr/linking.rs | 9 ++++----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/hugr-core/src/core.rs b/hugr-core/src/core.rs index d7edbcf2ab..4578aa5357 100644 --- a/hugr-core/src/core.rs +++ b/hugr-core/src/core.rs @@ -298,13 +298,6 @@ pub enum Visibility { Private, } -impl Visibility { - /// Returns `true` iff this is [Self::Public] - pub fn is_public(&self) -> bool { - self == &Self::Public - } -} - impl From for Visibility { fn from(value: hugr_model::v0::Visibility) -> Self { match value { diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 301fb5c2a9..0c6bdf1cfa 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, fmt::Display}; use itertools::{Either, Itertools}; use crate::{ - Hugr, HugrView, Node, + Hugr, HugrView, Node, Visibility, core::HugrNode, hugr::{HugrMut, hugrmut::InsertedForest, internal::HugrMutInternals}, ops::OpType, @@ -485,10 +485,9 @@ fn link_sig(h: &H, n: H::Node) -> Option> { OpType::Const(_) => return Some(LinkSig::Private), _ => return None, }; - Some(if vis.is_public() { - LinkSig::Public { name, is_defn, sig } - } else { - LinkSig::Private + Some(match vis { + Visibility::Public => LinkSig::Public { name, is_defn, sig }, + Visibility::Private => LinkSig::Private, }) } From 23e680ae3be91408a73d83e874dce437e87701f3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 16 Sep 2025 12:28:06 +0100 Subject: [PATCH 68/70] Use derive_more::From --- hugr-core/src/hugr/linking.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 0c6bdf1cfa..a2666e5990 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -282,7 +282,7 @@ pub enum NewFuncHandling { /// have a [Visibility::Public] FuncDefn with the same name and signature. /// /// [Visibility::Public]: crate::Visibility::Public -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, derive_more::From)] #[non_exhaustive] // could consider e.g. disconnections pub enum MultipleImplHandling { /// Keep the implementation already in the target Hugr. (Edges in the source @@ -293,13 +293,7 @@ pub enum MultipleImplHandling { /// function in the target Hugr will be removed.) UseNew, /// Proceed as per the specified [NewFuncHandling]. - NewFunc(NewFuncHandling), -} - -impl From for MultipleImplHandling { - fn from(value: NewFuncHandling) -> Self { - Self::NewFunc(value) - } + NewFunc(#[from] NewFuncHandling), } /// An error in using names to determine how to link functions in source and target Hugrs. From 2a608dfe25708e9368e2a250f065c4e4ec25c7bd Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 16 Sep 2025 12:35:02 +0100 Subject: [PATCH 69/70] Drop S: ?Sized on to_node_linking --- hugr-core/src/hugr/linking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index a2666e5990..51cc9fb71e 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -384,7 +384,7 @@ impl NameLinkingPolicy { /// Computes how this policy will act on a specified source (inserted) and target /// (host) Hugr. #[allow(clippy::type_complexity)] - pub fn to_node_linking( + pub fn to_node_linking( &self, target: &T, source: &S, From 5ca0e5f51c764803b47a01f3386c3bb2d925ce78 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 16 Sep 2025 12:36:25 +0100 Subject: [PATCH 70/70] Add LinkAction From --- hugr-core/src/hugr/linking.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hugr-core/src/hugr/linking.rs b/hugr-core/src/hugr/linking.rs index 51cc9fb71e..9963bbfdc4 100644 --- a/hugr-core/src/hugr/linking.rs +++ b/hugr-core/src/hugr/linking.rs @@ -420,7 +420,7 @@ impl NameLinkingPolicy { } } }; - res.insert(n, LinkAction::LinkNode(dirv)); + res.insert(n, dirv.into()); } Ok(res) @@ -525,11 +525,11 @@ pub type NodeLinkingDirectives = HashMap>; /// A separate enum from [NodeLinkingDirective] to allow [NameLinkingPolicy::to_node_linking] /// to specify a greater range of actions than that supported by /// [HugrLinking::insert_link_hugr_by_node] and [HugrLinking::insert_link_view_by_node]. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, derive_more::From)] #[non_exhaustive] pub enum LinkAction { /// Just apply the specified [NodeLinkingDirective]. - LinkNode(NodeLinkingDirective), + LinkNode(#[from] NodeLinkingDirective), } /// Details the concrete actions to implement a specific source Hugr into a specific target Hugr.