1use crate::shape::{check_shape, checked_num_elements, infer};
5use crate::{Error, Graph, Limits, Op, Source, TensorType, Value, ValueId};
6use std::collections::HashMap;
7use std::sync::Arc;
8
9pub struct GraphBuilder {
10 limits: Limits,
11 values: Vec<Value>,
12 inputs: Vec<ValueId>,
13 names: HashMap<String, ValueId>,
14 total_elements: usize,
15}
16
17impl GraphBuilder {
18 pub fn new(limits: Limits) -> Self {
19 Self {
20 limits,
21 values: Vec::new(),
22 inputs: Vec::new(),
23 names: HashMap::new(),
24 total_elements: 0,
25 }
26 }
27
28 pub fn lookup(&self, name: &str) -> Option<ValueId> {
29 self.names.get(name).copied()
30 }
31
32 pub fn input(&mut self, name: &str, shape: Vec<usize>) -> Result<ValueId, Error> {
33 check_shape(name, &shape, &self.limits)?;
34 let id = self.push(name, TensorType::f32(shape), Source::Input)?;
35 self.inputs.push(id);
36 Ok(id)
37 }
38
39 pub fn constant(
40 &mut self,
41 name: &str,
42 shape: Vec<usize>,
43 data: Vec<f32>,
44 ) -> Result<ValueId, Error> {
45 self.constant_shared(name, shape, data.into())
46 }
47
48 pub fn constant_shared(
51 &mut self,
52 name: &str,
53 shape: Vec<usize>,
54 data: Arc<[f32]>,
55 ) -> Result<ValueId, Error> {
56 let expected = check_shape(name, &shape, &self.limits)?;
57 if data.len() != expected {
58 return Err(Error::DataLength {
59 name: name.to_owned(),
60 shape,
61 expected,
62 found: data.len(),
63 });
64 }
65 self.push(name, TensorType::f32(shape), Source::Constant(data))
66 }
67
68 pub fn node(&mut self, name: &str, op: Op, inputs: &[ValueId]) -> Result<ValueId, Error> {
69 let mut types = Vec::with_capacity(inputs.len());
70 for id in inputs {
71 let value = self.values.get(id.index()).ok_or_else(|| {
72 Error::Verify(format!("node `{name}` uses undefined value %{}", id.0))
73 })?;
74 types.push(&value.ty);
75 }
76 let ty = infer(name, &op, &types)?;
77 check_shape(name, &ty.shape, &self.limits)?;
78 self.push(
79 name,
80 ty,
81 Source::Node {
82 op,
83 inputs: inputs.to_vec(),
84 },
85 )
86 }
87
88 pub fn finish(self, outputs: &[ValueId]) -> Result<Graph, Error> {
89 let names = outputs
90 .iter()
91 .map(|id| match self.values.get(id.index()) {
92 Some(Value {
93 name: Some(name), ..
94 }) => (name.clone(), *id),
95 _ => (format!("%{}", id.0), *id),
96 })
97 .collect();
98 self.finish_named(names)
99 }
100
101 pub fn finish_named(self, outputs: Vec<(String, ValueId)>) -> Result<Graph, Error> {
102 let (output_names, outputs): (Vec<String>, Vec<ValueId>) = outputs.into_iter().unzip();
103 let graph = Graph {
104 values: self.values,
105 inputs: self.inputs,
106 outputs,
107 output_names,
108 };
109 graph.verify()?;
110 Ok(graph)
111 }
112
113 fn push(&mut self, name: &str, ty: TensorType, source: Source) -> Result<ValueId, Error> {
114 let cap = self.limits.max_values.min(u32::MAX as usize);
116 if self.values.len() >= cap {
117 return Err(Error::LimitExceeded {
118 what: "values",
119 found: self.values.len() + 1,
120 limit: cap,
121 });
122 }
123 let total = self
124 .total_elements
125 .saturating_add(checked_num_elements(&ty.shape).unwrap_or(usize::MAX));
126 if total > self.limits.max_total_elements {
127 return Err(Error::TensorLimit {
128 name: name.to_owned(),
129 what: "total tensor elements",
130 found: total,
131 limit: self.limits.max_total_elements,
132 });
133 }
134 if self.names.contains_key(name) {
135 return Err(Error::DuplicateName {
136 name: name.to_owned(),
137 });
138 }
139 let id =
140 ValueId(u32::try_from(self.values.len()).expect("the value cap keeps ids within u32"));
141 self.total_elements = total;
142 self.names.insert(name.to_owned(), id);
143 self.values.push(Value {
144 ty,
145 source,
146 name: Some(name.to_owned()),
147 });
148 Ok(id)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn builder() -> GraphBuilder {
157 GraphBuilder::new(Limits::default())
158 }
159
160 #[test]
161 fn builds_input_relu_graph() {
162 let mut b = builder();
163 let x = b.input("x", vec![2, 3]).unwrap();
164 let y = b.node("y", Op::Relu, &[x]).unwrap();
165 let g = b.finish(&[y]).unwrap();
166 assert_eq!(g.values().len(), 2);
167 assert_eq!(g.inputs(), &[x]);
168 assert_eq!(g.outputs(), &[y]);
169 assert_eq!(g.value(y).ty, TensorType::f32(vec![2, 3]));
170 assert_eq!(g.label(y), "y");
171 }
172
173 #[test]
174 fn lookup_finds_defined_names_only() {
175 let mut b = builder();
176 let x = b.input("x", vec![1]).unwrap();
177 assert_eq!(b.lookup("x"), Some(x));
178 assert_eq!(b.lookup("nope"), None);
179 }
180
181 #[test]
182 fn scalar_input_is_allowed() {
183 assert!(builder().input("s", vec![]).is_ok());
184 }
185
186 #[test]
187 fn constant_length_mismatch_is_rejected() {
188 let err = builder()
189 .constant("c", vec![2, 3], vec![0.0; 5])
190 .unwrap_err();
191 assert_eq!(
192 err,
193 Error::DataLength {
194 name: "c".into(),
195 shape: vec![2, 3],
196 expected: 6,
197 found: 5
198 }
199 );
200 }
201
202 #[test]
203 fn duplicate_name_across_kinds_is_rejected() {
204 let mut b = builder();
205 b.input("x", vec![1]).unwrap();
206 let err = b.constant("x", vec![1], vec![0.0]).unwrap_err();
207 assert_eq!(err, Error::DuplicateName { name: "x".into() });
208 }
209
210 #[test]
211 fn wrong_arity_is_rejected() {
212 let mut b = builder();
213 let x = b.input("x", vec![1]).unwrap();
214 let err = b.node("y", Op::Relu, &[x, x]).unwrap_err();
215 assert!(matches!(
216 err,
217 Error::Arity {
218 expected: 1,
219 found: 2,
220 ..
221 }
222 ));
223 }
224
225 #[test]
226 fn undefined_input_value_is_rejected_not_a_panic() {
227 let err = builder().node("y", Op::Relu, &[ValueId(9)]).unwrap_err();
228 assert!(matches!(err, Error::Verify(_)));
229 }
230
231 #[test]
232 fn graph_without_outputs_is_rejected() {
233 let mut b = builder();
234 b.input("x", vec![1]).unwrap();
235 assert_eq!(b.finish(&[]).unwrap_err(), Error::NoOutputs);
236 }
237
238 #[test]
239 fn value_count_limit_is_enforced() {
240 let limits = Limits {
241 max_values: 2,
242 ..Limits::default()
243 };
244 let mut b = GraphBuilder::new(limits);
245 b.input("a", vec![1]).unwrap();
246 b.input("b", vec![1]).unwrap();
247 let err = b.input("c", vec![1]).unwrap_err();
248 assert!(matches!(err, Error::LimitExceeded { what: "values", .. }));
249 }
250
251 #[test]
252 fn huge_shape_is_rejected_before_allocation() {
253 let mut b = builder();
254 let overflow = b.input("a", vec![usize::MAX, 2]).unwrap_err();
255 assert!(matches!(overflow, Error::InvalidShape { .. }));
256 let too_big = b.input("b", vec![1 << 20, 1 << 20]).unwrap_err();
257 assert!(matches!(
258 too_big,
259 Error::TensorLimit {
260 what: "tensor elements",
261 ..
262 }
263 ));
264 let too_deep = b.input("c", vec![1; 9]).unwrap_err();
265 assert!(matches!(
266 too_deep,
267 Error::TensorLimit {
268 what: "tensor rank",
269 ..
270 }
271 ));
272 }
273
274 #[test]
275 fn zero_dimension_is_rejected() {
276 let err = builder().input("x", vec![3, 0]).unwrap_err();
277 assert!(matches!(err, Error::InvalidShape { .. }));
278 }
279
280 #[test]
281 fn scalar_operand_on_either_side_keeps_the_tensor_shape() {
282 let mut b = builder();
283 let t = b.input("t", vec![2, 3]).unwrap();
284 let s = b.input("s", vec![]).unwrap();
285 let left = b.node("l", Op::Add, &[s, t]).unwrap();
286 let right = b.node("r", Op::Mul, &[t, s]).unwrap();
287 let g = b.finish(&[left, right]).unwrap();
288 assert_eq!(g.value(left).ty, TensorType::f32(vec![2, 3]));
289 assert_eq!(g.value(right).ty, TensorType::f32(vec![2, 3]));
290 }
291
292 #[test]
293 fn mismatched_elementwise_shapes_are_rejected() {
294 let mut b = builder();
295 let x = b.input("x", vec![2, 3]).unwrap();
296 let z = b.input("z", vec![3, 2]).unwrap();
297 let err = b.node("y", Op::Add, &[x, z]).unwrap_err();
298 assert!(matches!(err, Error::ShapeMismatch { .. }));
299 }
300
301 #[test]
302 fn matmul_output_over_the_element_limit_is_rejected() {
303 let mut b = builder();
304 let a = b.input("a", vec![1 << 15, 1]).unwrap();
305 let c = b.input("c", vec![1, 1 << 15]).unwrap();
306 let err = b.node("y", Op::MatMul, &[a, c]).unwrap_err();
307 assert!(matches!(
308 err,
309 Error::TensorLimit {
310 what: "tensor elements",
311 ..
312 }
313 ));
314 }
315
316 #[test]
317 fn reshape_beyond_the_rank_limit_is_rejected() {
318 let mut b = builder();
319 let x = b.input("x", vec![512]).unwrap();
320 let op = Op::Reshape { shape: vec![2; 9] };
321 let err = b.node("y", op, &[x]).unwrap_err();
322 assert!(matches!(
323 err,
324 Error::TensorLimit {
325 what: "tensor rank",
326 ..
327 }
328 ));
329 }
330
331 #[test]
332 fn total_allocation_across_values_is_bounded() {
333 let mut b = builder();
334 let a = b.input("a", vec![1 << 14, 1]).unwrap();
335 let c = b.input("c", vec![1, 1 << 14]).unwrap();
336 b.node("y1", Op::MatMul, &[a, c]).unwrap();
337 let err = b.node("y2", Op::MatMul, &[a, c]).unwrap_err();
338 assert_eq!(
339 err,
340 Error::TensorLimit {
341 name: "y2".into(),
342 what: "total tensor elements",
343 found: 2 * (1 << 14) + 2 * (1 << 28),
344 limit: 1 << 29,
345 }
346 );
347 }
348
349 #[test]
350 fn many_small_values_still_fit_the_total_budget() {
351 let mut b = builder();
352 let mut last = b.input("x0", vec![64]).unwrap();
353 for i in 1..200 {
354 last = b.node(&format!("n{i}"), Op::Relu, &[last]).unwrap();
355 }
356 assert!(b.finish(&[last]).is_ok());
357 }
358
359 #[test]
360 fn finish_names_outputs_after_their_values() {
361 let mut b = builder();
362 let x = b.input("x", vec![1]).unwrap();
363 let y = b.node("y", Op::Relu, &[x]).unwrap();
364 let g = b.finish(&[y, x]).unwrap();
365 assert_eq!(g.output_names(), ["y", "x"]);
366 }
367
368 #[test]
369 fn finish_named_keeps_the_given_names_even_for_duplicates() {
370 let mut b = builder();
371 let x = b.input("x", vec![1]).unwrap();
372 let y = b.node("y", Op::Relu, &[x]).unwrap();
373 let g = b
374 .finish_named(vec![("first".to_owned(), y), ("second".to_owned(), y)])
375 .unwrap();
376 assert_eq!(g.output_names(), ["first", "second"]);
377 assert_eq!(g.outputs(), &[y, y]);
378 }
379
380 #[test]
381 fn constant_shared_does_not_copy_the_data() {
382 let mut b = builder();
383 let data: std::sync::Arc<[f32]> = std::sync::Arc::from(vec![1.0, 2.0]);
384 let c = b
385 .constant_shared("c", vec![2], std::sync::Arc::clone(&data))
386 .unwrap();
387 let g = b.finish(&[c]).unwrap();
388 match &g.value(c).source {
389 Source::Constant(stored) => assert!(std::sync::Arc::ptr_eq(stored, &data)),
390 other => panic!("expected a constant, got {other:?}"),
391 }
392 }
393
394 #[test]
395 fn value_limit_error_reports_the_exact_numbers() {
396 let limits = Limits {
397 max_values: 2,
398 ..Limits::default()
399 };
400 let mut b = GraphBuilder::new(limits);
401 b.input("a", vec![1]).unwrap();
402 b.input("b", vec![1]).unwrap();
403 assert_eq!(
404 b.input("c", vec![1]).unwrap_err(),
405 Error::LimitExceeded {
406 what: "values",
407 found: 3,
408 limit: 2
409 }
410 );
411 }
412
413 #[test]
414 fn total_element_budget_accepts_exactly_the_limit_and_rejects_one_more() {
415 let limits = Limits {
416 max_total_elements: 10,
417 ..Limits::default()
418 };
419 let mut b = GraphBuilder::new(limits);
420 b.input("a", vec![4]).unwrap();
421 b.input("b", vec![6]).unwrap();
422 assert_eq!(
423 b.input("c", vec![1]).unwrap_err(),
424 Error::TensorLimit {
425 name: "c".into(),
426 what: "total tensor elements",
427 found: 11,
428 limit: 10
429 }
430 );
431 }
432}