hugr_core/hugr/
validate.rs

1//! HUGR invariant checks.
2
3use std::collections::HashMap;
4use std::iter;
5
6use itertools::Itertools;
7use petgraph::algo::dominators::{self, Dominators};
8use petgraph::visit::{Topo, Walker};
9use thiserror::Error;
10
11use crate::core::HugrNode;
12use crate::extension::SignatureError;
13
14use crate::ops::constant::ConstTypeError;
15use crate::ops::custom::{ExtensionOp, OpaqueOpError};
16use crate::ops::validate::{
17    ChildrenEdgeData, ChildrenValidationError, EdgeValidationError, OpValidityFlags,
18};
19use crate::ops::{NamedOp, OpName, OpTag, OpTrait, OpType, ValidateOp};
20use crate::types::EdgeKind;
21use crate::types::type_param::TypeParam;
22use crate::{Direction, Port};
23
24use super::ExtensionError;
25use super::internal::PortgraphNodeMap;
26use super::views::HugrView;
27
28/// Structure keeping track of pre-computed information used in the validation
29/// process.
30///
31/// TODO: Consider implementing updatable dominator trees and storing it in the
32/// Hugr to avoid recomputing it every time.
33pub(super) struct ValidationContext<'a, H: HugrView> {
34    hugr: &'a H,
35    /// Dominator tree for each CFG region, using the container node as index.
36    dominators: HashMap<H::Node, (Dominators<portgraph::NodeIndex>, H::RegionPortgraphNodes)>,
37}
38
39impl<'a, H: HugrView> ValidationContext<'a, H> {
40    /// Create a new validation context.
41    pub fn new(hugr: &'a H) -> Self {
42        let dominators = HashMap::new();
43        Self { hugr, dominators }
44    }
45
46    /// Check the validity of the HUGR.
47    pub fn validate(&mut self) -> Result<(), ValidationError<H::Node>> {
48        // Root node must be a root in the hierarchy.
49        if self.hugr.get_parent(self.hugr.module_root()).is_some() {
50            return Err(ValidationError::RootNotRoot {
51                node: self.hugr.module_root(),
52            });
53        }
54
55        // Node-specific checks
56        for node in self.hugr.nodes().map_into() {
57            self.validate_node(node)?;
58        }
59
60        // Hierarchy and children. No type variables declared outside the root.
61        self.validate_subtree(self.hugr.entrypoint(), &[])?;
62
63        // In tests we take the opportunity to verify that the hugr
64        // serialization round-trips. We verify the schema of the serialization
65        // format only when an environment variable is set. This allows
66        // a developer to modify the definition of serialized types locally
67        // without having to change the schema.
68        #[cfg(all(test, not(miri)))]
69        {
70            use crate::envelope::EnvelopeConfig;
71            use crate::hugr::hugrmut::HugrMut;
72            use crate::hugr::views::ExtractionResult;
73
74            let (mut hugr, node_map) = self.hugr.extract_hugr(self.hugr.module_root());
75            hugr.set_entrypoint(node_map.extracted_node(self.hugr.entrypoint()));
76            // TODO: Currently fails when using `hugr-model`
77            //crate::envelope::test::check_hugr_roundtrip(&hugr, EnvelopeConfig::binary());
78            crate::envelope::test::check_hugr_roundtrip(&hugr, EnvelopeConfig::text());
79        }
80
81        Ok(())
82    }
83
84    /// Compute the dominator tree for a CFG region, identified by its container
85    /// node.
86    ///
87    /// The results of this computation should be cached in `self.dominators`.
88    /// We don't do it here to avoid mutable borrows.
89    fn compute_dominator(
90        &self,
91        parent: H::Node,
92    ) -> (Dominators<portgraph::NodeIndex>, H::RegionPortgraphNodes) {
93        let (region, node_map) = self.hugr.region_portgraph(parent);
94        let entry_node = self.hugr.children(parent).next().unwrap();
95        let doms = dominators::simple_fast(&region, node_map.to_portgraph(entry_node));
96        (doms, node_map)
97    }
98
99    /// Check the constraints on a single node.
100    ///
101    /// This includes:
102    /// - Matching the number of ports with the signature
103    /// - Dataflow ports are correct. See `validate_df_port`
104    fn validate_node(&self, node: H::Node) -> Result<(), ValidationError<H::Node>> {
105        let op_type = self.hugr.get_optype(node);
106
107        if let OpType::OpaqueOp(opaque) = op_type {
108            Err(OpaqueOpError::UnresolvedOp(
109                node,
110                opaque.unqualified_id().clone(),
111                opaque.extension().clone(),
112            ))?;
113        }
114        // The Hugr can have only one root node.
115
116        for dir in Direction::BOTH {
117            // Check that we have the correct amount of ports and edges.
118            let num_ports = self.hugr.num_ports(node, dir);
119            if num_ports != op_type.port_count(dir) {
120                return Err(ValidationError::WrongNumberOfPorts {
121                    node,
122                    optype: op_type.clone(),
123                    actual: num_ports,
124                    expected: op_type.port_count(dir),
125                    dir,
126                });
127            }
128        }
129
130        if node != self.hugr.module_root() {
131            let Some(parent) = self.hugr.get_parent(node) else {
132                return Err(ValidationError::NoParent { node });
133            };
134
135            let parent_optype = self.hugr.get_optype(parent);
136            let allowed_children = parent_optype.validity_flags::<H::Node>().allowed_children;
137            if !allowed_children.is_superset(op_type.tag()) {
138                return Err(ValidationError::InvalidParentOp {
139                    child: node,
140                    child_optype: op_type.clone(),
141                    parent,
142                    parent_optype: parent_optype.clone(),
143                    allowed_children,
144                });
145            }
146        }
147
148        // Entrypoints must be region containers.
149        if node == self.hugr.entrypoint() {
150            let validity_flags: OpValidityFlags = op_type.validity_flags();
151            if validity_flags.allowed_children == OpTag::None {
152                return Err(ValidationError::EntrypointNotContainer {
153                    node,
154                    optype: op_type.clone(),
155                });
156            }
157        }
158
159        // Thirdly that the node has correct children
160        self.validate_children(node, op_type)?;
161
162        // OpType-specific checks.
163        // TODO Maybe we should delegate these checks to the OpTypes themselves.
164        if let OpType::Const(c) = op_type {
165            c.validate()?;
166        }
167
168        Ok(())
169    }
170
171    /// Check whether a port is valid.
172    /// - Input ports and output linear ports must be connected
173    /// - The linked port must have a compatible type.
174    fn validate_port(
175        &mut self,
176        node: H::Node,
177        port: Port,
178        op_type: &OpType,
179        var_decls: &[TypeParam],
180    ) -> Result<(), ValidationError<H::Node>> {
181        let port_kind = op_type.port_kind(port).unwrap();
182        let dir = port.direction();
183
184        let mut links = self.hugr.linked_ports(node, port).peekable();
185        // Linear dataflow values must be used, and control must have somewhere to flow.
186        let outgoing_is_linear = port_kind.is_linear() || port_kind == EdgeKind::ControlFlow;
187        let must_be_connected = match dir {
188            // Incoming ports must be connected, except for state order ports, branch case nodes,
189            // and CFG nodes.
190            Direction::Incoming => {
191                port_kind != EdgeKind::StateOrder
192                    && port_kind != EdgeKind::ControlFlow
193                    && op_type.tag() != OpTag::Case
194            }
195            Direction::Outgoing => outgoing_is_linear,
196        };
197        if must_be_connected && links.peek().is_none() {
198            return Err(ValidationError::UnconnectedPort {
199                node,
200                port,
201                port_kind,
202            });
203        }
204
205        // Avoid double checking connected port types.
206        if dir == Direction::Incoming {
207            return Ok(());
208        }
209
210        self.validate_port_kind(&port_kind, var_decls)
211            .map_err(|cause| ValidationError::SignatureError {
212                node,
213                op: op_type.name(),
214                cause,
215            })?;
216
217        let mut link_cnt = 0;
218        for (other_node, other_offset) in links {
219            link_cnt += 1;
220            if outgoing_is_linear && link_cnt > 1 {
221                return Err(ValidationError::TooManyConnections {
222                    node,
223                    port,
224                    port_kind,
225                });
226            }
227
228            let other_op = self.hugr.get_optype(other_node);
229            let Some(other_kind) = other_op.port_kind(other_offset) else {
230                panic!(
231                    "The number of ports in {other_node} does not match the operation definition. This should have been caught by `validate_node`."
232                );
233            };
234            if other_kind != port_kind {
235                return Err(ValidationError::IncompatiblePorts {
236                    from: node,
237                    from_port: port,
238                    from_kind: port_kind,
239                    to: other_node,
240                    to_port: other_offset,
241                    to_kind: other_kind,
242                });
243            }
244
245            self.validate_edge(node, port, op_type, other_node, other_offset)?;
246        }
247
248        Ok(())
249    }
250
251    fn validate_port_kind(
252        &self,
253        port_kind: &EdgeKind,
254        var_decls: &[TypeParam],
255    ) -> Result<(), SignatureError> {
256        match &port_kind {
257            EdgeKind::Value(ty) => ty.validate(var_decls),
258            // Static edges must *not* refer to type variables declared by enclosing FuncDefns
259            // as these are only types at runtime.
260            EdgeKind::Const(ty) => ty.validate(&[]),
261            EdgeKind::Function(pf) => pf.validate(),
262            _ => Ok(()),
263        }
264    }
265
266    /// Check operation-specific constraints.
267    ///
268    /// These are flags defined for each operation type as an [`OpValidityFlags`] object.
269    fn validate_children(
270        &self,
271        node: H::Node,
272        op_type: &OpType,
273    ) -> Result<(), ValidationError<H::Node>> {
274        let flags = op_type.validity_flags();
275
276        if self.hugr.children(node).count() > 0 {
277            if flags.allowed_children.is_empty() {
278                return Err(ValidationError::NonContainerWithChildren {
279                    node,
280                    optype: op_type.clone(),
281                });
282            }
283
284            let all_children = self.hugr.children(node);
285            let mut first_two_children = all_children.clone().take(2);
286            let first_child = self.hugr.get_optype(first_two_children.next().unwrap());
287            if !flags.allowed_first_child.is_superset(first_child.tag()) {
288                return Err(ValidationError::InvalidInitialChild {
289                    parent: node,
290                    parent_optype: op_type.clone(),
291                    optype: first_child.clone(),
292                    expected: flags.allowed_first_child,
293                    position: "first",
294                });
295            }
296
297            if let Some(second_child) = first_two_children
298                .next()
299                .map(|child| self.hugr.get_optype(child))
300            {
301                if !flags.allowed_second_child.is_superset(second_child.tag()) {
302                    return Err(ValidationError::InvalidInitialChild {
303                        parent: node,
304                        parent_optype: op_type.clone(),
305                        optype: second_child.clone(),
306                        expected: flags.allowed_second_child,
307                        position: "second",
308                    });
309                }
310            }
311            // Additional validations running over the full list of children optypes
312            let children_optypes = all_children.map(|c| (c, self.hugr.get_optype(c)));
313            if let Err(source) = op_type.validate_op_children(children_optypes) {
314                return Err(ValidationError::InvalidChildren {
315                    parent: node,
316                    parent_optype: op_type.clone(),
317                    source,
318                });
319            }
320
321            // Additional validations running over the edges of the contained graph
322            if let Some(edge_check) = flags.edge_check {
323                for source in self.hugr.children(node) {
324                    for target in self.hugr.output_neighbours(source) {
325                        if self.hugr.get_parent(target) != Some(node) {
326                            continue;
327                        }
328                        let source_op = self.hugr.get_optype(source);
329                        let target_op = self.hugr.get_optype(target);
330                        for [source_port, target_port] in self.hugr.node_connections(source, target)
331                        {
332                            let edge_data = ChildrenEdgeData {
333                                source,
334                                target,
335                                source_port,
336                                target_port,
337                                source_op: source_op.clone(),
338                                target_op: target_op.clone(),
339                            };
340                            if let Err(source) = edge_check(edge_data) {
341                                return Err(ValidationError::InvalidEdges {
342                                    parent: node,
343                                    parent_optype: op_type.clone(),
344                                    source,
345                                });
346                            }
347                        }
348                    }
349                }
350            }
351
352            if flags.requires_dag {
353                self.validate_children_dag(node, op_type)?;
354            }
355        } else if flags.requires_children {
356            return Err(ValidationError::ContainerWithoutChildren {
357                node,
358                optype: op_type.clone(),
359            });
360        }
361
362        Ok(())
363    }
364
365    /// Ensure that the children of a node form a directed acyclic graph.
366    ///
367    /// Inter-graph edges are ignored. Only internal dataflow, constant, or
368    /// state order edges are considered.
369    fn validate_children_dag(
370        &self,
371        parent: H::Node,
372        op_type: &OpType,
373    ) -> Result<(), ValidationError<H::Node>> {
374        if self.hugr.children(parent).next().is_none() {
375            // No children, nothing to do
376            return Ok(());
377        }
378
379        let (region, node_map) = self.hugr.region_portgraph(parent);
380        let postorder = Topo::new(&region);
381        let nodes_visited = postorder
382            .iter(&region)
383            .filter(|n| *n != node_map.to_portgraph(parent))
384            .count();
385        let node_count = self.hugr.children(parent).count();
386        if nodes_visited != node_count {
387            return Err(ValidationError::NotADag {
388                node: parent,
389                optype: op_type.clone(),
390            });
391        }
392
393        Ok(())
394    }
395
396    /// Check the edge is valid, i.e. the source/target nodes are at appropriate
397    /// positions in the hierarchy for some locality:
398    /// - Local edges, of any kind;
399    /// - External edges, for static and value edges only: from a node to a sibling's descendant.
400    ///   For Value edges, there must also be an order edge between the copy and the sibling.
401    /// - Dominator edges, for value edges only: from a node in a `BasicBlock` node to
402    ///   a descendant of a post-dominating sibling of the `BasicBlock`.
403    fn validate_edge(
404        &mut self,
405        from: H::Node,
406        from_offset: Port,
407        from_optype: &OpType,
408        to: H::Node,
409        to_offset: Port,
410    ) -> Result<(), InterGraphEdgeError<H::Node>> {
411        let from_parent = self
412            .hugr
413            .get_parent(from)
414            .expect("Root nodes cannot have ports");
415        let to_parent = self.hugr.get_parent(to);
416        let edge_kind = from_optype.port_kind(from_offset).unwrap();
417        if Some(from_parent) == to_parent {
418            return Ok(()); // Local edge
419        }
420        let is_static = edge_kind.is_static();
421        if !is_static && !matches!(&edge_kind, EdgeKind::Value(t) if t.copyable()) {
422            return Err(InterGraphEdgeError::NonCopyableData {
423                from,
424                from_offset,
425                to,
426                to_offset,
427                ty: edge_kind,
428            });
429        }
430
431        // To detect either external or dominator edges, we traverse the ancestors
432        // of the target until we find either `from_parent` (in the external
433        // case), or the parent of `from_parent` (in the dominator case).
434        //
435        // This search could be sped-up with a pre-computed LCA structure, but
436        // for valid Hugrs this search should be very short.
437        //
438        // For Value edges only, we record any FuncDefn we went through; if there is
439        // any such, then that is an error, but we report that only if the dom/ext
440        // relation was otherwise ok (an error about an edge "entering" some ancestor
441        // node could be misleading if the source isn't where it's expected)
442        let mut err_entered_func = None;
443        let from_parent_parent = self.hugr.get_parent(from_parent);
444        for (ancestor, ancestor_parent) in
445            iter::successors(to_parent, |&p| self.hugr.get_parent(p)).tuple_windows()
446        {
447            if !is_static && self.hugr.get_optype(ancestor).is_func_defn() {
448                err_entered_func.get_or_insert(InterGraphEdgeError::ValueEdgeIntoFunc {
449                    to,
450                    to_offset,
451                    from,
452                    from_offset,
453                    func: ancestor,
454                });
455            }
456            if ancestor_parent == from_parent {
457                // External edge.
458                err_entered_func.map_or(Ok(()), Err)?;
459                if !is_static {
460                    // Must have an order edge.
461                    self.hugr
462                        .node_connections(from, ancestor)
463                        .find(|&[p, _]| from_optype.port_kind(p) == Some(EdgeKind::StateOrder))
464                        .ok_or(InterGraphEdgeError::MissingOrderEdge {
465                            from,
466                            from_offset,
467                            to,
468                            to_offset,
469                            to_ancestor: ancestor,
470                        })?;
471                }
472                return Ok(());
473            } else if Some(ancestor_parent) == from_parent_parent && !is_static {
474                // Dominator edge
475                let ancestor_parent_op = self.hugr.get_optype(ancestor_parent);
476                if ancestor_parent_op.tag() != OpTag::Cfg {
477                    return Err(InterGraphEdgeError::NonCFGAncestor {
478                        from,
479                        from_offset,
480                        to,
481                        to_offset,
482                        ancestor_parent_op: ancestor_parent_op.clone(),
483                    });
484                }
485                err_entered_func.map_or(Ok(()), Err)?;
486                // Check domination
487                let (dominator_tree, node_map) =
488                    if let Some(tree) = self.dominators.get(&ancestor_parent) {
489                        tree
490                    } else {
491                        let (tree, node_map) = self.compute_dominator(ancestor_parent);
492                        self.dominators.insert(ancestor_parent, (tree, node_map));
493                        self.dominators.get(&ancestor_parent).unwrap()
494                    };
495                if !dominator_tree
496                    .dominators(node_map.to_portgraph(ancestor))
497                    .is_some_and(|mut ds| ds.any(|n| n == node_map.to_portgraph(from_parent)))
498                {
499                    return Err(InterGraphEdgeError::NonDominatedAncestor {
500                        from,
501                        from_offset,
502                        to,
503                        to_offset,
504                        from_parent,
505                        ancestor,
506                    });
507                }
508
509                return Ok(());
510            }
511        }
512
513        Err(InterGraphEdgeError::NoRelation {
514            from,
515            from_offset,
516            to,
517            to_offset,
518        })
519    }
520
521    /// Validates that `TypeArgs` are valid wrt the [`ExtensionRegistry`] and that nodes
522    /// only refer to type variables declared by the closest enclosing `FuncDefn`.
523    fn validate_subtree(
524        &mut self,
525        node: H::Node,
526        var_decls: &[TypeParam],
527    ) -> Result<(), ValidationError<H::Node>> {
528        let op_type = self.hugr.get_optype(node);
529        // The op_type must be defined only in terms of type variables defined outside the node
530
531        let validate_ext = |ext_op: &ExtensionOp| -> Result<(), ValidationError<H::Node>> {
532            // Check TypeArgs are valid, and if we can, fit the declared TypeParams
533            ext_op
534                .def()
535                .validate_args(ext_op.args(), var_decls)
536                .map_err(|cause| ValidationError::SignatureError {
537                    node,
538                    op: op_type.name(),
539                    cause,
540                })
541        };
542        match op_type {
543            OpType::ExtensionOp(ext_op) => validate_ext(ext_op)?,
544            OpType::OpaqueOp(opaque) => {
545                Err(OpaqueOpError::UnresolvedOp(
546                    node,
547                    opaque.qualified_id(),
548                    opaque.extension().clone(),
549                ))?;
550            }
551            OpType::Call(c) => {
552                c.validate()
553                    .map_err(|cause| ValidationError::SignatureError {
554                        node,
555                        op: op_type.name(),
556                        cause,
557                    })?;
558            }
559            OpType::LoadFunction(c) => {
560                c.validate()
561                    .map_err(|cause| ValidationError::SignatureError {
562                        node,
563                        op: op_type.name(),
564                        cause,
565                    })?;
566            }
567            _ => (),
568        }
569
570        // Check port connections.
571        //
572        // Root nodes are ignored, as they cannot have connected edges.
573        if node != self.hugr.entrypoint() {
574            for dir in Direction::BOTH {
575                for port in self.hugr.node_ports(node, dir) {
576                    self.validate_port(node, port, op_type, var_decls)?;
577                }
578            }
579        }
580
581        // For FuncDefn's, only the type variables declared by the FuncDefn can be referred to by nodes
582        // inside the function. (The same would be true for FuncDecl's, but they have no child nodes.)
583        let var_decls = if let OpType::FuncDefn(fd) = op_type {
584            fd.signature().params()
585        } else {
586            var_decls
587        };
588
589        for child in self.hugr.children(node) {
590            self.validate_subtree(child, var_decls)?;
591        }
592
593        Ok(())
594    }
595}
596
597/// Errors that can occur while validating a Hugr.
598#[derive(Debug, Clone, PartialEq, Error)]
599#[allow(missing_docs)]
600#[non_exhaustive]
601pub enum ValidationError<N: HugrNode> {
602    /// The root node of the Hugr is not a root in the hierarchy.
603    #[error("The root node of the Hugr ({node}) is not a root in the hierarchy.")]
604    RootNotRoot { node: N },
605    /// The node ports do not match the operation signature.
606    #[error(
607        "{node} has an invalid number of ports. The operation {optype} cannot have {actual} {dir:?} ports. Expected {expected}."
608    )]
609    WrongNumberOfPorts {
610        node: N,
611        optype: OpType,
612        actual: usize,
613        expected: usize,
614        dir: Direction,
615    },
616    /// A dataflow port is not connected.
617    #[error("{node} has an unconnected port {port} of type {port_kind}.")]
618    UnconnectedPort {
619        node: N,
620        port: Port,
621        port_kind: EdgeKind,
622    },
623    /// A linear port is connected to more than one thing.
624    #[error("{node} has a port {port} of type {port_kind} with more than one connection.")]
625    TooManyConnections {
626        node: N,
627        port: Port,
628        port_kind: EdgeKind,
629    },
630    /// Connected ports have different types, or non-unifiable types.
631    #[error(
632        "Connected ports {from_port} in {from} and {to_port} in {to} have incompatible kinds. Cannot connect {from_kind} to {to_kind}."
633    )]
634    IncompatiblePorts {
635        from: N,
636        from_port: Port,
637        from_kind: EdgeKind,
638        to: N,
639        to_port: Port,
640        to_kind: EdgeKind,
641    },
642    /// The non-root node has no parent.
643    #[error("{node} has no parent.")]
644    NoParent { node: N },
645    /// The parent node is not compatible with the child node.
646    #[error("The operation {parent_optype} cannot contain a {child_optype} as a child. Allowed children: {}. In {child} with parent {parent}.", allowed_children.description())]
647    InvalidParentOp {
648        child: N,
649        child_optype: OpType,
650        parent: N,
651        parent_optype: OpType,
652        allowed_children: OpTag,
653    },
654    /// Invalid first/second child.
655    #[error(
656        "A {optype} operation cannot be the {position} child of a {parent_optype}. Expected {expected}. In parent {parent}"
657    )]
658    InvalidInitialChild {
659        parent: N,
660        parent_optype: OpType,
661        optype: OpType,
662        expected: OpTag,
663        position: &'static str,
664    },
665    /// The children list has invalid elements.
666    #[error(
667        "An operation {parent_optype} contains invalid children: {source}. In parent {parent}, child {child}",
668        child=source.child(),
669    )]
670    InvalidChildren {
671        parent: N,
672        parent_optype: OpType,
673        source: ChildrenValidationError<N>,
674    },
675    /// The children graph has invalid edges.
676    #[error(
677        "An operation {parent_optype} contains invalid edges between its children: {source}. In parent {parent}, edge from {from:?} port {from_port:?} to {to:?} port {to_port:?}",
678        from=source.edge().source,
679        from_port=source.edge().source_port,
680        to=source.edge().target,
681        to_port=source.edge().target_port,
682    )]
683    InvalidEdges {
684        parent: N,
685        parent_optype: OpType,
686        source: EdgeValidationError<N>,
687    },
688    /// The node operation is not a container, but has children.
689    #[error("{node} with optype {optype} is not a container, but has children.")]
690    NonContainerWithChildren { node: N, optype: OpType },
691    /// The node must have children, but has none.
692    #[error("{node} with optype {optype} must have children, but has none.")]
693    ContainerWithoutChildren { node: N, optype: OpType },
694    /// The children of a node do not form a DAG.
695    #[error(
696        "The children of an operation {optype} must form a DAG. Loops are not allowed. In {node}."
697    )]
698    NotADag { node: N, optype: OpType },
699    /// There are invalid inter-graph edges.
700    #[error(transparent)]
701    InterGraphEdgeError(#[from] InterGraphEdgeError<N>),
702    /// There are errors in the extension deltas.
703    #[error(transparent)]
704    ExtensionError(#[from] ExtensionError),
705    /// A node claims to still be awaiting extension inference. Perhaps it is not acted upon by inference.
706    #[error(
707        "{node} needs a concrete ExtensionSet - inference will provide this for Case/CFG/Conditional/DataflowBlock/DFG/TailLoop only"
708    )]
709    ExtensionsNotInferred { node: N },
710    /// Error in a node signature
711    #[error("Error in signature of operation {op} at {node}: {cause}")]
712    SignatureError {
713        node: N,
714        op: OpName,
715        #[source]
716        cause: SignatureError,
717    },
718    /// Error in a [`ExtensionOp`] serialized as an [Opaque].
719    ///
720    /// [ExtensionOp]: crate::ops::ExtensionOp
721    /// [Opaque]: crate::ops::OpaqueOp
722    #[error(transparent)]
723    OpaqueOpError(#[from] OpaqueOpError<N>),
724    /// A [Const] contained a [Value] of unexpected [Type].
725    ///
726    /// [Const]: crate::ops::Const
727    /// [Value]: crate::ops::Value
728    /// [Type]: crate::types::Type
729    #[error(transparent)]
730    ConstTypeError(#[from] ConstTypeError),
731    /// The HUGR entrypoint must be a region container.
732    #[error("The HUGR entrypoint ({node}) must be a region container, but '{}' does not accept children.", optype.name())]
733    EntrypointNotContainer { node: N, optype: OpType },
734}
735
736/// Errors related to the inter-graph edge validations.
737#[derive(Debug, Clone, PartialEq, Error)]
738#[allow(missing_docs)]
739#[non_exhaustive]
740pub enum InterGraphEdgeError<N: HugrNode> {
741    /// Inter-Graph edges can only carry copyable data.
742    #[error(
743        "Inter-graph edges can only carry copyable data. In an inter-graph edge from {from} ({from_offset}) to {to} ({to_offset}) with type {ty}."
744    )]
745    NonCopyableData {
746        from: N,
747        from_offset: Port,
748        to: N,
749        to_offset: Port,
750        ty: EdgeKind,
751    },
752    /// Inter-Graph edges may not enter into `FuncDefns` unless they are static
753    #[error(
754        "Inter-graph Value edges cannot enter into FuncDefns. Inter-graph edge from {from} ({from_offset}) to {to} ({to_offset} enters FuncDefn {func}"
755    )]
756    ValueEdgeIntoFunc {
757        from: N,
758        from_offset: Port,
759        to: N,
760        to_offset: Port,
761        func: N,
762    },
763    /// The grandparent of a dominator inter-graph edge must be a CFG container.
764    #[error(
765        "The grandparent of a dominator inter-graph edge must be a CFG container. Found operation {ancestor_parent_op}. In a dominator inter-graph edge from {from} ({from_offset}) to {to} ({to_offset})."
766    )]
767    NonCFGAncestor {
768        from: N,
769        from_offset: Port,
770        to: N,
771        to_offset: Port,
772        ancestor_parent_op: OpType,
773    },
774    /// The sibling ancestors of the external inter-graph edge endpoints must be have an order edge between them.
775    #[error(
776        "Missing state order between the external inter-graph source {from} and the ancestor of the target {to_ancestor}. In an external inter-graph edge from {from} ({from_offset}) to {to} ({to_offset})."
777    )]
778    MissingOrderEdge {
779        from: N,
780        from_offset: Port,
781        to: N,
782        to_offset: Port,
783        to_ancestor: N,
784    },
785    /// The ancestors of an inter-graph edge are not related.
786    #[error(
787        "The ancestors of an inter-graph edge are not related. In an inter-graph edge from {from} ({from_offset}) to {to} ({to_offset})."
788    )]
789    NoRelation {
790        from: N,
791        from_offset: Port,
792        to: N,
793        to_offset: Port,
794    },
795    /// The basic block containing the source node does not dominate the basic block containing the target node.
796    #[error(
797        " The basic block containing the source node does not dominate the basic block containing the target node in the CFG. Expected {from_parent} to dominate {ancestor}. In a dominator inter-graph edge from {from} ({from_offset}) to {to} ({to_offset})."
798    )]
799    NonDominatedAncestor {
800        from: N,
801        from_offset: Port,
802        to: N,
803        to_offset: Port,
804        from_parent: N,
805        ancestor: N,
806    },
807}
808
809#[cfg(test)]
810pub(crate) mod test;