use std::collections::HashMap;
use std::iter;
use itertools::Itertools;
use petgraph::algo::dominators::{self, Dominators};
use petgraph::visit::{Topo, Walker};
use portgraph::{LinkView, PortView};
use thiserror::Error;
use crate::extension::{ExtensionRegistry, SignatureError, TO_BE_INFERRED};
use crate::ops::constant::ConstTypeError;
use crate::ops::custom::{ExtensionOp, OpaqueOpError};
use crate::ops::validate::{ChildrenEdgeData, ChildrenValidationError, EdgeValidationError};
use crate::ops::{FuncDefn, OpParent, OpTag, OpTrait, OpType, ValidateOp};
use crate::types::type_param::TypeParam;
use crate::types::{EdgeKind, Signature};
use crate::{Direction, Hugr, Node, Port};
use super::views::{HierarchyView, HugrView, SiblingGraph};
use super::ExtensionError;
struct ValidationContext<'a, 'b> {
hugr: &'a Hugr,
dominators: HashMap<Node, Dominators<Node>>,
extension_registry: &'b ExtensionRegistry,
}
impl Hugr {
pub fn validate(&self, extension_registry: &ExtensionRegistry) -> Result<(), ValidationError> {
self.validate_no_extensions(extension_registry)?;
#[cfg(feature = "extension_inference")]
self.validate_extensions()?;
Ok(())
}
pub fn validate_no_extensions(
&self,
extension_registry: &ExtensionRegistry,
) -> Result<(), ValidationError> {
let mut validator = ValidationContext::new(self, extension_registry);
validator.validate()
}
pub fn validate_extensions(&self) -> Result<(), ValidationError> {
for parent in self.nodes() {
let parent_op = self.get_optype(parent);
if parent_op.extension_delta().contains(&TO_BE_INFERRED) {
return Err(ValidationError::ExtensionsNotInferred { node: parent });
}
let parent_extensions = match parent_op.inner_function_type() {
Some(Signature { extension_reqs, .. }) => extension_reqs,
None => match parent_op.tag() {
OpTag::Cfg | OpTag::Conditional => parent_op.extension_delta(),
OpTag::ModuleRoot => continue,
_ => {
assert!(self.children(parent).next().is_none(),
"Unknown parent node type {} - not a DataflowParent, Module, Cfg or Conditional",
parent_op);
continue;
}
},
};
for child in self.children(parent) {
let child_extensions = self.get_optype(child).extension_delta();
if !parent_extensions.is_superset(&child_extensions) {
return Err(ExtensionError {
parent,
parent_extensions,
child,
child_extensions,
}
.into());
}
}
}
Ok(())
}
}
impl<'a, 'b> ValidationContext<'a, 'b> {
#[allow(unused_variables)]
pub fn new(hugr: &'a Hugr, extension_registry: &'b ExtensionRegistry) -> Self {
Self {
hugr,
dominators: HashMap::new(),
extension_registry,
}
}
pub fn validate(&mut self) -> Result<(), ValidationError> {
if !self.hugr.hierarchy.is_root(self.hugr.root) {
return Err(ValidationError::RootNotRoot {
node: self.hugr.root(),
});
}
for node in self.hugr.graph.nodes_iter().map_into() {
self.validate_node(node)?;
}
self.validate_subtree(self.hugr.root(), &[])?;
#[cfg(all(test, not(miri)))]
{
let test_schema = std::env::var("HUGR_TEST_SCHEMA").is_ok_and(|x| !x.is_empty());
crate::hugr::serialize::test::check_hugr_roundtrip(self.hugr, test_schema);
}
Ok(())
}
fn compute_dominator(&self, parent: Node) -> Dominators<Node> {
let region: SiblingGraph = SiblingGraph::try_new(self.hugr, parent).unwrap();
let entry_node = self.hugr.children(parent).next().unwrap();
dominators::simple_fast(®ion.as_petgraph(), entry_node)
}
fn validate_node(&self, node: Node) -> Result<(), ValidationError> {
let op_type = self.hugr.get_optype(node);
if let OpType::OpaqueOp(opaque) = op_type {
Err(OpaqueOpError::UnresolvedOp(
node,
opaque.op_name().clone(),
opaque.extension().clone(),
))?;
}
for dir in Direction::BOTH {
let num_ports = self.hugr.graph.num_ports(node.pg_index(), dir);
if num_ports != op_type.port_count(dir) {
return Err(ValidationError::WrongNumberOfPorts {
node,
optype: op_type.clone(),
actual: num_ports,
expected: op_type.port_count(dir),
dir,
});
}
}
if node == self.hugr.root() {
if self.hugr.all_linked_inputs(node).next().is_some()
|| self.hugr.all_linked_outputs(node).next().is_some()
{
return Err(ValidationError::RootWithEdges { node });
}
} else {
let Some(parent) = self.hugr.get_parent(node) else {
return Err(ValidationError::NoParent { node });
};
let parent_optype = self.hugr.get_optype(parent);
let allowed_children = parent_optype.validity_flags().allowed_children;
if !allowed_children.is_superset(op_type.tag()) {
return Err(ValidationError::InvalidParentOp {
child: node,
child_optype: op_type.clone(),
parent,
parent_optype: parent_optype.clone(),
allowed_children,
});
}
}
self.validate_children(node, op_type)?;
if let OpType::Const(c) = op_type {
c.validate()?;
};
Ok(())
}
fn validate_port(
&mut self,
node: Node,
port: Port,
port_index: portgraph::PortIndex,
op_type: &OpType,
var_decls: &[TypeParam],
) -> Result<(), ValidationError> {
let port_kind = op_type.port_kind(port).unwrap();
let dir = port.direction();
let mut links = self.hugr.graph.port_links(port_index).peekable();
let outgoing_is_linear = port_kind.is_linear() || port_kind == EdgeKind::ControlFlow;
let must_be_connected = match dir {
Direction::Incoming => {
port_kind != EdgeKind::StateOrder
&& port_kind != EdgeKind::ControlFlow
&& op_type.tag() != OpTag::Case
}
Direction::Outgoing => outgoing_is_linear,
};
if must_be_connected && links.peek().is_none() {
return Err(ValidationError::UnconnectedPort {
node,
port,
port_kind,
});
}
if dir == Direction::Incoming {
return Ok(());
}
self.validate_port_kind(&port_kind, var_decls)
.map_err(|cause| ValidationError::SignatureError { node, cause })?;
let mut link_cnt = 0;
for (_, link) in links {
link_cnt += 1;
if outgoing_is_linear && link_cnt > 1 {
return Err(ValidationError::TooManyConnections {
node,
port,
port_kind,
});
}
let other_node: Node = self.hugr.graph.port_node(link).unwrap().into();
let other_offset = self.hugr.graph.port_offset(link).unwrap().into();
let other_op = self.hugr.get_optype(other_node);
let Some(other_kind) = other_op.port_kind(other_offset) else {
panic!("The number of ports in {other_node} does not match the operation definition. This should have been caught by `validate_node`.");
};
if other_kind != port_kind {
return Err(ValidationError::IncompatiblePorts {
from: node,
from_port: port,
from_kind: port_kind,
to: other_node,
to_port: other_offset,
to_kind: other_kind,
});
}
self.validate_edge(node, port, op_type, other_node, other_offset)?;
}
Ok(())
}
fn validate_port_kind(
&self,
port_kind: &EdgeKind,
var_decls: &[TypeParam],
) -> Result<(), SignatureError> {
match &port_kind {
EdgeKind::Value(ty) => ty.validate(self.extension_registry, var_decls),
EdgeKind::Const(ty) => ty.validate(self.extension_registry, &[]),
EdgeKind::Function(pf) => pf.validate(self.extension_registry),
_ => Ok(()),
}
}
fn validate_children(&self, node: Node, op_type: &OpType) -> Result<(), ValidationError> {
let flags = op_type.validity_flags();
if self.hugr.hierarchy.child_count(node.pg_index()) > 0 {
if flags.allowed_children.is_empty() {
return Err(ValidationError::NonContainerWithChildren {
node,
optype: op_type.clone(),
});
}
let all_children = self.hugr.children(node);
let mut first_two_children = all_children.clone().take(2);
let first_child = self.hugr.get_optype(first_two_children.next().unwrap());
if !flags.allowed_first_child.is_superset(first_child.tag()) {
return Err(ValidationError::InvalidInitialChild {
parent: node,
parent_optype: op_type.clone(),
optype: first_child.clone(),
expected: flags.allowed_first_child,
position: "first",
});
}
if let Some(second_child) = first_two_children
.next()
.map(|child| self.hugr.get_optype(child))
{
if !flags.allowed_second_child.is_superset(second_child.tag()) {
return Err(ValidationError::InvalidInitialChild {
parent: node,
parent_optype: op_type.clone(),
optype: second_child.clone(),
expected: flags.allowed_second_child,
position: "second",
});
}
}
let children_optypes = all_children.map(|c| (c.pg_index(), self.hugr.get_optype(c)));
if let Err(source) = op_type.validate_op_children(children_optypes) {
return Err(ValidationError::InvalidChildren {
parent: node,
parent_optype: op_type.clone(),
source,
});
}
if let Some(edge_check) = flags.edge_check {
for source in self.hugr.hierarchy.children(node.pg_index()) {
for target in self.hugr.graph.output_neighbours(source) {
if self.hugr.hierarchy.parent(target) != Some(node.pg_index()) {
continue;
}
let source_op = self.hugr.get_optype(source.into());
let target_op = self.hugr.get_optype(target.into());
for (source_port, target_port) in
self.hugr.graph.get_connections(source, target)
{
let edge_data = ChildrenEdgeData {
source,
target,
source_port: self.hugr.graph.port_offset(source_port).unwrap(),
target_port: self.hugr.graph.port_offset(target_port).unwrap(),
source_op: source_op.clone(),
target_op: target_op.clone(),
};
if let Err(source) = edge_check(edge_data) {
return Err(ValidationError::InvalidEdges {
parent: node,
parent_optype: op_type.clone(),
source,
});
}
}
}
}
}
if flags.requires_dag {
self.validate_children_dag(node, op_type)?;
}
} else if flags.requires_children {
return Err(ValidationError::ContainerWithoutChildren {
node,
optype: op_type.clone(),
});
}
Ok(())
}
fn validate_children_dag(&self, parent: Node, op_type: &OpType) -> Result<(), ValidationError> {
if !self.hugr.hierarchy.has_children(parent.pg_index()) {
return Ok(());
};
let region: SiblingGraph = SiblingGraph::try_new(self.hugr, parent).unwrap();
let postorder = Topo::new(®ion.as_petgraph());
let nodes_visited = postorder
.iter(®ion.as_petgraph())
.filter(|n| *n != parent)
.count();
let node_count = self.hugr.children(parent).count();
if nodes_visited != node_count {
return Err(ValidationError::NotADag {
node: parent,
optype: op_type.clone(),
});
}
Ok(())
}
fn validate_edge(
&mut self,
from: Node,
from_offset: Port,
from_optype: &OpType,
to: Node,
to_offset: Port,
) -> Result<(), InterGraphEdgeError> {
let from_parent = self
.hugr
.get_parent(from)
.expect("Root nodes cannot have ports");
let to_parent = self.hugr.get_parent(to);
let edge_kind = from_optype.port_kind(from_offset).unwrap();
if Some(from_parent) == to_parent {
return Ok(()); }
let is_static = edge_kind.is_static();
if !is_static && !matches!(&edge_kind, EdgeKind::Value(t) if t.copyable()) {
return Err(InterGraphEdgeError::NonCopyableData {
from,
from_offset,
to,
to_offset,
ty: edge_kind,
});
};
let mut err_entered_func = None;
let from_parent_parent = self.hugr.get_parent(from_parent);
for (ancestor, ancestor_parent) in
iter::successors(to_parent, |&p| self.hugr.get_parent(p)).tuple_windows()
{
if !is_static && self.hugr.get_optype(ancestor).is_func_defn() {
err_entered_func.get_or_insert(InterGraphEdgeError::ValueEdgeIntoFunc {
to,
to_offset,
from,
from_offset,
func: ancestor,
});
}
if ancestor_parent == from_parent {
err_entered_func.map_or(Ok(()), Err)?;
if !is_static {
self.hugr
.graph
.get_connections(from.pg_index(), ancestor.pg_index())
.find(|&(p, _)| {
let offset = self.hugr.graph.port_offset(p).unwrap();
from_optype.port_kind(offset) == Some(EdgeKind::StateOrder)
})
.ok_or(InterGraphEdgeError::MissingOrderEdge {
from,
from_offset,
to,
to_offset,
to_ancestor: ancestor,
})?;
}
return Ok(());
} else if Some(ancestor_parent) == from_parent_parent && !is_static {
let ancestor_parent_op = self.hugr.get_optype(ancestor_parent);
if ancestor_parent_op.tag() != OpTag::Cfg {
return Err(InterGraphEdgeError::NonCFGAncestor {
from,
from_offset,
to,
to_offset,
ancestor_parent_op: ancestor_parent_op.clone(),
});
}
err_entered_func.map_or(Ok(()), Err)?;
let dominator_tree = match self.dominators.get(&ancestor_parent) {
Some(tree) => tree,
None => {
let tree = self.compute_dominator(ancestor_parent);
self.dominators.insert(ancestor_parent, tree);
self.dominators.get(&ancestor_parent).unwrap()
}
};
if !dominator_tree
.dominators(ancestor)
.map_or(false, |mut ds| ds.any(|n| n == from_parent))
{
return Err(InterGraphEdgeError::NonDominatedAncestor {
from,
from_offset,
to,
to_offset,
from_parent,
ancestor,
});
}
return Ok(());
}
}
Err(InterGraphEdgeError::NoRelation {
from,
from_offset,
to,
to_offset,
})
}
fn validate_subtree(
&mut self,
node: Node,
var_decls: &[TypeParam],
) -> Result<(), ValidationError> {
let op_type = self.hugr.get_optype(node);
let validate_ext = |ext_op: &ExtensionOp| -> Result<(), ValidationError> {
ext_op
.def()
.validate_args(ext_op.args(), self.extension_registry, var_decls)
.map_err(|cause| ValidationError::SignatureError { node, cause })
};
match op_type {
OpType::ExtensionOp(ext_op) => validate_ext(ext_op)?,
OpType::OpaqueOp(opaque) => {
Err(OpaqueOpError::UnresolvedOp(
node,
opaque.op_name().clone(),
opaque.extension().clone(),
))?;
}
OpType::Call(c) => {
c.validate(self.extension_registry)
.map_err(|cause| ValidationError::SignatureError { node, cause })?;
}
OpType::LoadFunction(c) => {
c.validate(self.extension_registry)
.map_err(|cause| ValidationError::SignatureError { node, cause })?;
}
_ => (),
}
if node != self.hugr.root() {
for dir in Direction::BOTH {
for (i, port_index) in self.hugr.graph.ports(node.pg_index(), dir).enumerate() {
let port = Port::new(dir, i);
self.validate_port(node, port, port_index, op_type, var_decls)?;
}
}
}
let var_decls = if let OpType::FuncDefn(FuncDefn { signature, .. }) = op_type {
signature.params()
} else {
var_decls
};
for child in self.hugr.children(node) {
self.validate_subtree(child, var_decls)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Error)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum ValidationError {
#[error("The root node of the Hugr {node} is not a root in the hierarchy.")]
RootNotRoot { node: Node },
#[error("The root node of the Hugr {node} has edges when it should not.")]
RootWithEdges { node: Node },
#[error("The node {node} has an invalid number of ports. The operation {optype} cannot have {actual} {dir:?} ports. Expected {expected}.")]
WrongNumberOfPorts {
node: Node,
optype: OpType,
actual: usize,
expected: usize,
dir: Direction,
},
#[error("The node {node} has an unconnected port {port} of type {port_kind}.")]
UnconnectedPort {
node: Node,
port: Port,
port_kind: EdgeKind,
},
#[error(
"The node {node} has a port {port} of type {port_kind} with more than one connection."
)]
TooManyConnections {
node: Node,
port: Port,
port_kind: EdgeKind,
},
#[error("Connected ports {from_port} in node {from} and {to_port} in node {to} have incompatible kinds. Cannot connect {from_kind} to {to_kind}.")]
IncompatiblePorts {
from: Node,
from_port: Port,
from_kind: EdgeKind,
to: Node,
to_port: Port,
to_kind: EdgeKind,
},
#[error("The node {node} has no parent.")]
NoParent { node: Node },
#[error("The operation {parent_optype} cannot contain a {child_optype} as a child. Allowed children: {}. In node {child} with parent {parent}.", allowed_children.description())]
InvalidParentOp {
child: Node,
child_optype: OpType,
parent: Node,
parent_optype: OpType,
allowed_children: OpTag,
},
#[error("A {optype} operation cannot be the {position} child of a {parent_optype}. Expected {expected}. In parent node {parent}")]
InvalidInitialChild {
parent: Node,
parent_optype: OpType,
optype: OpType,
expected: OpTag,
position: &'static str,
},
#[error(
"An operation {parent_optype} contains invalid children: {source}. In parent {parent}, child Node({child})",
child=source.child().index(),
)]
InvalidChildren {
parent: Node,
parent_optype: OpType,
source: ChildrenValidationError,
},
#[error(
"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:?}",
from=source.edge().source,
from_port=source.edge().source_port,
to=source.edge().target,
to_port=source.edge().target_port,
)]
InvalidEdges {
parent: Node,
parent_optype: OpType,
source: EdgeValidationError,
},
#[error("The node {node} with optype {optype} is not a container, but has children.")]
NonContainerWithChildren { node: Node, optype: OpType },
#[error("The node {node} with optype {optype} must have children, but has none.")]
ContainerWithoutChildren { node: Node, optype: OpType },
#[error("The children of an operation {optype} must form a DAG. Loops are not allowed. In node {node}.")]
NotADag { node: Node, optype: OpType },
#[error(transparent)]
InterGraphEdgeError(#[from] InterGraphEdgeError),
#[error(transparent)]
ExtensionError(#[from] ExtensionError),
#[error("Node {node} needs a concrete ExtensionSet - inference will provide this for Case/CFG/Conditional/DataflowBlock/DFG/TailLoop only")]
ExtensionsNotInferred { node: Node },
#[error("Error in signature of node {node}: {cause}")]
SignatureError {
node: Node,
#[source]
cause: SignatureError,
},
#[error(transparent)]
OpaqueOpError(#[from] OpaqueOpError),
#[error(transparent)]
ConstTypeError(#[from] ConstTypeError),
}
#[derive(Debug, Clone, PartialEq, Error)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum InterGraphEdgeError {
#[error("Inter-graph edges can only carry copyable data. In an inter-graph edge from {from} ({from_offset}) to {to} ({to_offset}) with type {ty}.")]
NonCopyableData {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
ty: EdgeKind,
},
#[error("Inter-graph Value edges cannot enter into FuncDefns. Inter-graph edge from {from} ({from_offset}) to {to} ({to_offset} enters FuncDefn {func}")]
ValueEdgeIntoFunc {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
func: Node,
},
#[error("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}).")]
NonCFGAncestor {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
ancestor_parent_op: OpType,
},
#[error("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}).")]
MissingOrderEdge {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
to_ancestor: Node,
},
#[error("The ancestors of an inter-graph edge are not related. In an inter-graph edge from {from} ({from_offset}) to {to} ({to_offset}).")]
NoRelation {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
},
#[error(" The basic block containing the source node does not dominate the basic block containing the target node in the CFG. Expected node {from_parent} to dominate {ancestor}. In a dominator inter-graph edge from {from} ({from_offset}) to {to} ({to_offset}).")]
NonDominatedAncestor {
from: Node,
from_offset: Port,
to: Node,
to_offset: Port,
from_parent: Node,
ancestor: Node,
},
}
#[cfg(test)]
pub(crate) mod test;