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