1use std::collections::HashMap;
9
10use crate::error::GraphError;
11use crate::graph::{Dimension, OperandDescriptor};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TensorKind {
16 Input,
18 Output,
20}
21
22impl TensorKind {
23 fn as_str(self) -> &'static str {
24 match self {
25 TensorKind::Input => "input",
26 TensorKind::Output => "output",
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
32struct BoundDynamicDim {
33 value: usize,
34}
35
36#[derive(Debug, Default, Clone)]
38pub struct RuntimeShapeState {
39 bound_dims: HashMap<String, BoundDynamicDim>,
40}
41
42impl RuntimeShapeState {
43 pub fn new() -> Self {
45 Self::default()
46 }
47
48 #[tracing::instrument(
51 name = "validate_shapes",
52 skip_all,
53 err(level = "debug"),
54 level = "debug",
55 fields(kind = ?kind, tensors = actual_shapes.len())
56 )]
57 pub fn validate_named_shapes(
58 &mut self,
59 actual_shapes: &HashMap<String, Vec<usize>>,
60 descriptors: &HashMap<String, OperandDescriptor>,
61 kind: TensorKind,
62 ) -> Result<(), GraphError> {
63 for name in descriptors.keys() {
64 if !actual_shapes.contains_key(name) {
65 return Err(GraphError::RuntimeTensorMissing {
66 kind: kind.as_str().to_string(),
67 name: name.clone(),
68 });
69 }
70 }
71
72 for name in actual_shapes.keys() {
73 if !descriptors.contains_key(name) {
74 return Err(GraphError::RuntimeTensorUnexpected {
75 kind: kind.as_str().to_string(),
76 name: name.clone(),
77 });
78 }
79 }
80
81 for (name, actual_shape) in actual_shapes {
82 let descriptor = descriptors.get(name).expect("checked above");
83 self.validate_shape(name, actual_shape, descriptor, kind)?;
84 }
85
86 Ok(())
87 }
88
89 pub fn validate_shape(
92 &mut self,
93 name: &str,
94 actual_shape: &[usize],
95 descriptor: &OperandDescriptor,
96 kind: TensorKind,
97 ) -> Result<(), GraphError> {
98 if actual_shape.len() != descriptor.shape.len() {
99 return Err(GraphError::RuntimeTensorRankMismatch {
100 kind: kind.as_str().to_string(),
101 name: name.to_string(),
102 expected_rank: descriptor.shape.len(),
103 actual_rank: actual_shape.len(),
104 });
105 }
106
107 for (axis, (actual, expected_dim)) in actual_shape
108 .iter()
109 .copied()
110 .zip(&descriptor.shape)
111 .enumerate()
112 {
113 match expected_dim {
114 Dimension::Static(expected) => {
115 if actual != *expected as usize {
116 return Err(GraphError::RuntimeStaticDimensionMismatch {
117 kind: kind.as_str().to_string(),
118 name: name.to_string(),
119 axis,
120 expected: *expected,
121 actual,
122 });
123 }
124 }
125 Dimension::Dynamic(dynamic) => {
126 if actual > dynamic.max_size as usize {
127 return Err(GraphError::RuntimeDynamicDimensionExceeded {
128 kind: kind.as_str().to_string(),
129 name: name.to_string(),
130 axis,
131 dim_name: dynamic.name.clone(),
132 max_size: dynamic.max_size,
133 actual,
134 });
135 }
136
137 if let Some(bound) = self.bound_dims.get(&dynamic.name) {
138 if bound.value != actual {
139 return Err(GraphError::RuntimeDynamicDimensionNameMismatch {
140 dim_name: dynamic.name.clone(),
141 expected: bound.value,
142 actual,
143 });
144 }
145 } else {
146 self.bound_dims
147 .insert(dynamic.name.clone(), BoundDynamicDim { value: actual });
148 }
149 }
150 }
151 }
152
153 Ok(())
154 }
155}
156
157#[tracing::instrument(
159 name = "validate_data_length",
160 skip_all,
161 err,
162 level = "debug",
163 fields(name = %name, shape = ?shape, elements = data_len)
164)]
165pub fn validate_shape_data_length(
166 name: &str,
167 shape: &[usize],
168 data_len: usize,
169) -> Result<(), GraphError> {
170 let expected = shape
171 .iter()
172 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
173 .ok_or_else(|| GraphError::RuntimeTensorShapeOverflow {
174 name: name.to_string(),
175 shape: shape.to_vec(),
176 })?;
177
178 if data_len != expected {
179 return Err(GraphError::RuntimeTensorDataLengthMismatch {
180 name: name.to_string(),
181 expected,
182 actual: data_len,
183 });
184 }
185
186 Ok(())
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::graph::{DataType, Dimension, DynamicDimension, OperandDescriptor};
193
194 fn input_desc(shape: Vec<Dimension>) -> OperandDescriptor {
195 OperandDescriptor {
196 data_type: DataType::Float32,
197 shape,
198 pending_permutation: vec![],
199 }
200 }
201
202 #[test]
203 fn validates_static_shape() {
204 let mut state = RuntimeShapeState::new();
205 let desc = input_desc(vec![Dimension::Static(2), Dimension::Static(3)]);
206 state
207 .validate_shape("x", &[2, 3], &desc, TensorKind::Input)
208 .unwrap();
209 }
210
211 #[test]
212 fn rejects_static_mismatch() {
213 let mut state = RuntimeShapeState::new();
214 let desc = input_desc(vec![Dimension::Static(2), Dimension::Static(3)]);
215 let err = state
216 .validate_shape("x", &[2, 4], &desc, TensorKind::Input)
217 .unwrap_err();
218 assert!(matches!(
219 err,
220 GraphError::RuntimeStaticDimensionMismatch { axis: 1, .. }
221 ));
222 }
223
224 #[test]
225 fn rejects_dynamic_greater_than_max_size() {
226 let mut state = RuntimeShapeState::new();
227 let desc = input_desc(vec![Dimension::Dynamic(DynamicDimension {
228 name: "batch".to_string(),
229 max_size: 8,
230 })]);
231 let err = state
232 .validate_shape("x", &[9], &desc, TensorKind::Input)
233 .unwrap_err();
234 assert!(matches!(
235 err,
236 GraphError::RuntimeDynamicDimensionExceeded { .. }
237 ));
238 }
239
240 #[test]
241 fn enforces_same_named_dynamic_dimensions_across_inputs_and_outputs() {
242 let mut state = RuntimeShapeState::new();
243 let input = input_desc(vec![
244 Dimension::Dynamic(DynamicDimension {
245 name: "batch".to_string(),
246 max_size: 16,
247 }),
248 Dimension::Static(64),
249 ]);
250 let output = input_desc(vec![
251 Dimension::Dynamic(DynamicDimension {
252 name: "batch".to_string(),
253 max_size: 16,
254 }),
255 Dimension::Static(10),
256 ]);
257
258 state
259 .validate_shape("x", &[4, 64], &input, TensorKind::Input)
260 .unwrap();
261
262 let err = state
263 .validate_shape("y", &[5, 10], &output, TensorKind::Output)
264 .unwrap_err();
265 assert!(matches!(
266 err,
267 GraphError::RuntimeDynamicDimensionNameMismatch { .. }
268 ));
269 }
270
271 #[test]
272 fn validates_named_shape_sets() {
273 let mut state = RuntimeShapeState::new();
274 let mut descs = HashMap::new();
275 descs.insert(
276 "x".to_string(),
277 input_desc(vec![Dimension::Static(2), Dimension::Static(3)]),
278 );
279 let mut actual = HashMap::new();
280 actual.insert("x".to_string(), vec![2, 3]);
281
282 state
283 .validate_named_shapes(&actual, &descs, TensorKind::Input)
284 .unwrap();
285 }
286
287 #[test]
288 fn rejects_missing_named_tensor() {
289 let mut state = RuntimeShapeState::new();
290 let mut descs = HashMap::new();
291 descs.insert("x".to_string(), input_desc(vec![Dimension::Static(1)]));
292 let actual = HashMap::new();
293
294 let err = state
295 .validate_named_shapes(&actual, &descs, TensorKind::Input)
296 .unwrap_err();
297 assert!(matches!(err, GraphError::RuntimeTensorMissing { .. }));
298 }
299
300 #[test]
301 fn rejects_unexpected_named_tensor() {
302 let mut state = RuntimeShapeState::new();
303 let descs = HashMap::new();
304 let mut actual = HashMap::new();
305 actual.insert("x".to_string(), vec![1]);
306
307 let err = state
308 .validate_named_shapes(&actual, &descs, TensorKind::Input)
309 .unwrap_err();
310 assert!(matches!(err, GraphError::RuntimeTensorUnexpected { .. }));
311 }
312
313 #[test]
314 fn validates_shape_data_length() {
315 validate_shape_data_length("x", &[2, 3], 6).unwrap();
316 validate_shape_data_length("x", &[], 1).unwrap();
317 }
318
319 #[test]
320 fn rejects_shape_data_length_mismatch() {
321 let err = validate_shape_data_length("x", &[2, 3], 7).unwrap_err();
322 assert!(matches!(
323 err,
324 GraphError::RuntimeTensorDataLengthMismatch { .. }
325 ));
326 }
327}