Skip to main content

onnx_runtime_optimizer/
constant_folding.rs

1//! Constant folding: replace a node whose inputs are *all* constant
2//! (initializers) with a precomputed initializer (see `docs/ORT2.md` §18.1).
3//!
4//! ## Boundary (deliberately conservative)
5//!
6//! Fully general constant folding needs a kernel executor — the optimizer has
7//! none — so this pass folds only what the IR can compute *directly and
8//! exactly*, reusing the lessons of the loader's "const-fold-lite"
9//! (`crates/onnx-runtime-loader/src/shape_inference.rs`):
10//!
11//! * **`Constant`** nodes are materialized into initializers (always safe).
12//! * **`Shape`** on a fully-static input becomes an `int64` initializer.
13//! * **Elementwise integer `Add`/`Sub`/`Mul`** on two *same-shape* constant
14//!   `int32`/`int64` tensors are evaluated with **checked** arithmetic; any
15//!   overflow aborts the fold rather than emit a wrong constant.
16//!
17//! Everything else is left untouched. Folding is bounded to
18//! [`MAX_FOLD_ELEMS`] elements so large weight tensors are never materialized,
19//! and dispatch is purely on op type — no model-specific names. The invariant
20//! is: **never produce a wrong constant.** When in doubt, do not fold.
21
22use std::cmp::Reverse;
23use std::collections::{BinaryHeap, HashMap};
24
25use onnx_runtime_ir::{
26    Attribute, DataType, Graph, NodeId, TensorData, ValueId, WeightRef, as_static_shape,
27    is_fully_static, static_shape,
28};
29
30use crate::error::Result;
31use crate::pass::{OptimizationPass, PassContext};
32
33/// Upper bound on the number of elements this pass will materialize. Keeps
34/// folding limited to shape-computation-sized tensors, never model weights.
35const MAX_FOLD_ELEMS: usize = 1024;
36
37/// Folds constant-input nodes into initializers (bounded, integer/shape only).
38#[derive(Clone, Copy, Debug, Default)]
39pub struct ConstantFolding;
40
41impl OptimizationPass for ConstantFolding {
42    fn name(&self) -> &str {
43        "ConstantFolding"
44    }
45
46    fn run(&self, graph: &mut Graph, _ctx: &PassContext) -> Result<()> {
47        let candidates: Vec<NodeId> = graph
48            .nodes
49            .iter()
50            .filter_map(|(nid, node)| is_candidate(node).then_some(nid))
51            .collect();
52        let mut unresolved = HashMap::with_capacity(candidates.len());
53        let mut dependents: HashMap<ValueId, Vec<NodeId>> =
54            HashMap::with_capacity(candidates.len());
55        // Match ascending fixpoint passes: higher IDs made ready during a wave
56        // join it, while lower/equal IDs wait for the next wave.
57        let mut current_wave = BinaryHeap::new();
58        let mut next_wave = BinaryHeap::new();
59
60        for nid in candidates {
61            let node = graph.node(nid);
62            let Some(inputs) = unresolved_inputs(graph, node) else {
63                continue;
64            };
65            let count = inputs.len();
66            unresolved.insert(nid, count);
67            if count == 0 {
68                current_wave.push(Reverse(nid.0));
69            } else {
70                for input in inputs {
71                    dependents.entry(input).or_default().push(nid);
72                }
73            }
74        }
75
76        while !current_wave.is_empty() {
77            while let Some(Reverse(raw_nid)) = current_wave.pop() {
78                let nid = NodeId(raw_nid);
79                if unresolved.remove(&nid).is_none() || !graph.nodes.contains(nid) {
80                    continue;
81                }
82                let (out, folded) = {
83                    let node = graph.node(nid);
84                    let folded = match node.op_type.as_str() {
85                        "Constant" => eval_constant(node),
86                        "Shape" => fold_shape(graph, node),
87                        "Add" | "Sub" | "Mul" => fold_binary_int(graph, node),
88                        _ => None,
89                    };
90                    (node.outputs[0], folded)
91                };
92                let Some(tensor) = folded else { continue };
93
94                // Only fold outputs that are still needed (have a consumer or
95                // are graph outputs); dead outputs are DCE's job and folding
96                // them would leave a stale initializer referencing a GC'd id.
97                let needed = graph.outputs.contains(&out)
98                    || graph
99                        .try_value(out)
100                        .is_some_and(|_| graph.has_uses(out));
101                if !needed {
102                    continue;
103                }
104
105                graph.remove_node(nid);
106                // The output survives because it is needed; retype it to the
107                // folded tensor and back it with an inline initializer.
108                if graph.try_value(out).is_none() {
109                    continue;
110                }
111                let dims = tensor.dims.clone();
112                let dtype = tensor.dtype;
113                let v = graph.value_mut(out);
114                v.dtype = dtype;
115                v.shape = static_shape(dims);
116                graph.set_initializer(out, WeightRef::Inline(tensor));
117
118                for consumer in dependents.remove(&out).unwrap_or_default() {
119                    let Some(count) = unresolved.get_mut(&consumer) else {
120                        continue;
121                    };
122                    *count -= 1;
123                    if *count == 0 {
124                        let wave = if consumer.0 > nid.0 {
125                            &mut current_wave
126                        } else {
127                            &mut next_wave
128                        };
129                        wave.push(Reverse(consumer.0));
130                    }
131                }
132            }
133            std::mem::swap(&mut current_wave, &mut next_wave);
134        }
135        Ok(())
136    }
137}
138
139fn is_candidate(node: &onnx_runtime_ir::Node) -> bool {
140    matches!(node.domain.as_str(), "" | "ai.onnx")
141        && node.outputs.len() == 1
142        && matches!(
143            node.op_type.as_str(),
144            "Constant" | "Shape" | "Add" | "Sub" | "Mul"
145        )
146}
147
148fn unresolved_inputs(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<Vec<ValueId>> {
149    match node.op_type.as_str() {
150        "Constant" => Some(Vec::new()),
151        "Shape" => {
152            if node.attr("start").is_some() || node.attr("end").is_some() {
153                return None;
154            }
155            let input = node.inputs.first().copied().flatten()?;
156            let shape = &graph.try_value(input)?.shape;
157            if shape.len() <= MAX_FOLD_ELEMS && is_fully_static(shape) {
158                Some(Vec::new())
159            } else {
160                Some(vec![input])
161            }
162        }
163        "Add" | "Sub" | "Mul" => {
164            if node.inputs.len() != 2 {
165                return None;
166            }
167            let inputs = [node.inputs[0]?, node.inputs[1]?];
168            Some(
169                inputs
170                    .into_iter()
171                    .filter(|&input| inline_const(graph, input).is_none())
172                    .collect(),
173            )
174        }
175        _ => None,
176    }
177}
178
179/// The inline constant tensor backing `value`, if any (external weights, which
180/// are large, are never folded).
181fn inline_const(graph: &Graph, value: ValueId) -> Option<&TensorData> {
182    match graph.initializers.get(&value)? {
183        WeightRef::Inline(t) => Some(t),
184        WeightRef::External { .. } => None,
185    }
186}
187
188/// Materialize a `Constant` node's value into a concrete [`TensorData`].
189fn eval_constant(node: &onnx_runtime_ir::Node) -> Option<TensorData> {
190    if let Some(Attribute::Tensor(t)) = node.attr("value") {
191        return Some(t.clone());
192    }
193    if let Some(ints) = node.attr("value_ints").and_then(Attribute::as_ints) {
194        let mut data = Vec::with_capacity(ints.len() * 8);
195        for &i in ints {
196            data.extend_from_slice(&i.to_le_bytes());
197        }
198        return Some(TensorData::from_raw(DataType::Int64, vec![ints.len()], data));
199    }
200    if let Some(i) = node.attr("value_int").and_then(Attribute::as_int) {
201        return Some(TensorData::from_raw(
202            DataType::Int64,
203            Vec::new(),
204            i.to_le_bytes().to_vec(),
205        ));
206    }
207    None
208}
209
210/// Fold `Shape(x)` when `x` has a fully-static shape into an `int64` vector.
211///
212/// Conservative: bails if `start`/`end` attributes are present (a slice of the
213/// shape) so we never emit a partial result.
214fn fold_shape(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
215    if node.attr("start").is_some() || node.attr("end").is_some() {
216        return None;
217    }
218    let input = node.inputs.first().copied().flatten()?;
219    let shape = &graph.try_value(input)?.shape;
220    let dims = as_static_shape(shape)?;
221    if dims.len() > MAX_FOLD_ELEMS {
222        return None;
223    }
224    let mut data = Vec::with_capacity(dims.len() * 8);
225    for &d in &dims {
226        data.extend_from_slice(&(d as i64).to_le_bytes());
227    }
228    Some(TensorData::from_raw(DataType::Int64, vec![dims.len()], data))
229}
230
231/// Fold elementwise integer `Add`/`Sub`/`Mul` on two same-shape constant
232/// tensors. Uses checked arithmetic; overflow aborts (returns `None`).
233fn fold_binary_int(graph: &Graph, node: &onnx_runtime_ir::Node) -> Option<TensorData> {
234    if node.inputs.len() != 2 {
235        return None;
236    }
237    let a = inline_const(graph, node.inputs[0]?)?;
238    let b = inline_const(graph, node.inputs[1]?)?;
239    if a.dtype != b.dtype || a.dims != b.dims {
240        return None; // no broadcasting / mixed dtype in v1
241    }
242    if !matches!(a.dtype, DataType::Int32 | DataType::Int64) {
243        return None;
244    }
245    let numel = a.numel();
246    if numel > MAX_FOLD_ELEMS {
247        return None;
248    }
249    let op = node.op_type.as_str();
250    let apply = |x: i64, y: i64| -> Option<i64> {
251        match op {
252            "Add" => x.checked_add(y),
253            "Sub" => x.checked_sub(y),
254            "Mul" => x.checked_mul(y),
255            _ => None,
256        }
257    };
258
259    match a.dtype {
260        DataType::Int64 => {
261            let (xs, ys) = (read_i64(a)?, read_i64(b)?);
262            let mut data = Vec::with_capacity(numel * 8);
263            for (x, y) in xs.into_iter().zip(ys) {
264                data.extend_from_slice(&apply(x, y)?.to_le_bytes());
265            }
266            Some(TensorData::from_raw(DataType::Int64, a.dims.clone(), data))
267        }
268        DataType::Int32 => {
269            let (xs, ys) = (read_i32(a)?, read_i32(b)?);
270            let mut data = Vec::with_capacity(numel * 4);
271            for (x, y) in xs.into_iter().zip(ys) {
272                let r = apply(x as i64, y as i64)?;
273                let r32: i32 = r.try_into().ok()?; // must fit back into i32
274                data.extend_from_slice(&r32.to_le_bytes());
275            }
276            Some(TensorData::from_raw(DataType::Int32, a.dims.clone(), data))
277        }
278        _ => None,
279    }
280}
281
282fn read_i64(t: &TensorData) -> Option<Vec<i64>> {
283    if t.data.len() != t.numel() * 8 {
284        return None;
285    }
286    Some(
287        t.data
288            .chunks_exact(8)
289            .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
290            .collect(),
291    )
292}
293
294fn read_i32(t: &TensorData) -> Option<Vec<i32>> {
295    if t.data.len() != t.numel() * 4 {
296        return None;
297    }
298    Some(
299        t.data
300            .chunks_exact(4)
301            .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
302            .collect(),
303    )
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use onnx_runtime_ir::{Node, NodeId};
310    use onnx_runtime_loader::{Model, encode_model};
311
312    fn int64_tensor(dims: Vec<usize>, vals: &[i64]) -> TensorData {
313        let mut data = Vec::new();
314        for &v in vals {
315            data.extend_from_slice(&v.to_le_bytes());
316        }
317        TensorData::from_raw(DataType::Int64, dims, data)
318    }
319
320    fn const_init(graph: &mut Graph, name: &str, dims: Vec<usize>, vals: &[i64]) -> ValueId {
321        let shape = static_shape(dims.clone());
322        let v = graph.create_named_value(name, DataType::Int64, shape);
323        graph.set_initializer(v, WeightRef::Inline(int64_tensor(dims, vals)));
324        v
325    }
326
327    fn run_reference_ascending_fixpoint(graph: &mut Graph) {
328        loop {
329            let mut changed = false;
330            let node_ids: Vec<NodeId> = graph.nodes.keys().collect();
331            for nid in node_ids {
332                if !graph.nodes.contains(nid) {
333                    continue;
334                }
335                let node = graph.node(nid).clone();
336                if !matches!(node.domain.as_str(), "" | "ai.onnx") || node.outputs.len() != 1 {
337                    continue;
338                }
339                let out = node.outputs[0];
340                let folded = match node.op_type.as_str() {
341                    "Constant" => eval_constant(&node),
342                    "Shape" => fold_shape(graph, &node),
343                    "Add" | "Sub" | "Mul" => fold_binary_int(graph, &node),
344                    _ => None,
345                };
346                let Some(tensor) = folded else { continue };
347                let needed = graph.outputs.contains(&out)
348                    || graph
349                        .try_value(out)
350                        .is_some_and(|_| graph.has_uses(out));
351                if !needed {
352                    continue;
353                }
354
355                graph.remove_node(nid);
356                if graph.try_value(out).is_some() {
357                    let dims = tensor.dims.clone();
358                    let dtype = tensor.dtype;
359                    let v = graph.value_mut(out);
360                    v.dtype = dtype;
361                    v.shape = static_shape(dims);
362                    graph.set_initializer(out, WeightRef::Inline(tensor));
363                    changed = true;
364                }
365            }
366            if !changed {
367                break;
368            }
369        }
370    }
371
372    fn serialized(graph: &Graph) -> Vec<u8> {
373        encode_model(&Model::new(graph)).expect("serialize graph")
374    }
375
376    fn schedule_sensitive_chain() -> (Graph, ValueId) {
377        let mut g = Graph::new();
378        g.opset_imports.insert(String::new(), 17);
379        let init = const_init(&mut g, "init", vec![1], &[2]);
380        let a = g.create_named_value("a", DataType::Int64, static_shape([1]));
381        let b = g.create_named_value("b", DataType::Int64, static_shape([1]));
382        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
383
384        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
385        constant.attributes.insert(
386            "value".into(),
387            Attribute::Tensor(int64_tensor(vec![1], &[3])),
388        );
389        g.insert_node(constant);
390        g.insert_node(Node::new(
391            NodeId(0),
392            "Add",
393            vec![Some(a), Some(init)],
394            vec![b],
395        ));
396        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
397        g.add_output(out);
398        (g, out)
399    }
400
401    #[test]
402    fn ascending_wave_folds_constant_add_before_shape_consumer() {
403        let (base, out) = schedule_sensitive_chain();
404        let mut reference = base.clone();
405        let mut worklist = base;
406        run_reference_ascending_fixpoint(&mut reference);
407        ConstantFolding
408            .run(&mut worklist, &PassContext::new())
409            .unwrap();
410
411        assert_eq!(worklist.num_nodes(), 0, "Constant, Add, and Shape fold");
412        assert!(!worklist.nodes.values().any(|node| node.op_type == "Add"));
413        assert_eq!(
414            read_i64(inline_const(&worklist, out).unwrap()),
415            Some(vec![1])
416        );
417        assert_eq!(serialized(&worklist), serialized(&reference));
418        assert!(worklist.validate().is_ok());
419    }
420
421    #[test]
422    fn ascending_wave_leaves_lower_dead_producer_unfolded() {
423        let mut base = Graph::new();
424        base.opset_imports.insert(String::new(), 17);
425        let init = const_init(&mut base, "init", vec![1], &[2]);
426        let a = base.create_named_value("a", DataType::Int64, static_shape([1]));
427        let b = base.create_named_value("b", DataType::Int64, static_shape([1]));
428        let out = base.create_named_value("out", DataType::Int64, static_shape([1]));
429
430        base.insert_node(Node::new(
431            NodeId(0),
432            "Add",
433            vec![Some(a), Some(init)],
434            vec![b],
435        ));
436        base.insert_node(Node::new(NodeId(0), "Shape", vec![Some(b)], vec![out]));
437        let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![a]);
438        constant.attributes.insert(
439            "value".into(),
440            Attribute::Tensor(int64_tensor(vec![1], &[3])),
441        );
442        base.insert_node(constant);
443        base.add_output(out);
444
445        let mut reference = base.clone();
446        let mut worklist = base;
447        run_reference_ascending_fixpoint(&mut reference);
448        ConstantFolding
449            .run(&mut worklist, &PassContext::new())
450            .unwrap();
451
452        assert_eq!(worklist.num_nodes(), 1);
453        assert_eq!(worklist.nodes.values().next().unwrap().op_type, "Add");
454        assert!(inline_const(&worklist, b).is_none());
455        assert_eq!(serialized(&worklist), serialized(&reference));
456        assert!(worklist.validate().is_ok());
457    }
458
459    fn seeded_dag(mut seed: u64, nodes: usize) -> Graph {
460        fn next(seed: &mut u64) -> u64 {
461            *seed = seed
462                .wrapping_mul(6_364_136_223_846_793_005)
463                .wrapping_add(1_442_695_040_888_963_407);
464            *seed
465        }
466
467        let mut g = Graph::new();
468        g.opset_imports.insert(String::new(), 17);
469        let init = const_init(&mut g, "init", vec![1], &[1]);
470        let values: Vec<ValueId> = (0..nodes)
471            .map(|i| g.create_named_value(format!("v{i}"), DataType::Int64, static_shape([1])))
472            .collect();
473        let mut definitions = Vec::with_capacity(nodes);
474        for i in 0..nodes {
475            let mut node = if i == 0 || next(&mut seed).is_multiple_of(4) {
476                let mut constant = Node::new(NodeId(0), "Constant", vec![], vec![values[i]]);
477                constant.attributes.insert(
478                    "value".into(),
479                    Attribute::Tensor(int64_tensor(vec![1], &[(next(&mut seed) % 8) as i64])),
480                );
481                constant
482            } else if next(&mut seed).is_multiple_of(3) {
483                let input = values[(next(&mut seed) as usize) % i];
484                Node::new(NodeId(0), "Shape", vec![Some(input)], vec![values[i]])
485            } else {
486                let pick_input = |seed: &mut u64| {
487                    if next(seed).is_multiple_of(4) {
488                        init
489                    } else {
490                        values[(next(seed) as usize) % i]
491                    }
492                };
493                Node::new(
494                    NodeId(0),
495                    "Add",
496                    vec![Some(pick_input(&mut seed)), Some(pick_input(&mut seed))],
497                    vec![values[i]],
498                )
499            };
500            node.name = format!("node_{i}");
501            definitions.push(node);
502        }
503
504        let mut order: Vec<usize> = (0..nodes).collect();
505        for i in (1..nodes).rev() {
506            order.swap(i, (next(&mut seed) as usize) % (i + 1));
507        }
508        for index in order {
509            g.insert_node(definitions[index].clone());
510        }
511        for (i, &value) in values.iter().enumerate() {
512            if i + 1 == nodes || (i > nodes / 2 && next(&mut seed).is_multiple_of(11)) {
513                g.add_output(value);
514            }
515        }
516        g
517    }
518
519    #[test]
520    fn seeded_dags_are_byte_identical_to_ascending_fixpoint() {
521        for seed in 0..32 {
522            let base = seeded_dag(seed, 96);
523            assert!(base.validate().is_ok(), "seed {seed}");
524            let mut reference = base.clone();
525            let mut worklist = base;
526            run_reference_ascending_fixpoint(&mut reference);
527            ConstantFolding
528                .run(&mut worklist, &PassContext::new())
529                .unwrap();
530            assert_eq!(serialized(&worklist), serialized(&reference), "seed {seed}");
531        }
532    }
533
534    #[test]
535    fn folds_add_of_two_const_inputs() {
536        let mut g = Graph::new();
537        g.opset_imports.insert(String::new(), 17);
538        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
539        let b = const_init(&mut g, "b", vec![3], &[10, 20, 30]);
540        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
541        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
542        g.add_output(out);
543
544        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
545
546        assert_eq!(g.num_nodes(), 0, "Add should be folded away");
547        let t = inline_const(&g, out).expect("out is now an initializer");
548        assert_eq!(read_i64(t).unwrap(), vec![11, 22, 33]);
549        assert!(g.validate().is_ok());
550    }
551
552    #[test]
553    fn folds_sub_and_mul() {
554        for (op, expect) in [("Sub", vec![9, 18, 27]), ("Mul", vec![10, 40, 90])] {
555            let mut g = Graph::new();
556            g.opset_imports.insert(String::new(), 17);
557            let a = const_init(&mut g, "a", vec![3], &[10, 20, 30]);
558            let b = const_init(&mut g, "b", vec![3], &[1, 2, 3]);
559            let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
560            g.insert_node(Node::new(NodeId(0), op, vec![Some(a), Some(b)], vec![out]));
561            g.add_output(out);
562
563            ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
564            let t = inline_const(&g, out).unwrap();
565            assert_eq!(read_i64(t).unwrap(), expect, "op {op}");
566        }
567    }
568
569    #[test]
570    fn does_not_fold_when_one_input_is_non_const() {
571        let mut g = Graph::new();
572        g.opset_imports.insert(String::new(), 17);
573        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
574        // `b` is a graph input, not a constant.
575        let b = g.create_named_value("b", DataType::Int64, static_shape([3]));
576        g.add_input(b);
577        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
578        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
579        g.add_output(out);
580
581        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
582        assert_eq!(g.num_nodes(), 1, "must not fold with a non-const input");
583        assert!(inline_const(&g, out).is_none());
584        assert!(g.validate().is_ok());
585    }
586
587    #[test]
588    fn does_not_fold_mismatched_shapes() {
589        let mut g = Graph::new();
590        g.opset_imports.insert(String::new(), 17);
591        let a = const_init(&mut g, "a", vec![3], &[1, 2, 3]);
592        let b = const_init(&mut g, "b", vec![2], &[10, 20]);
593        let out = g.create_named_value("out", DataType::Int64, static_shape([3]));
594        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
595        g.add_output(out);
596
597        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
598        assert_eq!(g.num_nodes(), 1, "no broadcasting in v1");
599    }
600
601    #[test]
602    fn does_not_fold_overflow() {
603        let mut g = Graph::new();
604        g.opset_imports.insert(String::new(), 17);
605        let a = const_init(&mut g, "a", vec![1], &[i64::MAX]);
606        let b = const_init(&mut g, "b", vec![1], &[1]);
607        let out = g.create_named_value("out", DataType::Int64, static_shape([1]));
608        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(a), Some(b)], vec![out]));
609        g.add_output(out);
610
611        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
612        assert_eq!(g.num_nodes(), 1, "overflow must abort the fold");
613    }
614
615    #[test]
616    fn folds_constant_node_to_initializer() {
617        let mut g = Graph::new();
618        g.opset_imports.insert(String::new(), 17);
619        let out = g.create_named_value("c", DataType::Int64, static_shape([2]));
620        let mut node = Node::new(NodeId(0), "Constant", vec![], vec![out]);
621        node.attributes
622            .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[7, 8])));
623        g.insert_node(node);
624        // Keep `out` alive with a consumer.
625        let sink = g.create_named_value("sink", DataType::Int64, static_shape([2]));
626        g.insert_node(Node::new(NodeId(0), "Identity", vec![Some(out)], vec![sink]));
627        g.add_output(sink);
628
629        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
630        assert!(g.try_node(NodeId(0)).is_none(), "Constant folded away");
631        let t = inline_const(&g, out).unwrap();
632        assert_eq!(read_i64(t).unwrap(), vec![7, 8]);
633        assert!(g.validate().is_ok());
634    }
635
636    #[test]
637    fn folds_shape_of_static_input() {
638        let mut g = Graph::new();
639        g.opset_imports.insert(String::new(), 17);
640        let x = g.create_named_value("x", DataType::Float32, static_shape([2, 3, 4]));
641        g.add_input(x);
642        let out = g.create_named_value("s", DataType::Int64, static_shape([3]));
643        g.insert_node(Node::new(NodeId(0), "Shape", vec![Some(x)], vec![out]));
644        g.add_output(out);
645
646        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
647        let t = inline_const(&g, out).expect("Shape folded to initializer");
648        assert_eq!(read_i64(t).unwrap(), vec![2, 3, 4]);
649        assert!(g.validate().is_ok());
650    }
651
652    #[test]
653    fn folds_transitively_to_fixpoint() {
654        // Constant c1, Constant c2, then Add(c1, c2) -> out. All should fold.
655        let mut g = Graph::new();
656        g.opset_imports.insert(String::new(), 17);
657
658        let c1 = g.create_named_value("c1", DataType::Int64, static_shape([2]));
659        let mut n1 = Node::new(NodeId(0), "Constant", vec![], vec![c1]);
660        n1.attributes
661            .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[1, 2])));
662        g.insert_node(n1);
663
664        let c2 = g.create_named_value("c2", DataType::Int64, static_shape([2]));
665        let mut n2 = Node::new(NodeId(0), "Constant", vec![], vec![c2]);
666        n2.attributes
667            .insert("value".into(), Attribute::Tensor(int64_tensor(vec![2], &[3, 4])));
668        g.insert_node(n2);
669
670        let out = g.create_named_value("out", DataType::Int64, static_shape([2]));
671        g.insert_node(Node::new(NodeId(0), "Add", vec![Some(c1), Some(c2)], vec![out]));
672        g.add_output(out);
673
674        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
675        assert_eq!(g.num_nodes(), 0, "both constants and the Add fold away");
676        let t = inline_const(&g, out).unwrap();
677        assert_eq!(read_i64(t).unwrap(), vec![4, 6]);
678        assert!(g.validate().is_ok());
679    }
680
681    fn constant_chain(nodes: usize, reverse_node_ids: bool) -> (Graph, ValueId) {
682        let mut g = Graph::new();
683        g.opset_imports.insert(String::new(), 17);
684        let zero = const_init(&mut g, "zero", vec![1], &[0]);
685        let one = const_init(&mut g, "one", vec![1], &[1]);
686        let mut values = Vec::with_capacity(nodes + 1);
687        values.push(zero);
688        for _ in 0..nodes {
689            values.push(g.create_value(DataType::Int64, static_shape([1])));
690        }
691
692        if reverse_node_ids {
693            for i in (1..=nodes).rev() {
694                g.insert_node(Node::new(
695                    NodeId(0),
696                    "Add",
697                    vec![Some(values[i - 1]), Some(one)],
698                    vec![values[i]],
699                ));
700            }
701        } else {
702            for i in 1..=nodes {
703                g.insert_node(Node::new(
704                    NodeId(0),
705                    "Add",
706                    vec![Some(values[i - 1]), Some(one)],
707                    vec![values[i]],
708                ));
709            }
710        }
711
712        let out = values[nodes];
713        g.add_output(out);
714        (g, out)
715    }
716
717    #[test]
718    fn reverse_node_id_chain_matches_forward_order() {
719        let (mut forward, forward_out) = constant_chain(64, false);
720        let (mut reverse, reverse_out) = constant_chain(64, true);
721
722        let reverse_ids = reverse.topological_order().unwrap();
723        assert!(
724            reverse_ids.windows(2).all(|ids| ids[0].0 > ids[1].0),
725            "test graph must have reverse dependency NodeIds"
726        );
727
728        ConstantFolding
729            .run(&mut forward, &PassContext::new())
730            .unwrap();
731        ConstantFolding
732            .run(&mut reverse, &PassContext::new())
733            .unwrap();
734
735        assert_eq!(forward.num_nodes(), 0);
736        assert_eq!(reverse.num_nodes(), 0);
737        assert_eq!(
738            inline_const(&forward, forward_out),
739            inline_const(&reverse, reverse_out)
740        );
741        assert_eq!(
742            read_i64(inline_const(&reverse, reverse_out).unwrap()),
743            Some(vec![64])
744        );
745        assert!(forward.validate().is_ok());
746        assert!(reverse.validate().is_ok());
747    }
748
749    #[test]
750    fn does_not_fold_float_binary() {
751        let mut g = Graph::new();
752        g.opset_imports.insert(String::new(), 17);
753        let mk = |g: &mut Graph, name: &str| {
754            let v = g.create_named_value(name, DataType::Float32, static_shape([2]));
755            g.set_initializer(
756                v,
757                WeightRef::Inline(TensorData::from_raw(
758                    DataType::Float32,
759                    vec![2],
760                    vec![0u8; 8],
761                )),
762            );
763            v
764        };
765        let a = mk(&mut g, "a");
766        let b = mk(&mut g, "b");
767        let out = g.create_named_value("out", DataType::Float32, static_shape([2]));
768        g.insert_node(Node::new(NodeId(0), "Mul", vec![Some(a), Some(b)], vec![out]));
769        g.add_output(out);
770
771        ConstantFolding.run(&mut g, &PassContext::new()).unwrap();
772        assert_eq!(g.num_nodes(), 1, "float folding is out of scope in v1");
773    }
774}