Skip to main content

hurray_core/
shape.rs

1//! Tensor shape — rank and dimension sizes.
2//!
3//! A [`Shape`] is an ordered sequence of dimension sizes. Each size is a
4//! `u64`; the sentinel value [`DYNAMIC`] (`u64::MAX`) marks a dimension whose
5//! concrete size is not known at descriptor-write time.
6//!
7//! See `docs/spec/data-model.md` for the normative definition.
8
9use std::fmt;
10
11use crate::Error;
12
13/// Sentinel value for a **dynamic dimension** whose concrete size is not known
14/// at descriptor-write time.
15///
16/// In the wire encoding this is `0xFFFF_FFFF_FFFF_FFFF` (`UINT64_MAX`).
17/// A reader MUST NOT compute buffer sizes, strides, or element counts for a
18/// tensor containing a dynamic dimension without first resolving it to a
19/// concrete value.
20///
21/// # Examples
22///
23/// ```
24/// use hurray_core::{Shape, DYNAMIC};
25///
26/// let shape = Shape::new(vec![1, DYNAMIC, 768]).expect("valid shape");
27/// assert!(shape.has_dynamic());
28/// assert!(shape.element_count().is_none());
29/// ```
30pub const DYNAMIC: u64 = u64::MAX;
31
32/// Maximum permitted tensor rank.
33///
34/// A writer MUST NOT emit a descriptor with `rank > MAX_RANK`. A reader MUST
35/// reject any descriptor whose rank field exceeds this value.
36///
37/// The cap matches PyTorch's `MAX_DIMS = 64` and bounds the shape-array size
38/// at 512 bytes (64 × 8 bytes per dimension), enabling stack allocation on the
39/// descriptor-parsing hot path.
40///
41/// # Examples
42///
43/// ```
44/// use hurray_core::MAX_RANK;
45///
46/// assert_eq!(MAX_RANK, 64);
47/// ```
48pub const MAX_RANK: usize = 64;
49
50/// The shape of a tensor: an ordered sequence of dimension sizes.
51///
52/// Each dimension size is a [`u64`]. The special value [`DYNAMIC`] marks a
53/// dimension whose concrete size is unknown at descriptor-write time. A size
54/// of `0` denotes an empty dimension (the tensor contains no elements along
55/// that axis).
56///
57/// A [`Shape`] with no dimensions (`rank == 0`) represents a **scalar tensor**
58/// containing exactly one element.
59///
60/// # Examples
61///
62/// ```
63/// use hurray_core::{Shape, DYNAMIC};
64///
65/// // 3-D tensor with all static dimensions.
66/// let s = Shape::new(vec![3, 4, 5]).expect("valid");
67/// assert_eq!(s.rank(), 3);
68/// assert_eq!(s.element_count(), Some(60));
69///
70/// // Scalar tensor.
71/// let scalar = Shape::scalar();
72/// assert_eq!(scalar.rank(), 0);
73/// assert_eq!(scalar.element_count(), Some(1));
74///
75/// // Shape with a dynamic dimension.
76/// let dyn_shape = Shape::new(vec![1, DYNAMIC, 768]).expect("valid");
77/// assert!(dyn_shape.has_dynamic());
78/// assert!(dyn_shape.element_count().is_none());
79/// ```
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub struct Shape(Vec<u64>);
82
83impl Shape {
84    /// Creates a new [`Shape`] from the given dimension sizes.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`Error::RankExceedsMaximum`] if `dims.len() > MAX_RANK` (64).
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use hurray_core::{Shape, Error};
94    ///
95    /// let s = Shape::new(vec![3, 4, 5]).expect("valid");
96    /// assert_eq!(s.rank(), 3);
97    ///
98    /// // Rank 65 must be rejected.
99    /// let too_many = Shape::new(vec![1u64; 65]);
100    /// assert!(matches!(too_many, Err(Error::RankExceedsMaximum { rank: 65, max: 64 })));
101    /// ```
102    pub fn new(dims: impl Into<Vec<u64>>) -> crate::Result<Self> {
103        let dims = dims.into();
104        if dims.len() > MAX_RANK {
105            return Err(Error::RankExceedsMaximum {
106                rank: dims.len() as u32,
107                max: MAX_RANK as u32,
108            });
109        }
110        Ok(Self(dims))
111    }
112
113    /// Returns a [`Shape`] representing a **scalar tensor** (rank 0, one element).
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use hurray_core::Shape;
119    ///
120    /// let s = Shape::scalar();
121    /// assert_eq!(s.rank(), 0);
122    /// assert_eq!(s.dims(), &[]);
123    /// assert_eq!(s.element_count(), Some(1));
124    /// ```
125    #[inline]
126    pub fn scalar() -> Self {
127        Self(Vec::new())
128    }
129
130    /// Returns the number of dimensions (the rank) of this shape.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use hurray_core::Shape;
136    ///
137    /// assert_eq!(Shape::scalar().rank(), 0);
138    /// assert_eq!(Shape::new(vec![3, 4]).unwrap().rank(), 2);
139    /// ```
140    #[inline]
141    pub fn rank(&self) -> usize {
142        self.0.len()
143    }
144
145    /// Returns the dimension sizes as a slice.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use hurray_core::Shape;
151    ///
152    /// let s = Shape::new(vec![2, 3]).unwrap();
153    /// assert_eq!(s.dims(), &[2, 3]);
154    /// ```
155    #[inline]
156    pub fn dims(&self) -> &[u64] {
157        &self.0
158    }
159
160    /// Returns the total number of logical elements, or `None` if any
161    /// dimension is [`DYNAMIC`] or if the product overflows `u64`.
162    ///
163    /// The product of an empty sequence (scalar) is `1`. A tensor with any
164    /// zero-size dimension has `element_count == Some(0)`.
165    ///
166    /// A reader MUST NOT use this value to compute buffer sizes or strides
167    /// until all dynamic dimensions have been resolved.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use hurray_core::{Shape, DYNAMIC};
173    ///
174    /// // Static shape.
175    /// assert_eq!(Shape::new(vec![3, 4, 5]).unwrap().element_count(), Some(60));
176    ///
177    /// // Scalar: product of empty sequence is 1.
178    /// assert_eq!(Shape::scalar().element_count(), Some(1));
179    ///
180    /// // Empty tensor: any zero dimension makes the count 0.
181    /// assert_eq!(Shape::new(vec![3, 0, 5]).unwrap().element_count(), Some(0));
182    ///
183    /// // Dynamic dimension: result is unknown.
184    /// assert_eq!(Shape::new(vec![1, DYNAMIC, 768]).unwrap().element_count(), None);
185    /// ```
186    pub fn element_count(&self) -> Option<u64> {
187        let mut product: u64 = 1;
188        for &dim in &self.0 {
189            if dim == DYNAMIC {
190                return None;
191            }
192            product = product.checked_mul(dim)?;
193        }
194        Some(product)
195    }
196
197    /// Returns `true` if any dimension has size `0`.
198    ///
199    /// An empty tensor is valid; its data buffer has size 0 bytes. Note that
200    /// a [`DYNAMIC`] dimension does not make a tensor empty — its size is
201    /// unknown, not zero.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use hurray_core::{Shape, DYNAMIC};
207    ///
208    /// assert!(Shape::new(vec![3, 0, 5]).unwrap().is_empty_tensor());
209    /// assert!(!Shape::new(vec![3, 4, 5]).unwrap().is_empty_tensor());
210    /// // Dynamic is not empty.
211    /// assert!(!Shape::new(vec![1, DYNAMIC, 768]).unwrap().is_empty_tensor());
212    /// ```
213    #[inline]
214    pub fn is_empty_tensor(&self) -> bool {
215        self.0.contains(&0)
216    }
217
218    /// Returns `true` if any dimension equals [`DYNAMIC`].
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use hurray_core::{Shape, DYNAMIC};
224    ///
225    /// assert!(Shape::new(vec![1, DYNAMIC, 768]).unwrap().has_dynamic());
226    /// assert!(!Shape::new(vec![1, 128, 768]).unwrap().has_dynamic());
227    /// ```
228    #[inline]
229    pub fn has_dynamic(&self) -> bool {
230        self.0.contains(&DYNAMIC)
231    }
232}
233
234impl fmt::Display for Shape {
235    /// Formats the shape as a bracket-enclosed, comma-separated list of sizes.
236    ///
237    /// Dynamic dimensions are shown as `?`. An empty (scalar) shape is `[]`.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use hurray_core::{Shape, DYNAMIC};
243    ///
244    /// assert_eq!(Shape::scalar().to_string(), "[]");
245    /// assert_eq!(Shape::new(vec![3, 4, 5]).unwrap().to_string(), "[3, 4, 5]");
246    /// assert_eq!(Shape::new(vec![1, DYNAMIC, 768]).unwrap().to_string(), "[1, ?, 768]");
247    /// ```
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        f.write_str("[")?;
250        for (i, &dim) in self.0.iter().enumerate() {
251            if i > 0 {
252                f.write_str(", ")?;
253            }
254            if dim == DYNAMIC {
255                f.write_str("?")?;
256            } else {
257                write!(f, "{dim}")?;
258            }
259        }
260        f.write_str("]")
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    // ── Shape::new ───────────────────────────────────────────────────────────
269
270    #[test]
271    fn new_rank_0_succeeds() {
272        let s = Shape::new(vec![]).expect("rank 0 is valid");
273        assert_eq!(s.rank(), 0);
274    }
275
276    #[test]
277    fn new_rank_1_succeeds() {
278        let s = Shape::new(vec![42]).expect("rank 1 is valid");
279        assert_eq!(s.rank(), 1);
280        assert_eq!(s.dims(), &[42]);
281    }
282
283    #[test]
284    fn new_rank_64_succeeds() {
285        let s = Shape::new(vec![1u64; 64]).expect("rank 64 is valid (MAX_RANK)");
286        assert_eq!(s.rank(), 64);
287    }
288
289    /// Spec § data-model: a writer MUST NOT emit a descriptor with rank > MAX_RANK;
290    /// a reader MUST reject any descriptor whose rank field exceeds this value.
291    #[test]
292    fn new_rank_65_returns_rank_exceeds_maximum() {
293        let result = Shape::new(vec![1u64; 65]);
294        assert!(
295            matches!(result, Err(Error::RankExceedsMaximum { rank: 65, max: 64 })),
296            "expected RankExceedsMaximum for rank 65, got {result:?}"
297        );
298    }
299
300    #[test]
301    fn new_rank_100_returns_rank_exceeds_maximum() {
302        let result = Shape::new(vec![1u64; 100]);
303        assert!(matches!(
304            result,
305            Err(Error::RankExceedsMaximum { rank: 100, max: 64 })
306        ));
307    }
308
309    // ── Shape::scalar ────────────────────────────────────────────────────────
310
311    #[test]
312    fn scalar_has_rank_0() {
313        assert_eq!(Shape::scalar().rank(), 0);
314    }
315
316    #[test]
317    fn scalar_dims_is_empty() {
318        assert_eq!(Shape::scalar().dims(), &[] as &[u64]);
319    }
320
321    #[test]
322    fn scalar_element_count_is_one() {
323        assert_eq!(Shape::scalar().element_count(), Some(1));
324    }
325
326    #[test]
327    fn scalar_is_not_empty_tensor() {
328        assert!(!Shape::scalar().is_empty_tensor());
329    }
330
331    #[test]
332    fn scalar_has_no_dynamic() {
333        assert!(!Shape::scalar().has_dynamic());
334    }
335
336    // ── rank ─────────────────────────────────────────────────────────────────
337
338    #[test]
339    fn rank_matches_dims_len() {
340        for n in [0, 1, 2, 10, 64] {
341            let s = Shape::new(vec![3u64; n]).expect("valid");
342            assert_eq!(s.rank(), n, "rank mismatch for n={n}");
343        }
344    }
345
346    // ── dims ─────────────────────────────────────────────────────────────────
347
348    #[test]
349    fn dims_returns_original_slice() {
350        let input = vec![3u64, 4, 5];
351        let s = Shape::new(input.clone()).expect("valid");
352        assert_eq!(s.dims(), input.as_slice());
353    }
354
355    #[test]
356    fn dims_is_empty_for_scalar() {
357        assert!(Shape::scalar().dims().is_empty());
358    }
359
360    // ── element_count ────────────────────────────────────────────────────────
361
362    #[test]
363    fn element_count_static_shape_3x4x5() {
364        let s = Shape::new(vec![3, 4, 5]).expect("valid");
365        assert_eq!(s.element_count(), Some(60));
366    }
367
368    #[test]
369    fn element_count_1d_shape() {
370        assert_eq!(Shape::new(vec![7]).unwrap().element_count(), Some(7));
371    }
372
373    #[test]
374    fn element_count_scalar_is_one() {
375        assert_eq!(Shape::scalar().element_count(), Some(1));
376    }
377
378    #[test]
379    fn element_count_with_zero_dim_is_zero() {
380        assert_eq!(Shape::new(vec![3, 0, 5]).unwrap().element_count(), Some(0));
381    }
382
383    #[test]
384    fn element_count_all_zero_dims_is_zero() {
385        assert_eq!(Shape::new(vec![0, 0]).unwrap().element_count(), Some(0));
386    }
387
388    #[test]
389    fn element_count_with_dynamic_is_none() {
390        assert_eq!(
391            Shape::new(vec![1, DYNAMIC, 768]).unwrap().element_count(),
392            None
393        );
394    }
395
396    #[test]
397    fn element_count_all_dynamic_is_none() {
398        assert_eq!(
399            Shape::new(vec![DYNAMIC, DYNAMIC]).unwrap().element_count(),
400            None
401        );
402    }
403
404    /// Overflow during product computation must return None, not panic.
405    #[test]
406    fn element_count_overflow_returns_none() {
407        // u64::MAX / 2 + 1 times two dims = overflow
408        let big = u64::MAX / 2 + 1;
409        let s = Shape::new(vec![big, 2]).expect("rank 2 is valid");
410        assert_eq!(s.element_count(), None);
411    }
412
413    // ── is_empty_tensor ──────────────────────────────────────────────────────
414
415    #[test]
416    fn is_empty_tensor_true_when_one_dim_is_zero() {
417        assert!(Shape::new(vec![3, 0, 5]).unwrap().is_empty_tensor());
418    }
419
420    #[test]
421    fn is_empty_tensor_true_when_all_dims_zero() {
422        assert!(Shape::new(vec![0]).unwrap().is_empty_tensor());
423    }
424
425    #[test]
426    fn is_empty_tensor_false_for_static_nonzero_shape() {
427        assert!(!Shape::new(vec![3, 4, 5]).unwrap().is_empty_tensor());
428    }
429
430    /// A DYNAMIC dimension is unknown, not zero — the tensor is not empty.
431    #[test]
432    fn is_empty_tensor_false_for_dynamic_dim() {
433        assert!(!Shape::new(vec![1, DYNAMIC, 768]).unwrap().is_empty_tensor());
434    }
435
436    #[test]
437    fn is_empty_tensor_false_for_scalar() {
438        assert!(!Shape::scalar().is_empty_tensor());
439    }
440
441    // ── has_dynamic ──────────────────────────────────────────────────────────
442
443    #[test]
444    fn has_dynamic_true_when_one_dim_is_dynamic() {
445        assert!(Shape::new(vec![1, DYNAMIC, 768]).unwrap().has_dynamic());
446    }
447
448    #[test]
449    fn has_dynamic_true_when_all_dims_dynamic() {
450        assert!(Shape::new(vec![DYNAMIC, DYNAMIC]).unwrap().has_dynamic());
451    }
452
453    #[test]
454    fn has_dynamic_false_for_fully_static_shape() {
455        assert!(!Shape::new(vec![1, 128, 768]).unwrap().has_dynamic());
456    }
457
458    #[test]
459    fn has_dynamic_false_for_scalar() {
460        assert!(!Shape::scalar().has_dynamic());
461    }
462
463    // ── Display ──────────────────────────────────────────────────────────────
464
465    #[test]
466    fn display_scalar_is_empty_brackets() {
467        assert_eq!(Shape::scalar().to_string(), "[]");
468    }
469
470    #[test]
471    fn display_1d_shape() {
472        assert_eq!(Shape::new(vec![5]).unwrap().to_string(), "[5]");
473    }
474
475    #[test]
476    fn display_3d_static_shape() {
477        assert_eq!(Shape::new(vec![3, 4, 5]).unwrap().to_string(), "[3, 4, 5]");
478    }
479
480    #[test]
481    fn display_dynamic_dimension_shown_as_question_mark() {
482        assert_eq!(
483            Shape::new(vec![1, DYNAMIC, 768]).unwrap().to_string(),
484            "[1, ?, 768]"
485        );
486    }
487
488    #[test]
489    fn display_all_dynamic() {
490        assert_eq!(
491            Shape::new(vec![DYNAMIC, DYNAMIC]).unwrap().to_string(),
492            "[?, ?]"
493        );
494    }
495
496    #[test]
497    fn display_zero_dimension() {
498        assert_eq!(Shape::new(vec![3, 0, 5]).unwrap().to_string(), "[3, 0, 5]");
499    }
500
501    // ── DYNAMIC and MAX_RANK constants ───────────────────────────────────────
502
503    #[test]
504    fn dynamic_constant_is_u64_max() {
505        assert_eq!(DYNAMIC, u64::MAX);
506    }
507
508    #[test]
509    fn max_rank_constant_is_64() {
510        assert_eq!(MAX_RANK, 64);
511    }
512}