1use crate::error::{NirError, Result};
15use crate::nodes::NirNode;
16use crate::types::MetadataValue;
17use indexmap::IndexMap;
18use std::collections::{HashMap, HashSet};
19
20#[derive(Debug, Clone, PartialEq, Default)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct NirGraph {
28 pub nodes: IndexMap<String, NirNode>,
30 pub edges: Vec<(String, String)>,
32 pub metadata: HashMap<String, MetadataValue>,
34 pub version: Option<String>,
36}
37
38impl NirGraph {
39 #[must_use]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn insert_node(&mut self, name: impl Into<String>, node: NirNode) -> Result<()> {
51 let name = name.into();
52 if self.nodes.contains_key(&name) {
53 return Err(NirError::DuplicateNode(name));
54 }
55 self.nodes.insert(name, node);
56 Ok(())
57 }
58
59 pub fn add_edge(&mut self, from: impl Into<String>, to: impl Into<String>) {
64 self.edges.push((from.into(), to.into()));
65 }
66
67 #[must_use]
69 pub fn get(&self, name: &str) -> Option<&NirNode> {
70 self.nodes.get(name)
71 }
72
73 pub fn get_mut(&mut self, name: &str) -> Option<&mut NirNode> {
75 self.nodes.get_mut(name)
76 }
77
78 #[must_use]
80 pub fn len(&self) -> usize {
81 self.nodes.len()
82 }
83
84 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.nodes.is_empty()
88 }
89
90 pub const MAX_NESTING_DEPTH: usize = 1024;
102
103 pub fn validate_structure(&self) -> Result<()> {
130 let mut frames: Vec<PathFrame<'_>> = Vec::new();
138 let mut work: Vec<(&NirGraph, Option<usize>)> = vec![(self, None)];
139
140 while let Some((graph, frame_idx)) = work.pop() {
141 graph
142 .validate_local_structure()
143 .map_err(|err| wrap_frame_error(&frames, frame_idx, err))?;
144
145 let parent_depth = frame_idx.map_or(0, |idx| frames[idx].depth);
146
147 let mut nested = Vec::new();
148 for (name, node) in &graph.nodes {
149 let NirNode::Graph(sub) = node else {
150 continue;
151 };
152 let child_depth = parent_depth + 1;
153 let child_idx = frames.len();
154 frames.push(PathFrame {
155 name: name.as_str(),
156 parent: frame_idx,
157 depth: child_depth,
158 });
159 if parent_depth >= Self::MAX_NESTING_DEPTH {
160 return Err(wrap_frame_error(
161 &frames,
162 Some(child_idx),
163 NirError::InvalidGraph(format!(
164 "graph nesting depth exceeds {}",
165 Self::MAX_NESTING_DEPTH
166 )),
167 ));
168 }
169 nested.push((sub.as_ref(), Some(child_idx)));
170 }
171 work.extend(nested.into_iter().rev());
172 }
173
174 Ok(())
175 }
176
177 fn validate_local_structure(&self) -> Result<()> {
179 let node_keys: HashSet<&str> = self.nodes.keys().map(String::as_str).collect();
180
181 for (src, dst) in &self.edges {
182 if !node_keys.contains(src.as_str()) {
183 return Err(NirError::MissingNode(src.clone()));
184 }
185 if !node_keys.contains(dst.as_str()) {
186 return Err(NirError::MissingNode(dst.clone()));
187 }
188 }
189
190 let mut seen_edges: HashSet<(&str, &str)> = HashSet::new();
191 for (src, dst) in &self.edges {
192 let key = (src.as_str(), dst.as_str());
193 if !seen_edges.insert(key) {
194 return Err(NirError::DuplicateEdge(src.clone(), dst.clone()));
195 }
196 }
197
198 Ok(())
199 }
200
201 pub fn validate_parameters(&self) -> Result<()> {
220 crate::validation::validate_graph(self)
221 }
222}
223
224#[derive(Clone, Copy)]
229struct PathFrame<'a> {
230 name: &'a str,
231 parent: Option<usize>,
232 depth: usize,
233}
234
235fn prefix_subgraph_error(name: &str, err: NirError) -> NirError {
241 match err {
242 NirError::MissingNode(n) => {
243 NirError::InvalidGraph(format!("in subgraph {name:?}: missing node: {n}"))
244 }
245 NirError::DuplicateEdge(a, b) => {
246 NirError::InvalidGraph(format!("in subgraph {name:?}: duplicate edge: ({a}, {b})"))
247 }
248 NirError::DuplicateNode(n) => {
249 NirError::InvalidGraph(format!("in subgraph {name:?}: duplicate node: {n}"))
250 }
251 NirError::InvalidGraph(msg) => {
252 NirError::InvalidGraph(format!("in subgraph {name:?}: {msg}"))
253 }
254 other => NirError::InvalidGraph(format!("in subgraph {name:?}: {other}")),
255 }
256}
257
258fn wrap_frame_error(
260 frames: &[PathFrame<'_>],
261 mut curr: Option<usize>,
262 mut err: NirError,
263) -> NirError {
264 while let Some(idx) = curr {
265 let frame = &frames[idx];
266 err = prefix_subgraph_error(frame.name, err);
267 curr = frame.parent;
268 }
269 err
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::nodes::{Affine, Input, Lif, Output};
276 use crate::types::Tensor;
277
278 fn input(shape: Vec<usize>) -> NirNode {
279 NirNode::Input(Input {
280 shape,
281 metadata: Default::default(),
282 })
283 }
284
285 fn output(shape: Vec<usize>) -> NirNode {
286 NirNode::Output(Output {
287 shape,
288 metadata: Default::default(),
289 })
290 }
291
292 #[test]
293 fn empty_graph_default() {
294 let g = NirGraph::new();
295 assert!(g.is_empty());
296 assert_eq!(g.len(), 0);
297 assert!(g.validate_structure().is_ok());
298 }
299
300 #[test]
301 fn insert_node_rejects_duplicate() {
302 let mut g = NirGraph::new();
303 g.insert_node("a", input(vec![4])).unwrap();
304 let err = g.insert_node("a", input(vec![2])).unwrap_err();
305 assert_eq!(err, NirError::DuplicateNode("a".into()));
306 }
307
308 #[test]
309 fn get_returns_inserted_node() {
310 let mut g = NirGraph::new();
311 g.insert_node("in", input(vec![3])).unwrap();
312 assert!(matches!(g.get("in"), Some(NirNode::Input(_))));
313 assert!(g.get("missing").is_none());
314 }
315
316 #[test]
317 fn validate_missing_source_endpoint() {
318 let mut g = NirGraph::new();
319 g.insert_node("b", output(vec![1])).unwrap();
320 g.add_edge("ghost", "b");
321 let err = g.validate_structure().unwrap_err();
322 assert_eq!(err, NirError::MissingNode("ghost".into()));
323 }
324
325 #[test]
326 fn validate_missing_dest_endpoint() {
327 let mut g = NirGraph::new();
328 g.insert_node("a", input(vec![1])).unwrap();
329 g.add_edge("a", "ghost");
330 let err = g.validate_structure().unwrap_err();
331 assert_eq!(err, NirError::MissingNode("ghost".into()));
332 }
333
334 #[test]
335 fn validate_duplicate_edge() {
336 let mut g = NirGraph::new();
337 g.insert_node("a", input(vec![1])).unwrap();
338 g.insert_node("b", output(vec![1])).unwrap();
339 g.add_edge("a", "b");
340 g.add_edge("a", "b");
341 let err = g.validate_structure().unwrap_err();
342 assert_eq!(err, NirError::DuplicateEdge("a".into(), "b".into()));
343 }
344
345 #[test]
346 fn cycles_are_allowed() {
347 let mut g = NirGraph::new();
348 g.insert_node("a", input(vec![1])).unwrap();
349 g.insert_node("b", output(vec![1])).unwrap();
350 g.add_edge("a", "b");
351 g.add_edge("b", "a");
352 assert!(g.validate_structure().is_ok());
353 }
354
355 #[test]
356 fn integration_input_affine_lif_output() {
357 let weight = Tensor::from_f32(vec![2, 4], vec![0.1; 8]).unwrap();
358 let bias = Tensor::from_f32(vec![2], vec![0.0, 0.0]).unwrap();
359 let tau = Tensor::from_f64(vec![2], vec![10.0, 10.0]).unwrap();
360 let r = Tensor::from_f64(vec![2], vec![1.0, 1.0]).unwrap();
361 let v_leak = Tensor::from_f64(vec![2], vec![0.0, 0.0]).unwrap();
362 let v_th = Tensor::from_f64(vec![2], vec![1.0, 1.0]).unwrap();
363
364 let mut g = NirGraph::new();
365 g.version = Some("1.0.0".into());
366 g.metadata.insert(
367 "origin".into(),
368 MetadataValue::String("integration-test".into()),
369 );
370
371 g.insert_node("input", input(vec![4])).unwrap();
372 g.insert_node(
373 "fc",
374 NirNode::Affine(Affine {
375 weight,
376 bias,
377 metadata: Default::default(),
378 }),
379 )
380 .unwrap();
381 g.insert_node(
382 "lif",
383 NirNode::Lif(Lif {
384 tau,
385 r,
386 v_leak,
387 v_threshold: v_th,
388 v_reset: None,
389 metadata: Default::default(),
390 }),
391 )
392 .unwrap();
393 g.insert_node("output", output(vec![2])).unwrap();
394
395 g.add_edge("input", "fc");
396 g.add_edge("fc", "lif");
397 g.add_edge("lif", "output");
398
399 assert!(g.validate_structure().is_ok());
400 assert_eq!(g.len(), 4);
401 assert_eq!(g.edges.len(), 3);
402 assert_eq!(g.get("lif").unwrap().type_name(), "LIF");
403 assert_eq!(g.get("fc").unwrap().type_name(), "Affine");
404 }
405
406 #[test]
407 fn nested_graph_validates() {
408 let mut inner = NirGraph::default();
409 inner.insert_node("i", input(vec![1])).unwrap();
410 inner.insert_node("o", output(vec![1])).unwrap();
411 inner.add_edge("i", "o");
412
413 let mut outer = NirGraph::default();
414 outer
415 .insert_node("sub", NirNode::Graph(Box::new(inner)))
416 .unwrap();
417 outer.insert_node("out", output(vec![1])).unwrap();
418 outer.add_edge("sub", "out");
419 assert!(outer.validate_structure().is_ok());
420 }
421
422 #[test]
423 fn nested_graph_reports_inner_failure() {
424 let mut inner = NirGraph::default();
425 inner.insert_node("i", input(vec![1])).unwrap();
426 inner.add_edge("i", "missing");
428
429 let mut outer = NirGraph::default();
430 outer
431 .insert_node("sub", NirNode::Graph(Box::new(inner)))
432 .unwrap();
433 let err = outer.validate_structure().unwrap_err();
434 assert_eq!(
435 err,
436 NirError::InvalidGraph("in subgraph \"sub\": missing node: missing".into())
437 );
438 }
439
440 #[test]
441 fn nested_duplicate_edge_keeps_path_context() {
442 let mut inner = NirGraph::default();
443 inner.insert_node("a", input(vec![1])).unwrap();
444 inner.insert_node("b", output(vec![1])).unwrap();
445 inner.add_edge("a", "b");
446 inner.add_edge("a", "b");
447
448 let mut outer = NirGraph::default();
449 outer
450 .insert_node("sub", NirNode::Graph(Box::new(inner)))
451 .unwrap();
452 let err = outer.validate_structure().unwrap_err();
453 assert_eq!(
454 err,
455 NirError::InvalidGraph("in subgraph \"sub\": duplicate edge: (a, b)".into())
456 );
457 }
458
459 #[test]
460 fn nested_path_context_is_outermost_first() {
461 let mut inner = NirGraph::default();
462 inner.insert_node("i", input(vec![1])).unwrap();
463 inner.add_edge("i", "ghost");
464
465 let mut mid = NirGraph::default();
466 mid.insert_node("inner", NirNode::Graph(Box::new(inner)))
467 .unwrap();
468
469 let mut outer = NirGraph::default();
470 outer
471 .insert_node("mid", NirNode::Graph(Box::new(mid)))
472 .unwrap();
473 let err = outer.validate_structure().unwrap_err();
474 assert_eq!(
475 err,
476 NirError::InvalidGraph(
477 "in subgraph \"mid\": in subgraph \"inner\": missing node: ghost".into()
478 )
479 );
480 }
481
482 #[test]
483 fn outer_endpoint_error_precedes_nested_failure() {
484 let mut inner = NirGraph::default();
485 inner.insert_node("i", input(vec![1])).unwrap();
486 inner.add_edge("i", "missing_inner");
487
488 let mut outer = NirGraph::default();
489 outer
490 .insert_node("sub", NirNode::Graph(Box::new(inner)))
491 .unwrap();
492 outer.add_edge("ghost", "sub");
493 let err = outer.validate_structure().unwrap_err();
494 assert_eq!(err, NirError::MissingNode("ghost".into()));
495 }
496
497 #[test]
498 fn first_nested_sibling_error_is_reported() {
499 let mut first = NirGraph::default();
500 first.insert_node("i", input(vec![1])).unwrap();
501 first.add_edge("i", "missing_a");
502
503 let mut second = NirGraph::default();
504 second.insert_node("i", input(vec![1])).unwrap();
505 second.add_edge("i", "missing_b");
506
507 let mut outer = NirGraph::default();
508 outer
509 .insert_node("sub_a", NirNode::Graph(Box::new(first)))
510 .unwrap();
511 outer
512 .insert_node("sub_b", NirNode::Graph(Box::new(second)))
513 .unwrap();
514 let err = outer.validate_structure().unwrap_err();
515 assert_eq!(
516 err,
517 NirError::InvalidGraph("in subgraph \"sub_a\": missing node: missing_a".into())
518 );
519 }
520
521 #[test]
522 fn nested_cycles_are_allowed() {
523 let mut inner = NirGraph::default();
524 inner.insert_node("a", input(vec![1])).unwrap();
525 inner.insert_node("b", output(vec![1])).unwrap();
526 inner.add_edge("a", "b");
527 inner.add_edge("b", "a");
528
529 let mut outer = NirGraph::default();
530 outer
531 .insert_node("loop", NirNode::Graph(Box::new(inner)))
532 .unwrap();
533 assert!(outer.validate_structure().is_ok());
534 }
535
536 fn wrap_depth(depth: usize, leaf: NirGraph) -> NirGraph {
538 let mut g = leaf;
539 for i in (0..depth).rev() {
540 let mut outer = NirGraph::default();
541 outer
542 .insert_node(format!("n{i}"), NirNode::Graph(Box::new(g)))
543 .unwrap();
544 g = outer;
545 }
546 g
547 }
548
549 fn leaf_graph() -> NirGraph {
550 let mut g = NirGraph::default();
551 g.insert_node("leaf", input(vec![1])).unwrap();
552 g
553 }
554
555 #[test]
556 fn nesting_at_max_depth_validates() {
557 let g = wrap_depth(NirGraph::MAX_NESTING_DEPTH, leaf_graph());
558 assert!(g.validate_structure().is_ok());
559 }
560
561 #[test]
562 fn nesting_beyond_max_depth_is_invalid_graph() {
563 let g = wrap_depth(NirGraph::MAX_NESTING_DEPTH + 1, leaf_graph());
564 let err = g.validate_structure().unwrap_err();
565 match err {
566 NirError::InvalidGraph(msg) => {
567 assert!(msg.contains("graph nesting depth exceeds"), "{msg}");
568 assert!(
569 msg.contains(&NirGraph::MAX_NESTING_DEPTH.to_string()),
570 "{msg}"
571 );
572 assert!(
573 msg.contains(&format!("n{}", NirGraph::MAX_NESTING_DEPTH)),
574 "{msg}"
575 );
576 assert!(msg.contains("n0"), "{msg}");
577 }
578 other => panic!("expected InvalidGraph, got {other:?}"),
579 }
580 }
581
582 #[test]
583 fn wide_nesting_is_not_a_depth_limit() {
584 let mut outer = NirGraph::default();
585 for i in 0..64 {
586 outer
587 .insert_node(format!("sub{i}"), NirNode::Graph(Box::new(leaf_graph())))
588 .unwrap();
589 }
590 assert!(outer.validate_structure().is_ok());
591 }
592
593 #[test]
594 fn wide_frontier_with_shared_path_frames_validates() {
595 let mut root = NirGraph::default();
596 for i in 0..50 {
597 let mut sub = NirGraph::default();
598 for j in 0..20 {
599 sub.insert_node(format!("leaf_{j}"), input(vec![1]))
600 .unwrap();
601 }
602 root.insert_node(format!("branch_{i}"), NirNode::Graph(Box::new(sub)))
603 .unwrap();
604 }
605 assert!(root.validate_structure().is_ok());
606 }
607
608 fn validate_structure_recursive(graph: &NirGraph) -> Result<()> {
610 graph.validate_local_structure()?;
611 for (name, node) in &graph.nodes {
612 if let NirNode::Graph(sub) = node {
613 validate_structure_recursive(sub).map_err(|e| prefix_subgraph_error(name, e))?;
614 }
615 }
616 Ok(())
617 }
618
619 fn assert_matches_recursive(graph: &NirGraph) {
620 assert_eq!(
621 graph.validate_structure(),
622 validate_structure_recursive(graph)
623 );
624 }
625
626 #[test]
627 fn shallow_graphs_match_recursive_oracle() {
628 assert_matches_recursive(&NirGraph::default());
629 assert_matches_recursive(&leaf_graph());
630
631 let mut missing = NirGraph::default();
632 missing.insert_node("a", input(vec![1])).unwrap();
633 missing.add_edge("a", "ghost");
634 assert_matches_recursive(&missing);
635
636 let mut dup = NirGraph::default();
637 dup.insert_node("a", input(vec![1])).unwrap();
638 dup.insert_node("b", output(vec![1])).unwrap();
639 dup.add_edge("a", "b");
640 dup.add_edge("a", "b");
641 assert_matches_recursive(&dup);
642
643 let mut cyc = NirGraph::default();
644 cyc.insert_node("a", input(vec![1])).unwrap();
645 cyc.insert_node("b", output(vec![1])).unwrap();
646 cyc.add_edge("a", "b");
647 cyc.add_edge("b", "a");
648 assert_matches_recursive(&cyc);
649
650 for depth in 0..=4 {
651 let mut inner = NirGraph::default();
652 inner.insert_node("i", input(vec![1])).unwrap();
653 inner.add_edge("i", "ghost");
654 assert_matches_recursive(&wrap_depth(depth, inner));
655 assert_matches_recursive(&wrap_depth(depth, leaf_graph()));
656 }
657
658 let mut first = NirGraph::default();
659 first.insert_node("i", input(vec![1])).unwrap();
660 first.add_edge("i", "missing_a");
661 let mut second = NirGraph::default();
662 second.insert_node("i", input(vec![1])).unwrap();
663 second.add_edge("i", "missing_b");
664 let mut outer = NirGraph::default();
665 outer
666 .insert_node("sub_a", NirNode::Graph(Box::new(first)))
667 .unwrap();
668 outer
669 .insert_node("sub_b", NirNode::Graph(Box::new(second)))
670 .unwrap();
671 outer.add_edge("nope", "sub_a");
672 assert_matches_recursive(&outer);
673 }
674
675 #[test]
676 fn insertion_order_preserved() {
677 let mut g = NirGraph::default();
678 g.insert_node("z", input(vec![1])).unwrap();
679 g.insert_node("a", output(vec![1])).unwrap();
680 let keys: Vec<&str> = g.nodes.keys().map(String::as_str).collect();
681 assert_eq!(keys, ["z", "a"]);
682 }
683
684 #[test]
685 fn small_stack_thread_validates_max_depth_without_overflow() {
686 let g = wrap_depth(NirGraph::MAX_NESTING_DEPTH, leaf_graph());
689 let builder = std::thread::Builder::new().stack_size(64 * 1024);
690 std::thread::scope(|s| {
691 builder
692 .spawn_scoped(s, || {
693 assert!(g.validate_structure().is_ok());
694 })
695 .unwrap()
696 .join()
697 .unwrap();
698 });
699 }
700}