Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions nir/ir/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,46 @@ def from_dict(cls, kwargs: Dict[str, Any]) -> "NIRGraph":
]
return super().from_dict(kwargs_local)

def validate_structure(self):
"""Validate the structural integrity of the graph.

Checks that all edge endpoints reference existing nodes and flags
duplicate edges. Called at the start of infer_types() so that
structural problems surface as clear errors rather than confusing
type inference failures.

Raises ValueError with a specific message for each violation.
"""
node_keys = set(self.nodes.keys())

for src, dst in self.edges:
if src not in node_keys:
raise ValueError(
f"Edge ({src!r}, {dst!r}) references source node "
f"{src!r} which does not exist in the graph. "
f"Available nodes: {sorted(node_keys)}"
)
if dst not in node_keys:
raise ValueError(
f"Edge ({src!r}, {dst!r}) references destination node "
f"{dst!r} which does not exist in the graph. "
f"Available nodes: {sorted(node_keys)}"
)

edge_set = set()
for edge in self.edges:
edge_tuple = (edge[0], edge[1])
if edge_tuple in edge_set:
raise ValueError(f"Duplicate edge: ({edge[0]!r}, {edge[1]!r})")
edge_set.add(edge_tuple)

for name, node in self.nodes.items():
if isinstance(node, NIRGraph):
try:
node.validate_structure()
except ValueError as e:
raise ValueError(f"In subgraph {name!r}: {e}") from e

def check_types(self):
"""Check that all nodes in the graph have input and output types.

Expand Down Expand Up @@ -233,6 +273,8 @@ def infer_types(self):
if not self.nodes:
return

self.validate_structure()

# Ensure all graph inputs flow through an Input node
all_node_keys = set(self.nodes.keys())
destination_nodes = {edge[1] for edge in self.edges}
Expand Down
66 changes: 66 additions & 0 deletions tests/test_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,72 @@ def test_type_check_recurrent():
)


def test_validate_structure_dangling_source():
"""Edge referencing a non-existent source node raises ValueError."""
with pytest.raises(ValueError, match="does not exist"):
nir.NIRGraph(
nodes={
"input": nir.Input(np.array([2])),
"output": nir.Output(np.array([2])),
},
edges=[("ghost", "output")],
)


def test_validate_structure_dangling_destination():
"""Edge referencing a non-existent destination node raises ValueError."""
with pytest.raises(ValueError, match="does not exist"):
nir.NIRGraph(
nodes={
"input": nir.Input(np.array([2])),
"output": nir.Output(np.array([2])),
},
edges=[("input", "ghost")],
)


def test_validate_structure_duplicate_edge():
"""Duplicate edges raise ValueError."""
with pytest.raises(ValueError, match="Duplicate edge"):
nir.NIRGraph(
nodes={
"input": nir.Input(np.array([2])),
"w": nir.Linear(weight=np.random.randn(2, 2)),
"output": nir.Output(np.array([2])),
},
edges=[("input", "w"), ("w", "output"), ("input", "w")],
)


def test_validate_structure_valid_graph_passes():
"""Well-formed graph passes structural validation without error."""
graph = nir.NIRGraph(
nodes={
"input": nir.Input(np.array([2])),
"w": nir.Linear(weight=np.random.randn(2, 2)),
"output": nir.Output(np.array([2])),
},
edges=[("input", "w"), ("w", "output")],
)
assert "input" in graph.nodes
assert "w" in graph.nodes
assert "output" in graph.nodes


def test_validate_structure_recurrent_valid():
"""Recurrent graph with all edges referencing valid nodes passes."""
graph = nir.NIRGraph(
nodes={
"input": nir.Input(np.array([2])),
"w": nir.Linear(weight=np.random.randn(2, 2)),
"neuron": nir.IF(r=np.random.randn(2), v_threshold=np.random.randn(2)),
"output": nir.Output(np.array([2])),
},
edges=[("input", "w"), ("w", "neuron"), ("neuron", "w"), ("neuron", "output")],
)
assert "neuron" in graph.nodes


def test_node():
try:
node = nir.ir.NIRNode()
Expand Down