Skip to main content

fuel_ir/
shape.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Shapes describe the dimensionality of tensors.
3//!
4//! A [`Shape`] holds an ordered list of dimension sizes. It is used to create tensors,
5//! verify operation compatibility, and query tensor structure.
6#![allow(clippy::redundant_closure_call)]
7use crate::symbol::{SymEnv, SymId};
8use crate::{DimVec, Error, Result};
9use smallvec::smallvec;
10
11/// A single dimension's **extent**: a build-time constant, or a bounded
12/// runtime symbol.
13///
14/// `Shape::dims()` reports the *bound* (a `Scalar`'s value, or a `Range`'s
15/// `max`/capacity) — which is what sizing/striding/iteration want. The *live*
16/// value of a `Range` is resolved per forward pass through a [`SymEnv`].
17/// `Scalar` carries no symbol: two build-time constants that must match
18/// already match by being equal, and a *runtime* dimension always has a
19/// capacity bound, so it is a `Range`, never a `Scalar`.
20///
21/// Phase D step 1b. See
22/// `docs/session-prompts/symbolic-extents-and-persistent-decode.md`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Extent {
25    /// A dimension fixed at graph-construction time.
26    Scalar(usize),
27    /// A bounded runtime dimension: live value in `[min, max]`, resolved via
28    /// the `SymEnv` under `sym`. `max` is the capacity (what `dims()` reports).
29    Range { min: usize, max: usize, sym: SymId },
30}
31
32impl Extent {
33    /// The capacity bound: a `Scalar`'s value, or a `Range`'s `max`. This is
34    /// what `Shape::dims()` reports for the axis.
35    pub fn bound(&self) -> usize {
36        match self {
37            Extent::Scalar(v) => *v,
38            Extent::Range { max, .. } => *max,
39        }
40    }
41
42    /// The lower bound: a `Scalar`'s value, or a `Range`'s `min`.
43    pub fn min(&self) -> usize {
44        match self {
45            Extent::Scalar(v) => *v,
46            Extent::Range { min, .. } => *min,
47        }
48    }
49
50    /// Whether this is a runtime (`Range`) extent rather than a constant.
51    pub fn is_dynamic(&self) -> bool {
52        matches!(self, Extent::Range { .. })
53    }
54
55    /// The symbol of a `Range`, or `None` for a `Scalar`.
56    pub fn sym(&self) -> Option<SymId> {
57        match self {
58            Extent::Scalar(_) => None,
59            Extent::Range { sym, .. } => Some(*sym),
60        }
61    }
62
63    /// The live value this pass: a `Scalar`'s value, or the `Range`'s `sym`
64    /// resolved through `env`. `None` if a `Range`'s symbol is unbound.
65    pub fn resolve(&self, env: &SymEnv) -> Option<usize> {
66        match self {
67            Extent::Scalar(v) => Some(*v),
68            Extent::Range { sym, .. } => env.get(*sym),
69        }
70    }
71}
72
73/// A sparse record marking one axis of a [`Shape`] as a bounded-symbolic
74/// `Range`. `max` is the corresponding `Shape::dims()` bound, so only `min`
75/// and `sym` are stored here. Phase D step 1b.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub struct DynAxis {
78    pub axis: usize,
79    pub min: usize,
80    pub sym: SymId,
81}
82
83/// Sparse list of a shape's dynamic axes, kept sorted by `axis` so equality is
84/// structural. Empty/`None` for the common all-concrete shape.
85type DynAxisVec = smallvec::SmallVec<[DynAxis; 2]>;
86
87/// A shape represents the dimensions of a tensor.
88///
89/// Internally backed by a [`SmallVec`](smallvec::SmallVec) that stores up to 6 dimensions
90/// on the stack without heap allocation. A scalar has rank 0 (no dimensions), a vector has
91/// rank 1, a matrix has rank 2, and so on.
92///
93/// `Shape` can be created from tuples, slices, `Vec<usize>`, or the [`Shape::from_dims`]
94/// constructor.
95/// `dims` holds the per-axis **bounds** (a `Scalar`'s value, or a `Range`'s
96/// `max`/capacity); `dynamic` sparsely marks which axes are bounded-symbolic.
97/// `dims()` borrows `dims` unchanged; the symbolic view is `extent()`. The
98/// common all-concrete shape has `dynamic: None`. Equality includes `dynamic`
99/// (a symbolic shape is distinct from a concrete one with the same bounds), so
100/// `DynAxis` entries are kept sorted by axis.
101#[derive(Clone, PartialEq, Eq)]
102pub struct Shape {
103    dims: DimVec,
104    dynamic: Option<DynAxisVec>,
105}
106
107/// A constant representing a scalar shape (rank 0, no dimensions).
108pub const SCALAR: Shape = Shape {
109    dims: smallvec::SmallVec::new_const(),
110    dynamic: None,
111};
112
113impl std::fmt::Debug for Shape {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{:?}", self.dims())?;
116        if let Some(dyn_axes) = &self.dynamic
117            && !dyn_axes.is_empty()
118        {
119            write!(f, "{{dyn: {dyn_axes:?}}}")?;
120        }
121        Ok(())
122    }
123}
124
125impl<const C: usize> From<&[usize; C]> for Shape {
126    fn from(dims: &[usize; C]) -> Self {
127        Self {
128            dims: DimVec::from_slice(dims),
129            dynamic: None,
130        }
131    }
132}
133
134impl From<&[usize]> for Shape {
135    fn from(dims: &[usize]) -> Self {
136        Self {
137            dims: DimVec::from_slice(dims),
138            dynamic: None,
139        }
140    }
141}
142
143impl From<&Shape> for Shape {
144    fn from(shape: &Shape) -> Self {
145        Self {
146            dims: shape.dims.clone(),
147            dynamic: shape.dynamic.clone(),
148        }
149    }
150}
151
152impl From<()> for Shape {
153    fn from(_: ()) -> Self {
154        Self {
155            dims: smallvec![],
156            dynamic: None,
157        }
158    }
159}
160
161impl From<usize> for Shape {
162    fn from(d1: usize) -> Self {
163        Self {
164            dims: smallvec![d1],
165            dynamic: None,
166        }
167    }
168}
169
170macro_rules! impl_from_tuple {
171    ($tuple:ty, $($index:tt),+) => {
172        impl From<$tuple> for Shape {
173            fn from(d: $tuple) -> Self {
174                Self { dims: smallvec![$(d.$index,)+], dynamic: None }
175            }
176        }
177    }
178}
179
180impl_from_tuple!((usize,), 0);
181impl_from_tuple!((usize, usize), 0, 1);
182impl_from_tuple!((usize, usize, usize), 0, 1, 2);
183impl_from_tuple!((usize, usize, usize, usize), 0, 1, 2, 3);
184impl_from_tuple!((usize, usize, usize, usize, usize), 0, 1, 2, 3, 4);
185impl_from_tuple!((usize, usize, usize, usize, usize, usize), 0, 1, 2, 3, 4, 5);
186
187impl From<Vec<usize>> for Shape {
188    fn from(dims: Vec<usize>) -> Self {
189        Self {
190            dims: DimVec::from(dims),
191            dynamic: None,
192        }
193    }
194}
195
196impl From<DimVec> for Shape {
197    fn from(dims: DimVec) -> Self {
198        Self {
199            dims,
200            dynamic: None,
201        }
202    }
203}
204
205/// Macro that generates dimension extraction functions, Shape methods, and TryInto impls.
206///
207/// For each invocation, creates:
208/// - A free function `$fn_name(dims: &[usize]) -> Result<$out_type>`
209/// - An `impl Shape` method with the same name
210/// - A `TryInto<$out_type> for Shape` impl
211#[macro_export]
212macro_rules! extract_dims {
213    ($(#[$fn_meta:meta])* $fn_name:ident, $cnt:tt, $dims:expr, $out_type:ty,
214     $(#[$shape_meta:meta])* shape) => {
215        $(#[$fn_meta])*
216        pub fn $fn_name(dims: &[usize]) -> $crate::Result<$out_type> {
217            if dims.len() != $cnt {
218                Err($crate::Error::UnexpectedNumberOfDims {
219                    expected: $cnt,
220                    got: dims.len(),
221                    shape: Box::new($crate::Shape::from(dims)),
222                }
223                .bt())
224            } else {
225                Ok($dims(dims))
226            }
227        }
228
229        impl $crate::Shape {
230            $(#[$shape_meta])*
231            pub fn $fn_name(&self) -> $crate::Result<$out_type> {
232                $fn_name(self.dims())
233            }
234        }
235
236        impl std::convert::TryInto<$out_type> for $crate::Shape {
237            type Error = $crate::Error;
238            fn try_into(self) -> std::result::Result<$out_type, Self::Error> {
239                self.$fn_name()
240            }
241        }
242    };
243}
244
245impl Shape {
246    /// Creates a shape from a slice of dimension sizes.
247    pub fn from_dims(dims: &[usize]) -> Self {
248        Self {
249            dims: DimVec::from_slice(dims),
250            dynamic: None,
251        }
252    }
253
254    /// The rank is the number of dimensions, 0 for a scalar value, 1 for a vector, etc.
255    pub fn rank(&self) -> usize {
256        self.dims.len()
257    }
258
259    /// Consumes the shape and returns its dimensions as a `Vec<usize>`.
260    pub fn into_dims(self) -> Vec<usize> {
261        self.dims.into_vec()
262    }
263
264    /// The dimensions as a slice of `usize` — the per-axis **bounds** (a
265    /// scalar's value, or a dynamic axis's capacity/`max`). Unchanged contract:
266    /// this is what sizing, striding, and iteration want. The live value of a
267    /// dynamic axis is obtained separately via [`Shape::extent`] +
268    /// [`Extent::resolve`] (or [`Shape::resolve`]); it is never folded in here.
269    pub fn dims(&self) -> &[usize] {
270        &self.dims
271    }
272
273    /// The [`Extent`] of `axis`: a `Range` if the axis is dynamic (carrying its
274    /// `min`, the `dims()` bound as `max`, and its `sym`), else a `Scalar` of
275    /// the bound. The symbolic *view* over `dims()`. Phase D step 1b.
276    pub fn extent(&self, axis: usize) -> Extent {
277        if let Some(dyn_axes) = &self.dynamic
278            && let Some(da) = dyn_axes.iter().find(|d| d.axis == axis)
279        {
280            return Extent::Range {
281                min: da.min,
282                max: self.dims[axis],
283                sym: da.sym,
284            };
285        }
286        Extent::Scalar(self.dims[axis])
287    }
288
289    /// The [`Extent`] of every axis, in order.
290    pub fn extents(&self) -> impl Iterator<Item = Extent> + '_ {
291        (0..self.dims.len()).map(move |i| self.extent(i))
292    }
293
294    /// Whether any axis is a bounded-symbolic `Range`.
295    pub fn has_dynamic(&self) -> bool {
296        self.dynamic.as_ref().is_some_and(|v| !v.is_empty())
297    }
298
299    /// Mark `axis` as a bounded-symbolic `Range` `[min, dims[axis]]` resolved
300    /// via `sym`. `dims()[axis]` stays the capacity bound; `extent(axis)` now
301    /// reports the `Range`. Replaces any prior dynamic mark on `axis`. The
302    /// KV-cache length axis uses this; K-length and V-length pass the *same*
303    /// `sym` to unify.
304    pub fn with_dynamic_axis(mut self, axis: usize, min: usize, sym: SymId) -> Self {
305        assert!(
306            axis < self.dims.len(),
307            "with_dynamic_axis: axis {axis} out of range for rank {}",
308            self.dims.len(),
309        );
310        let da = DynAxis { axis, min, sym };
311        let v = self.dynamic.get_or_insert_with(DynAxisVec::new);
312        // Keep sorted by axis so equality is structural.
313        match v.binary_search_by_key(&axis, |d| d.axis) {
314            Ok(i) => v[i] = da,
315            Err(i) => v.insert(i, da),
316        }
317        self
318    }
319
320    /// Resolve every dynamic axis through `env`, returning a fully-concrete
321    /// `Shape` (all `Scalar`). Reads `env` at call time, so it always reflects
322    /// the current bindings; never caches. Errors if a dynamic axis's symbol
323    /// is unbound. A shape with no dynamic axes resolves to a clone.
324    pub fn resolve(&self, env: &SymEnv) -> Result<Shape> {
325        let Some(dyn_axes) = &self.dynamic else {
326            return Ok(self.clone());
327        };
328        let mut dims = self.dims.clone();
329        for da in dyn_axes {
330            match env.get(da.sym) {
331                Some(v) => dims[da.axis] = v,
332                None => {
333                    return Err(Error::Msg(format!(
334                        "Shape::resolve: dynamic axis {} symbol {:?} is unbound",
335                        da.axis, da.sym,
336                    ))
337                    .bt());
338                }
339            }
340        }
341        Ok(Shape {
342            dims,
343            dynamic: None,
344        })
345    }
346
347    /// The dimension size for a specified dimension index.
348    ///
349    /// Supports both forward indexing (`usize`) and backward indexing via [`D`].
350    pub fn dim<D: Dim>(&self, dim: D) -> Result<usize> {
351        let dim = dim.to_index(self, "dim")?;
352        Ok(self.dims()[dim])
353    }
354
355    /// The total number of elements, this is the product of all dimension sizes.
356    ///
357    /// For a scalar (rank 0), the element count is 1.
358    pub fn elem_count(&self) -> usize {
359        self.dims.iter().product()
360    }
361
362    /// The strides given in number of elements for a contiguous n-dimensional
363    /// array using this shape. Returns signed strides
364    /// ([`crate::StrideVec`]) — a contiguous layout's strides are
365    /// always non-negative, but the type is signed so callers can
366    /// uniformly handle negative-stride view ops (Flip, etc.).
367    pub fn stride_contiguous(&self) -> crate::StrideVec {
368        let mut stride: crate::StrideVec = self
369            .dims
370            .iter()
371            .rev()
372            .scan(1_isize, |prod, &u| {
373                let prod_pre_mult = *prod;
374                *prod *= u as isize;
375                Some(prod_pre_mult)
376            })
377            .collect();
378        stride.reverse();
379        stride
380    }
381
382    /// Returns true if the strides are C contiguous (aka row major).
383    pub fn is_contiguous(&self, stride: &[isize]) -> bool {
384        if self.dims.len() != stride.len() {
385            return false;
386        }
387        let mut acc: isize = 1;
388        for (&stride, &dim) in stride.iter().zip(self.dims.iter()).rev() {
389            if dim > 1 && stride != acc {
390                return false;
391            }
392            acc *= dim as isize;
393        }
394        true
395    }
396
397    /// Returns true if the strides are Fortran contiguous (aka column major).
398    pub fn is_fortran_contiguous(&self, stride: &[isize]) -> bool {
399        if self.dims.len() != stride.len() {
400            return false;
401        }
402        let mut acc: isize = 1;
403        for (&stride, &dim) in stride.iter().zip(self.dims.iter()) {
404            if dim > 1 && stride != acc {
405                return false;
406            }
407            acc *= dim as isize;
408        }
409        true
410    }
411
412    /// Modifies the shape by adding a list of additional dimensions at the end of the existing
413    /// dimensions.
414    pub fn extend(mut self, additional_dims: &[usize]) -> Self {
415        // Appending axes leaves existing axis indices (and thus `dynamic`)
416        // valid; the new trailing axes are concrete.
417        self.dims.extend_from_slice(additional_dims);
418        self
419    }
420
421    /// Check whether the two shapes are compatible for broadcast, and if it is the case return the
422    /// broadcasted shape. This is to be used for binary pointwise ops.
423    pub fn broadcast_shape_binary_op(&self, rhs: &Self, op: &'static str) -> Result<Shape> {
424        let lhs = self;
425        let lhs_dims = lhs.dims();
426        let rhs_dims = rhs.dims();
427        let lhs_ndims = lhs_dims.len();
428        let rhs_ndims = rhs_dims.len();
429        let bcast_ndims = usize::max(lhs_ndims, rhs_ndims);
430        let mut bcast_dims = smallvec![0; bcast_ndims];
431        for (idx, bcast_value) in bcast_dims.iter_mut().enumerate() {
432            let rev_idx = bcast_ndims - idx;
433            let l_value = if lhs_ndims < rev_idx {
434                1
435            } else {
436                lhs_dims[lhs_ndims - rev_idx]
437            };
438            let r_value = if rhs_ndims < rev_idx {
439                1
440            } else {
441                rhs_dims[rhs_ndims - rev_idx]
442            };
443            *bcast_value = if l_value == r_value {
444                l_value
445            } else if l_value == 1 {
446                r_value
447            } else if r_value == 1 {
448                l_value
449            } else {
450                Err(Error::ShapeMismatchBinaryOp {
451                    lhs: Box::new(lhs.clone()),
452                    rhs: Box::new(rhs.clone()),
453                    op,
454                }
455                .bt())?
456            }
457        }
458        Ok(Shape::from(bcast_dims))
459    }
460
461    /// Returns the broadcasted shapes for matrix multiplication.
462    pub fn broadcast_shape_matmul(&self, rhs: &Self) -> Result<(Shape, Shape)> {
463        let lhs = self;
464        let lhs_dims = lhs.dims();
465        let rhs_dims = rhs.dims();
466        if lhs_dims.len() < 2 || rhs_dims.len() < 2 {
467            crate::bail!("only 2d matrixes are supported {lhs:?} {rhs:?}")
468        }
469        let (m, lhs_k) = (lhs_dims[lhs_dims.len() - 2], lhs_dims[lhs_dims.len() - 1]);
470        let (rhs_k, n) = (rhs_dims[rhs_dims.len() - 2], rhs_dims[rhs_dims.len() - 1]);
471        if lhs_k != rhs_k {
472            crate::bail!("different inner dimensions in broadcast matmul {lhs:?} {rhs:?}")
473        }
474
475        let lhs_b = Self::from(&lhs_dims[..lhs_dims.len() - 2]);
476        let rhs_b = Self::from(&rhs_dims[..rhs_dims.len() - 2]);
477        let bcast = lhs_b.broadcast_shape_binary_op(&rhs_b, "broadcast_matmul")?;
478        let bcast_dims = bcast.dims();
479
480        let bcast_lhs = [bcast_dims, &[m, lhs_k]].concat();
481        let bcast_rhs = [bcast_dims, &[rhs_k, n]].concat();
482        Ok((Shape::from(bcast_lhs), Shape::from(bcast_rhs)))
483    }
484}
485
486/// A trait for types that can be used as dimension indices.
487///
488/// This is implemented for `usize` (forward indexing) and [`D`] (backward indexing).
489pub trait Dim {
490    /// Converts this dimension reference to a concrete index.
491    fn to_index(&self, shape: &Shape, op: &'static str) -> Result<usize>;
492    /// Converts this dimension reference to an index, allowing one-past-the-end for insertion.
493    fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result<usize>;
494}
495
496impl Dim for usize {
497    fn to_index(&self, shape: &Shape, op: &'static str) -> Result<usize> {
498        let dim = *self;
499        if dim >= shape.dims().len() {
500            Err(Error::DimOutOfRange {
501                shape: Box::new(shape.clone()),
502                dim: dim as i32,
503                op,
504            }
505            .bt())?
506        } else {
507            Ok(dim)
508        }
509    }
510
511    fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result<usize> {
512        let dim = *self;
513        if dim > shape.dims().len() {
514            Err(Error::DimOutOfRange {
515                shape: Box::new(shape.clone()),
516                dim: dim as i32,
517                op,
518            }
519            .bt())?
520        } else {
521            Ok(dim)
522        }
523    }
524}
525
526/// Dimension indexing from the right (end) of a shape.
527///
528/// `D::Minus1` refers to the last dimension, `D::Minus2` to the second-to-last, and
529/// `D::Minus(n)` to the n-th from the end.
530#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
531pub enum D {
532    /// The last dimension.
533    Minus1,
534    /// The second-to-last dimension.
535    Minus2,
536    /// The n-th dimension from the end.
537    Minus(usize),
538}
539
540impl D {
541    fn out_of_range(&self, shape: &Shape, op: &'static str) -> Error {
542        let dim = match self {
543            Self::Minus1 => -1,
544            Self::Minus2 => -2,
545            Self::Minus(u) => -(*u as i32),
546        };
547        Error::DimOutOfRange {
548            shape: Box::new(shape.clone()),
549            dim,
550            op,
551        }
552        .bt()
553    }
554}
555
556impl Dim for D {
557    fn to_index(&self, shape: &Shape, op: &'static str) -> Result<usize> {
558        let rank = shape.rank();
559        match self {
560            Self::Minus1 if rank >= 1 => Ok(rank - 1),
561            Self::Minus2 if rank >= 2 => Ok(rank - 2),
562            Self::Minus(u) if *u > 0 && rank >= *u => Ok(rank - *u),
563            _ => Err(self.out_of_range(shape, op)),
564        }
565    }
566
567    fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result<usize> {
568        let rank = shape.rank();
569        match self {
570            Self::Minus1 => Ok(rank),
571            Self::Minus2 if rank >= 1 => Ok(rank - 1),
572            Self::Minus(u) if *u > 0 && rank + 1 >= *u => Ok(rank + 1 - *u),
573            _ => Err(self.out_of_range(shape, op)),
574        }
575    }
576}
577
578/// A trait for types representing multiple dimension indices.
579///
580/// Used by operations that act on multiple axes simultaneously.
581pub trait Dims: Sized {
582    /// Converts the dimension references to concrete indices, checking for duplicates.
583    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>>;
584
585    fn to_indexes(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
586        let dims = self.to_indexes_internal(shape, op)?;
587        for (i, &dim) in dims.iter().enumerate() {
588            if dims[..i].contains(&dim) {
589                Err(Error::DuplicateDimIndex {
590                    shape: Box::new(shape.clone()),
591                    dims: dims.clone(),
592                    op,
593                }
594                .bt())?
595            }
596            if dim >= shape.rank() {
597                Err(Error::DimOutOfRange {
598                    shape: Box::new(shape.clone()),
599                    dim: dim as i32,
600                    op,
601                }
602                .bt())?
603            }
604        }
605        Ok(dims)
606    }
607}
608
609impl Dims for Vec<usize> {
610    fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result<Vec<usize>> {
611        Ok(self)
612    }
613}
614
615impl<const N: usize> Dims for [usize; N] {
616    fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result<Vec<usize>> {
617        Ok(self.to_vec())
618    }
619}
620
621impl Dims for &[usize] {
622    fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result<Vec<usize>> {
623        Ok(self.to_vec())
624    }
625}
626
627impl Dims for () {
628    fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result<Vec<usize>> {
629        Ok(vec![])
630    }
631}
632
633impl<D: Dim + Sized> Dims for D {
634    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
635        let dim = self.to_index(shape, op)?;
636        Ok(vec![dim])
637    }
638}
639
640impl<D: Dim> Dims for (D,) {
641    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
642        let dim = self.0.to_index(shape, op)?;
643        Ok(vec![dim])
644    }
645}
646
647impl<D1: Dim, D2: Dim> Dims for (D1, D2) {
648    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
649        let d0 = self.0.to_index(shape, op)?;
650        let d1 = self.1.to_index(shape, op)?;
651        Ok(vec![d0, d1])
652    }
653}
654
655impl<D1: Dim, D2: Dim, D3: Dim> Dims for (D1, D2, D3) {
656    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
657        let d0 = self.0.to_index(shape, op)?;
658        let d1 = self.1.to_index(shape, op)?;
659        let d2 = self.2.to_index(shape, op)?;
660        Ok(vec![d0, d1, d2])
661    }
662}
663
664impl<D1: Dim, D2: Dim, D3: Dim, D4: Dim> Dims for (D1, D2, D3, D4) {
665    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
666        let d0 = self.0.to_index(shape, op)?;
667        let d1 = self.1.to_index(shape, op)?;
668        let d2 = self.2.to_index(shape, op)?;
669        let d3 = self.3.to_index(shape, op)?;
670        Ok(vec![d0, d1, d2, d3])
671    }
672}
673
674impl<D1: Dim, D2: Dim, D3: Dim, D4: Dim, D5: Dim> Dims for (D1, D2, D3, D4, D5) {
675    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
676        let d0 = self.0.to_index(shape, op)?;
677        let d1 = self.1.to_index(shape, op)?;
678        let d2 = self.2.to_index(shape, op)?;
679        let d3 = self.3.to_index(shape, op)?;
680        let d4 = self.4.to_index(shape, op)?;
681        Ok(vec![d0, d1, d2, d3, d4])
682    }
683}
684
685impl<D1: Dim, D2: Dim, D3: Dim, D4: Dim, D5: Dim, D6: Dim> Dims for (D1, D2, D3, D4, D5, D6) {
686    fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result<Vec<usize>> {
687        let d0 = self.0.to_index(shape, op)?;
688        let d1 = self.1.to_index(shape, op)?;
689        let d2 = self.2.to_index(shape, op)?;
690        let d3 = self.3.to_index(shape, op)?;
691        let d4 = self.4.to_index(shape, op)?;
692        let d5 = self.5.to_index(shape, op)?;
693        Ok(vec![d0, d1, d2, d3, d4, d5])
694    }
695}
696
697extract_dims!(
698    /// Validates 0 dimensions (scalar).
699    dims0, 0, |_: &[usize]| (), (),
700    /// Extracts dimensions from a scalar (rank-0) shape.
701    shape);
702
703extract_dims!(
704    /// Validates 1 dimension and returns its size.
705    dims1, 1, |d: &[usize]| d[0], usize,
706    /// Extracts the single dimension from a rank-1 shape.
707    shape);
708
709extract_dims!(
710    /// Validates 2 dimensions and returns them as a tuple.
711    dims2, 2, |d: &[usize]| (d[0], d[1]), (usize, usize),
712    /// Extracts the two dimensions from a rank-2 shape.
713    shape);
714
715extract_dims!(
716    /// Validates 3 dimensions and returns them as a tuple.
717    dims3, 3, |d: &[usize]| (d[0], d[1], d[2]), (usize, usize, usize),
718    /// Extracts the three dimensions from a rank-3 shape.
719    shape);
720
721extract_dims!(
722    /// Validates 4 dimensions and returns them as a tuple.
723    dims4, 4, |d: &[usize]| (d[0], d[1], d[2], d[3]), (usize, usize, usize, usize),
724    /// Extracts the four dimensions from a rank-4 shape.
725    shape);
726
727extract_dims!(
728    /// Validates 5 dimensions and returns them as a tuple.
729    dims5, 5, |d: &[usize]| (d[0], d[1], d[2], d[3], d[4]), (usize, usize, usize, usize, usize),
730    /// Extracts the five dimensions from a rank-5 shape.
731    shape);
732
733// Stride destructure helpers — sister set to dims2/3/4/5 above, but
734// for [`crate::Layout::stride()`] which returns `&[isize]`. These cast
735// to `usize` at the destructure boundary with a debug_assert that
736// every stride is non-negative, since current kernels are written
737// against unsigned-stride byte arithmetic. The Layout-side change to
738// signed strides unlocks negative-stride view ops (e.g. Op::Flip);
739// when those land, kernels that consume the resulting layouts
740// directly should iterate via [`crate::StridedIndex`] (which handles
741// signed) rather than reading raw stride bytes through these helpers.
742
743/// Validates 2 strides and returns them as `(usize, usize)`.
744pub fn stride_dims2(stride: &[isize]) -> Result<(usize, usize)> {
745    if stride.len() != 2 {
746        return Err(Error::UnexpectedNumberOfDims {
747            expected: 2,
748            got: stride.len(),
749            shape: Box::new(Shape::from(&[][..])),
750        }
751        .bt());
752    }
753    debug_assert!(
754        stride.iter().all(|&s| s >= 0),
755        "stride_dims2: negative stride"
756    );
757    Ok((stride[0] as usize, stride[1] as usize))
758}
759
760/// Validates 3 strides and returns them as `(usize, usize, usize)`.
761pub fn stride_dims3(stride: &[isize]) -> Result<(usize, usize, usize)> {
762    if stride.len() != 3 {
763        return Err(Error::UnexpectedNumberOfDims {
764            expected: 3,
765            got: stride.len(),
766            shape: Box::new(Shape::from(&[][..])),
767        }
768        .bt());
769    }
770    debug_assert!(
771        stride.iter().all(|&s| s >= 0),
772        "stride_dims3: negative stride"
773    );
774    Ok((stride[0] as usize, stride[1] as usize, stride[2] as usize))
775}
776
777/// Validates 4 strides and returns them as `(usize, usize, usize, usize)`.
778pub fn stride_dims4(stride: &[isize]) -> Result<(usize, usize, usize, usize)> {
779    if stride.len() != 4 {
780        return Err(Error::UnexpectedNumberOfDims {
781            expected: 4,
782            got: stride.len(),
783            shape: Box::new(Shape::from(&[][..])),
784        }
785        .bt());
786    }
787    debug_assert!(
788        stride.iter().all(|&s| s >= 0),
789        "stride_dims4: negative stride"
790    );
791    Ok((
792        stride[0] as usize,
793        stride[1] as usize,
794        stride[2] as usize,
795        stride[3] as usize,
796    ))
797}
798
799/// Validates 5 strides and returns them as `(usize, usize, usize, usize, usize)`.
800pub fn stride_dims5(stride: &[isize]) -> Result<(usize, usize, usize, usize, usize)> {
801    if stride.len() != 5 {
802        return Err(Error::UnexpectedNumberOfDims {
803            expected: 5,
804            got: stride.len(),
805            shape: Box::new(Shape::from(&[][..])),
806        }
807        .bt());
808    }
809    debug_assert!(
810        stride.iter().all(|&s| s >= 0),
811        "stride_dims5: negative stride"
812    );
813    Ok((
814        stride[0] as usize,
815        stride[1] as usize,
816        stride[2] as usize,
817        stride[3] as usize,
818        stride[4] as usize,
819    ))
820}
821
822/// A trait for shape specifications that may contain one unknown dimension marked with `()`.
823///
824/// When reshaping, you can use `()` as a placeholder for one dimension and the
825/// library will infer its size from the total element count.
826pub trait ShapeWithOneHole {
827    /// Resolves the shape given the total element count, filling in the `()` hole.
828    fn into_shape(self, el_count: usize) -> Result<Shape>;
829}
830
831impl<S: Into<Shape>> ShapeWithOneHole for S {
832    fn into_shape(self, _el_count: usize) -> Result<Shape> {
833        Ok(self.into())
834    }
835}
836
837impl ShapeWithOneHole for ((),) {
838    fn into_shape(self, el_count: usize) -> Result<Shape> {
839        Ok(el_count.into())
840    }
841}
842
843fn hole_size(el_count: usize, prod_d: usize, s: &dyn std::fmt::Debug) -> Result<usize> {
844    if prod_d == 0 {
845        crate::bail!("cannot reshape tensor of {el_count} elements to {s:?}")
846    }
847    if !el_count.is_multiple_of(prod_d) {
848        crate::bail!("cannot reshape tensor with {el_count} elements to {s:?}")
849    }
850    Ok(el_count / prod_d)
851}
852
853impl ShapeWithOneHole for ((), usize) {
854    fn into_shape(self, el_count: usize) -> Result<Shape> {
855        let ((), d1) = self;
856        Ok((hole_size(el_count, d1, &self)?, d1).into())
857    }
858}
859
860impl ShapeWithOneHole for (usize, ()) {
861    fn into_shape(self, el_count: usize) -> Result<Shape> {
862        let (d1, ()) = self;
863        Ok((d1, hole_size(el_count, d1, &self)?).into())
864    }
865}
866
867impl ShapeWithOneHole for ((), usize, usize) {
868    fn into_shape(self, el_count: usize) -> Result<Shape> {
869        let ((), d1, d2) = self;
870        Ok((hole_size(el_count, d1 * d2, &self)?, d1, d2).into())
871    }
872}
873
874impl ShapeWithOneHole for (usize, (), usize) {
875    fn into_shape(self, el_count: usize) -> Result<Shape> {
876        let (d1, (), d2) = self;
877        Ok((d1, hole_size(el_count, d1 * d2, &self)?, d2).into())
878    }
879}
880
881impl ShapeWithOneHole for (usize, usize, ()) {
882    fn into_shape(self, el_count: usize) -> Result<Shape> {
883        let (d1, d2, ()) = self;
884        Ok((d1, d2, hole_size(el_count, d1 * d2, &self)?).into())
885    }
886}
887
888impl ShapeWithOneHole for ((), usize, usize, usize) {
889    fn into_shape(self, el_count: usize) -> Result<Shape> {
890        let ((), d1, d2, d3) = self;
891        let d = hole_size(el_count, d1 * d2 * d3, &self)?;
892        Ok((d, d1, d2, d3).into())
893    }
894}
895
896impl ShapeWithOneHole for (usize, (), usize, usize) {
897    fn into_shape(self, el_count: usize) -> Result<Shape> {
898        let (d1, (), d2, d3) = self;
899        let d = hole_size(el_count, d1 * d2 * d3, &self)?;
900        Ok((d1, d, d2, d3).into())
901    }
902}
903
904impl ShapeWithOneHole for (usize, usize, (), usize) {
905    fn into_shape(self, el_count: usize) -> Result<Shape> {
906        let (d1, d2, (), d3) = self;
907        let d = hole_size(el_count, d1 * d2 * d3, &self)?;
908        Ok((d1, d2, d, d3).into())
909    }
910}
911
912impl ShapeWithOneHole for (usize, usize, usize, ()) {
913    fn into_shape(self, el_count: usize) -> Result<Shape> {
914        let (d1, d2, d3, ()) = self;
915        let d = hole_size(el_count, d1 * d2 * d3, &self)?;
916        Ok((d1, d2, d3, d).into())
917    }
918}
919
920impl ShapeWithOneHole for ((), usize, usize, usize, usize) {
921    fn into_shape(self, el_count: usize) -> Result<Shape> {
922        let ((), d1, d2, d3, d4) = self;
923        let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?;
924        Ok((d, d1, d2, d3, d4).into())
925    }
926}
927
928impl ShapeWithOneHole for (usize, (), usize, usize, usize) {
929    fn into_shape(self, el_count: usize) -> Result<Shape> {
930        let (d1, (), d2, d3, d4) = self;
931        let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?;
932        Ok((d1, d, d2, d3, d4).into())
933    }
934}
935
936impl ShapeWithOneHole for (usize, usize, (), usize, usize) {
937    fn into_shape(self, el_count: usize) -> Result<Shape> {
938        let (d1, d2, (), d3, d4) = self;
939        let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?;
940        Ok((d1, d2, d, d3, d4).into())
941    }
942}
943
944impl ShapeWithOneHole for (usize, usize, usize, (), usize) {
945    fn into_shape(self, el_count: usize) -> Result<Shape> {
946        let (d1, d2, d3, (), d4) = self;
947        let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?;
948        Ok((d1, d2, d3, d, d4).into())
949    }
950}
951
952impl ShapeWithOneHole for (usize, usize, usize, usize, ()) {
953    fn into_shape(self, el_count: usize) -> Result<Shape> {
954        let (d1, d2, d3, d4, ()) = self;
955        let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?;
956        Ok((d1, d2, d3, d4, d).into())
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    #[test]
965    fn stride() {
966        let shape = Shape::from(());
967        assert_eq!(shape.stride_contiguous().to_vec(), Vec::<isize>::new());
968        let shape = Shape::from(42);
969        assert_eq!(shape.stride_contiguous().to_vec(), [1_isize]);
970        let shape = Shape::from((42, 1337));
971        assert_eq!(shape.stride_contiguous().to_vec(), [1337_isize, 1]);
972        let shape = Shape::from((299, 792, 458));
973        assert_eq!(
974            shape.stride_contiguous().to_vec(),
975            [458_isize * 792, 458, 1]
976        );
977    }
978
979    #[test]
980    fn test_from_tuple() {
981        let shape = Shape::from((2,));
982        assert_eq!(shape.dims(), &[2]);
983        let shape = Shape::from((2, 3));
984        assert_eq!(shape.dims(), &[2, 3]);
985        let shape = Shape::from((2, 3, 4));
986        assert_eq!(shape.dims(), &[2, 3, 4]);
987        let shape = Shape::from((2, 3, 4, 5));
988        assert_eq!(shape.dims(), &[2, 3, 4, 5]);
989        let shape = Shape::from((2, 3, 4, 5, 6));
990        assert_eq!(shape.dims(), &[2, 3, 4, 5, 6]);
991        let shape = Shape::from((2, 3, 4, 5, 6, 7));
992        assert_eq!(shape.dims(), &[2, 3, 4, 5, 6, 7]);
993    }
994
995    /// PR step-1b: the symbolic-extent foundation. `dims()` returns the
996    /// capacity bounds unchanged; `extent()` is the symbolic view;
997    /// `resolve()` substitutes live values (erroring when unbound); two shapes
998    /// sharing a `sym` resolve together; equality includes `dynamic`; `Layout`
999    /// inherits `Extent` and resolves while keeping capacity strides. Born-red:
1000    /// fails if any of those contracts is wrong.
1001    #[test]
1002    fn symbolic_extent_foundation() {
1003        use crate::{Layout, SymEnv, SymId};
1004
1005        // A capacity-shaped KV buffer with a dynamic length axis (axis 2).
1006        let sym = SymId(0);
1007        let shape = Shape::from_dims(&[1, 32, 4096, 128]).with_dynamic_axis(2, 0, sym);
1008
1009        // dims() returns the BOUNDS (axis 2 = capacity 4096), contract unchanged.
1010        assert_eq!(shape.dims(), &[1, 32, 4096, 128]);
1011        assert!(shape.has_dynamic());
1012
1013        // extent() is the symbolic view.
1014        assert_eq!(shape.extent(0), Extent::Scalar(1));
1015        assert_eq!(
1016            shape.extent(2),
1017            Extent::Range {
1018                min: 0,
1019                max: 4096,
1020                sym
1021            }
1022        );
1023        assert_eq!(shape.extent(2).bound(), 4096);
1024        assert!(shape.extent(2).is_dynamic());
1025        assert_eq!(shape.extent(2).sym(), Some(sym));
1026        assert_eq!(shape.extent(0).sym(), None);
1027        assert_eq!(shape.extents().count(), 4);
1028
1029        // resolve() substitutes the live value; unbound is an error.
1030        let mut env = SymEnv::new();
1031        assert!(shape.resolve(&env).is_err(), "unbound symbol must error");
1032        env.bind(sym, 53).unwrap();
1033        let concrete = shape.resolve(&env).unwrap();
1034        assert_eq!(concrete.dims(), &[1, 32, 53, 128]);
1035        assert!(!concrete.has_dynamic());
1036        assert_eq!(shape.extent(2).resolve(&env), Some(53));
1037
1038        // Two shapes sharing a sym resolve together (unification via id).
1039        let other = Shape::from_dims(&[1, 32, 4096, 128]).with_dynamic_axis(2, 0, sym);
1040        assert_eq!(other.resolve(&env).unwrap().dims(), &[1, 32, 53, 128]);
1041
1042        // Equality includes `dynamic`: symbolic != concrete-with-same-bounds.
1043        let concrete_caps = Shape::from_dims(&[1, 32, 4096, 128]);
1044        assert_ne!(shape, concrete_caps);
1045        assert_eq!(shape, other);
1046        assert!(!concrete_caps.has_dynamic());
1047        // from_dims stays the all-Scalar constructor.
1048        assert_eq!(concrete_caps.extent(2), Extent::Scalar(4096));
1049
1050        // elem_count() is the capacity product (bound), not the live count.
1051        assert_eq!(shape.elem_count(), 32 * 4096 * 128);
1052
1053        // Layout inherits Extent via its embedded Shape; resolve keeps strides.
1054        let layout = Layout::contiguous_with_offset(shape.clone(), 0);
1055        assert!(layout.has_dynamic());
1056        let rlayout = layout.resolve(&env).unwrap();
1057        assert_eq!(rlayout.dims(), &[1, 32, 53, 128]);
1058        assert_eq!(
1059            rlayout.stride(),
1060            layout.stride(),
1061            "strides stay capacity-based"
1062        );
1063    }
1064
1065    #[test]
1066    fn broadcast_identical_shapes() {
1067        let a = Shape::from((2, 3));
1068        let b = Shape::from((2, 3));
1069        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1070        assert_eq!(result.dims(), &[2, 3]);
1071    }
1072
1073    #[test]
1074    fn broadcast_broadcasts_one_on_lhs() {
1075        let a = Shape::from((1, 3));
1076        let b = Shape::from((2, 3));
1077        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1078        assert_eq!(result.dims(), &[2, 3]);
1079    }
1080
1081    #[test]
1082    fn broadcast_broadcasts_one_on_rhs() {
1083        let a = Shape::from((2, 1));
1084        let b = Shape::from((2, 3));
1085        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1086        assert_eq!(result.dims(), &[2, 3]);
1087    }
1088
1089    #[test]
1090    fn broadcast_scalar_with_matrix() {
1091        let a = Shape::from(());
1092        let b = Shape::from((2, 3));
1093        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1094        assert_eq!(result.dims(), &[2, 3]);
1095    }
1096
1097    #[test]
1098    fn broadcast_different_ranks() {
1099        let a = Shape::from((1,));
1100        let b = Shape::from((3, 4, 5));
1101        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1102        assert_eq!(result.dims(), &[3, 4, 5]);
1103    }
1104
1105    #[test]
1106    fn broadcast_incompatible_shapes() {
1107        let a = Shape::from((2, 3));
1108        let b = Shape::from((4, 3));
1109        assert!(a.broadcast_shape_binary_op(&b, "add").is_err());
1110    }
1111
1112    #[test]
1113    fn broadcast_both_ones_differ() {
1114        let a = Shape::from((1, 3));
1115        let b = Shape::from((2, 1));
1116        let result = a.broadcast_shape_binary_op(&b, "add").unwrap();
1117        assert_eq!(result.dims(), &[2, 3]);
1118    }
1119
1120    #[test]
1121    fn broadcast_3d_mixed() {
1122        let a = Shape::from((2, 1, 4));
1123        let b = Shape::from((1, 3, 1));
1124        let result = a.broadcast_shape_binary_op(&b, "mul").unwrap();
1125        assert_eq!(result.dims(), &[2, 3, 4]);
1126    }
1127}