Skip to main content

strided_view/
view.rs

1//! Julia-like dynamic-rank strided view types.
2//!
3//! This module provides the canonical view types for strided operations,
4//! matching Julia's StridedViews.jl data model:
5//!
6//! - [`StridedView`]: Immutable dynamic-rank strided view with lazy element operations
7//! - [`StridedViewMut`]: Mutable dynamic-rank strided view (Identity op only)
8//! - [`StridedArray`]: Owned strided multidimensional array
9//!
10//! A view's shape and strides are a small inline-or-heap container with
11//! capacity [`crate::INLINE_RANK`]: every constructor, clone, and metadata
12//! transform allocates nothing up to that rank and spills gracefully above
13//! it. The view itself only borrows the element data.
14//!
15//! The metadata container is `tinyvec::ArrayVec`, unified across tensor4all
16//! repositories (tensor4all-agent-rules#22, strided-rs#312). `ArrayVec` is
17//! `Copy` for `Copy` items, so a clone is one fixed-size bitwise copy, and the
18//! heap fallback for rank above [`INLINE_RANK`] lives in a separate
19//! `Option<Box<SpilledMeta>>` so the inline value has no drop glue.
20
21use std::marker::PhantomData;
22use std::mem::MaybeUninit;
23use std::ops::{Index, IndexMut};
24
25use tinyvec::ArrayVec;
26
27use crate::element_op::{ComposableElementOp, ElementOp, ElementOpApply, Identity};
28use crate::{AxisVec, Result, StrideVec, StridedError, INLINE_RANK};
29
30/// Build an `ArrayVec` from at most [`INLINE_RANK`] elements.
31///
32/// The value is written with literal-count arms: a runtime-length
33/// `copy_from_slice` or loop is recognized by LLVM and lowered to a `memcpy`
34/// PLT call, whose overhead dominates a handful of words.
35#[inline(always)]
36fn arrayvec_from_slice<T: Copy + Default>(src: &[T]) -> ArrayVec<[T; INLINE_RANK]> {
37    debug_assert!(src.len() <= INLINE_RANK);
38    let n = src.len();
39    let mut buf = [T::default(); INLINE_RANK];
40    match n {
41        0 => {}
42        1 => buf[0] = src[0],
43        2 => {
44            buf[0] = src[0];
45            buf[1] = src[1];
46        }
47        3 => {
48            buf[0] = src[0];
49            buf[1] = src[1];
50            buf[2] = src[2];
51        }
52        4 => {
53            buf[0] = src[0];
54            buf[1] = src[1];
55            buf[2] = src[2];
56            buf[3] = src[3];
57        }
58        5 => {
59            buf[0] = src[0];
60            buf[1] = src[1];
61            buf[2] = src[2];
62            buf[3] = src[3];
63            buf[4] = src[4];
64        }
65        6 => {
66            buf[0] = src[0];
67            buf[1] = src[1];
68            buf[2] = src[2];
69            buf[3] = src[3];
70            buf[4] = src[4];
71            buf[5] = src[5];
72        }
73        7 => {
74            buf[0] = src[0];
75            buf[1] = src[1];
76            buf[2] = src[2];
77            buf[3] = src[3];
78            buf[4] = src[4];
79            buf[5] = src[5];
80            buf[6] = src[6];
81        }
82        _ => {
83            buf[0] = src[0];
84            buf[1] = src[1];
85            buf[2] = src[2];
86            buf[3] = src[3];
87            buf[4] = src[4];
88            buf[5] = src[5];
89            buf[6] = src[6];
90            buf[7] = src[7];
91        }
92    }
93    ArrayVec::from_array_len(buf, n)
94}
95
96/// Heap-owned metadata used only above [`INLINE_RANK`].
97#[derive(Clone, Debug)]
98struct SpilledMeta {
99    dims: Box<[usize]>,
100    strides: Box<[isize]>,
101}
102
103/// Shape and strides of a view or array.
104///
105/// Rank `<= INLINE_RANK` is stored inline in two `ArrayVec`s and the
106/// `spilled` slot is `None`, so cloning the whole metadata is a fixed-size
107/// bitwise copy with one predictable `None` check. A higher rank keeps the
108/// inline buffers empty and boxes both vectors.
109#[derive(Debug)]
110struct ViewMeta {
111    inline_dims: ArrayVec<[usize; INLINE_RANK]>,
112    inline_strides: ArrayVec<[isize; INLINE_RANK]>,
113    spilled: Option<Box<SpilledMeta>>,
114}
115
116impl Clone for ViewMeta {
117    #[inline]
118    fn clone(&self) -> Self {
119        Self {
120            inline_dims: self.inline_dims,
121            inline_strides: self.inline_strides,
122            spilled: self.spilled.clone(),
123        }
124    }
125}
126
127impl ViewMeta {
128    #[inline(always)]
129    fn from_slices(dims: &[usize], strides: &[isize]) -> Self {
130        debug_assert_eq!(dims.len(), strides.len());
131        if dims.len() <= INLINE_RANK {
132            Self {
133                inline_dims: arrayvec_from_slice(dims),
134                inline_strides: arrayvec_from_slice(strides),
135                spilled: None,
136            }
137        } else {
138            Self {
139                inline_dims: ArrayVec::new(),
140                inline_strides: ArrayVec::new(),
141                spilled: Some(Box::new(SpilledMeta {
142                    dims: dims.into(),
143                    strides: strides.into(),
144                })),
145            }
146        }
147    }
148
149    #[inline]
150    fn dims(&self) -> &[usize] {
151        match &self.spilled {
152            Some(spilled) => &spilled.dims,
153            None => self.inline_dims.as_slice(),
154        }
155    }
156
157    #[inline]
158    fn strides(&self) -> &[isize] {
159        match &self.spilled {
160            Some(spilled) => &spilled.strides,
161            None => self.inline_strides.as_slice(),
162        }
163    }
164}
165
166#[inline]
167fn empty_layout(dims: &[usize]) -> bool {
168    dims.contains(&0)
169}
170
171#[inline]
172fn empty_const_ptr<T>() -> *const T {
173    core::ptr::NonNull::<T>::dangling().as_ptr()
174}
175
176#[inline]
177fn empty_mut_ptr<T>() -> *mut T {
178    core::ptr::NonNull::<T>::dangling().as_ptr()
179}
180
181// ============================================================================
182// Validation helpers
183// ============================================================================
184
185/// Validate that all accessed offsets stay within `[0, len)`.
186#[inline]
187pub(crate) fn validate_bounds(
188    len: usize,
189    dims: &[usize],
190    strides: &[isize],
191    offset: isize,
192) -> Result<()> {
193    if dims.len() != strides.len() {
194        return Err(StridedError::StrideLengthMismatch);
195    }
196    // Empty array - no access needed
197    if dims.contains(&0) {
198        return Ok(());
199    }
200    // Compute min and max offsets
201    let mut min_offset = offset;
202    let mut max_offset = offset;
203    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
204        if dim > 1 {
205            let last = dim
206                .checked_sub(1)
207                .and_then(|value| isize::try_from(value).ok())
208                .ok_or(StridedError::OffsetOverflow)?;
209            let end = stride
210                .checked_mul(last)
211                .ok_or(StridedError::OffsetOverflow)?;
212            if end >= 0 {
213                max_offset = max_offset
214                    .checked_add(end)
215                    .ok_or(StridedError::OffsetOverflow)?;
216            } else {
217                min_offset = min_offset
218                    .checked_add(end)
219                    .ok_or(StridedError::OffsetOverflow)?;
220            }
221        }
222    }
223    if min_offset < 0 || max_offset < 0 {
224        return Err(StridedError::OffsetOverflow);
225    }
226    if max_offset as usize >= len {
227        return Err(StridedError::OffsetOverflow);
228    }
229    Ok(())
230}
231
232/// Offset of `indices` inside `dims`/`strides`, relative to `base`.
233///
234/// # Panics
235///
236/// Panics when `indices` is not the full index for `dims`, an index is out of
237/// bounds, or an indexed offset does not fit `isize`. Every panicking element
238/// accessor below reports those conditions through this helper.
239#[inline]
240fn indexed_offset(dims: &[usize], strides: &[isize], indices: &[usize], base: isize) -> isize {
241    assert_eq!(indices.len(), dims.len(), "wrong number of indices");
242    let mut offset = base;
243    for (i, &index) in indices.iter().enumerate() {
244        assert!(
245            index < dims[i],
246            "index {} out of bounds for dim {}",
247            index,
248            dims[i]
249        );
250        offset = isize::try_from(index)
251            .ok()
252            .and_then(|index| index.checked_mul(strides[i]))
253            .and_then(|step| offset.checked_add(step))
254            .expect("index offset overflow");
255    }
256    offset
257}
258
259/// Number of elements of `dims`: the checked product of the extents.
260///
261/// A zero extent wins before the product, so a zero-sized shape is empty even
262/// when its other extents would overflow. Returns
263/// [`StridedError::OffsetOverflow`] when the product does not fit `usize`
264/// (tensor4all/strided-rs#205).
265#[inline]
266fn checked_element_count(dims: &[usize]) -> Result<usize> {
267    if dims.contains(&0) {
268        return Ok(0);
269    }
270    dims.iter()
271        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
272        .ok_or(StridedError::OffsetOverflow)
273}
274
275/// [`checked_element_count`] for the infallible `len` accessors.
276///
277/// # Panics
278///
279/// Panics when the element count does not fit `usize`, which only a
280/// broadcast (stride-0) layout can reach.
281#[inline]
282fn element_count(dims: &[usize]) -> usize {
283    checked_element_count(dims).expect("element count overflows usize")
284}
285
286/// Dense strides of `dims` with the axes visited fastest first in `order`.
287///
288/// Each stride is the product of the extents of the faster axes and must fit
289/// `isize`; otherwise [`StridedError::OffsetOverflow`]. A zero-sized shape is
290/// never dereferenced, so its strides saturate instead of failing: an empty
291/// array with huge extents stays constructible.
292fn checked_dense_strides(dims: &[usize], order: impl Iterator<Item = usize>) -> Result<StrideVec> {
293    let empty = dims.contains(&0);
294    let mut strides = StrideVec::new();
295    strides.resize(dims.len(), 0isize);
296    let mut step = 1isize;
297    for axis in order {
298        strides[axis] = step;
299        let dim = isize::try_from(dims[axis]).ok();
300        step = match dim.and_then(|dim| step.checked_mul(dim)) {
301            Some(next) => next,
302            None if empty => isize::MAX,
303            // The last axis's step is never stored, but it is the element
304            // count, which must also fit for the allocation and the offsets.
305            None => return Err(StridedError::OffsetOverflow),
306        };
307    }
308    Ok(strides)
309}
310
311fn checked_col_major_strides(dims: &[usize]) -> Result<StrideVec> {
312    checked_dense_strides(dims, 0..dims.len())
313}
314
315fn checked_row_major_strides(dims: &[usize]) -> Result<StrideVec> {
316    checked_dense_strides(dims, (0..dims.len()).rev())
317}
318
319/// Compute column-major strides (Julia default: first index varies fastest).
320///
321/// Returns an inline [`StrideVec`]: the result does not allocate at rank
322/// `<= INLINE_RANK` and spills above it.
323///
324/// # Panics
325///
326/// Panics when a stride or the element count of a non-empty shape does not
327/// fit `isize`. A shape with a zero extent never panics.
328///
329/// # Examples
330///
331/// ```
332/// use strided_view::col_major_strides;
333///
334/// // Julia default: first index varies fastest.
335/// assert_eq!(col_major_strides(&[2, 3]).as_slice(), &[1, 2]);
336/// ```
337#[inline]
338pub fn col_major_strides(dims: &[usize]) -> StrideVec {
339    checked_col_major_strides(dims).expect("column-major strides overflow isize")
340}
341
342/// Compute row-major strides (C default: last index varies fastest).
343///
344/// Returns an inline [`StrideVec`]: the result does not allocate at rank
345/// `<= INLINE_RANK` and spills above it.
346///
347/// # Panics
348///
349/// Panics when a stride or the element count of a non-empty shape does not
350/// fit `isize`. A shape with a zero extent never panics.
351///
352/// # Examples
353///
354/// ```
355/// use strided_view::row_major_strides;
356///
357/// // C default: last index varies fastest.
358/// assert_eq!(row_major_strides(&[2, 3]).as_slice(), &[3, 1]);
359/// ```
360#[inline]
361pub fn row_major_strides(dims: &[usize]) -> StrideVec {
362    checked_row_major_strides(dims).expect("row-major strides overflow isize")
363}
364
365// ============================================================================
366// StridedView
367// ============================================================================
368
369/// Dynamic-rank immutable strided view with lazy element operations.
370///
371/// This is the Julia-equivalent `StridedView` type with:
372/// - Dynamic rank, with metadata stored inline for up to [`INLINE_RANK`]
373///   dimensions ([`StridedView::new`] copies the caller's layout, so the view
374///   borrows only the element data)
375/// - Lazy element operations via the `Op` type parameter
376/// - Zero-copy transformations (permute, transpose, adjoint, conj)
377///
378/// # Type Parameters
379/// - `'a`: Lifetime of the underlying data
380/// - `T`: Element type
381/// - `Op`: Element operation applied lazily on access (default: `Identity`)
382///
383/// # Size
384///
385/// The inline metadata makes the view 184 bytes on 64-bit targets for
386/// `usize`/`isize` strides.
387///
388/// # Auto traits
389///
390/// The view borrows its elements, so it is `Send` only when `T: Sync` (a
391/// shared slice is). A view of non-`Sync` data is not `Send`:
392///
393/// ```compile_fail
394/// use std::cell::Cell;
395/// use strided_view::StridedView;
396///
397/// fn assert_send<T: Send>() {}
398/// assert_send::<StridedView<'static, Cell<i32>>>();
399/// ```
400pub struct StridedView<'a, T, Op = Identity> {
401    ptr: *const T,
402    data: &'a [T],
403    meta: ViewMeta,
404    offset: isize,
405    _op: PhantomData<Op>,
406}
407
408// SAFETY: `StridedView` owns no `T`. It holds `&'a [T]` and a `*const T`
409// derived from it, so moving the view moves a shared borrow, which is `Send`
410// exactly when `T: Sync`. `Op` appears only in `PhantomData`, and `ViewMeta` is
411// owned metadata (an inline `tinyvec::ArrayVec` plus an optional box).
412unsafe impl<T: Sync, Op: Send> Send for StridedView<'_, T, Op> {}
413// SAFETY: a shared `&StridedView` hands out only `&T` (`get`, `ptr`, the
414// `ElementOp` read path) and never `&mut T`, so sharing is sound when `T: Sync`.
415// `Op` is `Sync` and appears only in `PhantomData`.
416unsafe impl<T: Sync, Op: Sync> Sync for StridedView<'_, T, Op> {}
417
418impl<T, Op> Clone for StridedView<'_, T, Op> {
419    #[inline]
420    fn clone(&self) -> Self {
421        Self {
422            ptr: self.ptr,
423            data: self.data,
424            meta: self.meta.clone(),
425            offset: self.offset,
426            _op: PhantomData,
427        }
428    }
429}
430
431impl<T: std::fmt::Debug, Op> std::fmt::Debug for StridedView<'_, T, Op> {
432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433        f.debug_struct("StridedView")
434            .field("dims", &self.meta.dims())
435            .field("strides", &self.meta.strides())
436            .field("offset", &self.offset)
437            .finish()
438    }
439}
440
441/// The cached base pointer of a view over `data` at `offset`.
442///
443/// An empty layout gets the dangling pointer rather than `data + offset`: a
444/// zero-sized extent may sit at an offset no element of `data` reaches.
445#[inline]
446fn base_ptr<T>(data: &[T], dims: &[usize], offset: isize) -> *const T {
447    if empty_layout(dims) {
448        empty_const_ptr()
449    } else {
450        // SAFETY: every caller reaches `base_ptr` from `from_meta`, whose
451        // `dims`/`strides`/`offset` were either accepted by `validate_bounds`
452        // (view.rs:186) — `StridedView::new`, `StridedViewMut::new`, the
453        // re-validated diagonal path, or a metadata transform that only reorders
454        // axes or zeroes strides and therefore cannot enlarge the reachable
455        // offset set — or were supplied under the `new_unchecked` contract. The
456        // empty-layout branch above excludes the case where `offset` may point
457        // past `data`.
458        unsafe { data.as_ptr().offset(offset) }
459    }
460}
461
462/// A view from already-validated metadata, with the element op chosen here.
463#[inline]
464fn from_meta<'a, T, Op>(
465    data: &'a [T],
466    dims: &[usize],
467    strides: &[isize],
468    offset: isize,
469) -> StridedView<'a, T, Op> {
470    let meta = ViewMeta::from_slices(dims, strides);
471    let ptr = base_ptr(data, meta.dims(), offset);
472    StridedView {
473        ptr,
474        data,
475        meta,
476        offset,
477        _op: PhantomData,
478    }
479}
480
481impl<'a, T, Op> StridedView<'a, T, Op> {
482    /// Create an immutable strided view, copying the caller's metadata inline.
483    ///
484    /// The layout is copied into the view's inline storage, so it does not
485    /// need to outlive the view and no heap allocation happens at rank
486    /// `<= INLINE_RANK`. The view's lifetime is tied to `data` only.
487    ///
488    /// # Errors
489    ///
490    /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
491    /// `strides` have different lengths, and
492    /// [`StridedError::OffsetOverflow`] when a reachable offset is not
493    /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
494    /// stride times that last index or the accumulated offsets overflow
495    /// `isize` or go negative, or the highest reachable offset is not below
496    /// the buffer length. A layout with a zero extent skips this check.
497    ///
498    /// # Examples
499    ///
500    /// ```
501    /// use strided_view::StridedView;
502    ///
503    /// let data = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
504    /// let view = StridedView::<f64>::new(&data, &[2, 3], &[1, 2], 0).unwrap();
505    /// assert_eq!(view.dims(), &[2, 3]);
506    /// assert_eq!(view.get(&[1, 2]), 6.0);
507    /// ```
508    #[inline]
509    pub fn new(data: &'a [T], dims: &[usize], strides: &[isize], offset: isize) -> Result<Self> {
510        validate_bounds(data.len(), dims, strides, offset)?;
511        Ok(from_meta(data, dims, strides, offset))
512    }
513
514    /// Create a view without bounds checking, copying the caller's metadata.
515    ///
516    /// # Safety
517    /// The caller must ensure all index combinations stay within bounds.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use strided_view::StridedView;
523    ///
524    /// let data = [1.0f64, 2.0, 3.0, 4.0];
525    /// let view = unsafe { StridedView::<f64>::new_unchecked(&data, &[2, 2], &[1, 2], 0) };
526    /// assert_eq!(view.ndim(), 2);
527    /// ```
528    #[inline]
529    pub unsafe fn new_unchecked(
530        data: &'a [T],
531        dims: &[usize],
532        strides: &[isize],
533        offset: isize,
534    ) -> Self {
535        from_meta(data, dims, strides, offset)
536    }
537
538    /// Returns the shape (dimension sizes) of this view.
539    #[inline]
540    pub fn dims(&self) -> &[usize] {
541        self.meta.dims()
542    }
543
544    /// Returns the strides (in units of `T`) for each dimension.
545    #[inline]
546    pub fn strides(&self) -> &[isize] {
547        self.meta.strides()
548    }
549
550    /// Returns the byte offset into the backing data.
551    #[inline]
552    pub fn offset(&self) -> isize {
553        self.offset
554    }
555
556    /// Returns the number of dimensions (rank).
557    #[inline]
558    pub fn ndim(&self) -> usize {
559        self.dims().len()
560    }
561
562    /// Returns the total number of elements.
563    ///
564    /// # Panics
565    ///
566    /// Panics when the element count does not fit `usize`, which only a
567    /// broadcast (stride-0) layout can reach.
568    #[inline]
569    pub fn len(&self) -> usize {
570        element_count(self.dims())
571    }
572
573    /// Returns `true` if any dimension is zero.
574    #[inline]
575    pub fn is_empty(&self) -> bool {
576        self.dims().contains(&0)
577    }
578
579    /// Returns a reference to the backing data slice.
580    #[inline]
581    pub fn data(&self) -> &'a [T] {
582        self.data
583    }
584
585    /// Raw const pointer to element at the view's base offset.
586    #[inline]
587    pub fn ptr(&self) -> *const T {
588        self.ptr
589    }
590
591    /// Permute dimensions.
592    ///
593    /// # Errors
594    ///
595    /// Returns [`StridedError::RankMismatch`] when `perm` does not have one
596    /// entry per axis, and [`StridedError::InvalidAxis`] when an entry
597    /// names an axis outside the rank or repeats an axis.
598    pub fn permute(&self, perm: &[usize]) -> Result<StridedView<'a, T, Op>> {
599        let rank = self.dims().len();
600        if perm.len() != rank {
601            return Err(StridedError::RankMismatch(perm.len(), rank));
602        }
603        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
604        seen.resize(rank, false);
605        for &p in perm {
606            if p >= rank {
607                return Err(StridedError::InvalidAxis { axis: p, rank });
608            }
609            if seen[p] {
610                return Err(StridedError::InvalidAxis { axis: p, rank });
611            }
612            seen[p] = true;
613        }
614        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
615        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
616        Ok(from_meta(self.data, &new_dims, &new_strides, self.offset))
617    }
618
619    /// Create a diagonal view by fusing repeated axis pairs via stride trick (zero-copy).
620    ///
621    /// Each pair `(a, b)` ties axes `a` and `b` to one index. Pairs that share
622    /// an axis chain into one class (`[(0, 1), (1, 2)]` and `[(0, 1), (0, 2)]`
623    /// both tie axes 0, 1 and 2), and each class becomes one axis:
624    /// - New stride = the sum of the strides of the axes in the class
625    /// - New dim = the minimum of their dims
626    /// - The class sits at its lowest-numbered axis; the others are removed
627    /// - Pairs use **original** axis numbering; their order does not matter,
628    ///   and repeating a pair has no further effect
629    ///
630    /// # Errors
631    ///
632    /// Returns [`StridedError::InvalidAxis`] when a pair names an axis outside
633    /// the rank or names the same axis twice, and
634    /// [`StridedError::OffsetOverflow`] when a fused stride does not fit
635    /// `isize` or the fused layout reaches past the backing allocation.
636    ///
637    /// # Example
638    /// `A[i,i,j]` shape=`[n,n,m]` strides=`[s0,s1,s2]` -> shape=`[n,m]` strides=`[s0+s1, s2]`
639    pub fn diagonal_view(&self, axis_pairs: &[(usize, usize)]) -> Result<StridedView<'a, T, Op>> {
640        let ndim = self.ndim();
641        // Each pair ties two axes to one index, so the pairs partition the
642        // axes into classes. Track each class's root as its lowest axis; the
643        // result is then independent of pair order and of how pairs chain
644        // (tensor4all/strided-rs#329).
645        let mut root: tinyvec::TinyVec<[usize; INLINE_RANK]> = (0..ndim).collect();
646        fn find(root: &mut [usize], mut ax: usize) -> usize {
647            while root[ax] != ax {
648                root[ax] = root[root[ax]];
649                ax = root[ax];
650            }
651            ax
652        }
653        for &(a, b) in axis_pairs {
654            let hi = a.max(b);
655            if hi >= ndim {
656                return Err(StridedError::InvalidAxis {
657                    axis: hi,
658                    rank: ndim,
659                });
660            }
661            if a == b {
662                return Err(StridedError::InvalidAxis {
663                    axis: a,
664                    rank: ndim,
665                });
666            }
667            let (ra, rb) = (find(&mut root, a), find(&mut root, b));
668            root[ra.max(rb)] = ra.min(rb);
669        }
670
671        // Every class collapses onto its root: the root's stride sums the
672        // strides of all axes in the class and its dim is their minimum. A
673        // root is lower than every other axis of its class, so it has already
674        // been kept when a later member folds into it.
675        let mut dims: AxisVec = AxisVec::new();
676        let mut strides: StrideVec = StrideVec::new();
677        let mut position: tinyvec::TinyVec<[usize; INLINE_RANK]> =
678            tinyvec::TinyVec::with_capacity(ndim);
679        for ax in 0..ndim {
680            let r = find(&mut root, ax);
681            if r == ax {
682                position.push(dims.len());
683                dims.push(self.dims()[ax]);
684                strides.push(self.strides()[ax]);
685            } else {
686                position.push(usize::MAX);
687                let p = position[r];
688                strides[p] = strides[p]
689                    .checked_add(self.strides()[ax])
690                    .ok_or(StridedError::OffsetOverflow)?;
691                dims[p] = dims[p].min(self.dims()[ax]);
692            }
693        }
694
695        // A fused stride sums several input strides, so the result carries no
696        // bound proof of its own and is validated here instead
697        // (tensor4all/strided-rs#204).
698        validate_bounds(self.data().len(), &dims, &strides, self.offset())?;
699
700        Ok(from_meta(self.data(), &dims, &strides, self.offset()))
701    }
702
703    /// Broadcast this view to a target shape.
704    ///
705    /// Size-1 dimensions are expanded (stride set to 0) to match target.
706    ///
707    /// # Errors
708    ///
709    /// Returns [`StridedError::RankMismatch`] when `target_dims` does not
710    /// have one entry per axis, and [`StridedError::ShapeMismatch`] when an
711    /// axis is neither already the target extent nor of size one.
712    pub fn broadcast(&self, target_dims: &[usize]) -> Result<StridedView<'a, T, Op>> {
713        if self.dims().len() != target_dims.len() {
714            return Err(StridedError::RankMismatch(
715                self.dims().len(),
716                target_dims.len(),
717            ));
718        }
719        let mut new_strides = StrideVec::with_capacity(self.dims().len());
720        for i in 0..self.dims().len() {
721            if self.dims()[i] == target_dims[i] {
722                new_strides.push(self.strides()[i]);
723            } else if self.dims()[i] == 1 {
724                new_strides.push(0);
725            } else {
726                return Err(StridedError::ShapeMismatch(
727                    self.dims().to_vec(),
728                    target_dims.to_vec(),
729                ));
730            }
731        }
732        Ok(from_meta(self.data, target_dims, &new_strides, self.offset))
733    }
734}
735
736/// Composition methods: require `T: ElementOpApply` and `Op: ComposableElementOp<T>`.
737impl<'a, T: Copy + ElementOpApply, Op: ComposableElementOp<T>> StridedView<'a, T, Op> {
738    /// Transpose a 2D view: reverses dimensions and composes the Transpose element op.
739    ///
740    /// Julia equivalent: `Base.transpose(a::AbstractStridedView{<:Any, 2})`
741    ///
742    /// # Errors
743    ///
744    /// Returns [`StridedError::RankMismatch`] when the view is not 2-D.
745    pub fn transpose_2d(&self) -> Result<StridedView<'a, T, Op::ComposeTranspose>> {
746        if self.dims().len() != 2 {
747            return Err(StridedError::RankMismatch(self.dims().len(), 2));
748        }
749        Ok(from_meta(
750            self.data,
751            &[self.dims()[1], self.dims()[0]],
752            &[self.strides()[1], self.strides()[0]],
753            self.offset,
754        ))
755    }
756
757    /// Adjoint (conjugate transpose) of a 2D view.
758    ///
759    /// Julia equivalent: `Base.adjoint(a::AbstractStridedView{<:Any, 2})`
760    ///
761    /// # Errors
762    ///
763    /// Returns [`StridedError::RankMismatch`] when the view is not 2-D.
764    pub fn adjoint_2d(&self) -> Result<StridedView<'a, T, Op::ComposeAdjoint>> {
765        if self.dims().len() != 2 {
766            return Err(StridedError::RankMismatch(self.dims().len(), 2));
767        }
768        Ok(from_meta(
769            self.data,
770            &[self.dims()[1], self.dims()[0]],
771            &[self.strides()[1], self.strides()[0]],
772            self.offset,
773        ))
774    }
775
776    /// Complex conjugate (compose Conj without changing dims/strides).
777    ///
778    /// Julia equivalent: `Base.conj(a::AbstractStridedView)`
779    #[inline]
780    pub fn conj(&self) -> StridedView<'a, T, Op::ComposeConj> {
781        StridedView {
782            ptr: self.ptr,
783            data: self.data,
784            meta: self.meta.clone(),
785            offset: self.offset,
786            _op: PhantomData,
787        }
788    }
789}
790
791/// Element access: requires `Op: ElementOp<T>`.
792impl<'a, T: Copy, Op: ElementOp<T>> StridedView<'a, T, Op> {
793    /// Get an element with the element operation applied.
794    ///
795    /// # Panics
796    ///
797    /// Panics if `indices.len()` is not the view's rank, an index is out of
798    /// bounds, or the indexed offset does not fit `isize`.
799    pub fn get(&self, indices: &[usize]) -> T {
800        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
801        // SAFETY: `indexed_offset` (view.rs:239) asserts the index count and
802        // `index < dims[i]`, so the offset it returns lies within the reachable
803        // extent that `validate_bounds` (view.rs:186) proved stays inside
804        // `data` for this view's construction.
805        Op::apply(unsafe { *self.ptr.offset(offset) })
806    }
807
808    /// Get an element without bounds checking.
809    ///
810    /// # Safety
811    /// Caller must ensure indices are within bounds.
812    #[inline]
813    pub unsafe fn get_unchecked(&self, indices: &[usize]) -> T {
814        let mut idx = 0isize;
815        for (i, &index) in indices.iter().enumerate() {
816            idx += index as isize * self.strides()[i];
817        }
818        Op::apply(*self.ptr.offset(idx))
819    }
820}
821
822// ============================================================================
823// StridedViewMut
824// ============================================================================
825
826/// Dynamic-rank mutable strided view.
827///
828/// Always uses `Identity` element operation for write simplicity.
829/// Julia typically applies ops on the read side.
830pub struct StridedViewMut<'a, T> {
831    /// Element base: `base` plus `offset`, or a dangling pointer for an empty
832    /// layout. `get` and `set` go through it, so it stays cached.
833    ///
834    /// Nothing here reborrows the caller's `&mut [T]` any more: a stored slice
835    /// field was retagged at construction and by `data_mut`, which invalidated
836    /// this pointer and made every later `get`, `set`, and `as_view` access
837    /// undefined behavior.
838    ptr: *mut T,
839    /// Base of the whole backing slice.
840    ///
841    /// `base` with `src_len` is the handle `data` and `data_mut` re-derive the
842    /// slice from, so the backing extent stays independent of the shape (a
843    /// `[0]` shape still has a backing slice).
844    base: *mut T,
845    /// Length of the whole backing slice.
846    src_len: usize,
847    meta: ViewMeta,
848    offset: isize,
849    /// The view borrows the caller's slice for `'a` without holding it.
850    _marker: PhantomData<&'a mut [T]>,
851}
852
853// SAFETY: `StridedViewMut` uniquely owns its `&'a mut [T]` (held as
854// `PhantomData<&'a mut [T]>` plus the two raw pointers `base` and `ptr` derived
855// from it), and there is no `Sync` impl for it, so sending it moves that
856// exclusive borrow; `T: Send` is the bound `&mut [T]: Send` requires.
857unsafe impl<T: Send> Send for StridedViewMut<'_, T> {}
858
859impl<T: std::fmt::Debug> std::fmt::Debug for StridedViewMut<'_, T> {
860    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
861        f.debug_struct("StridedViewMut")
862            .field("dims", &self.meta.dims())
863            .field("strides", &self.meta.strides())
864            .field("offset", &self.offset)
865            .finish()
866    }
867}
868
869impl<'a, T> StridedViewMut<'a, T> {
870    /// A view from already-validated metadata.
871    #[inline]
872    fn from_meta(data: &'a mut [T], dims: &[usize], strides: &[isize], offset: isize) -> Self {
873        let meta = ViewMeta::from_slices(dims, strides);
874        let src_len = data.len();
875        let base = data.as_mut_ptr();
876        let ptr = if empty_layout(meta.dims()) {
877            empty_mut_ptr()
878        } else {
879            // SAFETY: as `base_ptr` above — `offset` was accepted by
880            // `validate_bounds` (view.rs:186) in `StridedViewMut::new`, or by a
881            // metadata transform that only reorders axes or zeroes strides, or
882            // came from the `new_unchecked` contract. `base` is this view's own
883            // live handle on `data` and the empty-layout branch above excludes
884            // the case where `offset` may point past it.
885            unsafe { base.offset(offset) }
886        };
887        Self {
888            ptr,
889            base,
890            src_len,
891            meta,
892            offset,
893            _marker: PhantomData,
894        }
895    }
896
897    /// Create a mutable view, copying the caller's metadata inline.
898    ///
899    /// The layout is copied, so it does not need to outlive the view and no
900    /// heap allocation happens at rank `<= INLINE_RANK`.
901    ///
902    /// # Errors
903    ///
904    /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
905    /// `strides` have different lengths, and
906    /// [`StridedError::OffsetOverflow`] when a reachable offset is not
907    /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
908    /// stride times that last index or the accumulated offsets overflow
909    /// `isize` or go negative, or the highest reachable offset is not below
910    /// the buffer length. A layout with a zero extent skips this check.
911    ///
912    /// # Examples
913    ///
914    /// ```
915    /// use strided_view::StridedViewMut;
916    ///
917    /// let mut data = [0.0f64; 4];
918    /// let mut view = StridedViewMut::<f64>::new(&mut data, &[2, 2], &[1, 2], 0).unwrap();
919    /// view.set(&[1, 1], 5.0);
920    /// assert_eq!(data[3], 5.0);
921    /// ```
922    #[inline]
923    pub fn new(
924        data: &'a mut [T],
925        dims: &[usize],
926        strides: &[isize],
927        offset: isize,
928    ) -> Result<Self> {
929        validate_bounds(data.len(), dims, strides, offset)?;
930        Ok(Self::from_meta(data, dims, strides, offset))
931    }
932
933    /// Create without bounds checking, copying the caller's metadata.
934    ///
935    /// # Safety
936    /// Caller must ensure all index combinations stay within bounds.
937    ///
938    /// # Examples
939    ///
940    /// ```
941    /// use strided_view::StridedViewMut;
942    ///
943    /// let mut data = [0.0f64; 4];
944    /// let view = unsafe { StridedViewMut::<f64>::new_unchecked(&mut data, &[2, 2], &[1, 2], 0) };
945    /// assert_eq!(view.ndim(), 2);
946    /// ```
947    #[inline]
948    pub unsafe fn new_unchecked(
949        data: &'a mut [T],
950        dims: &[usize],
951        strides: &[isize],
952        offset: isize,
953    ) -> Self {
954        Self::from_meta(data, dims, strides, offset)
955    }
956
957    /// Returns the shape (dimension sizes) of this view.
958    #[inline]
959    pub fn dims(&self) -> &[usize] {
960        self.meta.dims()
961    }
962
963    /// Returns the strides (in units of `T`) for each dimension.
964    #[inline]
965    pub fn strides(&self) -> &[isize] {
966        self.meta.strides()
967    }
968
969    /// Returns the byte offset into the backing data.
970    #[inline]
971    pub fn offset(&self) -> isize {
972        self.offset
973    }
974
975    /// Returns the number of dimensions (rank).
976    #[inline]
977    pub fn ndim(&self) -> usize {
978        self.dims().len()
979    }
980
981    /// Returns the total number of elements.
982    ///
983    /// # Panics
984    ///
985    /// Panics when the element count does not fit `usize`, which only a
986    /// broadcast (stride-0) layout can reach.
987    #[inline]
988    pub fn len(&self) -> usize {
989        element_count(self.dims())
990    }
991
992    /// Returns `true` if any dimension is zero.
993    #[inline]
994    pub fn is_empty(&self) -> bool {
995        self.dims().contains(&0)
996    }
997
998    /// Raw const pointer to element at the view's base offset.
999    #[inline]
1000    pub fn ptr(&self) -> *const T {
1001        self.ptr as *const T
1002    }
1003
1004    /// Raw mutable pointer to element at the view's base offset.
1005    #[inline]
1006    pub fn as_mut_ptr(&self) -> *mut T {
1007        self.ptr
1008    }
1009
1010    /// Returns a reference to the backing data slice.
1011    ///
1012    /// The length is the backing slice's, not the shape's: a view whose shape
1013    /// has a zero extent still reports the whole buffer.
1014    #[inline]
1015    pub fn data(&self) -> &[T] {
1016        // SAFETY: `base` is the caller's `data.as_mut_ptr()` and `src_len` is
1017        // that slice's length, both recorded at construction, so this rebuilds
1018        // exactly the borrowed slice. No length is derived from the shape, which
1019        // is why a zero-extent layout still reports the whole buffer. `T` is
1020        // initialized across it, and `&self` restricts this to a shared borrow.
1021        unsafe { std::slice::from_raw_parts(self.base, self.src_len) }
1022    }
1023
1024    /// Returns a mutable reference to the backing data slice.
1025    ///
1026    /// The slice is re-derived from the view's storage handle, so `ptr`, `get`,
1027    /// and `set` stay valid after the returned borrow ends.
1028    #[inline]
1029    pub fn data_mut(&mut self) -> &mut [T] {
1030        // SAFETY: as `data` above — the same `base`/`src_len` pair, so this
1031        // rebuilds the caller's slice rather than a shape-sized one. `&mut self`
1032        // keeps the returned slice the only reference into the view while it
1033        // lives. In a non-empty layout `ptr` is `base + offset`, derived without
1034        // a retag, so re-deriving the slice here does not invalidate it — the
1035        // property `get`/`set` rely on; an empty layout caches a dangling `ptr`
1036        // that no element access dereferences.
1037        unsafe { std::slice::from_raw_parts_mut(self.base, self.src_len) }
1038    }
1039
1040    /// Permute dimensions, consuming the mutable view.
1041    ///
1042    /// Returns a new mutable view with reordered dimensions and strides.
1043    /// Takes `self` by value to prevent aliasing of mutable views.
1044    ///
1045    /// # Errors
1046    ///
1047    /// Returns [`StridedError::RankMismatch`] when `perm` does not have one
1048    /// entry per axis, and [`StridedError::InvalidAxis`] when an entry
1049    /// names an axis outside the rank or repeats an axis.
1050    pub fn permute(self, perm: &[usize]) -> Result<StridedViewMut<'a, T>> {
1051        let rank = self.dims().len();
1052        if perm.len() != rank {
1053            return Err(StridedError::RankMismatch(perm.len(), rank));
1054        }
1055        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
1056        seen.resize(rank, false);
1057        for &p in perm {
1058            if p >= rank {
1059                return Err(StridedError::InvalidAxis { axis: p, rank });
1060            }
1061            if seen[p] {
1062                return Err(StridedError::InvalidAxis { axis: p, rank });
1063            }
1064            seen[p] = true;
1065        }
1066        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
1067        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
1068        Ok(Self {
1069            meta: ViewMeta::from_slices(&new_dims, &new_strides),
1070            ..self
1071        })
1072    }
1073
1074    /// Reborrow as an immutable view.
1075    ///
1076    /// The reborrow copies this view's metadata into the immutable view's
1077    /// inline storage, which allocates nothing up to [`INLINE_RANK`] and
1078    /// spills above it (the mutable owner always keeps its own copy).
1079    ///
1080    /// # Examples
1081    ///
1082    /// ```
1083    /// use strided_view::StridedViewMut;
1084    ///
1085    /// let mut data = [1.0f64, 2.0, 3.0];
1086    /// let view = StridedViewMut::<f64>::new(&mut data, &[3], &[1], 0).unwrap();
1087    /// let read = view.as_view();
1088    /// assert_eq!(read.get(&[2]), 3.0);
1089    /// ```
1090    #[inline]
1091    pub fn as_view(&self) -> StridedView<'_, T, Identity> {
1092        // SAFETY: `data()` re-derives the slice from this view's own handle
1093        // (`base` + `src_len`), and `ptr` is `base + offset` for the same
1094        // `offset`, so the immutable view ends up pointing at the same storage
1095        // the mutable view owns. A zero-extent layout yields a dangling pointer
1096        // on both sides (`empty_mut_ptr` here, the `empty_layout` branch of
1097        // `base_ptr` in the immutable view), so they agree there too. `&self`
1098        // keeps that storage exclusively borrowed for the returned view's
1099        // lifetime, so the shared view can only read it, and `T` is initialized
1100        // across the whole slice.
1101        unsafe { StridedView::new_unchecked(self.data(), self.dims(), self.strides(), self.offset) }
1102    }
1103}
1104
1105impl<'a, T: Copy> StridedViewMut<'a, T> {
1106    /// Get an element.
1107    ///
1108    /// # Panics
1109    ///
1110    /// Panics if `indices.len()` is not the view's rank, an index is out of
1111    /// bounds, or the indexed offset does not fit `isize`.
1112    pub fn get(&self, indices: &[usize]) -> T {
1113        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
1114        // SAFETY: in a non-empty layout `ptr` is `base + offset`, derived from
1115        // the caller's `&'a mut [T]` through raw pointers only, so a reborrow of
1116        // the backing slice does not invalidate it: `data_mut` re-derives that
1117        // slice from `base` and never rewrites `ptr`, and offsetting a raw
1118        // pointer keeps `base`'s provenance. (An empty layout caches a dangling
1119        // `ptr`, but no index satisfies `index < dims[i]` there, so this access
1120        // is unreachable.) `indexed_offset` (view.rs:239) asserts the index
1121        // count and `index < dims[i]`, so the relative offset it returns keeps
1122        // `ptr + offset` inside the extent `validate_bounds` (view.rs:186)
1123        // proved reachable, i.e. inside `base..base + src_len`, where every
1124        // element is initialized because the view borrows an initialized
1125        // `&'a mut [T]` and no method shortens that borrow.
1126        unsafe { *self.ptr.offset(offset) }
1127    }
1128
1129    /// Set an element.
1130    ///
1131    /// # Panics
1132    ///
1133    /// Panics if `indices.len()` is not the view's rank, an index is out of
1134    /// bounds, or the indexed offset does not fit `isize`.
1135    pub fn set(&mut self, indices: &[usize], value: T) {
1136        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
1137        // SAFETY: as `get` above — the same `ptr` survives an earlier `data_mut`
1138        // reborrow, and `indexed_offset` (view.rs:239) bounds-checks every index,
1139        // so the store lands inside `base..base + src_len`. `&mut self` makes
1140        // this the only access through the view for the call, and the view holds
1141        // the sole `&'a mut [T]` borrow of that storage, so nothing else can
1142        // alias the written place.
1143        unsafe {
1144            *self.ptr.offset(offset) = value;
1145        }
1146    }
1147}
1148
1149// ============================================================================
1150// StridedArray
1151// ============================================================================
1152
1153/// Owned strided multidimensional array.
1154///
1155/// Supports both column-major (Julia default) and row-major (C default) layouts.
1156pub struct StridedArray<T> {
1157    data: Vec<T>,
1158    meta: ViewMeta,
1159    offset: isize,
1160}
1161
1162impl<T: std::fmt::Debug> std::fmt::Debug for StridedArray<T> {
1163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1164        f.debug_struct("StridedArray")
1165            .field("dims", &self.meta.dims())
1166            .field("strides", &self.meta.strides())
1167            .field("offset", &self.offset)
1168            .finish()
1169    }
1170}
1171
1172impl<T: Clone> Clone for StridedArray<T> {
1173    fn clone(&self) -> Self {
1174        Self {
1175            data: self.data.clone(),
1176            meta: self.meta.clone(),
1177            offset: self.offset,
1178        }
1179    }
1180}
1181
1182impl<T: Clone + Default> StridedArray<T> {
1183    /// Create a column-major (Julia default) tensor filled with Default values.
1184    ///
1185    /// The shape and strides are stored inline, so a rank `<= INLINE_RANK`
1186    /// tensor allocates only its element buffer.
1187    ///
1188    /// # Examples
1189    ///
1190    /// ```
1191    /// use strided_view::StridedArray;
1192    ///
1193    /// let a = StridedArray::<f64>::col_major(&[2, 3]);
1194    /// assert_eq!(a.dims(), &[2, 3]);
1195    /// assert_eq!(a.strides(), &[1, 2]);
1196    /// ```
1197    ///
1198    /// # Panics
1199    ///
1200    /// Panics when the element count or a stride does not fit `isize`; use
1201    /// [`Self::try_col_major`] to get [`StridedError::OffsetOverflow`] instead.
1202    pub fn col_major(dims: &[usize]) -> Self {
1203        Self::try_col_major(dims).expect("col_major: shape overflows isize")
1204    }
1205
1206    /// Fallible [`Self::col_major`].
1207    ///
1208    /// # Errors
1209    ///
1210    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1211    /// stride does not fit `isize`.
1212    ///
1213    /// # Examples
1214    ///
1215    /// ```
1216    /// use strided_view::{StridedArray, StridedError};
1217    ///
1218    /// assert_eq!(StridedArray::<f64>::try_col_major(&[2, 3]).unwrap().strides(), &[1, 2]);
1219    /// assert!(matches!(
1220    ///     StridedArray::<f64>::try_col_major(&[1 << 62, 4]),
1221    ///     Err(StridedError::OffsetOverflow)
1222    /// ));
1223    /// ```
1224    pub fn try_col_major(dims: &[usize]) -> Result<Self> {
1225        let strides = checked_col_major_strides(dims)?;
1226        let total = checked_element_count(dims)?;
1227        Ok(Self {
1228            data: vec![T::default(); total],
1229            meta: ViewMeta::from_slices(dims, &strides),
1230            offset: 0,
1231        })
1232    }
1233
1234    /// Create a row-major (C default) tensor filled with Default values.
1235    ///
1236    /// The shape and strides are stored inline, so a rank `<= INLINE_RANK`
1237    /// tensor allocates only its element buffer.
1238    ///
1239    /// # Examples
1240    ///
1241    /// ```
1242    /// use strided_view::StridedArray;
1243    ///
1244    /// let a = StridedArray::<f64>::row_major(&[2, 3]);
1245    /// assert_eq!(a.dims(), &[2, 3]);
1246    /// assert_eq!(a.strides(), &[3, 1]);
1247    /// ```
1248    ///
1249    /// # Panics
1250    ///
1251    /// Panics when the element count or a stride does not fit `isize`; use
1252    /// [`Self::try_row_major`] to get [`StridedError::OffsetOverflow`] instead.
1253    pub fn row_major(dims: &[usize]) -> Self {
1254        Self::try_row_major(dims).expect("row_major: shape overflows isize")
1255    }
1256
1257    /// Fallible [`Self::row_major`].
1258    ///
1259    /// # Errors
1260    ///
1261    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1262    /// stride does not fit `isize`.
1263    pub fn try_row_major(dims: &[usize]) -> Result<Self> {
1264        let strides = checked_row_major_strides(dims)?;
1265        let total = checked_element_count(dims)?;
1266        Ok(Self {
1267            data: vec![T::default(); total],
1268            meta: ViewMeta::from_slices(dims, &strides),
1269            offset: 0,
1270        })
1271    }
1272
1273    /// Create a column-major tensor with values produced by a function.
1274    ///
1275    /// The function is called with indices in column-major iteration order.
1276    ///
1277    /// # Panics
1278    ///
1279    /// Panics when the element count or a stride does not fit `isize`; use
1280    /// [`Self::try_from_fn_col_major`] to get [`StridedError::OffsetOverflow`] instead.
1281    pub fn from_fn_col_major(dims: &[usize], f: impl FnMut(&[usize]) -> T) -> Self {
1282        Self::try_from_fn_col_major(dims, f).expect("from_fn_col_major: shape overflows isize")
1283    }
1284
1285    /// Fallible [`Self::from_fn_col_major`]; `f` is not called on error.
1286    ///
1287    /// # Errors
1288    ///
1289    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1290    /// stride does not fit `isize`.
1291    pub fn try_from_fn_col_major(dims: &[usize], mut f: impl FnMut(&[usize]) -> T) -> Result<Self> {
1292        let strides = checked_col_major_strides(dims)?;
1293        let total = checked_element_count(dims)?;
1294        let rank = dims.len();
1295        let mut data = Vec::with_capacity(total);
1296        let mut idx: AxisVec = AxisVec::new();
1297        idx.resize(rank, 0usize);
1298        // Borrow the cursor once; indexing the `TinyVec` per access would pay
1299        // its inline/heap match in the element-count loop.
1300        let idx: &mut [usize] = &mut idx[..];
1301        for _ in 0..total {
1302            data.push(f(idx));
1303            for d in 0..rank {
1304                idx[d] += 1;
1305                if idx[d] < dims[d] {
1306                    break;
1307                }
1308                idx[d] = 0;
1309            }
1310        }
1311        Ok(Self {
1312            data,
1313            meta: ViewMeta::from_slices(dims, &strides),
1314            offset: 0,
1315        })
1316    }
1317
1318    /// Create a row-major tensor with values produced by a function.
1319    ///
1320    /// The function is called with indices in row-major iteration order.
1321    ///
1322    /// # Panics
1323    ///
1324    /// Panics when the element count or a stride does not fit `isize`; use
1325    /// [`Self::try_from_fn_row_major`] to get [`StridedError::OffsetOverflow`] instead.
1326    pub fn from_fn_row_major(dims: &[usize], f: impl FnMut(&[usize]) -> T) -> Self {
1327        Self::try_from_fn_row_major(dims, f).expect("from_fn_row_major: shape overflows isize")
1328    }
1329
1330    /// Fallible [`Self::from_fn_row_major`]; `f` is not called on error.
1331    ///
1332    /// # Errors
1333    ///
1334    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1335    /// stride does not fit `isize`.
1336    pub fn try_from_fn_row_major(dims: &[usize], mut f: impl FnMut(&[usize]) -> T) -> Result<Self> {
1337        let strides = checked_row_major_strides(dims)?;
1338        let total = checked_element_count(dims)?;
1339        let rank = dims.len();
1340        let mut data = Vec::with_capacity(total);
1341        let mut idx: AxisVec = AxisVec::new();
1342        idx.resize(rank, 0usize);
1343        // Borrow the cursor once; see `from_fn_col_major`.
1344        let idx: &mut [usize] = &mut idx[..];
1345        for _ in 0..total {
1346            data.push(f(idx));
1347            for d in (0..rank).rev() {
1348                idx[d] += 1;
1349                if idx[d] < dims[d] {
1350                    break;
1351                }
1352                idx[d] = 0;
1353            }
1354        }
1355        Ok(Self {
1356            data,
1357            meta: ViewMeta::from_slices(dims, &strides),
1358            offset: 0,
1359        })
1360    }
1361}
1362
1363impl<T> StridedArray<T> {
1364    /// Create from raw parts.
1365    ///
1366    /// # Errors
1367    ///
1368    /// Returns [`StridedError::StrideLengthMismatch`] when `dims` and
1369    /// `strides` have different lengths, and
1370    /// [`StridedError::OffsetOverflow`] when a reachable offset is not
1371    /// representable: `dim - 1` of a non-unit extent does not fit `isize`, a
1372    /// stride times that last index or the accumulated offsets overflow
1373    /// `isize` or go negative, or the highest reachable offset is not below
1374    /// the buffer length. A layout with a zero extent skips this check.
1375    pub fn from_parts(
1376        data: Vec<T>,
1377        dims: &[usize],
1378        strides: &[isize],
1379        offset: isize,
1380    ) -> Result<Self> {
1381        validate_bounds(data.len(), dims, strides, offset)?;
1382        Ok(Self {
1383            data,
1384            meta: ViewMeta::from_slices(dims, strides),
1385            offset,
1386        })
1387    }
1388
1389    /// Returns the shape (dimension sizes) of this array.
1390    #[inline]
1391    pub fn dims(&self) -> &[usize] {
1392        self.meta.dims()
1393    }
1394
1395    /// Returns the strides (in units of `T`) for each dimension.
1396    #[inline]
1397    pub fn strides(&self) -> &[isize] {
1398        self.meta.strides()
1399    }
1400
1401    /// Returns the number of dimensions (rank).
1402    #[inline]
1403    pub fn ndim(&self) -> usize {
1404        self.dims().len()
1405    }
1406
1407    /// Returns the total number of elements.
1408    ///
1409    /// # Panics
1410    ///
1411    /// Panics when the element count does not fit `usize`, which only a
1412    /// broadcast (stride-0) layout can reach.
1413    #[inline]
1414    pub fn len(&self) -> usize {
1415        element_count(self.dims())
1416    }
1417
1418    /// Returns `true` if any dimension is zero.
1419    #[inline]
1420    pub fn is_empty(&self) -> bool {
1421        self.dims().contains(&0)
1422    }
1423
1424    /// Returns a reference to the backing data slice.
1425    #[inline]
1426    pub fn data(&self) -> &[T] {
1427        &self.data
1428    }
1429
1430    /// Returns a mutable reference to the backing data slice.
1431    #[inline]
1432    pub fn data_mut(&mut self) -> &mut [T] {
1433        &mut self.data
1434    }
1435
1436    /// Create an immutable view over this tensor.
1437    #[inline]
1438    pub fn view(&self) -> StridedView<'_, T> {
1439        let ptr = if self.is_empty() {
1440            empty_const_ptr()
1441        } else {
1442            // SAFETY: `offset` is either 0 from a constructor or was accepted by
1443            // `validate_bounds` in `StridedArray::from_parts` (view.rs:1374), so
1444            // `data.as_ptr() + offset` stays inside `data`. The empty branch
1445            // above excludes a zero-sized layout, whose offset need not be
1446            // reachable.
1447            unsafe { self.data.as_ptr().offset(self.offset) }
1448        };
1449        StridedView {
1450            ptr,
1451            data: &self.data,
1452            meta: self.meta.clone(),
1453            offset: self.offset,
1454            _op: PhantomData,
1455        }
1456    }
1457
1458    /// Create a mutable view over this tensor.
1459    #[inline]
1460    pub fn view_mut(&mut self) -> StridedViewMut<'_, T> {
1461        let src_len = self.data.len();
1462        let base = self.data.as_mut_ptr();
1463        let ptr = if self.is_empty() {
1464            empty_mut_ptr()
1465        } else {
1466            // SAFETY: as `view` above — `self.offset` is either 0 from a
1467            // constructor or was accepted by `validate_bounds` in
1468            // `StridedArray::from_parts` (view.rs:1374), and `&mut self` keeps
1469            // this reborrow exclusive. The empty branch above excludes a
1470            // zero-sized layout, whose offset need not be reachable.
1471            unsafe { base.offset(self.offset) }
1472        };
1473        StridedViewMut {
1474            ptr,
1475            base,
1476            src_len,
1477            meta: self.meta.clone(),
1478            offset: self.offset,
1479            _marker: PhantomData,
1480        }
1481    }
1482
1483    /// Permute dimensions (metadata-only reorder, no data copy).
1484    ///
1485    /// Returns a new array with reordered dims and strides.
1486    /// The underlying data buffer is not touched.
1487    ///
1488    /// # Errors
1489    ///
1490    /// Returns [`StridedError::RankMismatch`] when `perm` does not have one
1491    /// entry per axis, and [`StridedError::InvalidAxis`] when an entry
1492    /// names an axis outside the rank or repeats an axis.
1493    pub fn permuted(self, perm: &[usize]) -> Result<Self> {
1494        let rank = self.dims().len();
1495        if perm.len() != rank {
1496            return Err(StridedError::RankMismatch(perm.len(), rank));
1497        }
1498        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
1499        seen.resize(rank, false);
1500        for &p in perm {
1501            if p >= rank {
1502                return Err(StridedError::InvalidAxis { axis: p, rank });
1503            }
1504            if seen[p] {
1505                return Err(StridedError::InvalidAxis { axis: p, rank });
1506            }
1507            seen[p] = true;
1508        }
1509        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
1510        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
1511        Ok(Self {
1512            data: self.data,
1513            meta: ViewMeta::from_slices(&new_dims, &new_strides),
1514            offset: self.offset,
1515        })
1516    }
1517
1518    /// Consume the array and return the backing `Vec<T>`.
1519    pub fn into_data(self) -> Vec<T> {
1520        self.data
1521    }
1522
1523    /// Iterate over all elements in memory order.
1524    pub fn iter(&self) -> std::slice::Iter<'_, T> {
1525        self.data.iter()
1526    }
1527
1528    /// Mutable iteration over all elements in memory order.
1529    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
1530        self.data.iter_mut()
1531    }
1532}
1533
1534impl<T: Default> StridedArray<T> {
1535    /// Create a column-major tensor reusing an existing buffer.
1536    ///
1537    /// If `buf` has at least `product(dims)` elements, it is truncated and
1538    /// zeroed.  Otherwise a fresh buffer is allocated.
1539    ///
1540    /// # Panics
1541    ///
1542    /// Panics when the element count or a stride does not fit `isize`; use
1543    /// [`Self::try_col_major_from_buffer`] to get
1544    /// [`StridedError::OffsetOverflow`] instead.
1545    pub fn col_major_from_buffer(buf: Vec<T>, dims: &[usize]) -> Self {
1546        Self::try_col_major_from_buffer(buf, dims)
1547            .expect("col_major_from_buffer: shape overflows isize")
1548    }
1549
1550    /// Fallible [`Self::col_major_from_buffer`]; `buf` is dropped on error.
1551    ///
1552    /// # Errors
1553    ///
1554    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1555    /// stride does not fit `isize`.
1556    pub fn try_col_major_from_buffer(mut buf: Vec<T>, dims: &[usize]) -> Result<Self> {
1557        let strides = checked_col_major_strides(dims)?;
1558        let total = checked_element_count(dims)?;
1559        if buf.len() >= total {
1560            buf.truncate(total);
1561        } else {
1562            buf.resize_with(total, T::default);
1563        }
1564        // Zero the reused region
1565        for v in buf.iter_mut() {
1566            *v = T::default();
1567        }
1568        Ok(Self {
1569            data: buf,
1570            meta: ViewMeta::from_slices(dims, &strides),
1571            offset: 0,
1572        })
1573    }
1574}
1575
1576impl<T: Copy> StridedArray<T> {
1577    /// Create a column-major tensor with **uninitialized** data.
1578    ///
1579    /// The element type of the returned array is [`MaybeUninit<T>`], so no slot
1580    /// ever claims to hold a `T` before the caller wrote one — which is what
1581    /// `bool`, `char` and reference elements need (#413).
1582    ///
1583    /// # Safety
1584    ///
1585    /// This is the pre-#413 contract kept on the signature: a slot may be read
1586    /// through [`MaybeUninit::assume_init`] only after it was written. Allocating
1587    /// the slots carries no memory-safety precondition of its own; the per-slot
1588    /// rule is the one [`MaybeUninit`] already states.
1589    ///
1590    /// # Panics
1591    ///
1592    /// Panics when the element count or a stride does not fit `isize`; use
1593    /// [`Self::try_col_major_uninit`] to get [`StridedError::OffsetOverflow`]
1594    /// instead.
1595    ///
1596    /// # Examples
1597    ///
1598    /// ```
1599    /// use std::mem::MaybeUninit;
1600    /// use strided_view::StridedArray;
1601    ///
1602    /// let mut a = unsafe { StridedArray::<bool>::col_major_uninit(&[2]) };
1603    /// a.set(&[0], MaybeUninit::new(true));
1604    /// a.set(&[1], MaybeUninit::new(false));
1605    /// // SAFETY: both slots were written above.
1606    /// assert!(unsafe { a.get(&[0]).assume_init() });
1607    /// ```
1608    pub unsafe fn col_major_uninit(dims: &[usize]) -> StridedArray<MaybeUninit<T>> {
1609        Self::try_col_major_uninit(dims).expect("col_major_uninit: shape overflows isize")
1610    }
1611
1612    /// Fallible [`Self::col_major_uninit`].
1613    ///
1614    /// # Safety
1615    ///
1616    /// As [`Self::col_major_uninit`].
1617    ///
1618    /// # Errors
1619    ///
1620    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1621    /// stride does not fit `isize`.
1622    pub unsafe fn try_col_major_uninit(dims: &[usize]) -> Result<StridedArray<MaybeUninit<T>>> {
1623        let strides = checked_col_major_strides(dims)?;
1624        let total = checked_element_count(dims)?;
1625        Ok(StridedArray {
1626            data: vec![MaybeUninit::uninit(); total],
1627            meta: ViewMeta::from_slices(dims, &strides),
1628            offset: 0,
1629        })
1630    }
1631
1632    /// Reuse an existing buffer as a column-major tensor **without zeroing**.
1633    ///
1634    /// The buffer is `Vec<MaybeUninit<T>>` for the same reason as
1635    /// [`Self::col_major_uninit`]: a slot the caller has not written yet is not a
1636    /// `T` (#413). Slots the buffer already holds are kept as they are.
1637    ///
1638    /// # Safety
1639    ///
1640    /// As [`Self::col_major_uninit`]: read a slot with
1641    /// [`MaybeUninit::assume_init`] only after it was written.
1642    ///
1643    /// # Panics
1644    ///
1645    /// Panics when the element count or a stride does not fit `isize`; use
1646    /// [`Self::try_col_major_from_buffer_uninit`] to get
1647    /// [`StridedError::OffsetOverflow`] instead.
1648    pub unsafe fn col_major_from_buffer_uninit(
1649        buf: Vec<MaybeUninit<T>>,
1650        dims: &[usize],
1651    ) -> StridedArray<MaybeUninit<T>> {
1652        Self::try_col_major_from_buffer_uninit(buf, dims)
1653            .expect("col_major_from_buffer_uninit: shape overflows isize")
1654    }
1655
1656    /// Fallible [`Self::col_major_from_buffer_uninit`]; `buf` is dropped on
1657    /// error.
1658    ///
1659    /// # Safety
1660    ///
1661    /// As [`Self::col_major_uninit`].
1662    ///
1663    /// # Errors
1664    ///
1665    /// Returns [`StridedError::OffsetOverflow`] when the element count or a
1666    /// stride does not fit `isize`.
1667    pub unsafe fn try_col_major_from_buffer_uninit(
1668        mut buf: Vec<MaybeUninit<T>>,
1669        dims: &[usize],
1670    ) -> Result<StridedArray<MaybeUninit<T>>> {
1671        let strides = checked_col_major_strides(dims)?;
1672        let total = checked_element_count(dims)?;
1673        // Truncates or grows to the shape; the retained prefix keeps whatever the
1674        // caller already put in it.
1675        buf.resize(total, MaybeUninit::uninit());
1676        Ok(StridedArray {
1677            data: buf,
1678            meta: ViewMeta::from_slices(dims, &strides),
1679            offset: 0,
1680        })
1681    }
1682}
1683
1684impl<T: Copy> StridedArray<T> {
1685    /// Get an element by multi-dimensional index.
1686    ///
1687    /// # Panics
1688    ///
1689    /// Panics if `indices.len()` is not the array's rank, an index is out of
1690    /// bounds, or the indexed offset does not fit `isize`.
1691    pub fn get(&self, indices: &[usize]) -> T {
1692        self.view().get(indices)
1693    }
1694
1695    /// Set an element by multi-dimensional index.
1696    ///
1697    /// # Panics
1698    ///
1699    /// Panics if `indices.len()` is not the array's rank, an index is out of
1700    /// bounds, or the indexed offset does not fit `isize`.
1701    pub fn set(&mut self, indices: &[usize], value: T) {
1702        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1703        self.data[offset as usize] = value;
1704    }
1705}
1706
1707impl<T: Copy> Index<&[usize]> for StridedArray<T> {
1708    type Output = T;
1709
1710    fn index(&self, indices: &[usize]) -> &T {
1711        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1712        &self.data[offset as usize]
1713    }
1714}
1715
1716impl<T: Copy> IndexMut<&[usize]> for StridedArray<T> {
1717    fn index_mut(&mut self, indices: &[usize]) -> &mut T {
1718        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1719        &mut self.data[offset as usize]
1720    }
1721}
1722
1723// ============================================================================
1724// Tests
1725// ============================================================================
1726
1727#[cfg(test)]
1728mod tests;