1use crate::{DType, Graph, Op, Source, TensorType};
5use std::fmt::{self, Display, Formatter};
6
7const 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
25fn 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}