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