hugr_core/
hugr.rs

1//! The Hugr data structure, and its basic component handles.
2
3pub mod hugrmut;
4
5pub(crate) mod ident;
6pub mod internal;
7pub mod patch;
8pub mod persistent;
9pub mod serialize;
10pub mod validate;
11pub mod views;
12
13use std::collections::VecDeque;
14use std::io;
15use std::iter;
16
17pub(crate) use self::hugrmut::HugrMut;
18pub use self::validate::ValidationError;
19
20pub use ident::{IdentList, InvalidIdentifier};
21use itertools::Itertools;
22pub use patch::{Patch, SimpleReplacement, SimpleReplacementError};
23
24use portgraph::multiportgraph::MultiPortGraph;
25use portgraph::{Hierarchy, PortMut, PortView, UnmanagedDenseMap};
26use thiserror::Error;
27
28pub use self::views::HugrView;
29use crate::core::NodeIndex;
30use crate::envelope::{self, EnvelopeConfig, EnvelopeError};
31use crate::extension::resolution::{
32    ExtensionResolutionError, WeakExtensionRegistry, resolve_op_extensions,
33    resolve_op_types_extensions,
34};
35use crate::extension::{EMPTY_REG, ExtensionRegistry, ExtensionSet};
36use crate::ops::{self, Module, NamedOp, OpName, OpTag, OpTrait};
37pub use crate::ops::{DEFAULT_OPTYPE, OpType};
38use crate::package::Package;
39use crate::{Direction, Node};
40
41/// The Hugr data structure.
42#[derive(Clone, Debug, PartialEq)]
43pub struct Hugr {
44    /// The graph encoding the adjacency structure of the HUGR.
45    graph: MultiPortGraph,
46
47    /// The node hierarchy.
48    hierarchy: Hierarchy,
49
50    /// The single root node in the portgraph hierarchy.
51    ///
52    /// This node is always a module node, containing all the other nodes.
53    module_root: portgraph::NodeIndex,
54
55    /// The distinguished entrypoint node of the HUGR.
56    entrypoint: portgraph::NodeIndex,
57
58    /// Operation types for each node.
59    op_types: UnmanagedDenseMap<portgraph::NodeIndex, OpType>,
60
61    /// Node metadata
62    metadata: UnmanagedDenseMap<portgraph::NodeIndex, Option<NodeMetadataMap>>,
63
64    /// Extensions used by the operations in the Hugr.
65    extensions: ExtensionRegistry,
66}
67
68impl Default for Hugr {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl AsRef<Hugr> for Hugr {
75    fn as_ref(&self) -> &Hugr {
76        self
77    }
78}
79
80impl AsMut<Hugr> for Hugr {
81    fn as_mut(&mut self) -> &mut Hugr {
82        self
83    }
84}
85
86/// Arbitrary metadata entry for a node.
87///
88/// Each entry is associated to a string key.
89pub type NodeMetadata = serde_json::Value;
90
91/// The container of all the metadata entries for a node.
92pub type NodeMetadataMap = serde_json::Map<String, NodeMetadata>;
93
94/// Public API for HUGRs.
95impl Hugr {
96    /// Create a new Hugr, with a single [`Module`] operation as the root node.
97    #[must_use]
98    pub fn new() -> Self {
99        make_module_hugr(Module::new().into(), 0, 0).unwrap()
100    }
101
102    /// Create a new Hugr, with a given entrypoint operation.
103    ///
104    /// If the optype is [`OpType::Module`], the HUGR module root will match the
105    /// entrypoint node. Otherwise, the entrypoint will be a descendent of the a
106    /// module initialized at the node hierarchy root. The specific HUGR created
107    /// depends on the operation type.
108    ///
109    /// # Error
110    ///
111    /// Returns [`HugrError::UnsupportedEntrypoint`] if the entrypoint operation
112    /// requires additional context to be defined. This is the case for
113    /// [`OpType::Case`], [`OpType::DataflowBlock`], and [`OpType::ExitBlock`]
114    /// since they are context-specific definitions.
115    pub fn new_with_entrypoint(entrypoint_op: impl Into<OpType>) -> Result<Self, HugrError> {
116        Self::with_capacity(entrypoint_op, 0, 0)
117    }
118
119    /// Create a new Hugr, with a given entrypoint operation and preallocated capacity.
120    ///
121    /// If the optype is [`OpType::Module`], the HUGR module root will match the
122    /// entrypoint node. Otherwise, the entrypoint will be a child of the a
123    /// module initialized at the node hierarchy root. The specific HUGR created
124    /// depends on the operation type.
125    ///
126    /// # Error
127    ///
128    /// Returns [`HugrError::UnsupportedEntrypoint`] if the entrypoint operation
129    /// requires additional context to be defined. This is the case for
130    /// [`OpType::Case`], [`OpType::DataflowBlock`], and [`OpType::ExitBlock`]
131    /// since they are context-specific definitions.
132    pub fn with_capacity(
133        entrypoint_op: impl Into<OpType>,
134        nodes: usize,
135        ports: usize,
136    ) -> Result<Self, HugrError> {
137        let entrypoint_op: OpType = entrypoint_op.into();
138        let op_name = entrypoint_op.name();
139        make_module_hugr(entrypoint_op, nodes, ports)
140            .ok_or(HugrError::UnsupportedEntrypoint { op: op_name })
141    }
142
143    /// Reserves enough capacity to insert at least the given number of
144    /// additional nodes and links.
145    ///
146    /// This method does not take into account already allocated free space left
147    /// after node removals, and may overallocate capacity.
148    pub fn reserve(&mut self, nodes: usize, links: usize) {
149        let ports = links * 2;
150        self.graph.reserve(nodes, ports);
151    }
152
153    /// Read a HUGR from an Envelope.
154    ///
155    /// To load a HUGR, all the extensions used in its definition must be
156    /// available. The Envelope may include some of the extensions, but any
157    /// additional extensions must be provided in the `extensions` parameter. If
158    /// `extensions` is `None`, the default [`crate::std_extensions::STD_REG`]
159    /// is used.
160    pub fn load(
161        reader: impl io::BufRead,
162        extensions: Option<&ExtensionRegistry>,
163    ) -> Result<Self, EnvelopeError> {
164        let pkg = Package::load(reader, extensions)?;
165        match pkg.modules.into_iter().exactly_one() {
166            Ok(hugr) => Ok(hugr),
167            Err(e) => Err(EnvelopeError::ExpectedSingleHugr { count: e.count() }),
168        }
169    }
170
171    /// Read a HUGR from an Envelope encoded in a string.
172    ///
173    /// Note that not all Envelopes are valid strings. In the general case,
174    /// it is recommended to use [`Hugr::load`] with a bytearray instead.
175    ///
176    /// To load a HUGR, all the extensions used in its definition must be
177    /// available. The Envelope may include some of the extensions, but any
178    /// additional extensions must be provided in the `extensions` parameter. If
179    /// `extensions` is `None`, the default [`crate::std_extensions::STD_REG`]
180    /// is used.
181    pub fn load_str(
182        envelope: impl AsRef<str>,
183        extensions: Option<&ExtensionRegistry>,
184    ) -> Result<Self, EnvelopeError> {
185        Self::load(envelope.as_ref().as_bytes(), extensions)
186    }
187
188    /// Store the HUGR in an Envelope.
189    ///
190    /// The Envelope will not include any extension definition, and will require
191    /// an adequate [`ExtensionRegistry`] to be loaded (see [`Hugr::load`]).
192    /// Use [`Hugr::store_with_exts`] to include additional extensions in the
193    /// Envelope.
194    pub fn store(
195        &self,
196        writer: impl io::Write,
197        config: EnvelopeConfig,
198    ) -> Result<(), EnvelopeError> {
199        self.store_with_exts(writer, config, &EMPTY_REG)
200    }
201
202    /// Store the HUGR in an Envelope.
203    ///
204    /// The Envelope will embed the definitions of the extensions in the
205    /// `extensions` registry. Any other extension used in the HUGR definition
206    /// must be passed to [`Hugr::load`] to load back the HUGR.
207    pub fn store_with_exts(
208        &self,
209        writer: impl io::Write,
210        config: EnvelopeConfig,
211        extensions: &ExtensionRegistry,
212    ) -> Result<(), EnvelopeError> {
213        envelope::write_envelope_impl(writer, [self], extensions, config)
214    }
215
216    /// Store the HUGR in an Envelope encoded in a string.
217    ///
218    /// Note that not all Envelopes are valid strings. In the general case,
219    /// it is recommended to use [`Hugr::store`] with a bytearray instead.
220    /// See [`EnvelopeFormat::ascii_printable`][crate::envelope::EnvelopeFormat::ascii_printable].
221    ///
222    /// The Envelope will not include any extension definition, and will require
223    /// an adequate [`ExtensionRegistry`] to be loaded (see [`Hugr::load_str`]).
224    /// Use [`Hugr::store_str_with_exts`] to include additional extensions in the
225    /// Envelope.
226    pub fn store_str(&self, config: EnvelopeConfig) -> Result<String, EnvelopeError> {
227        self.store_str_with_exts(config, &EMPTY_REG)
228    }
229
230    /// Store the HUGR in an Envelope encoded in a string.
231    ///
232    /// Note that not all Envelopes are valid strings. In the general case,
233    /// it is recommended to use [`Hugr::store_str`] with a bytearray instead.
234    /// See [`EnvelopeFormat::ascii_printable`][crate::envelope::EnvelopeFormat::ascii_printable].
235    ///
236    /// The Envelope will embed the definitions of the extensions in the
237    /// `extensions` registry. Any other extension used in the HUGR definition
238    /// must be passed to [`Hugr::load_str`] to load back the HUGR.
239    pub fn store_str_with_exts(
240        &self,
241        config: EnvelopeConfig,
242        extensions: &ExtensionRegistry,
243    ) -> Result<String, EnvelopeError> {
244        if !config.format.ascii_printable() {
245            return Err(EnvelopeError::NonASCIIFormat {
246                format: config.format,
247            });
248        }
249
250        let mut buf = Vec::new();
251        self.store_with_exts(&mut buf, config, extensions)?;
252        Ok(String::from_utf8(buf).expect("Envelope is valid utf8"))
253    }
254
255    /// Given a Hugr that has been deserialized, collect all extensions used to
256    /// define the HUGR while resolving all [`OpType::OpaqueOp`] operations into
257    /// [`OpType::ExtensionOp`]s and updating the extension pointer in all
258    /// internal [`crate::types::CustomType`]s to point to the extensions in the
259    /// register.
260    ///
261    /// When listing "used extensions" we only care about _definitional_
262    /// extension requirements, i.e., the operations and types that are required
263    /// to define the HUGR nodes and wire types. This is computed from the union
264    /// of all extension required across the HUGR.
265    ///
266    /// Updates the internal extension registry with the extensions used in the
267    /// definition.
268    ///
269    /// # Parameters
270    ///
271    /// - `extensions`: The extension set considered when resolving opaque
272    ///   operations and types. The original Hugr's internal extension
273    ///   registry is ignored and replaced with the newly computed one.
274    ///
275    /// # Errors
276    ///
277    /// - If an opaque operation cannot be resolved to an extension operation.
278    /// - If an extension operation references an extension that is missing from
279    ///   the registry.
280    /// - If a custom type references an extension that is missing from the
281    ///   registry.
282    pub fn resolve_extension_defs(
283        &mut self,
284        extensions: &ExtensionRegistry,
285    ) -> Result<(), ExtensionResolutionError> {
286        let mut used_extensions = ExtensionRegistry::default();
287
288        // Here we need to iterate the optypes in the hugr mutably, to avoid
289        // having to clone and accumulate all replacements before finally
290        // applying them.
291        //
292        // This is not something we want to expose it the API, so we manually
293        // iterate instead of writing it as a method.
294        //
295        // Since we don't have a non-borrowing iterator over all the possible
296        // NodeIds, we have to simulate it by iterating over all possible
297        // indices and checking if the node exists.
298        let weak_extensions: WeakExtensionRegistry = extensions.into();
299        for n in 0..self.graph.node_capacity() {
300            let pg_node = portgraph::NodeIndex::new(n);
301            let node: Node = pg_node.into();
302            if !self.contains_node(node) {
303                continue;
304            }
305
306            let op = &mut self.op_types[pg_node];
307
308            if let Some(extension) = resolve_op_extensions(node, op, extensions)? {
309                used_extensions.register_updated_ref(extension);
310            }
311            used_extensions.extend(
312                resolve_op_types_extensions(Some(node), op, &weak_extensions)?.map(|weak| {
313                    weak.upgrade()
314                        .expect("Extension comes from a valid registry")
315                }),
316            );
317        }
318
319        self.extensions = used_extensions;
320        Ok(())
321    }
322}
323
324/// Internal API for HUGRs, not intended for use by users.
325impl Hugr {
326    /// Add a node to the graph.
327    pub(crate) fn add_node(&mut self, nodetype: OpType) -> Node {
328        let node = self
329            .graph
330            .add_node(nodetype.input_count(), nodetype.output_count());
331        self.op_types[node] = nodetype;
332        node.into()
333    }
334
335    /// Produce a canonical ordering of the descendant nodes of a root,
336    /// following the graph hierarchy.
337    ///
338    /// This starts with the root, and then proceeds in BFS order through the
339    /// contained regions.
340    ///
341    /// Used by [`HugrMut::canonicalize_nodes`] and the serialization code.
342    fn canonical_order(&self, root: Node) -> impl Iterator<Item = Node> + '_ {
343        // Generate a BFS-ordered list of nodes based on the hierarchy
344        let mut queue = VecDeque::from([root]);
345        iter::from_fn(move || {
346            let node = queue.pop_front()?;
347            for child in self.children(node) {
348                queue.push_back(child);
349            }
350            Some(node)
351        })
352    }
353
354    /// Compact the nodes indices of the hugr to be contiguous, and order them as a breadth-first
355    /// traversal of the hierarchy.
356    ///
357    /// The rekey function is called for each moved node with the old and new indices.
358    ///
359    /// After this operation, a serialization and deserialization of the Hugr is guaranteed to
360    /// preserve the indices.
361    pub fn canonicalize_nodes(&mut self, mut rekey: impl FnMut(Node, Node)) {
362        // Generate the ordered list of nodes
363        let ordered = {
364            let mut v = Vec::with_capacity(self.num_nodes());
365            v.extend(self.canonical_order(self.module_root()));
366            v
367        };
368        let mut new_entrypoint = None;
369
370        // Permute the nodes in the graph to match the order.
371        //
372        // Invariant: All the elements before `position` are in the correct place.
373        for position in 0..ordered.len() {
374            let pg_target = portgraph::NodeIndex::new(position);
375            let mut source: Node = ordered[position];
376
377            // The (old) entrypoint appears exactly once in `ordered`:
378            if source.into_portgraph() == self.entrypoint {
379                let old = new_entrypoint.replace(pg_target);
380                debug_assert!(old.is_none());
381            }
382
383            // Find the element's current location. If it originally came from an earlier
384            // position then it has been swapped somewhere else, so follow the permutation chain.
385            while position > source.index() {
386                source = ordered[source.index()];
387            }
388
389            let pg_source = source.into_portgraph();
390            if pg_target != pg_source {
391                self.graph.swap_nodes(pg_target, pg_source);
392                self.op_types.swap(pg_target, pg_source);
393                self.hierarchy.swap_nodes(pg_target, pg_source);
394                rekey(source, pg_target.into());
395            }
396        }
397        self.module_root = portgraph::NodeIndex::new(0);
398        self.entrypoint = new_entrypoint.unwrap();
399
400        // Finish by compacting the copy nodes.
401        // The operation nodes will be left in place.
402        // This step is not strictly necessary.
403        self.graph.compact_nodes(|_, _| {});
404    }
405}
406
407#[derive(Debug, Clone, PartialEq, Error)]
408#[error(
409    "Parent node {parent} has extensions {parent_extensions} that are too restrictive for child node {child}, they must include child extensions {child_extensions}"
410)]
411/// An error in the extension deltas.
412pub struct ExtensionError {
413    parent: Node,
414    parent_extensions: ExtensionSet,
415    child: Node,
416    child_extensions: ExtensionSet,
417}
418
419/// Errors that can occur while manipulating a Hugr.
420#[derive(Debug, Clone, PartialEq, Eq, Error)]
421#[non_exhaustive]
422pub enum HugrError {
423    /// The node was not of the required [`OpTag`]
424    #[error("Invalid tag: required a tag in {required} but found {actual}")]
425    #[allow(missing_docs)]
426    InvalidTag { required: OpTag, actual: OpTag },
427    /// An invalid port was specified.
428    #[error("Invalid port direction {0:?}.")]
429    InvalidPortDirection(Direction),
430    /// Cannot initialize a HUGR with the given entrypoint operation type.
431    #[error("Cannot initialize a HUGR with entrypoint type {op}")]
432    UnsupportedEntrypoint {
433        /// The name of the unsupported operation.
434        op: OpName,
435    },
436}
437
438/// Create a new Hugr, with a given root node and preallocated capacity.
439///
440/// The root operation must be region root, i.e., define a node that can be
441/// assigned as the parent of other nodes.
442///
443/// If the root optype is [`OpType::Module`], the HUGR module root will match
444/// the root node.
445///
446/// Otherwise, the root node will be a child of the a module created at the node
447/// hierarchy root. The specific HUGR created depends on the operation type, and
448/// whether it can be contained in a module, function definition, etc.
449///
450/// Some operation types are not allowed and will result in a panic. This is the
451/// case for [`OpType::Case`] and [`OpType::DataflowBlock`] since they are
452/// context-specific operation.
453fn make_module_hugr(root_op: OpType, nodes: usize, ports: usize) -> Option<Hugr> {
454    let mut graph = MultiPortGraph::with_capacity(nodes, ports);
455    let hierarchy = Hierarchy::new();
456    let mut op_types = UnmanagedDenseMap::with_capacity(nodes);
457    let extensions = root_op.used_extensions().unwrap_or_default();
458
459    // Filter out operations that are not region roots.
460    let tag = root_op.tag();
461    let container_tags = [
462        OpTag::ModuleRoot,
463        OpTag::DataflowParent,
464        OpTag::Cfg,
465        OpTag::Conditional,
466    ];
467    if !container_tags.iter().any(|t| t.is_superset(tag)) {
468        return None;
469    }
470
471    let module = graph.add_node(0, 0);
472    op_types[module] = OpType::Module(ops::Module::new());
473
474    let mut hugr = Hugr {
475        graph,
476        hierarchy,
477        module_root: module,
478        entrypoint: module,
479        op_types,
480        metadata: UnmanagedDenseMap::with_capacity(nodes),
481        extensions,
482    };
483    let module: Node = module.into();
484
485    // Now the behaviour depends on the root node type.
486    if root_op.is_module() {
487        // The hugr is already a module, nothing to do.
488    }
489    // If possible, put the op directly in the module.
490    else if OpTag::ModuleOp.is_superset(tag) {
491        let node = hugr.add_node_with_parent(module, root_op);
492        hugr.set_entrypoint(node);
493    }
494    // If it can exist inside a function definition, create a "main" function
495    // and put the op inside it.
496    else if OpTag::DataflowChild.is_superset(tag) && !root_op.is_input() && !root_op.is_output() {
497        let signature = root_op
498            .dataflow_signature()
499            .unwrap_or_else(|| panic!("Dataflow child {} without signature", root_op.name()))
500            .into_owned();
501        let dataflow_inputs = signature.input_count();
502        let dataflow_outputs = signature.output_count();
503
504        let func = hugr.add_node_with_parent(module, ops::FuncDefn::new("main", signature.clone()));
505        let inp = hugr.add_node_with_parent(
506            func,
507            ops::Input {
508                types: signature.input.clone(),
509            },
510        );
511        let out = hugr.add_node_with_parent(
512            func,
513            ops::Output {
514                types: signature.output.clone(),
515            },
516        );
517        let entrypoint = hugr.add_node_with_parent(func, root_op);
518
519        // Wire the inputs and outputs of the entrypoint node to the function's
520        // inputs and outputs.
521        for port in 0..dataflow_inputs {
522            hugr.connect(inp, port, entrypoint, port);
523        }
524        for port in 0..dataflow_outputs {
525            hugr.connect(entrypoint, port, out, port);
526        }
527
528        hugr.set_entrypoint(entrypoint);
529    }
530    // Other more exotic ops are unsupported, and will cause a panic.
531    else {
532        debug_assert!(matches!(
533            root_op,
534            OpType::Input(_)
535                | OpType::Output(_)
536                | OpType::DataflowBlock(_)
537                | OpType::ExitBlock(_)
538                | OpType::Case(_)
539        ));
540        return None;
541    }
542
543    Some(hugr)
544}
545
546#[cfg(test)]
547pub(crate) mod test {
548    use std::{fs::File, io::BufReader};
549
550    use super::*;
551
552    use crate::builder::{Container, Dataflow, DataflowSubContainer, ModuleBuilder};
553    use crate::envelope::{EnvelopeError, PackageEncodingError};
554    use crate::extension::prelude::bool_t;
555    use crate::ops::OpaqueOp;
556    use crate::ops::handle::NodeHandle;
557    use crate::test_file;
558    use crate::types::Signature;
559    use cool_asserts::assert_matches;
560    use itertools::Either;
561    use portgraph::LinkView;
562    use rstest::rstest;
563
564    /// Check that two HUGRs are equivalent, up to node renumbering.
565    pub(crate) fn check_hugr_equality(lhs: &Hugr, rhs: &Hugr) {
566        // Original HUGR, with canonicalized node indices
567        //
568        // The internal port indices may still be different.
569        let mut lhs = lhs.clone();
570        lhs.canonicalize_nodes(|_, _| {});
571        let mut rhs = rhs.clone();
572        rhs.canonicalize_nodes(|_, _| {});
573
574        assert_eq!(rhs.module_root(), lhs.module_root());
575        assert_eq!(rhs.entrypoint(), lhs.entrypoint());
576        assert_eq!(rhs.hierarchy, lhs.hierarchy);
577        assert_eq!(rhs.metadata, lhs.metadata);
578
579        // Extension operations may have been downgraded to opaque operations.
580        for node in rhs.nodes() {
581            let new_op = rhs.get_optype(node);
582            let old_op = lhs.get_optype(node);
583            if !new_op.is_const() {
584                match (new_op, old_op) {
585                    (OpType::ExtensionOp(ext), OpType::OpaqueOp(opaque))
586                    | (OpType::OpaqueOp(opaque), OpType::ExtensionOp(ext)) => {
587                        let ext_opaque: OpaqueOp = ext.clone().into();
588                        assert_eq!(ext_opaque, opaque.clone());
589                    }
590                    _ => assert_eq!(new_op, old_op),
591                }
592            }
593        }
594
595        // Check that the graphs are equivalent up to port renumbering.
596        let new_graph = &rhs.graph;
597        let old_graph = &lhs.graph;
598        assert_eq!(new_graph.node_count(), old_graph.node_count());
599        assert_eq!(new_graph.port_count(), old_graph.port_count());
600        assert_eq!(new_graph.link_count(), old_graph.link_count());
601        for n in old_graph.nodes_iter() {
602            assert_eq!(new_graph.num_inputs(n), old_graph.num_inputs(n));
603            assert_eq!(new_graph.num_outputs(n), old_graph.num_outputs(n));
604            assert_eq!(
605                new_graph.output_neighbours(n).collect_vec(),
606                old_graph.output_neighbours(n).collect_vec()
607            );
608        }
609    }
610
611    #[test]
612    fn impls_send_and_sync() {
613        // Send and Sync are automatically impl'd by the compiler, if possible.
614        // This test will fail to compile if that wasn't possible.
615        #[allow(dead_code)]
616        trait Test: Send + Sync {}
617        impl Test for Hugr {}
618    }
619
620    #[test]
621    fn io_node() {
622        use crate::builder::test::simple_dfg_hugr;
623
624        let hugr = simple_dfg_hugr();
625        assert_matches!(hugr.get_io(hugr.entrypoint()), Some(_));
626    }
627
628    #[test]
629    #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
630    fn hugr_validation_0() {
631        // https://github.com/CQCL/hugr/issues/1091 bad case
632        let hugr = Hugr::load(
633            BufReader::new(File::open(test_file!("hugr-0.hugr")).unwrap()),
634            None,
635        );
636        assert_matches!(
637            hugr,
638            Err(EnvelopeError::PackageEncoding {
639                source: PackageEncodingError::JsonEncoding(_)
640            })
641        );
642    }
643
644    #[test]
645    #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
646    fn hugr_validation_1() {
647        // https://github.com/CQCL/hugr/issues/1091 good case
648        let hugr = Hugr::load(
649            BufReader::new(File::open(test_file!("hugr-1.hugr")).unwrap()),
650            None,
651        );
652        assert_matches!(&hugr, Ok(_));
653    }
654
655    #[test]
656    #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
657    fn hugr_validation_2() {
658        // https://github.com/CQCL/hugr/issues/1185 bad case
659        let hugr = Hugr::load(
660            BufReader::new(File::open(test_file!("hugr-2.hugr")).unwrap()),
661            None,
662        )
663        .unwrap();
664        assert_matches!(hugr.validate(), Err(_));
665    }
666
667    #[test]
668    #[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
669    fn hugr_validation_3() {
670        // https://github.com/CQCL/hugr/issues/1185 good case
671        let hugr = Hugr::load(
672            BufReader::new(File::open(test_file!("hugr-3.hugr")).unwrap()),
673            None,
674        );
675        assert_matches!(&hugr, Ok(_));
676    }
677
678    fn hugr_failing_2262() -> Hugr {
679        let sig = Signature::new(vec![bool_t(); 2], bool_t());
680        let mut mb = ModuleBuilder::new();
681        let mut fa = mb.define_function("a", sig.clone()).unwrap();
682        let mut dfg = fa.dfg_builder(sig.clone(), fa.input_wires()).unwrap();
683        // Add Call node - without a static target as we'll create that later
684        let call_op = ops::Call::try_new(sig.clone().into(), []).unwrap();
685        let call = dfg.add_dataflow_op(call_op, dfg.input_wires()).unwrap();
686        let dfg = dfg.finish_with_outputs(call.outputs()).unwrap();
687        fa.finish_with_outputs(dfg.outputs()).unwrap();
688        let fb = mb.define_function("b", sig).unwrap();
689        let [fst, _] = fb.input_wires_arr();
690        let fb = fb.finish_with_outputs([fst]).unwrap();
691        let mut h = mb.hugr().clone();
692
693        h.set_entrypoint(dfg.node()); // Entrypoint unused, but to highlight failing case
694        let static_in = h.get_optype(call.node()).static_input_port().unwrap();
695        let static_out = h.get_optype(fb.node()).static_output_port().unwrap();
696        assert_eq!(h.single_linked_output(call.node(), static_in), None);
697        h.disconnect(call.node(), static_in);
698        h.connect(fb.node(), static_out, call.node(), static_in);
699        h
700    }
701
702    #[rstest]
703    // Opening files is not supported in (isolated) miri
704    #[cfg_attr(not(miri), case(Either::Left(test_file!("hugr-1.hugr"))))]
705    #[cfg_attr(not(miri), case(Either::Left(test_file!("hugr-3.hugr"))))]
706    // Next was failing, https://github.com/CQCL/hugr/issues/2262:
707    #[case(Either::Right(hugr_failing_2262()))]
708    fn canonicalize_entrypoint(#[case] file_or_hugr: Either<&str, Hugr>) {
709        let hugr = match file_or_hugr {
710            Either::Left(file) => {
711                Hugr::load(BufReader::new(File::open(file).unwrap()), None).unwrap()
712            }
713            Either::Right(hugr) => hugr,
714        };
715        hugr.validate().unwrap();
716
717        for n in hugr.nodes() {
718            let mut h2 = hugr.clone();
719            h2.set_entrypoint(n);
720            if h2.validate().is_ok() {
721                h2.canonicalize_nodes(|_, _| {});
722                assert_eq!(hugr.get_optype(n), h2.entrypoint_optype());
723            }
724        }
725    }
726}