Skip to main content

onnx_runtime_ir/
graph.rs

1//! The mutable graph model: node/value arenas, edge-consistent mutation,
2//! topological ordering, and validation (see `docs/ORT2.md` §3.3 and §3.5).
3
4use std::collections::{BinaryHeap, HashMap, HashSet};
5
6use crate::arena::Arena;
7use crate::dtype::DataType;
8use crate::error::GraphError;
9use crate::node::{Node, NodeId};
10use crate::shape::{Shape, SymbolConstraints, SymbolId};
11use crate::tensor::WeightRef;
12use crate::value::{Value, ValueId};
13
14/// A computation graph in SSA form.
15///
16/// Nodes and values live in [`Arena`]s keyed by [`NodeId`] / [`ValueId`]. The
17/// mutation API keeps producer/consumer edges consistent, so optimization
18/// passes can rewrite the graph and then [`Graph::validate`] it.
19#[derive(Clone, Debug, Default)]
20pub struct Graph {
21    pub nodes: Arena<NodeId, Node>,
22    pub values: Arena<ValueId, Value>,
23    /// Graph inputs, in order. These have no producer.
24    pub inputs: Vec<ValueId>,
25    /// Graph outputs, in order.
26    pub outputs: Vec<ValueId>,
27    /// Constant initializer weights, keyed by the value they populate.
28    pub initializers: HashMap<ValueId, WeightRef>,
29    /// Constraints on symbolic dimensions.
30    pub symbol_constraints: HashMap<SymbolId, SymbolConstraints>,
31    /// Imported opsets: domain → version.
32    pub opset_imports: HashMap<String, u64>,
33    /// Subgraph bodies for control-flow ops, keyed by `(node, attr_name)`.
34    pub subgraphs: HashMap<(NodeId, String), Graph>,
35
36    next_symbol: u32,
37    symbol_names: HashMap<String, SymbolId>,
38    unknown_value_types: HashSet<ValueId>,
39    unknown_value_shapes: HashSet<ValueId>,
40}
41
42impl Graph {
43    /// An empty graph.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    // === Query API ===
49
50    /// Borrow a node. Panics if `id` is not live; use
51    /// [`Graph::try_node`] for a checked lookup.
52    pub fn node(&self, id: NodeId) -> &Node {
53        self.nodes.get(id).expect("node id not live in graph")
54    }
55
56    /// Mutably borrow a node. Panics if `id` is not live.
57    pub fn node_mut(&mut self, id: NodeId) -> &mut Node {
58        self.nodes.get_mut(id).expect("node id not live in graph")
59    }
60
61    /// Checked node lookup.
62    pub fn try_node(&self, id: NodeId) -> Option<&Node> {
63        self.nodes.get(id)
64    }
65
66    /// Borrow a value. Panics if `id` is not live; use
67    /// [`Graph::try_value`] for a checked lookup.
68    pub fn value(&self, id: ValueId) -> &Value {
69        self.values.get(id).expect("value id not live in graph")
70    }
71
72    /// Mutably borrow a value. Panics if `id` is not live.
73    pub fn value_mut(&mut self, id: ValueId) -> &mut Value {
74        self.values.get_mut(id).expect("value id not live in graph")
75    }
76
77    /// Checked value lookup.
78    pub fn try_value(&self, id: ValueId) -> Option<&Value> {
79        self.values.get(id)
80    }
81
82    /// Consuming input slots sorted by `(NodeId, input_index)`.
83    pub fn uses(&self, value: ValueId) -> Vec<(NodeId, u32)> {
84        self.value(value).consumers.uses()
85    }
86
87    /// Distinct consumer nodes sorted by ascending [`NodeId`].
88    pub fn consumers(&self, value: ValueId) -> Vec<NodeId> {
89        self.value(value).consumers.nodes()
90    }
91
92    /// Number of consuming input slots.
93    pub fn num_uses(&self, value: ValueId) -> usize {
94        self.value(value).consumers.len()
95    }
96
97    /// Whether at least one node input slot consumes `value`.
98    pub fn has_uses(&self, value: ValueId) -> bool {
99        !self.value(value).consumers.is_empty()
100    }
101
102    /// Number of live nodes.
103    pub fn num_nodes(&self) -> usize {
104        self.nodes.len()
105    }
106
107    /// Number of live values.
108    pub fn num_values(&self) -> usize {
109        self.values.len()
110    }
111
112    /// Whether a value's element type came from explicit source type information.
113    pub fn value_type_is_known(&self, id: ValueId) -> bool {
114        !self.unknown_value_types.contains(&id)
115    }
116
117    /// Whether a value's rank and dimensions came from explicit source shape information.
118    pub fn value_shape_is_known(&self, id: ValueId) -> bool {
119        !self.unknown_value_shapes.contains(&id)
120    }
121
122    /// Mark a value's placeholder element type as unknown.
123    pub fn mark_value_type_unknown(&mut self, id: ValueId) {
124        self.unknown_value_types.insert(id);
125    }
126
127    /// Mark a value's element type as known.
128    pub fn mark_value_type_known(&mut self, id: ValueId) {
129        self.unknown_value_types.remove(&id);
130    }
131
132    /// Mark a value's placeholder shape as unknown.
133    pub fn mark_value_shape_unknown(&mut self, id: ValueId) {
134        self.unknown_value_shapes.insert(id);
135    }
136
137    // === Symbolic dimensions ===
138
139    /// Allocate a fresh symbolic dimension with an optional name (no dedup).
140    pub fn create_symbol(&mut self, name: Option<String>) -> SymbolId {
141        let id = SymbolId(self.next_symbol);
142        self.next_symbol += 1;
143        self.symbol_constraints
144            .insert(id, SymbolConstraints::new(id, name.clone()));
145        if let Some(n) = name {
146            self.symbol_names.insert(n, id);
147        }
148        id
149    }
150
151    /// Intern a symbolic dimension by protobuf dim-param name: repeated names
152    /// resolve to the same [`SymbolId`] (graph-construction invariant §3.5.4).
153    pub fn intern_symbol(&mut self, name: &str) -> SymbolId {
154        if let Some(id) = self.symbol_names.get(name) {
155            return *id;
156        }
157        self.create_symbol(Some(name.to_string()))
158    }
159
160    // === Construction helpers ===
161
162    /// Create a new anonymous value with a contiguous default layout.
163    pub fn create_value(&mut self, dtype: DataType, shape: Shape) -> ValueId {
164        self.values
165            .insert_with(|vid| Value::new(vid, dtype, shape.clone()))
166    }
167
168    /// Create a new named value.
169    pub fn create_named_value(
170        &mut self,
171        name: impl Into<String>,
172        dtype: DataType,
173        shape: Shape,
174    ) -> ValueId {
175        let id = self.create_value(dtype, shape);
176        self.value_mut(id).name = Some(name.into());
177        id
178    }
179
180    /// Register `value` as a graph input.
181    pub fn add_input(&mut self, value: ValueId) {
182        self.value_mut(value).is_graph_input = true;
183        self.inputs.push(value);
184        debug_assert!(self.value(value).is_graph_input);
185    }
186
187    /// Register `value` as a graph output.
188    pub fn add_output(&mut self, value: ValueId) {
189        self.value_mut(value).is_graph_output = true;
190        self.outputs.push(value);
191        debug_assert!(self.value(value).is_graph_output);
192    }
193
194    /// Insert `value` into the ordered graph outputs.
195    pub fn insert_output(&mut self, index: usize, value: ValueId) {
196        self.value_mut(value).is_graph_output = true;
197        self.outputs.insert(index, value);
198        debug_assert!(self.value(value).is_graph_output);
199    }
200
201    /// Remove one ordered graph input.
202    pub fn remove_input(&mut self, index: usize) -> ValueId {
203        let value = self.inputs.remove(index);
204        if !self.inputs.contains(&value) {
205            self.value_mut(value).is_graph_input = false;
206        }
207        debug_assert_eq!(
208            self.value(value).is_graph_input,
209            self.inputs.contains(&value)
210        );
211        value
212    }
213
214    /// Remove one ordered graph output.
215    pub fn remove_output(&mut self, index: usize) -> ValueId {
216        let value = self.outputs.remove(index);
217        if !self.outputs.contains(&value) {
218            self.value_mut(value).is_graph_output = false;
219        }
220        debug_assert_eq!(
221            self.value(value).is_graph_output,
222            self.outputs.contains(&value)
223        );
224        value
225    }
226
227    /// Replace the complete ordered graph-input list.
228    pub fn set_inputs(&mut self, inputs: Vec<ValueId>) {
229        for value in self.inputs.drain(..) {
230            if let Some(metadata) = self.values.get_mut(value) {
231                metadata.is_graph_input = false;
232            }
233        }
234        for &value in &inputs {
235            self.value_mut(value).is_graph_input = true;
236        }
237        self.inputs = inputs;
238        debug_assert!(
239            self.inputs
240                .iter()
241                .all(|&value| self.value(value).is_graph_input)
242        );
243    }
244
245    /// Replace the complete ordered graph-output list.
246    pub fn set_outputs(&mut self, outputs: Vec<ValueId>) {
247        for value in self.outputs.drain(..) {
248            if let Some(metadata) = self.values.get_mut(value) {
249                metadata.is_graph_output = false;
250            }
251        }
252        for &value in &outputs {
253            self.value_mut(value).is_graph_output = true;
254        }
255        self.outputs = outputs;
256        debug_assert!(
257            self.outputs
258                .iter()
259                .all(|&value| self.value(value).is_graph_output)
260        );
261    }
262
263    /// Attach initializer weights to `value`.
264    pub fn set_initializer(&mut self, value: ValueId, weight: WeightRef) {
265        self.initializers.insert(value, weight);
266    }
267
268    // === Traversal ===
269
270    /// Direct predecessors: nodes that produce this node's inputs.
271    pub fn predecessors(&self, node: NodeId) -> Vec<NodeId> {
272        let mut out = Vec::new();
273        let mut seen = HashSet::new();
274        for v in self.node(node).input_values() {
275            if let Some(val) = self.values.get(v)
276                && let Some(prod) = val.producer
277                && seen.insert(prod)
278            {
279                out.push(prod);
280            }
281        }
282        out.sort_unstable_by_key(|node| node.0);
283        out
284    }
285
286    /// Direct successors: nodes that consume this node's outputs.
287    pub fn successors(&self, node: NodeId) -> Vec<NodeId> {
288        let mut out = Vec::new();
289        let mut seen = HashSet::new();
290        for &v in &self.node(node).outputs {
291            if let Some(val) = self.values.get(v) {
292                for c in val.consumers.nodes() {
293                    if seen.insert(c) {
294                        out.push(c);
295                    }
296                }
297            }
298        }
299        out.sort_unstable_by_key(|node| node.0);
300        out
301    }
302
303    /// All nodes that lie on a path between `inputs` and `outputs`.
304    ///
305    /// Walks backwards from `outputs` via producer edges, stopping at any value
306    /// in `inputs`. Used to extract subgraphs for EP capability claims (§3.4).
307    pub fn nodes_between(&self, inputs: &[ValueId], outputs: &[ValueId]) -> Vec<NodeId> {
308        let boundary: HashSet<ValueId> = inputs.iter().copied().collect();
309        let mut nodes = Vec::new();
310        let mut seen_nodes = HashSet::new();
311        let mut seen_values = HashSet::new();
312        let mut stack: Vec<ValueId> = outputs.to_vec();
313        while let Some(v) = stack.pop() {
314            if boundary.contains(&v) || !seen_values.insert(v) {
315                continue;
316            }
317            let Some(val) = self.values.get(v) else {
318                continue;
319            };
320            if let Some(prod) = val.producer {
321                if seen_nodes.insert(prod) {
322                    nodes.push(prod);
323                }
324                for iv in self.node(prod).input_values() {
325                    stack.push(iv);
326                }
327            }
328        }
329        nodes
330    }
331
332    /// Topological order of nodes via Kahn's algorithm.
333    ///
334    /// Ties are broken by ascending [`NodeId`] for deterministic output.
335    /// Returns [`GraphError::CycleDetected`] if the graph has a cycle.
336    pub fn topological_order(&self) -> Result<Vec<NodeId>, GraphError> {
337        const VACANT: usize = usize::MAX;
338        let mut in_degree = vec![VACANT; self.nodes.capacity()];
339        let mut adj = vec![Vec::<NodeId>::new(); self.nodes.capacity()];
340        for node in self.nodes.keys() {
341            in_degree[node.0 as usize] = 0;
342        }
343
344        for (nid, node) in self.nodes.iter() {
345            for v in node.input_values() {
346                if let Some(val) = self.values.get(v)
347                    && let Some(prod) = val.producer
348                    && self.nodes.contains(prod)
349                {
350                    adj[prod.0 as usize].push(nid);
351                    in_degree[nid.0 as usize] += 1;
352                }
353            }
354        }
355
356        // Min-heap on raw id for deterministic ordering.
357        let mut ready: BinaryHeap<std::cmp::Reverse<u32>> = in_degree
358            .iter()
359            .enumerate()
360            .filter(|(_, degree)| **degree == 0)
361            .map(|(raw, _)| std::cmp::Reverse(raw as u32))
362            .collect();
363
364        let mut order = Vec::with_capacity(self.nodes.len());
365        while let Some(std::cmp::Reverse(raw)) = ready.pop() {
366            let nid = NodeId(raw);
367            order.push(nid);
368            for &successor in &adj[raw as usize] {
369                let degree = &mut in_degree[successor.0 as usize];
370                *degree -= 1;
371                if *degree == 0 {
372                    ready.push(std::cmp::Reverse(successor.0));
373                }
374            }
375        }
376
377        if order.len() != self.nodes.len() {
378            return Err(GraphError::CycleDetected);
379        }
380        Ok(order)
381    }
382
383    // === Mutation API ===
384
385    /// Canonicalize the default ONNX operator domain to `""` throughout this
386    /// graph (nodes, opset-import keys) and recursively in every subgraph.
387    ///
388    /// After this pass the graph satisfies the post-load invariant: the default
389    /// domain is always the empty string; `"ai.onnx"` never appears. The loader
390    /// establishes this at proto-materialization time; this method lets
391    /// programmatic graph builders reach the same canonical form before session
392    /// construction. See [`crate::normalize_domain`].
393    pub fn normalize_domains(&mut self) {
394        for node in self.nodes.values_mut() {
395            if node.domain == crate::AI_ONNX_DOMAIN {
396                node.domain.clear();
397            }
398        }
399        if let Some(version) = self.opset_imports.remove(crate::AI_ONNX_DOMAIN) {
400            let entry = self.opset_imports.entry(String::new()).or_insert(version);
401            *entry = (*entry).max(version);
402        }
403        for subgraph in self.subgraphs.values_mut() {
404            subgraph.normalize_domains();
405        }
406    }
407
408    /// Insert a node, wiring its producer/consumer edges. The node's `id`
409    /// field is overwritten with the freshly allocated [`NodeId`].
410    pub fn insert_node(&mut self, node: Node) -> NodeId {
411        let id = self.nodes.insert_with(|nid| {
412            let mut node = node;
413            node.id = nid;
414            node
415        });
416        self.connect_edges(id);
417        id
418    }
419
420    /// Remove a node, disconnecting its edges. Output values left with no
421    /// consumers (and not graph I/O or initializers) are deleted.
422    pub fn remove_node(&mut self, id: NodeId) {
423        if !self.nodes.contains(id) {
424            return;
425        }
426        self.disconnect_edges(id);
427        let outputs = self.node(id).outputs.clone();
428        self.nodes.remove(id);
429        for v in outputs {
430            self.gc_value_if_orphan(v);
431        }
432    }
433
434    /// Remove nodes in slice order.
435    ///
436    /// Each input edge is removed directly by `(NodeId, input_index)`, so this
437    /// remains linear in the number of removed edges even for a high-fanout
438    /// shared value.
439    pub fn remove_nodes(&mut self, ids: &[NodeId]) {
440        for &id in ids {
441            self.remove_node(id);
442        }
443    }
444
445    /// Replace disjoint node groups with one node each while updating shared
446    /// producer/consumer metadata in a batch.
447    ///
448    /// Each group is semantically equivalent to calling [`Graph::remove_node`]
449    /// for its IDs in slice order and then [`Graph::insert_node`] for the
450    /// replacement. In particular, replacement IDs and orphan-value collection
451    /// match that sequential operation. `graph_outputs` is retained for API
452    /// compatibility and checked against the per-value membership invariant in
453    /// debug builds.
454    pub fn replace_node_groups(
455        &mut self,
456        groups: Vec<(Vec<NodeId>, Node)>,
457        graph_outputs: &HashSet<ValueId>,
458    ) -> Vec<NodeId> {
459        debug_assert_eq!(
460            graph_outputs,
461            &self.outputs.iter().copied().collect::<HashSet<_>>()
462        );
463        let mut removed_nodes = HashSet::new();
464        for (node_ids, _) in &groups {
465            assert!(
466                !node_ids.is_empty(),
467                "replace_node_groups: group must not be empty"
468            );
469            for &id in node_ids {
470                assert!(
471                    self.nodes.contains(id),
472                    "replace_node_groups: node id not live"
473                );
474                assert!(
475                    removed_nodes.insert(id),
476                    "replace_node_groups: groups must be disjoint"
477                );
478            }
479        }
480
481        let mut inserted = Vec::with_capacity(groups.len());
482        for (node_ids, replacement) in groups {
483            for id in node_ids {
484                self.remove_node(id);
485            }
486            inserted.push(self.insert_node(replacement));
487        }
488
489        inserted
490    }
491
492    /// Replace node `old` in place with `new`, preserving the [`NodeId`].
493    ///
494    /// The old node's edges are disconnected and the new node's edges are
495    /// connected. Values that were outputs of `old` but not of `new` are left
496    /// in place (producer cleared); the caller may prune them.
497    pub fn replace_node(&mut self, old: NodeId, new: Node) -> NodeId {
498        assert!(self.nodes.contains(old), "replace_node: old id not live");
499        self.disconnect_edges(old);
500        {
501            let slot = self.nodes.get_mut(old).expect("old live");
502            let mut new = new;
503            new.id = old;
504            *slot = new;
505        }
506        self.connect_edges(old);
507        old
508    }
509
510    /// Splice `new_node` onto the edge feeding out of `value`:
511    /// `producer(value) → [new_node] → consumers(value)`.
512    ///
513    /// `new_node`'s single input becomes `value`, and it produces a fresh value
514    /// that replaces `value` in all of `value`'s original consumers.
515    pub fn insert_on_edge(&mut self, value: ValueId, new_node: Node) -> NodeId {
516        let (dtype, shape) = {
517            let v = self.value(value);
518            (v.dtype, v.shape.clone())
519        };
520        let new_value = self.create_value(dtype, shape);
521        // Redirect existing consumers onto the new value first (before the new
522        // node itself becomes a consumer of `value`).
523        self.replace_all_uses(value, new_value);
524
525        let mut new_node = new_node;
526        new_node.inputs = vec![Some(value)];
527        new_node.outputs = vec![new_value];
528        self.insert_node(new_node)
529    }
530
531    /// Replace one node input and update both values' consumer sets.
532    ///
533    /// This is constant-time on average for edge metadata. `None` disconnects
534    /// the slot and is used by node removal.
535    pub fn replace_input(
536        &mut self,
537        node: NodeId,
538        input_index: usize,
539        new_value: Option<ValueId>,
540    ) -> Option<ValueId> {
541        assert!(self.nodes.contains(node), "replace_input: node id not live");
542        assert!(
543            input_index < self.node(node).inputs.len(),
544            "replace_input: input index out of bounds"
545        );
546        if let Some(value) = new_value {
547            assert!(
548                self.values.contains(value),
549                "replace_input: value id not live"
550            );
551        }
552
553        let old_value = self.node(node).inputs[input_index];
554        if old_value == new_value {
555            return old_value;
556        }
557        if let Some(value) = old_value {
558            let removed = self
559                .value_mut(value)
560                .consumers
561                .remove(node, input_index as u32);
562            debug_assert!(removed, "old input edge must be present");
563        }
564        self.node_mut(node).inputs[input_index] = new_value;
565        if let Some(value) = new_value {
566            self.value_mut(value)
567                .consumers
568                .insert(node, input_index as u32);
569        }
570        old_value
571    }
572
573    /// Replace every use of `old_value` with `new_value` in consumer nodes and
574    /// in the graph output list, moving consumer edges accordingly.
575    pub fn replace_all_uses(&mut self, old_value: ValueId, new_value: ValueId) {
576        if old_value == new_value {
577            return;
578        }
579        let uses = match self.values.get(old_value) {
580            Some(value) => value.consumers.uses(),
581            None => return,
582        };
583        for (node, input_index) in uses {
584            if self.nodes.contains(node) {
585                self.replace_input(node, input_index as usize, Some(new_value));
586            }
587        }
588        if self.value(old_value).is_graph_output {
589            let mut outputs = self.outputs.clone();
590            for output in &mut outputs {
591                if *output == old_value {
592                    *output = new_value;
593                }
594            }
595            self.set_outputs(outputs);
596        }
597    }
598
599    // === Validation ===
600
601    /// Verify structural invariants (§3.3). Returns every defect found.
602    pub fn validate(&self) -> Result<(), Vec<GraphError>> {
603        let mut errors = Vec::new();
604        let graph_inputs: HashSet<_> = self.inputs.iter().copied().collect();
605        let graph_outputs: HashSet<_> = self.outputs.iter().copied().collect();
606        for (value, metadata) in self.values.iter() {
607            debug_assert_eq!(
608                metadata.is_graph_input,
609                graph_inputs.contains(&value),
610                "graph-input membership flag drifted for {value:?}"
611            );
612            debug_assert_eq!(
613                metadata.is_graph_output,
614                graph_outputs.contains(&value),
615                "graph-output membership flag drifted for {value:?}"
616            );
617        }
618
619        // 1. Node edges reference live values; collect produced values.
620        let mut produced: HashMap<ValueId, NodeId> = HashMap::new();
621        for (nid, node) in self.nodes.iter() {
622            for (input_index, input) in node.inputs.iter().enumerate() {
623                if let Some(value) = input {
624                    if !self.values.contains(*value) {
625                        errors.push(GraphError::DanglingValue(*value));
626                    } else if !self
627                        .value(*value)
628                        .consumers
629                        .contains(nid, input_index as u32)
630                    {
631                        errors.push(GraphError::ConsumerLinkMismatch(*value));
632                    }
633                }
634            }
635            for &v in &node.outputs {
636                if !self.values.contains(v) {
637                    errors.push(GraphError::DanglingValue(v));
638                    continue;
639                }
640                if produced.insert(v, nid).is_some() {
641                    errors.push(GraphError::DuplicateOutput(v));
642                }
643            }
644        }
645
646        // 2/3. Producer/consumer link consistency.
647        for (vid, val) in self.values.iter() {
648            if let Some(p) = val.producer {
649                if !self.nodes.contains(p) {
650                    errors.push(GraphError::DanglingNode(p));
651                } else if !self.node(p).outputs.contains(&vid) {
652                    errors.push(GraphError::ProducerLinkMismatch(vid));
653                }
654            }
655            for (consumer, input_index) in val.consumers.uses() {
656                if !self.nodes.contains(consumer) {
657                    errors.push(GraphError::DanglingNode(consumer));
658                } else if self.node(consumer).inputs.get(input_index as usize) != Some(&Some(vid)) {
659                    errors.push(GraphError::ConsumerLinkMismatch(vid));
660                }
661            }
662        }
663
664        // 4. Graph inputs must be sources.
665        for &inp in &self.inputs {
666            if let Some(val) = self.values.get(inp) {
667                if val.producer.is_some() {
668                    errors.push(GraphError::InputHasProducer(inp));
669                }
670                debug_assert!(val.is_graph_input);
671            } else {
672                errors.push(GraphError::DanglingValue(inp));
673            }
674        }
675
676        // 5. Graph outputs must be produced (unless they are graph inputs or
677        //    initializers passed straight through).
678        for &out in &self.outputs {
679            match self.values.get(out) {
680                Some(val) => {
681                    debug_assert!(val.is_graph_output);
682                    let is_source = val.is_graph_input || self.initializers.contains_key(&out);
683                    if val.producer.is_none() && !is_source {
684                        errors.push(GraphError::MissingProducer(out));
685                    }
686                }
687                None => errors.push(GraphError::DanglingValue(out)),
688            }
689        }
690
691        // 6. No cycles.
692        if let Err(e) = self.topological_order() {
693            errors.push(e);
694        }
695
696        // 7. Opset imports must have non-zero versions.
697        for (domain, &version) in &self.opset_imports {
698            if version == 0 {
699                errors.push(GraphError::InvalidOpsetImport {
700                    domain: domain.clone(),
701                    version,
702                });
703            }
704        }
705
706        // 8. Subgraphs validate recursively.
707        for sub in self.subgraphs.values() {
708            if let Err(mut sub_errors) = sub.validate() {
709                errors.append(&mut sub_errors);
710            }
711        }
712
713        if errors.is_empty() {
714            Ok(())
715        } else {
716            Err(errors)
717        }
718    }
719
720    // === Private edge maintenance ===
721
722    /// Wire a live node's edges into its input/output values.
723    fn connect_edges(&mut self, id: NodeId) {
724        let inputs = self.node(id).inputs.clone();
725        let outputs = self.node(id).outputs.clone();
726        for (input_index, input) in inputs.into_iter().enumerate() {
727            if let Some(value) = input
728                && let Some(metadata) = self.values.get_mut(value)
729            {
730                metadata.consumers.insert(id, input_index as u32);
731            }
732        }
733        for v in outputs {
734            if let Some(val) = self.values.get_mut(v) {
735                val.producer = Some(id);
736            }
737        }
738    }
739
740    /// Remove a live node's edges from its input/output values, without
741    /// deleting the values or the node itself.
742    fn disconnect_edges(&mut self, id: NodeId) {
743        let input_count = self.node(id).inputs.len();
744        let outputs = self.node(id).outputs.clone();
745        for input_index in 0..input_count {
746            self.replace_input(id, input_index, None);
747        }
748        for v in outputs {
749            if let Some(val) = self.values.get_mut(v)
750                && val.producer == Some(id)
751            {
752                val.producer = None;
753            }
754        }
755    }
756
757    /// Delete `value` if it has no producer, no consumers, and is not part of
758    /// the graph's I/O or initializers.
759    ///
760    /// Clears the value's entries in the unknown-type/shape sets so a later
761    /// arena slot reuse does not inherit stale "unknown" flags.
762    pub fn gc_value_if_orphan(&mut self, value: ValueId) {
763        let orphan = match self.values.get(value) {
764            Some(v) => {
765                v.producer.is_none()
766                    && v.consumers.is_empty()
767                    && !v.is_graph_input
768                    && !v.is_graph_output
769                    && !self.initializers.contains_key(&value)
770            }
771            None => false,
772        };
773        if orphan {
774            self.values.remove(value);
775            self.unknown_value_types.remove(&value);
776            self.unknown_value_shapes.remove(&value);
777        }
778    }
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784    use crate::shape::static_shape;
785    use crate::tensor::TensorData;
786
787    fn assert_graphs_identical(
788        mut batched: Graph,
789        mut sequential: Graph,
790        node_probes: usize,
791        value_probes: usize,
792        trial: usize,
793    ) {
794        assert_eq!(
795            format!("{batched:#?}"),
796            format!("{sequential:#?}"),
797            "graph mismatch on randomized trial {trial}"
798        );
799
800        // Arena free-list order is observable through subsequently allocated
801        // IDs, so exhaust the recycled slots as part of the equivalence check.
802        for _ in 0..node_probes {
803            let batched_id =
804                batched.insert_node(Node::new(NodeId(0), "Probe", Vec::new(), Vec::new()));
805            let sequential_id =
806                sequential.insert_node(Node::new(NodeId(0), "Probe", Vec::new(), Vec::new()));
807            assert_eq!(
808                batched_id, sequential_id,
809                "node arena mismatch on randomized trial {trial}"
810            );
811        }
812        for _ in 0..value_probes {
813            let batched_id = batched.create_value(DataType::Float32, static_shape([1]));
814            let sequential_id = sequential.create_value(DataType::Float32, static_shape([1]));
815            assert_eq!(
816                batched_id, sequential_id,
817                "value arena mismatch on randomized trial {trial}"
818            );
819        }
820    }
821
822    struct TestRng(u64);
823
824    impl TestRng {
825        fn next(&mut self) -> u64 {
826            self.0 ^= self.0 << 13;
827            self.0 ^= self.0 >> 7;
828            self.0 ^= self.0 << 17;
829            self.0
830        }
831
832        fn usize(&mut self, upper: usize) -> usize {
833            (self.next() as usize) % upper
834        }
835    }
836
837    fn reference_remove_node(graph: &mut Graph, id: NodeId) {
838        if !graph.nodes.contains(id) {
839            return;
840        }
841        let (inputs, outputs) = {
842            let node = graph.node(id);
843            (
844                node.input_values().collect::<Vec<_>>(),
845                node.outputs.clone(),
846            )
847        };
848        let unique_inputs: HashSet<_> = inputs.into_iter().collect();
849        for value in unique_inputs {
850            if let Some(metadata) = graph.values.get_mut(value) {
851                for (consumer, input_index) in metadata.consumers.uses() {
852                    if consumer == id {
853                        metadata.consumers.remove(consumer, input_index);
854                    }
855                }
856            }
857        }
858        for &value in &outputs {
859            if let Some(metadata) = graph.values.get_mut(value)
860                && metadata.producer == Some(id)
861            {
862                metadata.producer = None;
863            }
864        }
865        graph.nodes.remove(id);
866        for value in outputs {
867            let orphan = graph.values.get(value).is_some_and(|metadata| {
868                metadata.producer.is_none()
869                    && metadata.consumers.is_empty()
870                    && !graph.inputs.contains(&value)
871                    && !graph.outputs.contains(&value)
872                    && !graph.initializers.contains_key(&value)
873            });
874            if orphan {
875                graph.values.remove(value);
876                graph.unknown_value_types.remove(&value);
877                graph.unknown_value_shapes.remove(&value);
878            }
879        }
880    }
881
882    fn reference_topological_order(graph: &Graph) -> Result<Vec<NodeId>, GraphError> {
883        let mut in_degree: HashMap<NodeId, usize> =
884            graph.nodes.keys().map(|node| (node, 0)).collect();
885        let mut adjacency: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
886        for (node_id, node) in graph.nodes.iter() {
887            for value in node.input_values() {
888                if let Some(producer) = graph.value(value).producer
889                    && graph.nodes.contains(producer)
890                {
891                    adjacency.entry(producer).or_default().push(node_id);
892                    *in_degree.get_mut(&node_id).unwrap() += 1;
893                }
894            }
895        }
896        let mut ready: BinaryHeap<std::cmp::Reverse<u32>> = in_degree
897            .iter()
898            .filter(|(_, degree)| **degree == 0)
899            .map(|(node, _)| std::cmp::Reverse(node.0))
900            .collect();
901        let mut order = Vec::with_capacity(graph.num_nodes());
902        while let Some(std::cmp::Reverse(raw)) = ready.pop() {
903            let node = NodeId(raw);
904            order.push(node);
905            if let Some(successors) = adjacency.get(&node) {
906                for successor in successors {
907                    let degree = in_degree.get_mut(successor).unwrap();
908                    *degree -= 1;
909                    if *degree == 0 {
910                        ready.push(std::cmp::Reverse(successor.0));
911                    }
912                }
913            }
914        }
915        if order.len() == graph.num_nodes() {
916            Ok(order)
917        } else {
918            Err(GraphError::CycleDetected)
919        }
920    }
921
922    /// Build `a -> Relu -> b -> Add(b, c) -> d`, returning ids.
923    fn sample_graph() -> Graph {
924        let mut g = Graph::new();
925        g.opset_imports.insert(String::new(), 17);
926        let a = g.create_named_value("a", DataType::Float32, static_shape([4]));
927        let c = g.create_named_value("c", DataType::Float32, static_shape([4]));
928        g.add_input(a);
929        g.add_input(c);
930
931        let b = g.create_value(DataType::Float32, static_shape([4]));
932        let relu = Node::new(NodeId(0), "Relu", vec![Some(a)], vec![b]);
933        g.insert_node(relu);
934
935        let d = g.create_named_value("d", DataType::Float32, static_shape([4]));
936        let add = Node::new(NodeId(0), "Add", vec![Some(b), Some(c)], vec![d]);
937        g.insert_node(add);
938        g.add_output(d);
939        g
940    }
941
942    #[test]
943    fn edges_are_wired_on_insert() {
944        let g = sample_graph();
945        assert_eq!(g.num_nodes(), 2);
946        // b has a producer (Relu) and a consumer (Add)
947        let b = g.value(ValueId(2));
948        assert!(b.producer.is_some());
949        assert_eq!(b.consumers.len(), 1);
950    }
951
952    #[test]
953    fn topo_order_is_valid_and_deterministic() {
954        let g = sample_graph();
955        let order = g.topological_order().unwrap();
956        assert_eq!(order.len(), 2);
957        // Relu (NodeId 0) must come before Add (NodeId 1)
958        assert_eq!(order, vec![NodeId(0), NodeId(1)]);
959    }
960
961    #[test]
962    fn uses_preserve_input_multiplicity_and_replace_one_slot() {
963        let mut graph = Graph::new();
964        let old = graph.create_value(DataType::Float32, static_shape([1]));
965        let new = graph.create_value(DataType::Float32, static_shape([1]));
966        graph.add_input(old);
967        graph.add_input(new);
968        let output = graph.create_value(DataType::Float32, static_shape([1]));
969        let node = graph.insert_node(Node::new(
970            NodeId(0),
971            "Add",
972            vec![Some(old), Some(old)],
973            vec![output],
974        ));
975
976        assert_eq!(graph.uses(old), vec![(node, 0), (node, 1)]);
977        assert_eq!(graph.consumers(old), vec![node]);
978        assert_eq!(graph.replace_input(node, 1, Some(new)), Some(old));
979        assert_eq!(graph.uses(old), vec![(node, 0)]);
980        assert_eq!(graph.uses(new), vec![(node, 1)]);
981        assert!(graph.validate().is_ok());
982
983        graph.remove_node(node);
984        assert!(graph.uses(old).is_empty());
985        assert!(graph.uses(new).is_empty());
986    }
987
988    #[test]
989    fn io_membership_flags_follow_ordered_lists() {
990        let mut graph = Graph::new();
991        let a = graph.create_value(DataType::Float32, static_shape([1]));
992        let b = graph.create_value(DataType::Float32, static_shape([1]));
993        graph.add_input(a);
994        graph.add_output(a);
995        graph.insert_output(0, b);
996        assert!(graph.value(a).is_graph_input);
997        assert!(graph.value(a).is_graph_output);
998        assert!(graph.value(b).is_graph_output);
999
1000        assert_eq!(graph.remove_output(1), a);
1001        assert!(!graph.value(a).is_graph_output);
1002        graph.set_inputs(vec![b]);
1003        assert!(!graph.value(a).is_graph_input);
1004        assert!(graph.value(b).is_graph_input);
1005        graph.set_outputs(vec![a]);
1006        assert!(graph.value(a).is_graph_output);
1007        assert!(!graph.value(b).is_graph_output);
1008    }
1009
1010    #[test]
1011    fn consumer_hash_insertion_order_is_not_observable() {
1012        let mut first = Graph::new();
1013        let hub = first.create_value(DataType::Float32, static_shape([1]));
1014        first.add_input(hub);
1015        let mut nodes = Vec::new();
1016        for _ in 0..32 {
1017            let output = first.create_value(DataType::Float32, static_shape([1]));
1018            nodes.push(first.insert_node(Node::new(
1019                NodeId(0),
1020                "Add",
1021                vec![Some(hub), Some(hub)],
1022                vec![output],
1023            )));
1024        }
1025        let mut shuffled = first.clone();
1026        let mut uses = shuffled.uses(hub);
1027        for &(node, input_index) in &uses {
1028            shuffled.replace_input(node, input_index as usize, None);
1029        }
1030        let mut rng = TestRng(0x6a09_e667_f3bc_c909);
1031        for index in (1..uses.len()).rev() {
1032            uses.swap(index, rng.usize(index + 1));
1033        }
1034        for (node, input_index) in uses {
1035            shuffled.replace_input(node, input_index as usize, Some(hub));
1036        }
1037
1038        assert_eq!(first.uses(hub), shuffled.uses(hub));
1039        assert_eq!(first.consumers(hub), shuffled.consumers(hub));
1040        assert_eq!(first.topological_order(), shuffled.topological_order());
1041        assert_eq!(format!("{first:#?}"), format!("{shuffled:#?}"));
1042        assert_eq!(
1043            format!("{:#?}", sample_graph()),
1044            format!("{:#?}", sample_graph())
1045        );
1046        assert_eq!(nodes, first.consumers(hub));
1047    }
1048
1049    #[test]
1050    fn validate_accepts_wellformed_graph() {
1051        let g = sample_graph();
1052        assert!(g.validate().is_ok());
1053    }
1054
1055    #[test]
1056    fn predecessors_and_successors() {
1057        let g = sample_graph();
1058        assert_eq!(g.successors(NodeId(0)), vec![NodeId(1)]);
1059        assert_eq!(g.predecessors(NodeId(1)), vec![NodeId(0)]);
1060    }
1061
1062    #[test]
1063    fn nodes_between_walks_back() {
1064        let g = sample_graph();
1065        let between = g.nodes_between(&[ValueId(0), ValueId(1)], &[ValueId(3)]);
1066        assert!(between.contains(&NodeId(0)));
1067        assert!(between.contains(&NodeId(1)));
1068        assert_eq!(between.len(), 2);
1069    }
1070
1071    #[test]
1072    fn replace_all_uses_redirects_consumers() {
1073        let mut g = sample_graph();
1074        // New constant value replaces `b` as Add's input.
1075        let e = g.create_value(DataType::Float32, static_shape([4]));
1076        g.replace_all_uses(ValueId(2), e);
1077        // Add now consumes `e`, not `b`.
1078        let add = g.node(NodeId(1));
1079        assert!(add.input_values().any(|v| v == e));
1080        assert!(!add.input_values().any(|v| v == ValueId(2)));
1081        assert_eq!(g.consumers(e), vec![NodeId(1)]);
1082        assert!(g.value(ValueId(2)).consumers.is_empty());
1083    }
1084
1085    #[test]
1086    fn insert_on_edge_splices_node() {
1087        let mut g = sample_graph();
1088        // Insert an Identity between b (ValueId 2) and its consumer Add.
1089        let ident = Node::new(NodeId(0), "Identity", vec![], vec![]);
1090        let nid = g.insert_on_edge(ValueId(2), ident);
1091        // Add now consumes the new value produced by Identity.
1092        let new_out = g.node(nid).outputs[0];
1093        assert_eq!(g.value(new_out).producer, Some(nid));
1094        assert!(g.node(NodeId(1)).input_values().any(|v| v == new_out));
1095        assert!(g.validate().is_ok());
1096    }
1097
1098    #[test]
1099    fn remove_node_disconnects_and_gcs() {
1100        let mut g = sample_graph();
1101        g.remove_node(NodeId(1)); // remove Add
1102        assert_eq!(g.num_nodes(), 1);
1103        // `d` was Add's only output and a graph output -> kept, producer cleared
1104        assert!(g.value(ValueId(3)).producer.is_none());
1105        // b lost its consumer
1106        assert!(g.value(ValueId(2)).consumers.is_empty());
1107    }
1108
1109    #[test]
1110    fn remove_nodes_filters_wide_shared_input_once() {
1111        let mut graph = Graph::new();
1112        let input = graph.create_value(DataType::Float32, static_shape([1]));
1113        graph.add_input(input);
1114        let mut dead = Vec::new();
1115        let mut outputs = Vec::new();
1116        for _ in 0..1_000 {
1117            let output = graph.create_value(DataType::Float32, static_shape([1]));
1118            dead.push(graph.insert_node(Node::new(
1119                NodeId(0),
1120                "Relu",
1121                vec![Some(input)],
1122                vec![output],
1123            )));
1124            outputs.push(output);
1125        }
1126
1127        graph.remove_nodes(&dead);
1128
1129        assert_eq!(graph.num_nodes(), 0);
1130        assert!(graph.value(input).consumers.is_empty());
1131        assert!(
1132            outputs
1133                .into_iter()
1134                .all(|output| graph.try_value(output).is_none())
1135        );
1136    }
1137
1138    #[test]
1139    fn remove_nodes_keeps_surviving_shared_consumer() {
1140        let mut graph = Graph::new();
1141        let input = graph.create_value(DataType::Float32, static_shape([1]));
1142        graph.add_input(input);
1143        let mut nodes = Vec::new();
1144        for op_type in ["Relu", "Neg", "Abs"] {
1145            let output = graph.create_value(DataType::Float32, static_shape([1]));
1146            nodes.push(graph.insert_node(Node::new(
1147                NodeId(0),
1148                op_type,
1149                vec![Some(input)],
1150                vec![output],
1151            )));
1152        }
1153
1154        graph.remove_nodes(&nodes[..2]);
1155
1156        assert_eq!(graph.consumers(input), vec![nodes[2]]);
1157        assert!(graph.try_node(nodes[2]).is_some());
1158        assert!(graph.validate().is_ok());
1159    }
1160
1161    #[test]
1162    fn remove_nodes_collects_value_after_all_consumers_are_removed() {
1163        let mut graph = Graph::new();
1164        let input = graph.create_value(DataType::Float32, static_shape([1]));
1165        graph.add_input(input);
1166        let shared = graph.create_value(DataType::Float32, static_shape([1]));
1167        let producer = graph.insert_node(Node::new(
1168            NodeId(0),
1169            "Relu",
1170            vec![Some(input)],
1171            vec![shared],
1172        ));
1173        let mut consumers = Vec::new();
1174        for op_type in ["Neg", "Abs"] {
1175            let output = graph.create_value(DataType::Float32, static_shape([1]));
1176            consumers.push(graph.insert_node(Node::new(
1177                NodeId(0),
1178                op_type,
1179                vec![Some(shared)],
1180                vec![output],
1181            )));
1182        }
1183
1184        graph.remove_nodes(&[consumers[0], consumers[1], producer]);
1185
1186        assert!(graph.try_value(shared).is_none());
1187    }
1188
1189    #[test]
1190    fn remove_nodes_keeps_graph_outputs_and_initializers() {
1191        let mut graph = Graph::new();
1192        let input = graph.create_value(DataType::Float32, static_shape([1]));
1193        graph.add_input(input);
1194
1195        let graph_output = graph.create_value(DataType::Float32, static_shape([1]));
1196        let output_node = graph.insert_node(Node::new(
1197            NodeId(0),
1198            "Relu",
1199            vec![Some(input)],
1200            vec![graph_output],
1201        ));
1202        graph.add_output(graph_output);
1203
1204        let initializer = graph.create_value(DataType::Float32, static_shape([1]));
1205        let initializer_node = graph.insert_node(Node::new(
1206            NodeId(0),
1207            "Neg",
1208            vec![Some(input)],
1209            vec![initializer],
1210        ));
1211        graph.set_initializer(
1212            initializer,
1213            WeightRef::Inline(TensorData::from_raw(
1214                DataType::Float32,
1215                vec![1],
1216                0.0f32.to_le_bytes().to_vec(),
1217            )),
1218        );
1219
1220        graph.remove_nodes(&[output_node, initializer_node]);
1221
1222        assert!(graph.try_value(graph_output).is_some());
1223        assert!(graph.value(graph_output).producer.is_none());
1224        assert!(graph.try_value(initializer).is_some());
1225        assert!(graph.value(initializer).producer.is_none());
1226    }
1227
1228    #[test]
1229    fn remove_nodes_ignores_duplicate_and_nonlive_ids() {
1230        let graph = sample_graph();
1231        let ids = [NodeId(u32::MAX), NodeId(1), NodeId(1)];
1232        let mut sequential = graph.clone();
1233        for id in ids {
1234            sequential.remove_node(id);
1235        }
1236        let mut batched = graph;
1237        batched.remove_nodes(&ids);
1238
1239        assert_graphs_identical(batched, sequential, 3, 5, 0);
1240    }
1241
1242    #[test]
1243    fn remove_nodes_matches_sequential_removal_on_random_dags() {
1244        let mut rng = TestRng(0x4d59_5df4_d0f3_3173);
1245
1246        for trial in 0..10_000 {
1247            let input_count = 1 + rng.usize(3);
1248            let node_count = 1 + rng.usize(12);
1249            let mut graph = Graph::new();
1250            let mut values = Vec::new();
1251            for _ in 0..input_count {
1252                let input = graph.create_value(DataType::Float32, static_shape([1]));
1253                graph.add_input(input);
1254                values.push(input);
1255            }
1256
1257            let mut nodes = Vec::with_capacity(node_count);
1258            for _ in 0..node_count {
1259                let input_arity = 1 + rng.usize(3);
1260                let inputs = (0..input_arity)
1261                    .map(|_| Some(values[rng.usize(values.len())]))
1262                    .collect();
1263                let output = graph.create_value(DataType::Float32, static_shape([1]));
1264                nodes.push(graph.insert_node(Node::new(NodeId(0), "Random", inputs, vec![output])));
1265                if rng.usize(5) == 0 {
1266                    graph.mark_value_type_unknown(output);
1267                }
1268                if rng.usize(5) == 0 {
1269                    graph.mark_value_shape_unknown(output);
1270                }
1271                values.push(output);
1272            }
1273
1274            for _ in 0..rng.usize(4) {
1275                let output = values[rng.usize(values.len())];
1276                graph.add_output(output);
1277            }
1278
1279            for i in (1..nodes.len()).rev() {
1280                let j = rng.usize(i + 1);
1281                nodes.swap(i, j);
1282            }
1283            nodes.truncate(rng.usize(nodes.len() + 1));
1284
1285            let mut sequential = graph.clone();
1286            for &id in &nodes {
1287                sequential.remove_node(id);
1288            }
1289            let mut batched = graph;
1290            batched.remove_nodes(&nodes);
1291
1292            assert_graphs_identical(
1293                batched,
1294                sequential,
1295                node_count + 1,
1296                input_count + node_count + 1,
1297                trial,
1298            );
1299        }
1300    }
1301
1302    #[test]
1303    fn single_node_removal_matches_vector_reference_on_random_dags() {
1304        let mut rng = TestRng(0xbb67_ae85_84ca_a73b);
1305        for trial in 0..2_000 {
1306            let mut graph = Graph::new();
1307            let input_count = 1 + rng.usize(3);
1308            let node_count = 1 + rng.usize(24);
1309            let mut values = Vec::new();
1310            for _ in 0..input_count {
1311                let value = graph.create_value(DataType::Float32, static_shape([1]));
1312                graph.add_input(value);
1313                values.push(value);
1314            }
1315            let mut nodes = Vec::new();
1316            for _ in 0..node_count {
1317                let input_count = 1 + rng.usize(4);
1318                let inputs = (0..input_count)
1319                    .map(|_| Some(values[rng.usize(values.len())]))
1320                    .collect();
1321                let output = graph.create_value(DataType::Float32, static_shape([1]));
1322                nodes.push(graph.insert_node(Node::new(NodeId(0), "Random", inputs, vec![output])));
1323                values.push(output);
1324            }
1325            for _ in 0..rng.usize(4) {
1326                graph.add_output(values[rng.usize(values.len())]);
1327            }
1328
1329            let mut removals = nodes.clone();
1330            for index in (1..removals.len()).rev() {
1331                removals.swap(index, rng.usize(index + 1));
1332            }
1333            removals.truncate(rng.usize(removals.len() + 1));
1334            if let Some(&duplicate) = removals.first() {
1335                removals.push(duplicate);
1336            }
1337            removals.push(NodeId(u32::MAX));
1338
1339            let mut actual = graph.clone();
1340            let mut reference = graph;
1341            for &node in &removals {
1342                actual.remove_node(node);
1343                reference_remove_node(&mut reference, node);
1344            }
1345
1346            assert_eq!(
1347                format!("{actual:#?}"),
1348                format!("{reference:#?}"),
1349                "debug mismatch on trial {trial}"
1350            );
1351            assert_eq!(
1352                actual.topological_order(),
1353                reference.topological_order(),
1354                "topology mismatch on trial {trial}"
1355            );
1356            for value in actual.values.keys() {
1357                assert_eq!(
1358                    actual.uses(value),
1359                    reference.uses(value),
1360                    "uses mismatch for {value:?} on trial {trial}"
1361                );
1362                assert_eq!(
1363                    actual.consumers(value),
1364                    reference.consumers(value),
1365                    "consumers mismatch for {value:?} on trial {trial}"
1366                );
1367            }
1368        }
1369    }
1370
1371    #[test]
1372    fn vec_indexed_topology_matches_hashmap_reference_on_random_dags() {
1373        let mut rng = TestRng(0x3c6e_f372_fe94_f82b);
1374        for trial in 0..2_000 {
1375            let mut graph = Graph::new();
1376            let input = graph.create_value(DataType::Float32, static_shape([1]));
1377            graph.add_input(input);
1378            let mut values = vec![input];
1379            let mut nodes = Vec::new();
1380            for _ in 0..(1 + rng.usize(48)) {
1381                let inputs = (0..(1 + rng.usize(4)))
1382                    .map(|_| Some(values[rng.usize(values.len())]))
1383                    .collect();
1384                let output = graph.create_value(DataType::Float32, static_shape([1]));
1385                nodes.push(graph.insert_node(Node::new(NodeId(0), "Random", inputs, vec![output])));
1386                values.push(output);
1387            }
1388            for &node in nodes.iter().filter(|_| rng.usize(5) == 0) {
1389                graph.remove_node(node);
1390            }
1391            assert_eq!(
1392                graph.topological_order(),
1393                reference_topological_order(&graph),
1394                "topology mismatch on trial {trial}"
1395            );
1396        }
1397    }
1398
1399    #[test]
1400    fn replace_node_groups_matches_sequential_mutation() {
1401        let mut sequential = Graph::new();
1402        let input = sequential.create_value(DataType::Float32, static_shape([1]));
1403        sequential.add_input(input);
1404        let interior = sequential.create_value(DataType::Float32, static_shape([1]));
1405        let first = sequential.insert_node(Node::new(
1406            NodeId(0),
1407            "Relu",
1408            vec![Some(input)],
1409            vec![interior],
1410        ));
1411        let output = sequential.create_value(DataType::Float32, static_shape([1]));
1412        let second = sequential.insert_node(Node::new(
1413            NodeId(0),
1414            "Relu",
1415            vec![Some(interior)],
1416            vec![output],
1417        ));
1418        sequential.add_output(output);
1419        let sibling_output = sequential.create_value(DataType::Float32, static_shape([1]));
1420        let sibling = sequential.insert_node(Node::new(
1421            NodeId(0),
1422            "Neg",
1423            vec![Some(input)],
1424            vec![sibling_output],
1425        ));
1426        sequential.add_output(sibling_output);
1427
1428        let mut batched = sequential.clone();
1429        let graph_outputs: HashSet<_> = batched.outputs.iter().copied().collect();
1430        let replacement0 = Node::new(NodeId(0), "EPContext", vec![Some(input)], vec![output]);
1431        let replacement1 = Node::new(
1432            NodeId(0),
1433            "EPContext",
1434            vec![Some(input)],
1435            vec![sibling_output],
1436        );
1437
1438        sequential.remove_node(first);
1439        sequential.remove_node(second);
1440        let replacement0_id = sequential.insert_node(replacement0.clone());
1441        sequential.remove_node(sibling);
1442        let replacement1_id = sequential.insert_node(replacement1.clone());
1443
1444        let inserted = batched.replace_node_groups(
1445            vec![
1446                (vec![first, second], replacement0),
1447                (vec![sibling], replacement1),
1448            ],
1449            &graph_outputs,
1450        );
1451        assert_eq!(inserted, vec![replacement0_id, replacement1_id]);
1452
1453        let sequential_nodes: Vec<_> = sequential
1454            .nodes
1455            .iter()
1456            .map(|(id, node)| {
1457                (
1458                    id,
1459                    node.op_type.clone(),
1460                    node.inputs.clone(),
1461                    node.outputs.clone(),
1462                )
1463            })
1464            .collect();
1465        let batched_nodes: Vec<_> = batched
1466            .nodes
1467            .iter()
1468            .map(|(id, node)| {
1469                (
1470                    id,
1471                    node.op_type.clone(),
1472                    node.inputs.clone(),
1473                    node.outputs.clone(),
1474                )
1475            })
1476            .collect();
1477        assert_eq!(batched_nodes, sequential_nodes);
1478
1479        let sequential_values: Vec<_> = sequential
1480            .values
1481            .iter()
1482            .map(|(id, value)| (id, value.producer, value.consumers.clone()))
1483            .collect();
1484        let batched_values: Vec<_> = batched
1485            .values
1486            .iter()
1487            .map(|(id, value)| (id, value.producer, value.consumers.clone()))
1488            .collect();
1489        assert_eq!(batched_values, sequential_values);
1490
1491        let next_sequential =
1492            sequential.insert_node(Node::new(NodeId(0), "Identity", Vec::new(), Vec::new()));
1493        let next_batched =
1494            batched.insert_node(Node::new(NodeId(0), "Identity", Vec::new(), Vec::new()));
1495        assert_eq!(next_batched, next_sequential);
1496    }
1497
1498    #[test]
1499    fn replace_node_groups_matches_sequential_orphan_collection() {
1500        let mut sequential = Graph::new();
1501        let input = sequential.create_value(DataType::Float32, static_shape([1]));
1502        sequential.add_input(input);
1503        let interior = sequential.create_value(DataType::Float32, static_shape([1]));
1504        let producer = sequential.insert_node(Node::new(
1505            NodeId(0),
1506            "Relu",
1507            vec![Some(input)],
1508            vec![interior],
1509        ));
1510        let output = sequential.create_value(DataType::Float32, static_shape([1]));
1511        let consumer = sequential.insert_node(Node::new(
1512            NodeId(0),
1513            "Relu",
1514            vec![Some(interior)],
1515            vec![output],
1516        ));
1517        sequential.add_output(output);
1518
1519        let mut batched = sequential.clone();
1520        let graph_outputs: HashSet<_> = batched.outputs.iter().copied().collect();
1521        let replacement = Node::new(NodeId(0), "EPContext", vec![Some(input)], vec![output]);
1522
1523        sequential.remove_node(consumer);
1524        sequential.remove_node(producer);
1525        sequential.insert_node(replacement.clone());
1526        batched.replace_node_groups(
1527            vec![(vec![consumer, producer], replacement)],
1528            &graph_outputs,
1529        );
1530
1531        assert!(sequential.try_value(interior).is_none());
1532        assert!(batched.try_value(interior).is_none());
1533        let next_sequential = sequential.create_value(DataType::Float32, static_shape([1]));
1534        let next_batched = batched.create_value(DataType::Float32, static_shape([1]));
1535        assert_eq!(next_batched, next_sequential);
1536    }
1537
1538    #[test]
1539    fn replace_node_preserves_id() {
1540        let mut g = sample_graph();
1541        let d = g.node(NodeId(1)).outputs[0];
1542        let b = g.node(NodeId(1)).inputs[0];
1543        let c = g.node(NodeId(1)).inputs[1];
1544        let sub = Node::new(NodeId(0), "Sub", vec![b, c], vec![d]);
1545        let id = g.replace_node(NodeId(1), sub);
1546        assert_eq!(id, NodeId(1));
1547        assert_eq!(g.node(NodeId(1)).op_type, "Sub");
1548        assert!(g.validate().is_ok());
1549    }
1550
1551    #[test]
1552    fn cycle_is_detected() {
1553        let mut g = Graph::new();
1554        let v0 = g.create_value(DataType::Float32, static_shape([1]));
1555        let v1 = g.create_value(DataType::Float32, static_shape([1]));
1556        // n0: v1 -> v0 ; n1: v0 -> v1  (cycle)
1557        g.insert_node(Node::new(NodeId(0), "A", vec![Some(v1)], vec![v0]));
1558        g.insert_node(Node::new(NodeId(0), "B", vec![Some(v0)], vec![v1]));
1559        assert_eq!(g.topological_order(), Err(GraphError::CycleDetected));
1560        assert!(g.validate().is_err());
1561    }
1562
1563    #[test]
1564    fn intern_symbol_dedups_by_name() {
1565        let mut g = Graph::new();
1566        let s1 = g.intern_symbol("batch");
1567        let s2 = g.intern_symbol("batch");
1568        let s3 = g.intern_symbol("seq");
1569        assert_eq!(s1, s2);
1570        assert_ne!(s1, s3);
1571    }
1572}