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