Skip to main content

ndslice/
slice.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use std::iter::zip;
10
11use serde::Deserialize;
12use serde::Serialize;
13
14/// The type of error for slice operations.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum SliceError {
18    #[error("invalid dims: expected {expected}, got {got}")]
19    InvalidDims { expected: usize, got: usize },
20
21    #[error("nonrectangular shape")]
22    NonrectangularShape,
23
24    #[error("nonunique strides")]
25    NonuniqueStrides,
26
27    #[error("stride {stride} must be larger than size of previous space {space}")]
28    StrideTooSmall { stride: usize, space: usize },
29
30    #[error("index {index} out of range {total}")]
31    IndexOutOfRange { index: usize, total: usize },
32
33    #[error("value {value} not in slice")]
34    ValueNotInSlice { value: usize },
35
36    #[error("incompatible view: {reason}")]
37    IncompatibleView { reason: String },
38
39    #[error("noncontiguous shape")]
40    NonContiguous,
41
42    #[error("empty range: {begin}..{end} (step {step})")]
43    EmptyRange {
44        begin: usize,
45        end: usize,
46        step: usize,
47    },
48
49    #[error("dimension {dim} out of range for {ndims}-dimensional slice")]
50    DimensionOutOfRange { dim: usize, ndims: usize },
51
52    #[error("slice arithmetic overflow")]
53    ArithmeticOverflow,
54}
55
56/// Slice is a compact representation of indices into the flat
57/// representation of an n-dimensional array. Given an offset, sizes of
58/// each dimension, and strides for each dimension, Slice can compute
59/// indices into the flat array.
60///
61/// For example, the following describes a dense 4x4x4 array in row-major
62/// order:
63/// ```
64/// # use ndslice::Slice;
65/// let s = Slice::new(0, vec![4, 4, 4], vec![16, 4, 1]).unwrap();
66/// assert!(s.iter().eq(0..(4 * 4 * 4)));
67/// ```
68///
69/// Slices allow easy slicing by subsetting and striding. For example,
70/// we can fix the index of the second dimension by dropping it and
71/// adding that index (multiplied by the previous size) to the offset.
72///
73/// ```
74/// # use ndslice::Slice;
75/// let s = Slice::new(0, vec![2, 4, 2], vec![8, 2, 1]).unwrap();
76/// let selected_index = 3;
77/// let sub = Slice::new(2 * selected_index, vec![2, 2], vec![8, 1]).unwrap();
78/// let coords = [[0, 0], [0, 1], [1, 0], [1, 1]];
79/// for coord @ [x, y] in coords {
80///     assert_eq!(
81///         sub.location(&coord).unwrap(),
82///         s.location(&[x, 3, y]).unwrap()
83///     );
84/// }
85/// ```
86// TODO: Consider representing this by arrays parameterized by the slice
87// dimensionality.
88#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)]
89pub struct Slice {
90    offset: usize,
91    sizes: Vec<usize>,
92    strides: Vec<usize>,
93}
94
95impl Slice {
96    /// Create a new Slice with the provided offset, sizes, and
97    /// strides. New performs validation to ensure that sizes and strides
98    /// are compatible:
99    ///   - They have to be the same length (i.e., same number of dimensions)
100    ///   - They have to be rectangular (i.e., stride n+1 has to evenly divide into stride n)
101    ///   - Strides must be nonoverlapping (each stride has to be larger than the previous space)
102    pub fn new(offset: usize, sizes: Vec<usize>, strides: Vec<usize>) -> Result<Self, SliceError> {
103        if sizes.len() != strides.len() {
104            return Err(SliceError::InvalidDims {
105                expected: sizes.len(),
106                got: strides.len(),
107            });
108        }
109        let mut combined: Vec<(usize, usize)> =
110            strides.iter().cloned().zip(sizes.iter().cloned()).collect();
111        combined.sort();
112
113        let mut prev_stride: Option<usize> = None;
114        let mut prev_size: Option<usize> = None;
115        let mut total: usize = 1;
116        for (stride, size) in combined {
117            if let Some(prev_stride) = prev_stride {
118                if stride % prev_stride != 0 {
119                    return Err(SliceError::NonrectangularShape);
120                }
121                // Strides for single element dimensions can repeat, because they are unused
122                if stride == prev_stride && size != 1 && prev_size.unwrap_or(1) != 1 {
123                    return Err(SliceError::NonuniqueStrides);
124                }
125            }
126            if total > stride {
127                return Err(SliceError::StrideTooSmall {
128                    stride,
129                    space: total,
130                });
131            }
132            total = stride * size;
133            prev_stride = Some(stride);
134            prev_size = Some(size);
135        }
136
137        Ok(Slice {
138            offset,
139            sizes,
140            strides,
141        })
142    }
143
144    /// Deconstruct the slice into its offset, sizes, and strides.
145    pub fn into_inner(self) -> (usize, Vec<usize>, Vec<usize>) {
146        let Slice {
147            offset,
148            sizes,
149            strides,
150        } = self;
151        (offset, sizes, strides)
152    }
153
154    /// Create a new slice of the given sizes in row-major order.
155    pub fn new_row_major(sizes: impl Into<Vec<usize>>) -> Self {
156        let sizes = sizes.into();
157        // "flip it and reverse it" --Missy Elliott
158        let mut strides: Vec<usize> = sizes.clone();
159        let _ = strides.iter_mut().rev().fold(1, |acc, n| {
160            let next = *n * acc;
161            *n = acc;
162            next
163        });
164        Self {
165            offset: 0,
166            sizes,
167            strides,
168        }
169    }
170
171    /// Create one celled slice.
172    pub fn new_single_multi_dim_cell(dims: usize) -> Self {
173        Self {
174            offset: 0,
175            sizes: vec![1; dims],
176            strides: vec![1; dims],
177        }
178    }
179
180    /// The number of dimensions in this slice.
181    pub fn num_dim(&self) -> usize {
182        self.sizes.len()
183    }
184
185    /// This is the offset from which the first value in the Slice begins.
186    pub fn offset(&self) -> usize {
187        self.offset
188    }
189
190    /// The shape of the slice; that is, the size of each dimension.
191    pub fn sizes(&self) -> &[usize] {
192        &self.sizes
193    }
194
195    /// The strides of the slice; that is, the distance between each
196    /// element at a given index in the underlying array.
197    pub fn strides(&self) -> &[usize] {
198        &self.strides
199    }
200
201    /// Express this slice in the ordinal space of `parent`.
202    ///
203    /// Returns `None` if this slice cannot be represented as one affine slice
204    /// in the ordinal space of `parent`.
205    ///
206    /// ```text
207    /// parent ranks (offset=100, sizes=[4, 4], strides=[8, 2]):
208    /// 100  102   104  106
209    /// 108 [110] [112] 114
210    /// 116  118   120  122
211    /// 124 [126] [128] 130
212    ///
213    /// self:              offset=110, sizes=[2, 2], strides=[16, 2]
214    /// selected ranks:   [110, 112, 126, 128]
215    /// parent ordinals:  [  5,   6,  13,  14]
216    /// result: offset=5, sizes=[2, 2], strides=[8, 1]
217    /// ```
218    pub fn relative_ordinal_slice(&self, parent: &Self) -> Option<Self> {
219        if parent.num_dim() != self.num_dim() {
220            return None;
221        }
222
223        // parent_coordinates: [1, 1]
224        let parent_coordinates = parent.coordinates(self.offset()).ok()?;
225        // offset: 5
226        let offset = parent.index(self.offset()).ok()?;
227        // parent_ordinal_strides: [4, 1]
228        let parent_ordinal_strides = Slice::new_row_major(parent.sizes().to_vec());
229        let strides = self
230            .sizes()
231            .iter()
232            .enumerate()
233            .map(|(dimension, &extent)| -> Option<usize> {
234                if extent == 1 {
235                    return Some(parent_ordinal_strides.strides()[dimension]);
236                }
237
238                let parent_stride = parent.strides()[dimension];
239                let selected_stride = self.strides()[dimension];
240                if !selected_stride.is_multiple_of(parent_stride) {
241                    return None;
242                }
243
244                let step = selected_stride / parent_stride;
245                {
246                    let last_coordinate = parent_coordinates[dimension].checked_add(
247                        extent
248                            .checked_sub(1)
249                            .and_then(|extent| extent.checked_mul(step))?,
250                    )?;
251
252                    if last_coordinate >= parent.sizes()[dimension] {
253                        return None;
254                    }
255                }
256
257                parent_ordinal_strides.strides()[dimension].checked_mul(step)
258            })
259            .collect::<Option<_>>()?;
260
261        Self::new(offset, self.sizes().to_vec(), strides).ok()
262    }
263
264    pub fn is_contiguous(&self) -> bool {
265        let mut expected_stride = 1;
266        for (stride, size) in zip(self.strides.iter(), self.sizes.iter()).rev() {
267            if *stride != expected_stride {
268                return false;
269            }
270            expected_stride *= *size
271        }
272        true
273    }
274
275    /// Select a single index along a dimension, removing that
276    /// dimension entirely.
277    ///
278    /// This reduces the dimensionality by 1 by "fixing" one
279    /// coordinate to a specific value. Think of it like taking a
280    /// cross-section: selecting index 2 from the first dimension of a
281    /// 3D array gives you a 2D slice, like cutting a plane from a 3D
282    /// space at a fixed position.
283    ///
284    /// This reduces the dimensionality by 1 by "fixing" one
285    /// coordinate to a specific value. The fixed coordinate's
286    /// contribution (index × stride) gets absorbed into the base
287    /// offset, while the remaining dimensions keep their original
288    /// strides unchanged - they still describe the same memory
289    /// distances between elements.
290    ///
291    /// # Example intuition
292    /// - 3D array → select `at(dim=0, index=2)` → 2D slice (like a
293    ///   plane)
294    /// - 2D matrix → select `at(dim=1, index=3)` → 1D vector (like a
295    ///   column)
296    /// - 1D vector → select `at(dim=0, index=5)` → 0D scalar (single
297    ///   element)
298    ///
299    /// # Arguments
300    /// * `dim` - The dimension index to select from
301    /// * `index` - The index within that dimension
302    ///
303    /// # Returns
304    /// A new slice with one fewer dimension
305    ///
306    /// # Errors
307    /// * `IndexOutOfRange` if `dim >= self.sizes.len()` or `index >=
308    ///   self.sizes[dim]`
309    pub fn at(&self, dim: usize, index: usize) -> Result<Self, SliceError> {
310        if dim >= self.sizes.len() {
311            return Err(SliceError::DimensionOutOfRange {
312                dim,
313                ndims: self.num_dim(),
314            });
315        }
316        if index >= self.sizes[dim] {
317            return Err(SliceError::IndexOutOfRange {
318                index,
319                total: self.sizes[dim],
320            });
321        }
322
323        let new_offset = self.offset + index * self.strides[dim];
324        let mut new_sizes = self.sizes.clone();
325        let mut new_strides = self.strides.clone();
326        new_sizes.remove(dim);
327        new_strides.remove(dim);
328        let slice = Slice::new(new_offset, new_sizes, new_strides)?;
329        Ok(slice)
330    }
331
332    /// A slice defines a **strided view**; a triple (`offset,
333    /// `sizes`, `strides`). Each coordinate maps to a flat memory
334    /// index using the formula:
335    /// ```text
336    /// index = offset + ∑ iₖ × strides[k]
337    /// ```
338    /// where `iₖ` is the coordinate in dimension `k`.
339    ///
340    /// The `select(dim, range)` operation restricts the view to a
341    /// subrange along a single dimension. It calculates a new slice
342    /// from a base slice by updating the `offset`, `sizes[dim]`, and
343    /// `strides[dim]` to describe a logically reindexed subregion:
344    /// ```text
345    /// offset       += begin × strides[dim]
346    /// sizes[dim]    = ⎡(end - begin) / step⎤
347    /// strides[dim] ×= step
348    /// ```
349    ///
350    /// This transformation preserves the strided layout and avoids
351    /// copying data. After `select`, the view behaves as if indexing
352    /// starts at zero in the selected dimension, with a new length
353    /// and stride. From the user's perspective, nothing changes;
354    /// indexing remains zero-based, and the resulting shape can be
355    /// used like any other. The transformation is internal: the
356    /// view's offset and stride absorb the selection logic.
357    pub fn select(
358        &self,
359        dim: usize,
360        begin: usize,
361        end: usize,
362        step: usize,
363    ) -> Result<Self, SliceError> {
364        if dim >= self.sizes.len() {
365            return Err(SliceError::IndexOutOfRange {
366                index: dim,
367                total: self.sizes.len(),
368            });
369        }
370        if begin >= self.sizes[dim] {
371            return Err(SliceError::IndexOutOfRange {
372                index: begin,
373                total: self.sizes[dim],
374            });
375        }
376        if end <= begin {
377            return Err(SliceError::EmptyRange { begin, end, step });
378        }
379
380        let mut offset = self.offset();
381        let mut sizes = self.sizes().to_vec();
382        let mut strides = self.strides().to_vec();
383
384        offset += begin * strides[dim];
385        // The # of elems in `begin..end` with step `step`. This is
386        // ⌈(end - begin) / stride⌉ — the number of steps that fit in
387        // the half-open interval.
388        sizes[dim] = (end - begin).div_ceil(step);
389        strides[dim] *= step;
390
391        let slice = Slice::new(offset, sizes, strides)?;
392        Ok(slice)
393    }
394
395    /// Return the location of the provided coordinates.
396    pub fn location(&self, coord: &[usize]) -> Result<usize, SliceError> {
397        if coord.len() != self.sizes.len() {
398            return Err(SliceError::InvalidDims {
399                expected: self.sizes.len(),
400                got: coord.len(),
401            });
402        }
403        Ok(self.offset
404            + coord
405                .iter()
406                .zip(&self.strides)
407                .map(|(pos, stride)| pos * stride)
408                .sum::<usize>())
409    }
410
411    /// Return the coordinates of the provided value in the n-d space of this
412    /// Slice.
413    pub fn coordinates(&self, value: usize) -> Result<Vec<usize>, SliceError> {
414        let mut pos = value
415            .checked_sub(self.offset)
416            .ok_or(SliceError::ValueNotInSlice { value })?;
417        let mut result = vec![0; self.sizes.len()];
418        let mut sorted_info: Vec<_> = self
419            .strides
420            .iter()
421            .zip(self.sizes.iter().enumerate())
422            .collect();
423        sorted_info.sort_by_key(|&(stride, _)| *stride);
424        for &(stride, (i, &size)) in sorted_info.iter().rev() {
425            let (index, new_pos) = if size > 1 {
426                (pos / stride, pos % stride)
427            } else {
428                (0, pos)
429            };
430            if index >= size {
431                return Err(SliceError::ValueNotInSlice { value });
432            }
433            result[i] = index;
434            pos = new_pos;
435        }
436        if pos != 0 {
437            return Err(SliceError::ValueNotInSlice { value });
438        }
439        Ok(result)
440    }
441
442    /// Returns whether the provided rank is contained in this slice.
443    pub fn contains(&self, value: usize) -> bool {
444        self.coordinates(value).is_ok()
445    }
446
447    /// The total length of the slice's indices.
448    pub fn len(&self) -> usize {
449        self.sizes.iter().product()
450    }
451
452    pub fn is_empty(&self) -> bool {
453        self.len() == 0
454    }
455
456    /// Iterator over the slice's indices.
457    pub fn iter(&self) -> SliceIterator {
458        SliceIterator {
459            slice: self.clone(),
460            pos: CartesianIterator::new(self.sizes.clone()),
461        }
462    }
463
464    /// Iterator over sub-dimensions of the slice.
465    pub fn dim_iter(&self, dims: usize) -> DimSliceIterator {
466        DimSliceIterator {
467            pos: CartesianIterator::new(self.sizes[0..dims].to_vec()),
468        }
469    }
470
471    /// The linear index formula calculates the logical rank of a
472    /// multidimensional point in a row-major flattened array,
473    /// assuming dense gapless storage with zero offset:
474    ///
475    /// ```plain
476    ///     index := Σ(coordinate[i] × ∏(sizes[j] for j > i))
477    /// ```
478    ///
479    /// For example, given a 3x2 row-major base array B:
480    ///
481    /// ```plain
482    ///       0 1 2         1
483    /// B =   3 4 5    V =  4
484    ///       6 7 8         7
485    /// ```
486    ///
487    /// Let V be the first column of B. Then,
488    ///
489    /// ```plain
490    /// V      | loc   | index
491    /// -------+-------+------
492    /// (0, 0) |  1    | 0
493    /// (1, 0) |  4    | 1
494    /// (2, 0) |  7    | 2
495    /// ```
496    ///
497    /// # Conditions Under Which `loc = index`
498    ///
499    /// The physical offset formula computes the memory location of a
500    /// point `p` as:
501    ///
502    /// ```plain
503    /// loc := offset + Σ(coordinate[i] × stride[i])
504    /// ```
505    ///
506    /// Let the layout be dense row-major and offset = 0.
507    /// Then,
508    /// ```plain
509    /// stride[i] := ∏(sizes[j] for j > i).
510    /// ```
511    /// and substituting into the physical offset formula:
512    /// ```plain
513    ///   loc = Σ(coordinate[i] × stride[i])
514    ///       = Σ(coordinate[i] × ∏(sizes[j] for j > i))
515    ///       = index.
516    /// ```
517    ///
518    /// Thus, ∀ p = (i, j) ∈ B, loc_B(p) = index_B(p).
519    ///
520    /// # See also
521    ///
522    /// The [`get`] function performs an inverse operation: given a
523    /// logical index in row-major order, it computes the physical
524    /// memory offset according to the slice layout. So, if the layout
525    /// is row-major then `s.get(s.index(loc)) = loc`.
526    pub fn index(&self, value: usize) -> Result<usize, SliceError> {
527        let coords = self.coordinates(value)?;
528        let mut stride = 1;
529        let mut result = 0;
530
531        for (idx, size) in coords.iter().rev().zip(self.sizes.iter().rev()) {
532            result += *idx * stride;
533            stride *= size;
534        }
535
536        Ok(result)
537    }
538
539    /// Given a logical index (in row-major order), return the
540    /// physical memory offset of that element according to this
541    /// slice’s layout.
542    ///
543    /// The index is interpreted as a position in row-major traversal
544    /// that is, iterating across columns within rows. This method
545    /// converts logical row-major index to physical offset by:
546    ///
547    /// 1. Decomposing index into multidimensional coordinates
548    /// 2. Computing offset = base + Σ(coordinate[i] × stride[i])
549    ///
550    /// For example, with shape `[3, 4]` (3 rows, 4 columns) and
551    /// column-major layout:
552    ///
553    /// ```text
554    /// sizes  = [3, 4]         // rows, cols
555    /// strides = [1, 3]        // column-major: down, then right
556    ///
557    /// Logical matrix:
558    ///   A  B  C  D
559    ///   E  F  G  H
560    ///   I  J  K  L
561    ///
562    /// Memory layout:
563    /// offset 0  → [0, 0] = A
564    /// offset 1  → [1, 0] = E
565    /// offset 2  → [2, 0] = I
566    /// offset 3  → [0, 1] = B
567    /// offset 4  → [1, 1] = F
568    /// offset 5  → [2, 1] = J
569    /// offset 6  → [0, 2] = C
570    /// offset 7  → [1, 2] = G
571    /// offset 8  → [2, 2] = K
572    /// offset 9  → [0, 3] = D
573    /// offset 10 → [1, 3] = H
574    /// offset 11 → [2, 3] = L
575    ///
576    /// Then:
577    ///   index = 1  → coordinate [0, 1]  → offset = 0*1 + 1*3 = 3
578    /// ```
579    ///
580    /// # Errors
581    ///
582    /// Returns an error if `index >= product(sizes)`.
583    ///
584    /// # See also
585    ///
586    /// The [`index`] function performs an inverse operation: given a
587    /// memory offset, it returns the logical position of that element
588    /// in the slice's row-major iteration order.
589    pub fn get(&self, index: usize) -> Result<usize, SliceError> {
590        let mut val = self.offset;
591        let mut rest = index;
592        let mut total = 1;
593        for (size, stride) in self.sizes.iter().zip(self.strides.iter()).rev() {
594            total *= size;
595            val += (rest % size) * stride;
596            rest /= size;
597        }
598        if index < total {
599            Ok(val)
600        } else {
601            Err(SliceError::IndexOutOfRange { index, total })
602        }
603    }
604
605    /// The returned [`MapSlice`] is a view of this slice, with its elements
606    /// mapped using the provided mapping function.
607    pub fn map<T, F>(&self, mapper: F) -> MapSlice<'_, T, F>
608    where
609        F: Fn(usize) -> T,
610    {
611        MapSlice {
612            slice: self,
613            mapper,
614        }
615    }
616
617    /// Returns a new [`Slice`] with the given shape by reinterpreting
618    /// the layout of this slice.
619    ///
620    /// Constructs a new shape with standard row-major strides, using
621    /// the same base offset. Returns an error if the reshaped view
622    /// would access coordinates not valid in the original slice.
623    ///
624    /// # Requirements
625    ///
626    /// - This slice must be contiguous and have offset == 0.
627    /// - The number of elements must match:
628    ///   `self.sizes().iter().product() == new_sizes.iter().product()`
629    /// - Each flat offset in the proposed view must be valid in `self`.
630    ///
631    /// # Errors
632    ///
633    /// Returns [`SliceError::IncompatibleView`] if:
634    /// - The element count differs
635    /// - The base offset is nonzero
636    /// - Any offset in the view is not reachable in the original slice
637    ///
638    /// # Example
639    ///
640    /// ```rust
641    /// use ndslice::Slice;
642    /// let base = Slice::new_row_major(&[2, 3, 4]);
643    /// let reshaped = base.view(&[6, 4]).unwrap();
644    /// ```
645    pub fn view(&self, new_sizes: &[usize]) -> Result<Slice, SliceError> {
646        let view_elems: usize = new_sizes.iter().product();
647        let base_elems: usize = self.sizes().iter().product();
648
649        // TODO: This version of `view` requires that `self` be
650        // "dense":
651        //
652        //   - `self.offset == 0`
653        //   - `self.strides` match the row-major layout for
654        //     `self.sizes`
655        //   - `self.len() == self.sizes.iter().product::<usize>()`
656        //
657        // Future iterations of this function will aim to relax or
658        // remove the "dense" requirement where possible.
659
660        if view_elems != base_elems {
661            return Err(SliceError::IncompatibleView {
662                reason: format!(
663                    "element count mismatch: base has {}, view wants {}",
664                    base_elems, view_elems
665                ),
666            });
667        }
668        if self.offset != 0 {
669            return Err(SliceError::IncompatibleView {
670                reason: format!("view requires base offset = 0, but found {}", self.offset),
671            });
672        }
673        // Compute row-major strides.
674        let mut new_strides = vec![1; new_sizes.len()];
675        for i in (0..new_sizes.len().saturating_sub(1)).rev() {
676            new_strides[i] = new_strides[i + 1] * new_sizes[i + 1];
677        }
678
679        // Validate that every address in the new view maps to a valid
680        // coordinate in base.
681        for coord in CartesianIterator::new(new_sizes.to_vec()) {
682            #[allow(clippy::identity_op)]
683            let offset_in_view = 0 + coord
684                .iter()
685                .zip(&new_strides)
686                .map(|(i, s)| i * s)
687                .sum::<usize>();
688
689            if self.coordinates(offset_in_view).is_err() {
690                return Err(SliceError::IncompatibleView {
691                    reason: format!("offset {} not reachable in base", offset_in_view),
692                });
693            }
694        }
695
696        Ok(Slice {
697            offset: 0,
698            sizes: new_sizes.to_vec(),
699            strides: new_strides,
700        })
701    }
702
703    /// Returns a sub-slice of `self` starting at `starts`, of size `lens`.
704    pub fn subview(&self, starts: &[usize], lens: &[usize]) -> Result<Slice, SliceError> {
705        if starts.len() != self.num_dim() || lens.len() != self.num_dim() {
706            return Err(SliceError::InvalidDims {
707                expected: self.num_dim(),
708                got: starts.len().max(lens.len()),
709            });
710        }
711
712        for (d, (&start, &len)) in starts.iter().zip(lens).enumerate() {
713            if start + len > self.sizes[d] {
714                return Err(SliceError::IndexOutOfRange {
715                    index: start + len,
716                    total: self.sizes[d],
717                });
718            }
719        }
720
721        let offset = self.location(starts)?;
722        Ok(Slice {
723            offset,
724            sizes: lens.to_vec(),
725            strides: self.strides.clone(),
726        })
727    }
728
729    /// Ensures that every storage offset used by `self` is valid in
730    /// `other`.
731    ///
732    /// That is, for all p ∈ self:
733    /// `other.coordinates(self.location(p))` is defined.
734    ///
735    /// Returns `self` on success, enabling fluent chaining.
736    ///
737    /// # Examples
738    ///
739    /// ```
740    /// use ndslice::Slice;
741    ///
742    /// let base = Slice::new(0, vec![4, 4], vec![4, 1]).unwrap();
743    /// let view = base.subview(&[1, 1], &[2, 2]).unwrap();
744    /// assert_eq!(view.enforce_embedding(&base).unwrap().len(), 4);
745    ///
746    /// let small = Slice::new(0, vec![2, 2], vec![2, 1]).unwrap();
747    /// assert!(view.enforce_embedding(&small).is_err());
748    ///  ```
749    pub fn enforce_embedding<'a>(&'a self, other: &'_ Slice) -> Result<&'a Slice, SliceError> {
750        self.iter()
751            .try_for_each(|loc| other.coordinates(loc).map(|_| ()))?;
752        Ok(self)
753    }
754}
755
756impl std::fmt::Display for Slice {
757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758        write!(f, "{:?}", self)
759    }
760}
761
762impl IntoIterator for &Slice {
763    type Item = usize;
764    type IntoIter = SliceIterator;
765    fn into_iter(self) -> Self::IntoIter {
766        self.iter()
767    }
768}
769
770pub struct SliceIterator {
771    pub(crate) slice: Slice,
772    pos: CartesianIterator,
773}
774
775impl Iterator for SliceIterator {
776    type Item = usize;
777
778    fn next(&mut self) -> Option<Self::Item> {
779        match self.pos.next() {
780            None => None,
781            Some(pos) => Some(self.slice.location(&pos).unwrap()),
782        }
783    }
784}
785
786/// Iterates over the Cartesian product of a list of dimension sizes.
787///
788/// Given a list of dimension sizes `[d₀, d₁, ..., dₖ₋₁]`, this yields
789/// all coordinate tuples `[i₀, i₁, ..., iₖ₋₁]` where each `iⱼ ∈
790/// 0..dⱼ`.
791///
792/// Coordinates are yielded in row-major order (last dimension varies
793/// fastest).
794pub struct DimSliceIterator {
795    pos: CartesianIterator,
796}
797
798impl Iterator for DimSliceIterator {
799    type Item = Vec<usize>;
800
801    fn next(&mut self) -> Option<Self::Item> {
802        self.pos.next()
803    }
804}
805
806/// Iterates over all coordinate tuples in an N-dimensional space.
807///
808/// Yields each point in row-major order for the shape defined by
809/// `dims`, where each coordinate lies in `[0..dims[i])`.
810/// # Example
811/// ```ignore
812/// let iter = CartesianIterator::new(vec![2, 3]);
813/// let coords: Vec<_> = iter.collect();
814/// assert_eq!(coords, vec![
815///     vec![0, 0], vec![0, 1], vec![0, 2],
816///     vec![1, 0], vec![1, 1], vec![1, 2],
817/// ]);
818/// ```
819pub(crate) struct CartesianIterator {
820    dims: Vec<usize>,
821    index: usize,
822}
823
824impl CartesianIterator {
825    pub(crate) fn new(dims: Vec<usize>) -> Self {
826        CartesianIterator { dims, index: 0 }
827    }
828}
829
830impl Iterator for CartesianIterator {
831    type Item = Vec<usize>;
832
833    fn next(&mut self) -> Option<Self::Item> {
834        if self.index >= self.dims.iter().product::<usize>() {
835            return None;
836        }
837
838        let mut result: Vec<usize> = vec![0; self.dims.len()];
839        let mut rest = self.index;
840        for (i, dim) in self.dims.iter().enumerate().rev() {
841            result[i] = rest % dim;
842            rest /= dim;
843        }
844        self.index += 1;
845        Some(result)
846    }
847}
848
849/// MapSlice is a view of the underlying Slice that maps each rank
850/// into a different type.
851pub struct MapSlice<'a, T, F>
852where
853    F: Fn(usize) -> T,
854{
855    slice: &'a Slice,
856    mapper: F,
857}
858
859impl<T, F> MapSlice<'_, T, F>
860where
861    F: Fn(usize) -> T,
862{
863    /// The underlying slice sizes.
864    pub fn sizes(&self) -> &[usize] {
865        &self.slice.sizes
866    }
867
868    /// The underlying slice strides.
869    pub fn strides(&self) -> &[usize] {
870        &self.slice.strides
871    }
872
873    /// The mapped value at the provided coordinates. See [`Slice::location`].
874    pub fn location(&self, coord: &[usize]) -> Result<T, SliceError> {
875        self.slice.location(coord).map(&self.mapper)
876    }
877
878    /// The mapped value at the provided index. See [`Slice::get`].
879    pub fn get(&self, index: usize) -> Result<T, SliceError> {
880        self.slice.get(index).map(&self.mapper)
881    }
882
883    /// The underlying slice length.
884    pub fn len(&self) -> usize {
885        self.slice.len()
886    }
887
888    /// Whether the underlying slice is empty.
889    pub fn is_empty(&self) -> bool {
890        self.slice.is_empty()
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use std::assert_matches;
897    use std::vec;
898
899    use super::*;
900
901    #[test]
902    fn test_cartesian_iterator() {
903        let dims = vec![2, 2, 2];
904        let iter = CartesianIterator::new(dims);
905        let products: Vec<Vec<usize>> = iter.collect();
906        assert_eq!(
907            products,
908            vec![
909                vec![0, 0, 0],
910                vec![0, 0, 1],
911                vec![0, 1, 0],
912                vec![0, 1, 1],
913                vec![1, 0, 0],
914                vec![1, 0, 1],
915                vec![1, 1, 0],
916                vec![1, 1, 1],
917            ]
918        );
919    }
920
921    #[test]
922    #[allow(clippy::explicit_counter_loop)]
923    fn test_slice() {
924        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
925        for i in 0..4 {
926            assert_eq!(s.get(i).unwrap(), i);
927        }
928
929        {
930            // Test IntoIter
931            let mut current = 0;
932            for index in &s {
933                assert_eq!(index, current);
934                current += 1;
935            }
936        }
937
938        let s = Slice::new(0, vec![3, 4, 5], vec![20, 5, 1]).unwrap();
939        assert_eq!(s.get(3 * 4 + 1).unwrap(), 13);
940
941        let s = Slice::new(0, vec![2, 2, 2], vec![4, 32, 1]).unwrap();
942        assert_eq!(s.get(0).unwrap(), 0);
943        assert_eq!(s.get(1).unwrap(), 1);
944        assert_eq!(s.get(2).unwrap(), 32);
945        assert_eq!(s.get(3).unwrap(), 33);
946        assert_eq!(s.get(4).unwrap(), 4);
947        assert_eq!(s.get(5).unwrap(), 5);
948        assert_eq!(s.get(6).unwrap(), 36);
949        assert_eq!(s.get(7).unwrap(), 37);
950
951        let s = Slice::new(0, vec![2, 2, 2], vec![32, 4, 1]).unwrap();
952        assert_eq!(s.get(0).unwrap(), 0);
953        assert_eq!(s.get(1).unwrap(), 1);
954        assert_eq!(s.get(2).unwrap(), 4);
955        assert_eq!(s.get(4).unwrap(), 32);
956    }
957
958    #[test]
959    fn test_slice_iter() {
960        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
961        assert!(s.iter().eq(0..6));
962
963        let s = Slice::new(10, vec![10, 2], vec![10, 5]).unwrap();
964        assert!(s.iter().eq((10..=105).step_by(5)));
965
966        // Implementaion corresponds with Slice::get.
967        assert!(s.iter().eq((0..s.len()).map(|i| s.get(i).unwrap())));
968    }
969
970    #[test]
971    fn test_dim_slice_iter() {
972        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
973        let sub_dims: Vec<_> = s.dim_iter(1).collect();
974        assert_eq!(sub_dims, vec![vec![0], vec![1]]);
975    }
976
977    #[test]
978    fn test_slice_coordinates() {
979        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
980        assert_eq!(s.coordinates(0).unwrap(), vec![0, 0]);
981        assert_eq!(s.coordinates(3).unwrap(), vec![1, 0]);
982        assert_matches!(
983            s.coordinates(6),
984            Err(SliceError::ValueNotInSlice { value: 6 })
985        );
986
987        let s = Slice::new(10, vec![2, 3], vec![3, 1]).unwrap();
988        assert_matches!(
989            s.coordinates(6),
990            Err(SliceError::ValueNotInSlice { value: 6 })
991        );
992        assert_eq!(s.coordinates(10).unwrap(), vec![0, 0]);
993        assert_eq!(s.coordinates(13).unwrap(), vec![1, 0]);
994
995        let s = Slice::new(0, vec![2, 1, 1], vec![1, 1, 1]).unwrap();
996        assert_eq!(s.coordinates(1).unwrap(), vec![1, 0, 0]);
997    }
998
999    #[test]
1000    fn test_slice_index() {
1001        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
1002        assert_eq!(s.index(3).unwrap(), 3);
1003        assert!(s.index(14).is_err());
1004
1005        let s = Slice::new(0, vec![2, 2], vec![4, 2]).unwrap();
1006        assert_eq!(s.index(2).unwrap(), 1);
1007    }
1008
1009    #[test]
1010    fn relative_ordinal_slice_maps_selected_ranks_into_parent_ordinals() {
1011        let parent = Slice::new(100, vec![4, 4], vec![8, 2]).expect("parent should be valid");
1012        let selected = Slice::new(110, vec![2, 2], vec![16, 2]).expect("selection should be valid");
1013
1014        assert_eq!(
1015            selected
1016                .relative_ordinal_slice(&parent)
1017                .expect("selection should map into parent ordinals"),
1018            Slice::new(5, vec![2, 2], vec![8, 1]).expect("result should be valid")
1019        );
1020    }
1021
1022    #[test]
1023    fn relative_ordinal_slice_rejects_selection_that_wraps_parent_dimension() {
1024        let parent = Slice::new_row_major(vec![2, 4]);
1025        let selected = Slice::new(1, vec![1, 4], vec![4, 1]).expect("selection should be valid");
1026
1027        assert!(selected.relative_ordinal_slice(&parent).is_none());
1028    }
1029
1030    #[test]
1031    fn test_slice_map() {
1032        let s = Slice::new(0, vec![2, 3], vec![3, 1]).unwrap();
1033        let m = s.map(|i| i * 2);
1034        assert_eq!(m.get(0).unwrap(), 0);
1035        assert_eq!(m.get(3).unwrap(), 6);
1036        assert_eq!(m.get(5).unwrap(), 10);
1037    }
1038
1039    #[test]
1040    fn test_slice_size_one() {
1041        let s = Slice::new(0, vec![1, 1], vec![1, 1]).unwrap();
1042        assert_eq!(s.get(0).unwrap(), 0);
1043    }
1044
1045    #[test]
1046    fn test_row_major() {
1047        let s = Slice::new_row_major(vec![4, 4, 4]);
1048        assert_eq!(s.offset(), 0);
1049        assert_eq!(s.sizes(), &[4, 4, 4]);
1050        assert_eq!(s.strides(), &[16, 4, 1]);
1051    }
1052
1053    #[test]
1054    fn test_slice_view_smoke() {
1055        use crate::Slice;
1056
1057        let base = Slice::new_row_major([2, 3, 4]);
1058
1059        // Reshape: compatible shape and layout
1060        let view = base.view(&[6, 4]).unwrap();
1061        assert_eq!(view.sizes(), &[6, 4]);
1062        assert_eq!(view.offset(), 0);
1063        assert_eq!(view.strides(), &[4, 1]);
1064        assert_eq!(
1065            view.location(&[5, 3]).unwrap(),
1066            base.location(&[1, 2, 3]).unwrap()
1067        );
1068
1069        // Reshape: identity (should succeed)
1070        let view = base.view(&[2, 3, 4]).unwrap();
1071        assert_eq!(view.sizes(), base.sizes());
1072        assert_eq!(view.strides(), base.strides());
1073
1074        // Reshape: incompatible shape (wrong element count)
1075        let err = base.view(&[5, 4]);
1076        assert!(err.is_err());
1077
1078        // Reshape: incompatible layout (simulate select)
1079        let selected = Slice::new(1, vec![2, 3], vec![6, 1]).unwrap(); // not offset=0
1080        let err = selected.view(&[3, 2]);
1081        assert!(err.is_err());
1082
1083        // Reshape: flat 1D view
1084        let flat = base.view(&[24]).unwrap();
1085        assert_eq!(flat.sizes(), &[24]);
1086        assert_eq!(flat.strides(), &[1]);
1087        assert_eq!(
1088            flat.location(&[23]).unwrap(),
1089            base.location(&[1, 2, 3]).unwrap()
1090        );
1091    }
1092
1093    #[test]
1094    fn test_view_of_view_when_dense() {
1095        // Start with a dense base: 2 × 3 × 4 = 24 elements.
1096        let base = Slice::new_row_major([2, 3, 4]);
1097
1098        // First view: flatten to 1D.
1099        let flat = base.view(&[24]).unwrap();
1100        assert_eq!(flat.sizes(), &[24]);
1101        assert_eq!(flat.strides(), &[1]);
1102        assert_eq!(flat.offset(), 0); // Still dense.
1103
1104        // Second view: reshape 1D to 6 × 4.
1105        let reshaped = flat.view(&[6, 4]).unwrap();
1106        assert_eq!(reshaped.sizes(), &[6, 4]);
1107        assert_eq!(reshaped.strides(), &[4, 1]);
1108        assert_eq!(reshaped.offset(), 0);
1109
1110        // Location agreement check
1111        assert_eq!(
1112            reshaped.location(&[5, 3]).unwrap(),
1113            base.location(&[1, 2, 3]).unwrap()
1114        );
1115    }
1116
1117    #[test]
1118    fn test_at_1d_to_0d() {
1119        let slice = Slice::new_row_major(vec![5]);
1120        assert_eq!(slice.num_dim(), 1);
1121        assert_eq!(slice.sizes(), &[5]);
1122        assert_eq!(slice.strides(), &[1]);
1123
1124        let result = slice.at(0, 3).unwrap();
1125        assert_eq!(result.num_dim(), 0);
1126        assert_eq!(result.sizes(), &[] as &[usize]);
1127        assert_eq!(result.strides(), &[] as &[usize]);
1128        assert_eq!(result.offset(), 3);
1129        assert_eq!(result.location(&[] as &[usize]).unwrap(), 3);
1130    }
1131
1132    #[test]
1133    fn test_at_2d_to_1d() {
1134        let slice = Slice::new_row_major(vec![3, 4]);
1135        assert_eq!(slice.num_dim(), 2);
1136        assert_eq!(slice.sizes(), &[3, 4]);
1137        assert_eq!(slice.strides(), &[4, 1]);
1138
1139        let result = slice.at(0, 1).unwrap();
1140        assert_eq!(result.num_dim(), 1);
1141        assert_eq!(result.sizes(), &[4]);
1142        assert_eq!(result.strides(), &[1]);
1143        assert_eq!(result.offset(), 4);
1144    }
1145
1146    #[test]
1147    fn test_at_3d_to_2d() {
1148        let slice = Slice::new_row_major(vec![2, 3, 4]);
1149        assert_eq!(slice.num_dim(), 3);
1150        assert_eq!(slice.sizes(), &[2, 3, 4]);
1151        assert_eq!(slice.strides(), &[12, 4, 1]);
1152
1153        let result = slice.at(0, 1).unwrap();
1154        assert_eq!(result.num_dim(), 2);
1155        assert_eq!(result.sizes(), &[3, 4]);
1156        assert_eq!(result.strides(), &[4, 1]);
1157        assert_eq!(result.offset(), 12);
1158    }
1159
1160    #[test]
1161    fn test_get_index_inverse_relationship() {
1162        // Start with a 3 x 3 dense row major matrix.
1163        //
1164        // 0 1 2
1165        // 3 4 5
1166        // 6 7 8
1167        let m = Slice::new_row_major([3, 3]);
1168        assert_eq!(m.offset, 0);
1169        assert_eq!(m.sizes(), &[3, 3]);
1170        assert_eq!(m.strides(), &[3, 1]);
1171
1172        // Slice `m` is 0-offset, row-major, dense, gapless.
1173        for loc in m.iter() {
1174            // ∀ `loc` ∈ `m`, `m.index(loc) == loc`.
1175            assert_eq!(m.index(loc).unwrap(), loc);
1176            // ∀ `loc` ∈ `m`, `m.get(m.index(loc)) == loc`.
1177            assert_eq!(m.get(m.index(loc).unwrap()).unwrap(), loc);
1178        }
1179
1180        // Slice out the middle column.
1181        //    1
1182        //    4
1183        //    7
1184        let c = m.select(1, 1, 2, 1).unwrap();
1185        assert_eq!(c.sizes(), &[3, 1]);
1186        assert_eq!(c.strides(), &[3, 1]);
1187
1188        // Slice `c` has a non-zero offset.
1189        for loc in c.iter() {
1190            // Local rank of `loc` in `c` != loc.
1191            assert_ne!(c.index(loc).unwrap(), loc);
1192            // ∀ `loc` ∈ `c`, `c.get(c.index(loc)) == loc`.
1193            assert_eq!(c.get(c.index(loc).unwrap()).unwrap(), loc);
1194        }
1195    }
1196
1197    #[test]
1198    fn embedding_succeeds_for_contained_view() {
1199        let base = Slice::new(0, vec![4, 4], vec![4, 1]).unwrap(); // 4×4 matrix, row-major
1200        let view = Slice::new(5, vec![2, 2], vec![4, 1]).unwrap(); // a 2×2 submatrix starting at (1,1)
1201
1202        assert!(view.enforce_embedding(&base).is_ok());
1203    }
1204
1205    #[test]
1206    fn embedding_fails_for_out_of_bounds_view() {
1207        let base = Slice::new(0, vec![4, 4], vec![4, 1]).unwrap(); // 4×4 matrix
1208        let view = Slice::new(14, vec![2, 2], vec![4, 1]).unwrap(); // starts at (3,2), accesses (4,3)
1209
1210        assert!(view.enforce_embedding(&base).is_err());
1211    }
1212}