1use crate::Op;
8use fuel_ir::{DType, Error, Shape};
9
10fn need<'a>(shapes: &'a [Shape], n: usize, op: &Op) -> Result<&'a [Shape], Error> {
11 if shapes.len() < n {
12 return Err(Error::Msg(format!(
13 "primitive_shape: {op:?} needs {n} input shape(s), got {}",
14 shapes.len()
15 ))
16 .bt());
17 }
18 Ok(shapes)
19}
20
21pub fn primitive_shape(
38 op: &Op,
39 input_shapes: &[Shape],
40 input_dtypes: &[DType],
41) -> Result<(Shape, DType), Error> {
42 use Op::*;
43 let elem0 = || -> Result<(Shape, DType), Error> {
45 need(input_shapes, 1, op)?;
46 Ok((input_shapes[0].clone(), input_dtypes[0]))
47 };
48 Ok(match op {
49 Add
51 | Sub
52 | Mul
53 | Div
54 | Maximum
55 | Minimum
56 | Pow
57 | Rem
58 | Neg
59 | Abs
60 | Sqr
61 | Sqrt
62 | Rsqrt
63 | Recip
64 | Exp
65 | Log
66 | Sin
67 | Cos
68 | Tanh
69 | Sigmoid
70 | Silu
71 | GeluTanh
72 | Gelu
73 | Relu
74 | Erf
75 | Step
76 | Floor
77 | Ceil
78 | Round
79 | Sign
80 | Contiguize
81 | AddScalar(_)
82 | MulScalar(_)
83 | PowI(_)
84 | Clamp { .. }
85 | CumSum { .. }
86 | Flip { .. }
87 | Roll { .. }
88 | Triu { .. }
89 | Tril { .. }
90 | LogSoftmaxLastDim
91 | LogSoftmaxLastDimBackward
92 | MaskedFill { .. } => elem0()?,
93
94 Equal | Ne | Lt | Le | Gt | Ge => {
101 need(input_shapes, 1, op)?;
102 (input_shapes[0].clone(), DType::Bool)
103 }
104
105 Where => {
107 need(input_shapes, 2, op)?;
108 (input_shapes[0].clone(), input_dtypes[1])
109 }
110
111 Cast(dt) => {
113 need(input_shapes, 1, op)?;
114 (input_shapes[0].clone(), *dt)
115 }
116
117 Reshape(sh) | BroadcastTo(sh) | ReduceSumTo(sh) | ReduceMaxTo(sh) => {
119 need(input_shapes, 1, op)?;
120 (sh.clone(), input_dtypes[0])
121 }
122
123 Transpose => {
124 need(input_shapes, 1, op)?;
125 let d = input_shapes[0].dims();
126 if d.len() < 2 {
127 return Err(Error::Msg(format!(
128 "primitive_shape: Transpose needs rank>=2, got {d:?}"
129 ))
130 .bt());
131 }
132 let mut out = d.to_vec();
133 let r = out.len();
134 out.swap(r - 2, r - 1);
135 (Shape::from_dims(&out), input_dtypes[0])
136 }
137 Permute(axes) => {
138 need(input_shapes, 1, op)?;
139 let d = input_shapes[0].dims();
140 if axes.len() != d.len() || axes.iter().any(|&a| a >= d.len()) {
141 return Err(Error::Msg(format!(
142 "primitive_shape: Permute axes {axes:?} invalid for {d:?}"
143 ))
144 .bt());
145 }
146 let out: Vec<usize> = axes.iter().map(|&a| d[a]).collect();
147 (Shape::from_dims(&out), input_dtypes[0])
148 }
149 Unsqueeze { dim } => {
150 need(input_shapes, 1, op)?;
151 let d = input_shapes[0].dims();
152 if *dim > d.len() {
153 return Err(Error::Msg(format!(
154 "primitive_shape: Unsqueeze dim {dim} > rank {}",
155 d.len()
156 ))
157 .bt());
158 }
159 let mut out = d.to_vec();
160 out.insert(*dim, 1);
161 (Shape::from_dims(&out), input_dtypes[0])
162 }
163 Squeeze { dim } => {
164 need(input_shapes, 1, op)?;
165 let d = input_shapes[0].dims();
166 if *dim >= d.len() {
167 return Err(Error::Msg(format!(
168 "primitive_shape: Squeeze dim {dim} >= rank {}",
169 d.len()
170 ))
171 .bt());
172 }
173 let out: Vec<usize> = d
174 .iter()
175 .enumerate()
176 .filter(|(i, _)| *i != *dim)
177 .map(|(_, &v)| v)
178 .collect();
179 (Shape::from_dims(&out), input_dtypes[0])
180 }
181 Slice { dim, start, len } => {
182 need(input_shapes, 1, op)?;
183 let d = input_shapes[0].dims();
184 if *dim >= d.len() || start + len > d[*dim] {
185 return Err(Error::Msg(format!(
186 "primitive_shape: Slice{{dim:{dim},start:{start},len:{len}}} invalid for {d:?}"
187 ))
188 .bt());
189 }
190 let mut out = d.to_vec();
191 out[*dim] = *len;
192 (Shape::from_dims(&out), input_dtypes[0])
193 }
194 Concat { dim } => {
195 need(input_shapes, 1, op)?;
201 let a = input_shapes[0].dims();
202 if *dim >= a.len() {
203 return Err(Error::Msg(format!(
204 "primitive_shape: Concat dim {dim} out of range for rank-{} input {a:?}",
205 a.len(),
206 ))
207 .bt());
208 }
209 let mut total = 0usize;
210 for (i, sh) in input_shapes.iter().enumerate() {
211 let d = sh.dims();
212 if d.len() != a.len() {
213 return Err(Error::Msg(format!(
214 "primitive_shape: Concat input {i} has rank {} but input 0 has rank {}",
215 d.len(),
216 a.len(),
217 ))
218 .bt());
219 }
220 for (ax, (&da, &d0)) in d.iter().zip(a.iter()).enumerate() {
221 if ax != *dim && da != d0 {
222 return Err(Error::Msg(format!(
223 "primitive_shape: Concat non-join dim {ax} mismatch (input {i}: {d:?} vs {a:?})"
224 ))
225 .bt());
226 }
227 }
228 total += d[*dim];
229 }
230 let mut out = a.to_vec();
231 out[*dim] = total;
232 (Shape::from_dims(&out), input_dtypes[0])
233 }
234 Pad { padding, .. } => {
235 need(input_shapes, 1, op)?;
236 let d = input_shapes[0].dims();
237 if padding.len() != d.len() {
238 return Err(Error::Msg(format!(
239 "primitive_shape: Pad padding {padding:?} rank != {d:?}"
240 ))
241 .bt());
242 }
243 let out: Vec<usize> = d
244 .iter()
245 .zip(padding)
246 .map(|(&x, (b, a))| x + b + a)
247 .collect();
248 (Shape::from_dims(&out), input_dtypes[0])
249 }
250
251 SumDim(dim) | MaxDim(dim) | MinDim(dim) | MeanDim(dim) => {
253 need(input_shapes, 1, op)?;
254 (
255 reduce_remove(input_shapes[0].dims(), *dim, op)?,
256 input_dtypes[0],
257 )
258 }
259 ArgMaxDim(dim) | ArgMinDim(dim) => {
260 need(input_shapes, 1, op)?;
261 (reduce_remove(input_shapes[0].dims(), *dim, op)?, DType::U32)
262 }
263 SumAll | MaxAll | MinAll | MeanAll => {
264 need(input_shapes, 1, op)?;
265 (Shape::from_dims(&[]), input_dtypes[0])
266 }
267
268 MatMul => {
270 need(input_shapes, 2, op)?;
271 let l = input_shapes[0].dims();
272 let r = input_shapes[1].dims();
273 if l.len() < 2 || r.len() != l.len() {
274 return Err(Error::Msg(format!(
275 "primitive_shape: MatMul needs same-rank>=2 operands, got {l:?} / {r:?}"
276 ))
277 .bt());
278 }
279 let rank = l.len();
280 let (m, k) = (l[rank - 2], l[rank - 1]);
281 let (k2, n) = (r[rank - 2], r[rank - 1]);
282 if k != k2 {
283 return Err(Error::Msg(format!(
284 "primitive_shape: MatMul inner-dim mismatch {k} vs {k2}"
285 ))
286 .bt());
287 }
288 let mut out = l[..rank - 2].to_vec();
289 out.push(m);
290 out.push(n);
291 (Shape::from_dims(&out), input_dtypes[0])
292 }
293
294 IndexSelect { dim } => {
296 need(input_shapes, 2, op)?;
297 let d = input_shapes[0].dims();
298 if *dim >= d.len() {
299 return Err(Error::Msg(format!(
300 "primitive_shape: IndexSelect dim {dim} invalid for {d:?}"
301 ))
302 .bt());
303 }
304 let mut out = d.to_vec();
305 out[*dim] = input_shapes[1].elem_count();
306 (Shape::from_dims(&out), input_dtypes[0])
307 }
308 Gather { .. } => {
309 need(input_shapes, 2, op)?;
310 (input_shapes[1].clone(), input_dtypes[0])
311 }
312 IndexAdd { .. } | ScatterAdd { .. } => {
313 need(input_shapes, 1, op)?;
314 (input_shapes[0].clone(), input_dtypes[0])
315 }
316
317 Iota { len } => (Shape::from_dims(&[*len]), DType::F32),
319
320 _ => {
322 return Err(Error::Msg(format!(
323 "primitive_shape: {op:?} is not a first-order shape-inferable primitive"
324 ))
325 .bt());
326 }
327 })
328}
329
330fn reduce_remove(dims: &[usize], dim: usize, op: &Op) -> Result<Shape, Error> {
331 if dim >= dims.len() {
332 return Err(Error::Msg(format!(
333 "primitive_shape: {op:?} dim {dim} out of range for {dims:?}"
334 ))
335 .bt());
336 }
337 let out: Vec<usize> = dims
338 .iter()
339 .enumerate()
340 .filter(|(i, _)| *i != dim)
341 .map(|(_, &v)| v)
342 .collect();
343 Ok(Shape::from_dims(&out))
344}
345
346#[cfg(test)]
347mod tests {
348 use super::primitive_shape;
349 use crate::{Op, PadMode};
350 use fuel_ir::{DType, Shape};
351
352 fn s(d: &[usize]) -> Shape {
353 Shape::from_dims(d)
354 }
355
356 #[test]
357 fn elementwise_preserves_shape_and_dtype() {
358 let (sh, dt) = primitive_shape(
359 &Op::Add,
360 &[s(&[2, 3]), s(&[2, 3])],
361 &[DType::F32, DType::F32],
362 )
363 .unwrap();
364 assert_eq!((sh, dt), (s(&[2, 3]), DType::F32));
365 let (sh, dt) = primitive_shape(&Op::Neg, &[s(&[4])], &[DType::BF16]).unwrap();
366 assert_eq!((sh, dt), (s(&[4]), DType::BF16));
367 }
368
369 #[test]
370 fn comparison_forces_bool() {
371 let (sh, dt) =
374 primitive_shape(&Op::Lt, &[s(&[5]), s(&[5])], &[DType::F32, DType::F32]).unwrap();
375 assert_eq!((sh, dt), (s(&[5]), DType::Bool));
376 }
377
378 #[test]
379 fn cast_takes_target_dtype() {
380 let (sh, dt) =
381 primitive_shape(&Op::Cast(DType::F16), &[s(&[2, 2])], &[DType::F32]).unwrap();
382 assert_eq!((sh, dt), (s(&[2, 2]), DType::F16));
383 }
384
385 #[test]
386 fn slice_shrinks_named_dim() {
387 let (sh, dt) = primitive_shape(
388 &Op::Slice {
389 dim: 1,
390 start: 2,
391 len: 3,
392 },
393 &[s(&[4, 8])],
394 &[DType::F32],
395 )
396 .unwrap();
397 assert_eq!((sh, dt), (s(&[4, 3]), DType::F32));
398 }
399
400 #[test]
401 fn concat_sums_the_join_dim() {
402 let (sh, _) = primitive_shape(
403 &Op::Concat { dim: 0 },
404 &[s(&[2, 4]), s(&[5, 4])],
405 &[DType::F32, DType::F32],
406 )
407 .unwrap();
408 assert_eq!(sh, s(&[7, 4]));
409 }
410
411 #[test]
412 fn concat_sums_the_join_dim_for_n_ary_inputs() {
413 let (sh, _) = primitive_shape(
416 &Op::Concat { dim: 1 },
417 &[s(&[18, 1]), s(&[18, 1]), s(&[18, 1]), s(&[18, 1])],
418 &[DType::F64; 4],
419 )
420 .unwrap();
421 assert_eq!(sh, s(&[18, 4]));
422 }
423
424 #[test]
425 fn concat_rejects_non_join_dim_mismatch() {
426 let r = primitive_shape(
427 &Op::Concat { dim: 0 },
428 &[s(&[2, 4]), s(&[5, 7])],
429 &[DType::F32, DType::F32],
430 );
431 assert!(r.is_err(), "non-join dim mismatch (4 vs 7) must error");
432 }
433
434 #[test]
435 fn permute_reorders_axes() {
436 let (sh, _) =
437 primitive_shape(&Op::Permute(vec![2, 0, 1]), &[s(&[2, 3, 4])], &[DType::F32]).unwrap();
438 assert_eq!(sh, s(&[4, 2, 3]));
439 }
440
441 #[test]
442 fn reshape_and_broadcast_take_target() {
443 let (sh, _) = primitive_shape(&Op::Reshape(s(&[6])), &[s(&[2, 3])], &[DType::F32]).unwrap();
444 assert_eq!(sh, s(&[6]));
445 let (sh, _) =
446 primitive_shape(&Op::BroadcastTo(s(&[2, 3])), &[s(&[1, 3])], &[DType::F32]).unwrap();
447 assert_eq!(sh, s(&[2, 3]));
448 }
449
450 #[test]
451 fn mean_dim_removes_the_dim() {
452 let (sh, dt) = primitive_shape(&Op::MeanDim(1), &[s(&[2, 3, 4])], &[DType::F32]).unwrap();
453 assert_eq!((sh, dt), (s(&[2, 4]), DType::F32));
454 }
455
456 #[test]
457 fn reduce_to_and_scalar_reductions() {
458 let (sh, _) =
459 primitive_shape(&Op::ReduceMaxTo(s(&[2, 1])), &[s(&[2, 5])], &[DType::F32]).unwrap();
460 assert_eq!(sh, s(&[2, 1]));
461 let (sh, _) = primitive_shape(&Op::SumAll, &[s(&[3, 3])], &[DType::F32]).unwrap();
462 assert_eq!(sh, s(&[]));
463 }
464
465 #[test]
466 fn argmax_produces_u32() {
467 let (sh, dt) = primitive_shape(&Op::ArgMaxDim(0), &[s(&[4, 6])], &[DType::F32]).unwrap();
468 assert_eq!((sh, dt), (s(&[6]), DType::U32));
469 }
470
471 #[test]
472 fn matmul_contracts_inner_dim() {
473 let (sh, _) = primitive_shape(
474 &Op::MatMul,
475 &[s(&[2, 3]), s(&[3, 5])],
476 &[DType::F32, DType::F32],
477 )
478 .unwrap();
479 assert_eq!(sh, s(&[2, 5]));
480 let (sh, _) = primitive_shape(
482 &Op::MatMul,
483 &[s(&[7, 2, 3]), s(&[7, 3, 5])],
484 &[DType::F32, DType::F32],
485 )
486 .unwrap();
487 assert_eq!(sh, s(&[7, 2, 5]));
488 }
489
490 #[test]
491 fn where_is_cond_shape_a_dtype() {
492 let (sh, dt) = primitive_shape(
493 &Op::Where,
494 &[s(&[4]), s(&[4]), s(&[4])],
495 &[DType::U8, DType::F32, DType::F32],
496 )
497 .unwrap();
498 assert_eq!((sh, dt), (s(&[4]), DType::F32));
499 }
500
501 #[test]
502 fn index_ops() {
503 let (sh, _) = primitive_shape(
505 &Op::IndexSelect { dim: 0 },
506 &[s(&[8, 4]), s(&[3])],
507 &[DType::F32, DType::U32],
508 )
509 .unwrap();
510 assert_eq!(sh, s(&[3, 4]));
511 let (sh, _) = primitive_shape(
513 &Op::Gather { dim: 1 },
514 &[s(&[2, 8]), s(&[2, 3])],
515 &[DType::F32, DType::U32],
516 )
517 .unwrap();
518 assert_eq!(sh, s(&[2, 3]));
519 }
520
521 #[test]
522 fn pad_extends_each_axis() {
523 let (sh, _) = primitive_shape(
524 &Op::Pad {
525 padding: vec![(1, 1), (0, 2)],
526 mode: PadMode::Constant,
527 value: 0.0,
528 },
529 &[s(&[3, 4])],
530 &[DType::F32],
531 )
532 .unwrap();
533 assert_eq!(sh, s(&[5, 6]));
534 }
535
536 #[test]
537 fn iota_is_a_len_vector() {
538 let (sh, dt) = primitive_shape(&Op::Iota { len: 9 }, &[], &[]).unwrap();
539 assert_eq!((sh, dt), (s(&[9]), DType::F32));
540 }
541
542 #[test]
543 fn out_of_scope_ops_error_not_panic() {
544 assert!(primitive_shape(&Op::Const, &[], &[]).is_err());
545 assert!(primitive_shape(&Op::Release, &[s(&[1])], &[DType::F32]).is_err());
546 }
547
548 #[test]
549 fn malformed_input_is_err_not_panic() {
550 assert!(primitive_shape(&Op::Add, &[], &[]).is_err());
552 assert!(
554 primitive_shape(
555 &Op::Slice {
556 dim: 5,
557 start: 0,
558 len: 1
559 },
560 &[s(&[2, 2])],
561 &[DType::F32]
562 )
563 .is_err()
564 );
565 assert!(
567 primitive_shape(
568 &Op::MatMul,
569 &[s(&[2, 3]), s(&[4, 5])],
570 &[DType::F32, DType::F32]
571 )
572 .is_err()
573 );
574 }
575}