1use std::collections::HashMap;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TensorShape {
33 pub dims: Vec<usize>,
35}
36
37impl TensorShape {
38 pub fn new(dims: Vec<usize>) -> Self {
40 Self { dims }
41 }
42
43 pub fn rank(&self) -> usize {
45 self.dims.len()
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ShapeOp {
52 Add,
54 MatMul,
56 Reshape,
58 Transpose,
60 Concat,
62 Slice,
64 Broadcast,
66}
67
68#[derive(Debug, Clone)]
71pub struct InferenceRule {
72 pub op: ShapeOp,
74 pub input_shapes: Vec<TensorShape>,
76 pub params: HashMap<String, usize>,
85}
86
87#[derive(Debug, Clone)]
89pub struct ShapeInferenceStats {
90 pub rules_applied: u64,
92 pub errors: u64,
94}
95
96pub struct TensorShapeInference {
100 rules_applied: u64,
101 errors: u64,
102}
103
104impl TensorShapeInference {
105 pub fn new() -> Self {
107 Self {
108 rules_applied: 0,
109 errors: 0,
110 }
111 }
112
113 pub fn infer(&mut self, rule: &InferenceRule) -> Result<TensorShape, String> {
115 let result = match rule.op {
116 ShapeOp::Add => {
117 if rule.input_shapes.len() < 2 {
118 return Err(self.record_error("Add requires at least 2 inputs".to_string()));
119 }
120 Self::broadcast_shape(&rule.input_shapes[0], &rule.input_shapes[1])
121 }
122 ShapeOp::MatMul => {
123 if rule.input_shapes.len() < 2 {
124 return Err(self.record_error("MatMul requires 2 inputs".to_string()));
125 }
126 Self::matmul_shape(&rule.input_shapes[0], &rule.input_shapes[1])
127 }
128 ShapeOp::Reshape => {
129 if rule.input_shapes.is_empty() {
130 return Err(self.record_error("Reshape requires 1 input".to_string()));
131 }
132 let new_dims = Self::extract_dims(&rule.params)?;
133 Self::reshape_shape(&rule.input_shapes[0], &new_dims)
134 }
135 ShapeOp::Transpose => {
136 if rule.input_shapes.is_empty() {
137 return Err(self.record_error("Transpose requires 1 input".to_string()));
138 }
139 Ok(Self::transpose_shape(&rule.input_shapes[0]))
140 }
141 ShapeOp::Concat => {
142 if rule.input_shapes.is_empty() {
143 return Err(self.record_error("Concat requires at least 1 input".to_string()));
144 }
145 let axis = *rule
146 .params
147 .get("axis")
148 .ok_or_else(|| "Concat requires 'axis' parameter".to_string())?;
149 Self::concat_shape(&rule.input_shapes, axis)
150 }
151 ShapeOp::Slice => {
152 if rule.input_shapes.is_empty() {
153 return Err(self.record_error("Slice requires 1 input".to_string()));
154 }
155 let axis = *rule
156 .params
157 .get("axis")
158 .ok_or_else(|| "Slice requires 'axis' parameter".to_string())?;
159 let start = *rule
160 .params
161 .get("start")
162 .ok_or_else(|| "Slice requires 'start' parameter".to_string())?;
163 let end = *rule
164 .params
165 .get("end")
166 .ok_or_else(|| "Slice requires 'end' parameter".to_string())?;
167 Self::slice_shape(&rule.input_shapes[0], axis, start, end)
168 }
169 ShapeOp::Broadcast => {
170 if rule.input_shapes.is_empty() {
171 return Err(self.record_error("Broadcast requires 1 input".to_string()));
172 }
173 let target_dims = Self::extract_dims(&rule.params)?;
174 let target = TensorShape::new(target_dims);
175 Self::broadcast_shape(&rule.input_shapes[0], &target)
176 }
177 };
178
179 match result {
180 Ok(shape) => {
181 self.rules_applied += 1;
182 Ok(shape)
183 }
184 Err(e) => Err(self.record_error(e)),
185 }
186 }
187
188 pub fn broadcast_shape(a: &TensorShape, b: &TensorShape) -> Result<TensorShape, String> {
196 let max_rank = a.rank().max(b.rank());
197 let mut result_dims = Vec::with_capacity(max_rank);
198
199 for i in 0..max_rank {
200 let da = if i < a.rank() {
202 a.dims[a.rank() - 1 - i]
203 } else {
204 1
205 };
206 let db = if i < b.rank() {
207 b.dims[b.rank() - 1 - i]
208 } else {
209 1
210 };
211
212 if da == db {
213 result_dims.push(da);
214 } else if da == 1 {
215 result_dims.push(db);
216 } else if db == 1 {
217 result_dims.push(da);
218 } else {
219 return Err(format!(
220 "Shapes are not broadcast-compatible: {:?} vs {:?} (dimension {} from right: {} vs {})",
221 a.dims, b.dims, i, da, db
222 ));
223 }
224 }
225
226 result_dims.reverse();
227 Ok(TensorShape::new(result_dims))
228 }
229
230 pub fn matmul_shape(a: &TensorShape, b: &TensorShape) -> Result<TensorShape, String> {
236 if a.rank() < 2 || b.rank() < 2 {
237 return Err(format!(
238 "MatMul requires at least 2-D tensors, got ranks {} and {}",
239 a.rank(),
240 b.rank()
241 ));
242 }
243
244 let a_rows = a.dims[a.rank() - 2];
245 let a_cols = a.dims[a.rank() - 1];
246 let b_rows = b.dims[b.rank() - 2];
247 let b_cols = b.dims[b.rank() - 1];
248
249 if a_cols != b_rows {
250 return Err(format!(
251 "MatMul inner dimensions mismatch: {} vs {}",
252 a_cols, b_rows
253 ));
254 }
255
256 let a_batch = TensorShape::new(a.dims[..a.rank() - 2].to_vec());
258 let b_batch = TensorShape::new(b.dims[..b.rank() - 2].to_vec());
259 let batch = Self::broadcast_shape(&a_batch, &b_batch)?;
260
261 let mut result_dims = batch.dims;
262 result_dims.push(a_rows);
263 result_dims.push(b_cols);
264 Ok(TensorShape::new(result_dims))
265 }
266
267 pub fn reshape_shape(input: &TensorShape, new_dims: &[usize]) -> Result<TensorShape, String> {
270 let input_elems = Self::total_elements(input);
271 let output_elems: usize = new_dims.iter().product();
272
273 if input_elems != output_elems {
274 return Err(format!(
275 "Reshape: total elements mismatch ({} vs {})",
276 input_elems, output_elems
277 ));
278 }
279
280 Ok(TensorShape::new(new_dims.to_vec()))
281 }
282
283 pub fn transpose_shape(input: &TensorShape) -> TensorShape {
285 let mut dims = input.dims.clone();
286 dims.reverse();
287 TensorShape::new(dims)
288 }
289
290 pub fn concat_shape(inputs: &[TensorShape], axis: usize) -> Result<TensorShape, String> {
293 if inputs.is_empty() {
294 return Err("Concat requires at least 1 input".to_string());
295 }
296
297 let rank = inputs[0].rank();
298 if axis >= rank {
299 return Err(format!(
300 "Concat axis {} is out of bounds for rank {}",
301 axis, rank
302 ));
303 }
304
305 let mut concat_dim = 0usize;
306 for (i, shape) in inputs.iter().enumerate() {
307 if shape.rank() != rank {
308 return Err(format!(
309 "Concat: all inputs must have the same rank, input 0 has rank {} but input {} has rank {}",
310 rank, i, shape.rank()
311 ));
312 }
313 for d in 0..rank {
314 if d != axis && shape.dims[d] != inputs[0].dims[d] {
315 return Err(format!(
316 "Concat: dimension {} mismatch between input 0 ({}) and input {} ({})",
317 d, inputs[0].dims[d], i, shape.dims[d]
318 ));
319 }
320 }
321 concat_dim = concat_dim
322 .checked_add(shape.dims[axis])
323 .ok_or_else(|| "Concat: dimension overflow".to_string())?;
324 }
325
326 let mut result_dims = inputs[0].dims.clone();
327 result_dims[axis] = concat_dim;
328 Ok(TensorShape::new(result_dims))
329 }
330
331 pub fn slice_shape(
334 input: &TensorShape,
335 axis: usize,
336 start: usize,
337 end: usize,
338 ) -> Result<TensorShape, String> {
339 if axis >= input.rank() {
340 return Err(format!(
341 "Slice axis {} is out of bounds for rank {}",
342 axis,
343 input.rank()
344 ));
345 }
346
347 if start > end {
348 return Err(format!(
349 "Slice: start ({}) must not exceed end ({})",
350 start, end
351 ));
352 }
353
354 if end > input.dims[axis] {
355 return Err(format!(
356 "Slice: end ({}) exceeds dimension size ({}) on axis {}",
357 end, input.dims[axis], axis
358 ));
359 }
360
361 let mut result_dims = input.dims.clone();
362 result_dims[axis] = end - start;
363 Ok(TensorShape::new(result_dims))
364 }
365
366 pub fn total_elements(shape: &TensorShape) -> usize {
369 if shape.dims.is_empty() {
370 return 1;
371 }
372 shape.dims.iter().product()
373 }
374
375 pub fn is_scalar(shape: &TensorShape) -> bool {
378 shape.dims.is_empty() || shape.dims.iter().all(|&d| d == 1)
379 }
380
381 pub fn stats(&self) -> ShapeInferenceStats {
383 ShapeInferenceStats {
384 rules_applied: self.rules_applied,
385 errors: self.errors,
386 }
387 }
388
389 fn record_error(&mut self, msg: String) -> String {
393 self.errors += 1;
394 msg
395 }
396
397 fn extract_dims(params: &HashMap<String, usize>) -> Result<Vec<usize>, String> {
400 let ndims = *params
401 .get("ndims")
402 .ok_or_else(|| "Missing 'ndims' parameter".to_string())?;
403 let mut dims = Vec::with_capacity(ndims);
404 for i in 0..ndims {
405 let key = format!("dim{}", i);
406 let d = *params
407 .get(&key)
408 .ok_or_else(|| format!("Missing '{}' parameter", key))?;
409 dims.push(d);
410 }
411 Ok(dims)
412 }
413}
414
415impl Default for TensorShapeInference {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn shape(dims: &[usize]) -> TensorShape {
429 TensorShape::new(dims.to_vec())
430 }
431
432 fn make_params(entries: &[(&str, usize)]) -> HashMap<String, usize> {
433 entries.iter().map(|(k, v)| (k.to_string(), *v)).collect()
434 }
435
436 #[test]
439 fn broadcast_same_shape() {
440 let a = shape(&[3, 4, 5]);
441 let b = shape(&[3, 4, 5]);
442 let r = TensorShapeInference::broadcast_shape(&a, &b);
443 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![3, 4, 5]));
444 }
445
446 #[test]
447 fn broadcast_scalar_and_tensor() {
448 let scalar = shape(&[]);
449 let tensor = shape(&[2, 3]);
450 let r = TensorShapeInference::broadcast_shape(&scalar, &tensor);
451 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![2, 3]));
452 }
453
454 #[test]
455 fn broadcast_scalar_one_and_tensor() {
456 let scalar = shape(&[1]);
457 let tensor = shape(&[5, 3]);
458 let r = TensorShapeInference::broadcast_shape(&scalar, &tensor);
459 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![5, 3]));
460 }
461
462 #[test]
463 fn broadcast_different_ranks() {
464 let a = shape(&[3, 1]);
465 let b = shape(&[2, 3, 4]);
466 let r = TensorShapeInference::broadcast_shape(&a, &b);
467 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![2, 3, 4]));
468 }
469
470 #[test]
471 fn broadcast_incompatible() {
472 let a = shape(&[3]);
473 let b = shape(&[4]);
474 let r = TensorShapeInference::broadcast_shape(&a, &b);
475 assert!(r.is_err());
476 }
477
478 #[test]
479 fn broadcast_ones_expansion() {
480 let a = shape(&[1, 4]);
481 let b = shape(&[3, 1]);
482 let r = TensorShapeInference::broadcast_shape(&a, &b);
483 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![3, 4]));
484 }
485
486 #[test]
487 fn broadcast_high_rank() {
488 let a = shape(&[1, 1, 5]);
489 let b = shape(&[8, 1, 1]);
490 let r = TensorShapeInference::broadcast_shape(&a, &b);
491 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![8, 1, 5]));
492 }
493
494 #[test]
497 fn matmul_valid_2d() {
498 let a = shape(&[3, 4]);
499 let b = shape(&[4, 5]);
500 let r = TensorShapeInference::matmul_shape(&a, &b);
501 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![3, 5]));
502 }
503
504 #[test]
505 fn matmul_inner_dim_mismatch() {
506 let a = shape(&[3, 4]);
507 let b = shape(&[5, 6]);
508 let r = TensorShapeInference::matmul_shape(&a, &b);
509 assert!(r.is_err());
510 }
511
512 #[test]
513 fn matmul_1d_rejected() {
514 let a = shape(&[4]);
515 let b = shape(&[4, 3]);
516 let r = TensorShapeInference::matmul_shape(&a, &b);
517 assert!(r.is_err());
518 }
519
520 #[test]
521 fn matmul_batched() {
522 let a = shape(&[2, 3, 4]);
523 let b = shape(&[2, 4, 5]);
524 let r = TensorShapeInference::matmul_shape(&a, &b);
525 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![2, 3, 5]));
526 }
527
528 #[test]
529 fn matmul_batch_broadcast() {
530 let a = shape(&[1, 3, 4]);
531 let b = shape(&[5, 4, 2]);
532 let r = TensorShapeInference::matmul_shape(&a, &b);
533 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![5, 3, 2]));
534 }
535
536 #[test]
539 fn reshape_valid() {
540 let input = shape(&[2, 3, 4]);
541 let r = TensorShapeInference::reshape_shape(&input, &[6, 4]);
542 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![6, 4]));
543 }
544
545 #[test]
546 fn reshape_element_mismatch() {
547 let input = shape(&[2, 3]);
548 let r = TensorShapeInference::reshape_shape(&input, &[7]);
549 assert!(r.is_err());
550 }
551
552 #[test]
553 fn reshape_to_flat() {
554 let input = shape(&[3, 4, 5]);
555 let r = TensorShapeInference::reshape_shape(&input, &[60]);
556 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![60]));
557 }
558
559 #[test]
562 fn transpose_2d() {
563 let input = shape(&[3, 4]);
564 let r = TensorShapeInference::transpose_shape(&input);
565 assert_eq!(r.dims, vec![4, 3]);
566 }
567
568 #[test]
569 fn transpose_3d() {
570 let input = shape(&[2, 3, 4]);
571 let r = TensorShapeInference::transpose_shape(&input);
572 assert_eq!(r.dims, vec![4, 3, 2]);
573 }
574
575 #[test]
576 fn transpose_scalar() {
577 let input = shape(&[]);
578 let r = TensorShapeInference::transpose_shape(&input);
579 assert_eq!(r.dims, Vec::<usize>::new());
580 }
581
582 #[test]
585 fn concat_axis0() {
586 let a = shape(&[2, 3]);
587 let b = shape(&[4, 3]);
588 let r = TensorShapeInference::concat_shape(&[a, b], 0);
589 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![6, 3]));
590 }
591
592 #[test]
593 fn concat_axis1() {
594 let a = shape(&[2, 3]);
595 let b = shape(&[2, 5]);
596 let r = TensorShapeInference::concat_shape(&[a, b], 1);
597 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![2, 8]));
598 }
599
600 #[test]
601 fn concat_three_inputs() {
602 let a = shape(&[1, 4]);
603 let b = shape(&[2, 4]);
604 let c = shape(&[3, 4]);
605 let r = TensorShapeInference::concat_shape(&[a, b, c], 0);
606 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![6, 4]));
607 }
608
609 #[test]
610 fn concat_dim_mismatch() {
611 let a = shape(&[2, 3]);
612 let b = shape(&[2, 4]);
613 let r = TensorShapeInference::concat_shape(&[a, b], 0);
614 assert!(r.is_err());
615 }
616
617 #[test]
618 fn concat_axis_out_of_bounds() {
619 let a = shape(&[2, 3]);
620 let r = TensorShapeInference::concat_shape(&[a], 5);
621 assert!(r.is_err());
622 }
623
624 #[test]
627 fn slice_basic() {
628 let input = shape(&[10, 5]);
629 let r = TensorShapeInference::slice_shape(&input, 0, 2, 7);
630 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![5, 5]));
631 }
632
633 #[test]
634 fn slice_axis1() {
635 let input = shape(&[4, 8]);
636 let r = TensorShapeInference::slice_shape(&input, 1, 1, 5);
637 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![4, 4]));
638 }
639
640 #[test]
641 fn slice_out_of_bounds() {
642 let input = shape(&[5, 3]);
643 let r = TensorShapeInference::slice_shape(&input, 0, 2, 10);
644 assert!(r.is_err());
645 }
646
647 #[test]
648 fn slice_start_exceeds_end() {
649 let input = shape(&[5, 3]);
650 let r = TensorShapeInference::slice_shape(&input, 0, 4, 2);
651 assert!(r.is_err());
652 }
653
654 #[test]
655 fn slice_empty_result() {
656 let input = shape(&[5, 3]);
657 let r = TensorShapeInference::slice_shape(&input, 0, 3, 3);
658 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![0, 3]));
659 }
660
661 #[test]
664 fn total_elements_normal() {
665 assert_eq!(TensorShapeInference::total_elements(&shape(&[2, 3, 4])), 24);
666 }
667
668 #[test]
669 fn total_elements_scalar() {
670 assert_eq!(TensorShapeInference::total_elements(&shape(&[])), 1);
671 }
672
673 #[test]
674 fn total_elements_with_one() {
675 assert_eq!(TensorShapeInference::total_elements(&shape(&[1, 1, 1])), 1);
676 }
677
678 #[test]
681 fn is_scalar_empty() {
682 assert!(TensorShapeInference::is_scalar(&shape(&[])));
683 }
684
685 #[test]
686 fn is_scalar_all_ones() {
687 assert!(TensorShapeInference::is_scalar(&shape(&[1, 1, 1])));
688 }
689
690 #[test]
691 fn is_scalar_not() {
692 assert!(!TensorShapeInference::is_scalar(&shape(&[2, 1])));
693 }
694
695 #[test]
698 fn infer_add() {
699 let mut engine = TensorShapeInference::new();
700 let rule = InferenceRule {
701 op: ShapeOp::Add,
702 input_shapes: vec![shape(&[3, 1]), shape(&[1, 4])],
703 params: HashMap::new(),
704 };
705 let r = engine.infer(&rule);
706 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![3, 4]));
707 assert_eq!(engine.stats().rules_applied, 1);
708 }
709
710 #[test]
711 fn infer_reshape() {
712 let mut engine = TensorShapeInference::new();
713 let rule = InferenceRule {
714 op: ShapeOp::Reshape,
715 input_shapes: vec![shape(&[2, 6])],
716 params: make_params(&[("ndims", 3), ("dim0", 3), ("dim1", 2), ("dim2", 2)]),
717 };
718 let r = engine.infer(&rule);
719 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![3, 2, 2]));
720 }
721
722 #[test]
723 fn infer_concat_via_rule() {
724 let mut engine = TensorShapeInference::new();
725 let rule = InferenceRule {
726 op: ShapeOp::Concat,
727 input_shapes: vec![shape(&[2, 3]), shape(&[2, 4])],
728 params: make_params(&[("axis", 1)]),
729 };
730 let r = engine.infer(&rule);
731 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![2, 7]));
732 }
733
734 #[test]
735 fn infer_slice_via_rule() {
736 let mut engine = TensorShapeInference::new();
737 let rule = InferenceRule {
738 op: ShapeOp::Slice,
739 input_shapes: vec![shape(&[8, 3])],
740 params: make_params(&[("axis", 0), ("start", 1), ("end", 5)]),
741 };
742 let r = engine.infer(&rule);
743 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![4, 3]));
744 }
745
746 #[test]
747 fn infer_error_tracking() {
748 let mut engine = TensorShapeInference::new();
749 let rule = InferenceRule {
750 op: ShapeOp::Add,
751 input_shapes: vec![shape(&[3]), shape(&[4])],
752 params: HashMap::new(),
753 };
754 assert!(engine.infer(&rule).is_err());
755 assert_eq!(engine.stats().errors, 1);
756 assert_eq!(engine.stats().rules_applied, 0);
757 }
758
759 #[test]
762 fn chain_matmul_then_transpose() {
763 let mut engine = TensorShapeInference::new();
764 let matmul_rule = InferenceRule {
765 op: ShapeOp::MatMul,
766 input_shapes: vec![shape(&[3, 4]), shape(&[4, 5])],
767 params: HashMap::new(),
768 };
769 let intermediate = engine.infer(&matmul_rule).expect("matmul should succeed");
770 assert_eq!(intermediate.dims, vec![3, 5]);
771
772 let transpose_rule = InferenceRule {
773 op: ShapeOp::Transpose,
774 input_shapes: vec![intermediate],
775 params: HashMap::new(),
776 };
777 let result = engine
778 .infer(&transpose_rule)
779 .expect("transpose should succeed");
780 assert_eq!(result.dims, vec![5, 3]);
781 assert_eq!(engine.stats().rules_applied, 2);
782 }
783
784 #[test]
785 fn chain_concat_then_reshape() {
786 let mut engine = TensorShapeInference::new();
787
788 let concat_rule = InferenceRule {
790 op: ShapeOp::Concat,
791 input_shapes: vec![shape(&[2, 3]), shape(&[2, 3])],
792 params: make_params(&[("axis", 0)]),
793 };
794 let after_concat = engine.infer(&concat_rule).expect("concat should succeed");
795 assert_eq!(after_concat.dims, vec![4, 3]);
796
797 let reshape_rule = InferenceRule {
799 op: ShapeOp::Reshape,
800 input_shapes: vec![after_concat],
801 params: make_params(&[("ndims", 2), ("dim0", 2), ("dim1", 6)]),
802 };
803 let result = engine.infer(&reshape_rule).expect("reshape should succeed");
804 assert_eq!(result.dims, vec![2, 6]);
805 assert_eq!(engine.stats().rules_applied, 2);
806 }
807
808 #[test]
809 fn infer_broadcast_via_rule() {
810 let mut engine = TensorShapeInference::new();
811 let rule = InferenceRule {
812 op: ShapeOp::Broadcast,
813 input_shapes: vec![shape(&[1, 3])],
814 params: make_params(&[("ndims", 2), ("dim0", 4), ("dim1", 3)]),
815 };
816 let r = engine.infer(&rule);
817 assert_eq!(r.as_ref().map(|s| &s.dims), Ok(&vec![4, 3]));
818 }
819}