Skip to main content

oxfuse_core/
dump.rs

1//! Deterministic text form of the IR, used by `inspect`, `--dump-ir` and the
2//! snapshot tests. Same graph, same text, on every platform.
3
4use crate::{DType, Graph, Op, Source, TensorType};
5use std::fmt::{self, Display, Formatter};
6
7/// Constants longer than this print as a count, not their data.
8const MAX_SHOWN: usize = 8;
9
10impl Display for DType {
11    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
12        match self {
13            DType::F32 => f.write_str("f32"),
14        }
15    }
16}
17
18impl Display for TensorType {
19    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
20        let dims: Vec<String> = self.shape.iter().map(usize::to_string).collect();
21        write!(f, "{}[{}]", self.dtype, dims.join(", "))
22    }
23}
24
25/// Plain names print as they are, so ordinary graphs read naturally. Anything
26/// that could be mistaken for dump syntax (empty, spaces, control characters,
27/// a leading `%`) is quoted and escaped, so a name can never add a line or
28/// pass for a value id.
29fn shown(name: &str) -> String {
30    let plain = !name.is_empty()
31        && !name.starts_with('%')
32        && name
33            .chars()
34            .all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | ':'));
35    if plain {
36        name.to_owned()
37    } else {
38        format!("{name:?}")
39    }
40}
41
42fn op_with_attributes(op: &Op) -> String {
43    match op {
44        Op::Reshape { shape } => format!("Reshape[shape={shape:?}]"),
45        Op::Transpose { perm } => format!("Transpose[perm={perm:?}]"),
46        other => other.name().to_owned(),
47    }
48}
49
50impl Display for Graph {
51    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
52        writeln!(f, "graph {{")?;
53        for (index, value) in self.values.iter().enumerate() {
54            let name = match &value.name {
55                Some(name) => shown(name),
56                None => self.label(crate::ValueId(index as u32)),
57            };
58            let ty = &value.ty;
59            match &value.source {
60                Source::Input => writeln!(f, "  %{index} {name} = input : {ty}")?,
61                Source::Constant(data) if data.len() <= MAX_SHOWN => {
62                    writeln!(f, "  %{index} {name} = const : {ty} = {data:?}")?
63                }
64                Source::Constant(data) => writeln!(
65                    f,
66                    "  %{index} {name} = const : {ty} = [{} values]",
67                    data.len()
68                )?,
69                Source::Node { op, inputs } => {
70                    let args: Vec<String> = inputs.iter().map(|id| format!("%{}", id.0)).collect();
71                    writeln!(
72                        f,
73                        "  %{index} {name} = {}({}) : {ty}",
74                        op_with_attributes(op),
75                        args.join(", ")
76                    )?
77                }
78            }
79        }
80        for (name, id) in self.output_names.iter().zip(&self.outputs) {
81            writeln!(f, "  output {} = %{}", shown(name), id.0)?;
82        }
83        write!(f, "}}")
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use crate::{GraphBuilder, Limits, Op};
90
91    fn builder() -> GraphBuilder {
92        GraphBuilder::new(Limits::default())
93    }
94
95    #[test]
96    fn dump_shows_every_value_and_output() {
97        let mut b = builder();
98        let x = b.input("x", vec![2, 3]).unwrap();
99        let w = b
100            .constant("w", vec![3, 2], vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0])
101            .unwrap();
102        let p = b.node("projection", Op::MatMul, &[x, w]).unwrap();
103        let r = b.node("r", Op::Reshape { shape: vec![4] }, &[p]).unwrap();
104        let g = b.finish(&[r]).unwrap();
105        assert_eq!(
106            g.to_string(),
107            "graph {\n\
108             \x20 %0 x = input : f32[2, 3]\n\
109             \x20 %1 w = const : f32[3, 2] = [1.0, 0.0, 0.0, 1.0, 1.0, 1.0]\n\
110             \x20 %2 projection = MatMul(%0, %1) : f32[2, 2]\n\
111             \x20 %3 r = Reshape[shape=[4]](%2) : f32[4]\n\
112             \x20 output r = %3\n\
113             }"
114        );
115    }
116
117    #[test]
118    fn dump_prints_scalars_and_transpose_attributes() {
119        let mut b = builder();
120        let s = b.constant("s", vec![], vec![-0.0]).unwrap();
121        let m = b.input("m", vec![2, 3]).unwrap();
122        let t = b
123            .node("t", Op::Transpose { perm: vec![1, 0] }, &[m])
124            .unwrap();
125        let g = b.finish(&[s, t]).unwrap();
126        let text = g.to_string();
127        assert!(text.contains("%0 s = const : f32[] = [-0.0]"), "{text}");
128        assert!(
129            text.contains("%2 t = Transpose[perm=[1, 0]](%1) : f32[3, 2]"),
130            "{text}"
131        );
132    }
133
134    #[test]
135    fn long_constants_are_summarized() {
136        let mut b = builder();
137        let c = b.constant("big", vec![9], vec![0.5; 9]).unwrap();
138        let g = b.finish(&[c]).unwrap();
139        assert!(
140            g.to_string()
141                .contains("%0 big = const : f32[9] = [9 values]")
142        );
143    }
144
145    #[test]
146    fn output_names_are_printed_not_value_names() {
147        let mut b = builder();
148        let x = b.input("x", vec![1]).unwrap();
149        let g = b
150            .finish_named(vec![("first".to_owned(), x), ("second".to_owned(), x)])
151            .unwrap();
152        let text = g.to_string();
153        assert!(text.contains("output first = %0\n"), "{text}");
154        assert!(text.contains("output second = %0\n"), "{text}");
155    }
156
157    #[test]
158    fn dump_is_deterministic() {
159        let mut b = builder();
160        let x = b.input("x", vec![2]).unwrap();
161        let y = b.node("y", Op::Relu, &[x]).unwrap();
162        let g = b.finish(&[y]).unwrap();
163        assert_eq!(g.to_string(), g.clone().to_string());
164    }
165
166    #[test]
167    fn unusual_names_are_quoted_so_the_dump_stays_unambiguous() {
168        let mut b = builder();
169        b.input("", vec![1]).unwrap();
170        b.input("a\nb", vec![1]).unwrap();
171        b.input("%0", vec![1]).unwrap();
172        let plain = b.input("plain_name-1.x:y", vec![1]).unwrap();
173        let g = b.finish_named(vec![("out put".to_owned(), plain)]).unwrap();
174        let text = g.to_string();
175        assert!(text.contains("%0 \"\" = input"), "{text}");
176        assert!(text.contains("%1 \"a\\nb\" = input"), "{text}");
177        assert!(text.contains("%2 \"%0\" = input"), "{text}");
178        assert!(text.contains("%3 plain_name-1.x:y = input"), "{text}");
179        assert!(text.contains("output \"out put\" = %3"), "{text}");
180        assert_eq!(
181            text.lines().count(),
182            7,
183            "a name must never add a line: {text}"
184        );
185    }
186}