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/// Compute column-major strides (Julia default: first index varies fastest).
259///
260/// Returns an inline [`StrideVec`]: the result does not allocate at rank
261/// `<= INLINE_RANK` and spills above it.
262///
263/// # Examples
264///
265/// ```
266/// use strided_view::col_major_strides;
267///
268/// // Julia default: first index varies fastest.
269/// assert_eq!(col_major_strides(&[2, 3]).as_slice(), &[1, 2]);
270/// ```
271#[inline]
272pub fn col_major_strides(dims: &[usize]) -> StrideVec {
273    let rank = dims.len();
274    if rank == 0 {
275        return StrideVec::new();
276    }
277    let mut strides = StrideVec::new();
278    strides.resize(rank, 1isize);
279    for i in 1..rank {
280        strides[i] = strides[i - 1] * dims[i - 1] as isize;
281    }
282    strides
283}
284
285/// Compute row-major strides (C default: last index varies fastest).
286///
287/// Returns an inline [`StrideVec`]: the result does not allocate at rank
288/// `<= INLINE_RANK` and spills above it.
289///
290/// # Examples
291///
292/// ```
293/// use strided_view::row_major_strides;
294///
295/// // C default: last index varies fastest.
296/// assert_eq!(row_major_strides(&[2, 3]).as_slice(), &[3, 1]);
297/// ```
298#[inline]
299pub fn row_major_strides(dims: &[usize]) -> StrideVec {
300    let rank = dims.len();
301    if rank == 0 {
302        return StrideVec::new();
303    }
304    let mut strides = StrideVec::new();
305    strides.resize(rank, 1isize);
306    for i in (0..rank - 1).rev() {
307        strides[i] = strides[i + 1] * dims[i + 1] as isize;
308    }
309    strides
310}
311
312// ============================================================================
313// StridedView
314// ============================================================================
315
316/// Dynamic-rank immutable strided view with lazy element operations.
317///
318/// This is the Julia-equivalent `StridedView` type with:
319/// - Dynamic rank, with metadata stored inline for up to [`INLINE_RANK`]
320///   dimensions ([`StridedView::new`] copies the caller's layout, so the view
321///   borrows only the element data)
322/// - Lazy element operations via the `Op` type parameter
323/// - Zero-copy transformations (permute, transpose, adjoint, conj)
324///
325/// # Type Parameters
326/// - `'a`: Lifetime of the underlying data
327/// - `T`: Element type
328/// - `Op`: Element operation applied lazily on access (default: `Identity`)
329///
330/// # Size
331///
332/// The inline metadata makes the view 184 bytes on 64-bit targets for
333/// `usize`/`isize` strides.
334///
335/// # Auto traits
336///
337/// The view borrows its elements, so it is `Send` only when `T: Sync` (a
338/// shared slice is). A view of non-`Sync` data is not `Send`:
339///
340/// ```compile_fail
341/// use std::cell::Cell;
342/// use strided_view::StridedView;
343///
344/// fn assert_send<T: Send>() {}
345/// assert_send::<StridedView<'static, Cell<i32>>>();
346/// ```
347pub struct StridedView<'a, T, Op = Identity> {
348    ptr: *const T,
349    data: &'a [T],
350    meta: ViewMeta,
351    offset: isize,
352    _op: PhantomData<Op>,
353}
354
355unsafe impl<T: Sync, Op: Send> Send for StridedView<'_, T, Op> {}
356unsafe impl<T: Sync, Op: Sync> Sync for StridedView<'_, T, Op> {}
357
358impl<T, Op> Clone for StridedView<'_, T, Op> {
359    #[inline]
360    fn clone(&self) -> Self {
361        Self {
362            ptr: self.ptr,
363            data: self.data,
364            meta: self.meta.clone(),
365            offset: self.offset,
366            _op: PhantomData,
367        }
368    }
369}
370
371impl<T: std::fmt::Debug, Op> std::fmt::Debug for StridedView<'_, T, Op> {
372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        f.debug_struct("StridedView")
374            .field("dims", &self.meta.dims())
375            .field("strides", &self.meta.strides())
376            .field("offset", &self.offset)
377            .finish()
378    }
379}
380
381/// The cached base pointer of a view over `data` at `offset`.
382///
383/// An empty layout gets the dangling pointer rather than `data + offset`: a
384/// zero-sized extent may sit at an offset no element of `data` reaches.
385#[inline]
386fn base_ptr<T>(data: &[T], dims: &[usize], offset: isize) -> *const T {
387    if empty_layout(dims) {
388        empty_const_ptr()
389    } else {
390        unsafe { data.as_ptr().offset(offset) }
391    }
392}
393
394/// A view from already-validated metadata, with the element op chosen here.
395#[inline]
396fn from_meta<'a, T, Op>(
397    data: &'a [T],
398    dims: &[usize],
399    strides: &[isize],
400    offset: isize,
401) -> StridedView<'a, T, Op> {
402    let meta = ViewMeta::from_slices(dims, strides);
403    let ptr = base_ptr(data, meta.dims(), offset);
404    StridedView {
405        ptr,
406        data,
407        meta,
408        offset,
409        _op: PhantomData,
410    }
411}
412
413impl<'a, T, Op> StridedView<'a, T, Op> {
414    /// Create an immutable strided view, copying the caller's metadata inline.
415    ///
416    /// The layout is copied into the view's inline storage, so it does not
417    /// need to outlive the view and no heap allocation happens at rank
418    /// `<= INLINE_RANK`. The view's lifetime is tied to `data` only.
419    ///
420    /// # Examples
421    ///
422    /// ```
423    /// use strided_view::StridedView;
424    ///
425    /// let data = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
426    /// let view = StridedView::<f64>::new(&data, &[2, 3], &[1, 2], 0).unwrap();
427    /// assert_eq!(view.dims(), &[2, 3]);
428    /// assert_eq!(view.get(&[1, 2]), 6.0);
429    /// ```
430    #[inline]
431    pub fn new(data: &'a [T], dims: &[usize], strides: &[isize], offset: isize) -> Result<Self> {
432        validate_bounds(data.len(), dims, strides, offset)?;
433        Ok(from_meta(data, dims, strides, offset))
434    }
435
436    /// Create a view without bounds checking, copying the caller's metadata.
437    ///
438    /// # Safety
439    /// The caller must ensure all index combinations stay within bounds.
440    ///
441    /// # Examples
442    ///
443    /// ```
444    /// use strided_view::StridedView;
445    ///
446    /// let data = [1.0f64, 2.0, 3.0, 4.0];
447    /// let view = unsafe { StridedView::<f64>::new_unchecked(&data, &[2, 2], &[1, 2], 0) };
448    /// assert_eq!(view.ndim(), 2);
449    /// ```
450    #[inline]
451    pub unsafe fn new_unchecked(
452        data: &'a [T],
453        dims: &[usize],
454        strides: &[isize],
455        offset: isize,
456    ) -> Self {
457        from_meta(data, dims, strides, offset)
458    }
459
460    /// Returns the shape (dimension sizes) of this view.
461    #[inline]
462    pub fn dims(&self) -> &[usize] {
463        self.meta.dims()
464    }
465
466    /// Returns the strides (in units of `T`) for each dimension.
467    #[inline]
468    pub fn strides(&self) -> &[isize] {
469        self.meta.strides()
470    }
471
472    /// Returns the byte offset into the backing data.
473    #[inline]
474    pub fn offset(&self) -> isize {
475        self.offset
476    }
477
478    /// Returns the number of dimensions (rank).
479    #[inline]
480    pub fn ndim(&self) -> usize {
481        self.dims().len()
482    }
483
484    /// Returns the total number of elements.
485    #[inline]
486    pub fn len(&self) -> usize {
487        self.dims().iter().product()
488    }
489
490    /// Returns `true` if any dimension is zero.
491    #[inline]
492    pub fn is_empty(&self) -> bool {
493        self.dims().iter().any(|&d| d == 0)
494    }
495
496    /// Returns a reference to the backing data slice.
497    #[inline]
498    pub fn data(&self) -> &'a [T] {
499        self.data
500    }
501
502    /// Raw const pointer to element at the view's base offset.
503    #[inline]
504    pub fn ptr(&self) -> *const T {
505        self.ptr
506    }
507
508    /// Permute dimensions.
509    pub fn permute(&self, perm: &[usize]) -> Result<StridedView<'a, T, Op>> {
510        let rank = self.dims().len();
511        if perm.len() != rank {
512            return Err(StridedError::RankMismatch(perm.len(), rank));
513        }
514        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
515        seen.resize(rank, false);
516        for &p in perm {
517            if p >= rank {
518                return Err(StridedError::InvalidAxis { axis: p, rank });
519            }
520            if seen[p] {
521                return Err(StridedError::InvalidAxis { axis: p, rank });
522            }
523            seen[p] = true;
524        }
525        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
526        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
527        Ok(from_meta(self.data, &new_dims, &new_strides, self.offset))
528    }
529
530    /// Create a diagonal view by fusing repeated axis pairs via stride trick (zero-copy).
531    ///
532    /// Each pair `(a, b)` ties axes `a` and `b` to one index. Pairs that share
533    /// an axis chain into one class (`[(0, 1), (1, 2)]` and `[(0, 1), (0, 2)]`
534    /// both tie axes 0, 1 and 2), and each class becomes one axis:
535    /// - New stride = the sum of the strides of the axes in the class
536    /// - New dim = the minimum of their dims
537    /// - The class sits at its lowest-numbered axis; the others are removed
538    /// - Pairs use **original** axis numbering; their order does not matter,
539    ///   and repeating a pair has no further effect
540    ///
541    /// # Errors
542    ///
543    /// Returns [`StridedError::InvalidAxis`] when a pair names an axis outside
544    /// the rank or names the same axis twice, and
545    /// [`StridedError::OffsetOverflow`] when a fused stride does not fit
546    /// `isize` or the fused layout reaches past the backing allocation.
547    ///
548    /// # Example
549    /// `A[i,i,j]` shape=`[n,n,m]` strides=`[s0,s1,s2]` -> shape=`[n,m]` strides=`[s0+s1, s2]`
550    pub fn diagonal_view(&self, axis_pairs: &[(usize, usize)]) -> Result<StridedView<'a, T, Op>> {
551        let ndim = self.ndim();
552        // Each pair ties two axes to one index, so the pairs partition the
553        // axes into classes. Track each class's root as its lowest axis; the
554        // result is then independent of pair order and of how pairs chain
555        // (tensor4all/strided-rs#329).
556        let mut root: tinyvec::TinyVec<[usize; INLINE_RANK]> = (0..ndim).collect();
557        fn find(root: &mut [usize], mut ax: usize) -> usize {
558            while root[ax] != ax {
559                root[ax] = root[root[ax]];
560                ax = root[ax];
561            }
562            ax
563        }
564        for &(a, b) in axis_pairs {
565            let hi = a.max(b);
566            if hi >= ndim {
567                return Err(StridedError::InvalidAxis {
568                    axis: hi,
569                    rank: ndim,
570                });
571            }
572            if a == b {
573                return Err(StridedError::InvalidAxis {
574                    axis: a,
575                    rank: ndim,
576                });
577            }
578            let (ra, rb) = (find(&mut root, a), find(&mut root, b));
579            root[ra.max(rb)] = ra.min(rb);
580        }
581
582        // Every class collapses onto its root: the root's stride sums the
583        // strides of all axes in the class and its dim is their minimum. A
584        // root is lower than every other axis of its class, so it has already
585        // been kept when a later member folds into it.
586        let mut dims: AxisVec = AxisVec::new();
587        let mut strides: StrideVec = StrideVec::new();
588        let mut position: tinyvec::TinyVec<[usize; INLINE_RANK]> =
589            tinyvec::TinyVec::with_capacity(ndim);
590        for ax in 0..ndim {
591            let r = find(&mut root, ax);
592            if r == ax {
593                position.push(dims.len());
594                dims.push(self.dims()[ax]);
595                strides.push(self.strides()[ax]);
596            } else {
597                position.push(usize::MAX);
598                let p = position[r];
599                strides[p] = strides[p]
600                    .checked_add(self.strides()[ax])
601                    .ok_or(StridedError::OffsetOverflow)?;
602                dims[p] = dims[p].min(self.dims()[ax]);
603            }
604        }
605
606        // A fused stride sums several input strides, so the result carries no
607        // bound proof of its own and is validated here instead
608        // (tensor4all/strided-rs#204).
609        validate_bounds(self.data().len(), &dims, &strides, self.offset())?;
610
611        Ok(from_meta(self.data(), &dims, &strides, self.offset()))
612    }
613
614    /// Broadcast this view to a target shape.
615    ///
616    /// Size-1 dimensions are expanded (stride set to 0) to match target.
617    pub fn broadcast(&self, target_dims: &[usize]) -> Result<StridedView<'a, T, Op>> {
618        if self.dims().len() != target_dims.len() {
619            return Err(StridedError::RankMismatch(
620                self.dims().len(),
621                target_dims.len(),
622            ));
623        }
624        let mut new_strides = StrideVec::with_capacity(self.dims().len());
625        for i in 0..self.dims().len() {
626            if self.dims()[i] == target_dims[i] {
627                new_strides.push(self.strides()[i]);
628            } else if self.dims()[i] == 1 {
629                new_strides.push(0);
630            } else {
631                return Err(StridedError::ShapeMismatch(
632                    self.dims().to_vec(),
633                    target_dims.to_vec(),
634                ));
635            }
636        }
637        Ok(from_meta(self.data, target_dims, &new_strides, self.offset))
638    }
639}
640
641/// Composition methods: require `T: ElementOpApply` and `Op: ComposableElementOp<T>`.
642impl<'a, T: Copy + ElementOpApply, Op: ComposableElementOp<T>> StridedView<'a, T, Op> {
643    /// Transpose a 2D view: reverses dimensions and composes the Transpose element op.
644    ///
645    /// Julia equivalent: `Base.transpose(a::AbstractStridedView{<:Any, 2})`
646    pub fn transpose_2d(&self) -> Result<StridedView<'a, T, Op::ComposeTranspose>> {
647        if self.dims().len() != 2 {
648            return Err(StridedError::RankMismatch(self.dims().len(), 2));
649        }
650        Ok(from_meta(
651            self.data,
652            &[self.dims()[1], self.dims()[0]],
653            &[self.strides()[1], self.strides()[0]],
654            self.offset,
655        ))
656    }
657
658    /// Adjoint (conjugate transpose) of a 2D view.
659    ///
660    /// Julia equivalent: `Base.adjoint(a::AbstractStridedView{<:Any, 2})`
661    pub fn adjoint_2d(&self) -> Result<StridedView<'a, T, Op::ComposeAdjoint>> {
662        if self.dims().len() != 2 {
663            return Err(StridedError::RankMismatch(self.dims().len(), 2));
664        }
665        Ok(from_meta(
666            self.data,
667            &[self.dims()[1], self.dims()[0]],
668            &[self.strides()[1], self.strides()[0]],
669            self.offset,
670        ))
671    }
672
673    /// Complex conjugate (compose Conj without changing dims/strides).
674    ///
675    /// Julia equivalent: `Base.conj(a::AbstractStridedView)`
676    #[inline]
677    pub fn conj(&self) -> StridedView<'a, T, Op::ComposeConj> {
678        StridedView {
679            ptr: self.ptr,
680            data: self.data,
681            meta: self.meta.clone(),
682            offset: self.offset,
683            _op: PhantomData,
684        }
685    }
686}
687
688/// Element access: requires `Op: ElementOp<T>`.
689impl<'a, T: Copy, Op: ElementOp<T>> StridedView<'a, T, Op> {
690    /// Get an element with the element operation applied.
691    ///
692    /// # Panics
693    ///
694    /// Panics if `indices.len()` is not the view's rank, an index is out of
695    /// bounds, or the indexed offset does not fit `isize`.
696    pub fn get(&self, indices: &[usize]) -> T {
697        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
698        Op::apply(unsafe { *self.ptr.offset(offset) })
699    }
700
701    /// Get an element without bounds checking.
702    ///
703    /// # Safety
704    /// Caller must ensure indices are within bounds.
705    #[inline]
706    pub unsafe fn get_unchecked(&self, indices: &[usize]) -> T {
707        let mut idx = 0isize;
708        for (i, &index) in indices.iter().enumerate() {
709            idx += index as isize * self.strides()[i];
710        }
711        Op::apply(*self.ptr.offset(idx))
712    }
713}
714
715// ============================================================================
716// StridedViewMut
717// ============================================================================
718
719/// Dynamic-rank mutable strided view.
720///
721/// Always uses `Identity` element operation for write simplicity.
722/// Julia typically applies ops on the read side.
723pub struct StridedViewMut<'a, T> {
724    ptr: *mut T,
725    data: &'a mut [T],
726    meta: ViewMeta,
727    offset: isize,
728}
729
730unsafe impl<T: Send> Send for StridedViewMut<'_, T> {}
731
732impl<T: std::fmt::Debug> std::fmt::Debug for StridedViewMut<'_, T> {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        f.debug_struct("StridedViewMut")
735            .field("dims", &self.meta.dims())
736            .field("strides", &self.meta.strides())
737            .field("offset", &self.offset)
738            .finish()
739    }
740}
741
742impl<'a, T> StridedViewMut<'a, T> {
743    /// A view from already-validated metadata.
744    #[inline]
745    fn from_meta(data: &'a mut [T], dims: &[usize], strides: &[isize], offset: isize) -> Self {
746        let meta = ViewMeta::from_slices(dims, strides);
747        let ptr = if empty_layout(meta.dims()) {
748            empty_mut_ptr()
749        } else {
750            unsafe { data.as_mut_ptr().offset(offset) }
751        };
752        Self {
753            ptr,
754            data,
755            meta,
756            offset,
757        }
758    }
759
760    /// Create a mutable view, copying the caller's metadata inline.
761    ///
762    /// The layout is copied, so it does not need to outlive the view and no
763    /// heap allocation happens at rank `<= INLINE_RANK`.
764    ///
765    /// # Examples
766    ///
767    /// ```
768    /// use strided_view::StridedViewMut;
769    ///
770    /// let mut data = [0.0f64; 4];
771    /// let mut view = StridedViewMut::<f64>::new(&mut data, &[2, 2], &[1, 2], 0).unwrap();
772    /// view.set(&[1, 1], 5.0);
773    /// assert_eq!(data[3], 5.0);
774    /// ```
775    #[inline]
776    pub fn new(
777        data: &'a mut [T],
778        dims: &[usize],
779        strides: &[isize],
780        offset: isize,
781    ) -> Result<Self> {
782        validate_bounds(data.len(), dims, strides, offset)?;
783        Ok(Self::from_meta(data, dims, strides, offset))
784    }
785
786    /// Create without bounds checking, copying the caller's metadata.
787    ///
788    /// # Safety
789    /// Caller must ensure all index combinations stay within bounds.
790    ///
791    /// # Examples
792    ///
793    /// ```
794    /// use strided_view::StridedViewMut;
795    ///
796    /// let mut data = [0.0f64; 4];
797    /// let view = unsafe { StridedViewMut::<f64>::new_unchecked(&mut data, &[2, 2], &[1, 2], 0) };
798    /// assert_eq!(view.ndim(), 2);
799    /// ```
800    #[inline]
801    pub unsafe fn new_unchecked(
802        data: &'a mut [T],
803        dims: &[usize],
804        strides: &[isize],
805        offset: isize,
806    ) -> Self {
807        Self::from_meta(data, dims, strides, offset)
808    }
809
810    /// Returns the shape (dimension sizes) of this view.
811    #[inline]
812    pub fn dims(&self) -> &[usize] {
813        self.meta.dims()
814    }
815
816    /// Returns the strides (in units of `T`) for each dimension.
817    #[inline]
818    pub fn strides(&self) -> &[isize] {
819        self.meta.strides()
820    }
821
822    /// Returns the byte offset into the backing data.
823    #[inline]
824    pub fn offset(&self) -> isize {
825        self.offset
826    }
827
828    /// Returns the number of dimensions (rank).
829    #[inline]
830    pub fn ndim(&self) -> usize {
831        self.dims().len()
832    }
833
834    /// Returns the total number of elements.
835    #[inline]
836    pub fn len(&self) -> usize {
837        self.dims().iter().product()
838    }
839
840    /// Returns `true` if any dimension is zero.
841    #[inline]
842    pub fn is_empty(&self) -> bool {
843        self.dims().iter().any(|&d| d == 0)
844    }
845
846    /// Raw const pointer to element at the view's base offset.
847    #[inline]
848    pub fn ptr(&self) -> *const T {
849        self.ptr as *const T
850    }
851
852    /// Raw mutable pointer to element at the view's base offset.
853    #[inline]
854    pub fn as_mut_ptr(&self) -> *mut T {
855        self.ptr
856    }
857
858    /// Returns a reference to the backing data slice.
859    #[inline]
860    pub fn data(&self) -> &[T] {
861        &self.data
862    }
863
864    /// Returns a mutable reference to the backing data slice.
865    #[inline]
866    pub fn data_mut(&mut self) -> &mut [T] {
867        &mut self.data
868    }
869
870    /// Permute dimensions, consuming the mutable view.
871    ///
872    /// Returns a new mutable view with reordered dimensions and strides.
873    /// Takes `self` by value to prevent aliasing of mutable views.
874    pub fn permute(self, perm: &[usize]) -> Result<StridedViewMut<'a, T>> {
875        let rank = self.dims().len();
876        if perm.len() != rank {
877            return Err(StridedError::RankMismatch(perm.len(), rank));
878        }
879        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
880        seen.resize(rank, false);
881        for &p in perm {
882            if p >= rank {
883                return Err(StridedError::InvalidAxis { axis: p, rank });
884            }
885            if seen[p] {
886                return Err(StridedError::InvalidAxis { axis: p, rank });
887            }
888            seen[p] = true;
889        }
890        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
891        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
892        Ok(Self::from_meta(
893            self.data,
894            &new_dims,
895            &new_strides,
896            self.offset,
897        ))
898    }
899
900    /// Reborrow as an immutable view.
901    ///
902    /// The reborrow copies this view's metadata into the immutable view's
903    /// inline storage, which allocates nothing up to [`INLINE_RANK`] and
904    /// spills above it (the mutable owner always keeps its own copy).
905    ///
906    /// # Examples
907    ///
908    /// ```
909    /// use strided_view::StridedViewMut;
910    ///
911    /// let mut data = [1.0f64, 2.0, 3.0];
912    /// let view = StridedViewMut::<f64>::new(&mut data, &[3], &[1], 0).unwrap();
913    /// let read = view.as_view();
914    /// assert_eq!(read.get(&[2]), 3.0);
915    /// ```
916    #[inline]
917    pub fn as_view(&self) -> StridedView<'_, T, Identity> {
918        StridedView {
919            ptr: self.ptr as *const T,
920            data: unsafe { std::slice::from_raw_parts(self.data.as_ptr(), self.data.len()) },
921            meta: self.meta.clone(),
922            offset: self.offset,
923            _op: PhantomData,
924        }
925    }
926}
927
928impl<'a, T: Copy> StridedViewMut<'a, T> {
929    /// Get an element.
930    ///
931    /// # Panics
932    ///
933    /// Panics if `indices.len()` is not the view's rank, an index is out of
934    /// bounds, or the indexed offset does not fit `isize`.
935    pub fn get(&self, indices: &[usize]) -> T {
936        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
937        unsafe { *self.ptr.offset(offset) }
938    }
939
940    /// Set an element.
941    ///
942    /// # Panics
943    ///
944    /// Panics if `indices.len()` is not the view's rank, an index is out of
945    /// bounds, or the indexed offset does not fit `isize`.
946    pub fn set(&mut self, indices: &[usize], value: T) {
947        let offset = indexed_offset(self.dims(), self.strides(), indices, 0);
948        unsafe {
949            *self.ptr.offset(offset) = value;
950        }
951    }
952}
953
954// ============================================================================
955// StridedArray
956// ============================================================================
957
958/// Owned strided multidimensional array.
959///
960/// Supports both column-major (Julia default) and row-major (C default) layouts.
961pub struct StridedArray<T> {
962    data: Vec<T>,
963    meta: ViewMeta,
964    offset: isize,
965}
966
967impl<T: std::fmt::Debug> std::fmt::Debug for StridedArray<T> {
968    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
969        f.debug_struct("StridedArray")
970            .field("dims", &self.meta.dims())
971            .field("strides", &self.meta.strides())
972            .field("offset", &self.offset)
973            .finish()
974    }
975}
976
977impl<T: Clone> Clone for StridedArray<T> {
978    fn clone(&self) -> Self {
979        Self {
980            data: self.data.clone(),
981            meta: self.meta.clone(),
982            offset: self.offset,
983        }
984    }
985}
986
987impl<T: Clone + Default> StridedArray<T> {
988    /// Create a column-major (Julia default) tensor filled with Default values.
989    ///
990    /// The shape and strides are stored inline, so a rank `<= INLINE_RANK`
991    /// tensor allocates only its element buffer.
992    ///
993    /// # Examples
994    ///
995    /// ```
996    /// use strided_view::StridedArray;
997    ///
998    /// let a = StridedArray::<f64>::col_major(&[2, 3]);
999    /// assert_eq!(a.dims(), &[2, 3]);
1000    /// assert_eq!(a.strides(), &[1, 2]);
1001    /// ```
1002    pub fn col_major(dims: &[usize]) -> Self {
1003        let total: usize = dims.iter().product();
1004        let data = vec![T::default(); total];
1005        let strides = col_major_strides(dims);
1006        Self {
1007            data,
1008            meta: ViewMeta::from_slices(dims, &strides),
1009            offset: 0,
1010        }
1011    }
1012
1013    /// Create a row-major (C default) tensor filled with Default values.
1014    ///
1015    /// The shape and strides are stored inline, so a rank `<= INLINE_RANK`
1016    /// tensor allocates only its element buffer.
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```
1021    /// use strided_view::StridedArray;
1022    ///
1023    /// let a = StridedArray::<f64>::row_major(&[2, 3]);
1024    /// assert_eq!(a.dims(), &[2, 3]);
1025    /// assert_eq!(a.strides(), &[3, 1]);
1026    /// ```
1027    pub fn row_major(dims: &[usize]) -> Self {
1028        let total: usize = dims.iter().product();
1029        let data = vec![T::default(); total];
1030        let strides = row_major_strides(dims);
1031        Self {
1032            data,
1033            meta: ViewMeta::from_slices(dims, &strides),
1034            offset: 0,
1035        }
1036    }
1037
1038    /// Create a column-major tensor with values produced by a function.
1039    ///
1040    /// The function is called with indices in column-major iteration order.
1041    pub fn from_fn_col_major(dims: &[usize], mut f: impl FnMut(&[usize]) -> T) -> Self {
1042        let total: usize = dims.iter().product();
1043        let strides = col_major_strides(dims);
1044        let rank = dims.len();
1045        let mut data = Vec::with_capacity(total);
1046        let mut idx: AxisVec = AxisVec::new();
1047        idx.resize(rank, 0usize);
1048        // Borrow the cursor once; indexing the `TinyVec` per access would pay
1049        // its inline/heap match in the element-count loop.
1050        let idx: &mut [usize] = &mut idx[..];
1051        for _ in 0..total {
1052            data.push(f(idx));
1053            for d in 0..rank {
1054                idx[d] += 1;
1055                if idx[d] < dims[d] {
1056                    break;
1057                }
1058                idx[d] = 0;
1059            }
1060        }
1061        Self {
1062            data,
1063            meta: ViewMeta::from_slices(dims, &strides),
1064            offset: 0,
1065        }
1066    }
1067
1068    /// Create a row-major tensor with values produced by a function.
1069    ///
1070    /// The function is called with indices in row-major iteration order.
1071    pub fn from_fn_row_major(dims: &[usize], mut f: impl FnMut(&[usize]) -> T) -> Self {
1072        let total: usize = dims.iter().product();
1073        let strides = row_major_strides(dims);
1074        let rank = dims.len();
1075        let mut data = Vec::with_capacity(total);
1076        let mut idx: AxisVec = AxisVec::new();
1077        idx.resize(rank, 0usize);
1078        // Borrow the cursor once; see `from_fn_col_major`.
1079        let idx: &mut [usize] = &mut idx[..];
1080        for _ in 0..total {
1081            data.push(f(idx));
1082            for d in (0..rank).rev() {
1083                idx[d] += 1;
1084                if idx[d] < dims[d] {
1085                    break;
1086                }
1087                idx[d] = 0;
1088            }
1089        }
1090        Self {
1091            data,
1092            meta: ViewMeta::from_slices(dims, &strides),
1093            offset: 0,
1094        }
1095    }
1096}
1097
1098impl<T> StridedArray<T> {
1099    /// Create from raw parts.
1100    pub fn from_parts(
1101        data: Vec<T>,
1102        dims: &[usize],
1103        strides: &[isize],
1104        offset: isize,
1105    ) -> Result<Self> {
1106        validate_bounds(data.len(), dims, strides, offset)?;
1107        Ok(Self {
1108            data,
1109            meta: ViewMeta::from_slices(dims, strides),
1110            offset,
1111        })
1112    }
1113
1114    /// Returns the shape (dimension sizes) of this array.
1115    #[inline]
1116    pub fn dims(&self) -> &[usize] {
1117        self.meta.dims()
1118    }
1119
1120    /// Returns the strides (in units of `T`) for each dimension.
1121    #[inline]
1122    pub fn strides(&self) -> &[isize] {
1123        self.meta.strides()
1124    }
1125
1126    /// Returns the number of dimensions (rank).
1127    #[inline]
1128    pub fn ndim(&self) -> usize {
1129        self.dims().len()
1130    }
1131
1132    /// Returns the total number of elements.
1133    #[inline]
1134    pub fn len(&self) -> usize {
1135        self.dims().iter().product()
1136    }
1137
1138    /// Returns `true` if any dimension is zero.
1139    #[inline]
1140    pub fn is_empty(&self) -> bool {
1141        self.dims().iter().any(|&d| d == 0)
1142    }
1143
1144    /// Returns a reference to the backing data slice.
1145    #[inline]
1146    pub fn data(&self) -> &[T] {
1147        &self.data
1148    }
1149
1150    /// Returns a mutable reference to the backing data slice.
1151    #[inline]
1152    pub fn data_mut(&mut self) -> &mut [T] {
1153        &mut self.data
1154    }
1155
1156    /// Create an immutable view over this tensor.
1157    #[inline]
1158    pub fn view(&self) -> StridedView<'_, T> {
1159        let ptr = if self.is_empty() {
1160            empty_const_ptr()
1161        } else {
1162            unsafe { self.data.as_ptr().offset(self.offset) }
1163        };
1164        StridedView {
1165            ptr,
1166            data: &self.data,
1167            meta: self.meta.clone(),
1168            offset: self.offset,
1169            _op: PhantomData,
1170        }
1171    }
1172
1173    /// Create a mutable view over this tensor.
1174    #[inline]
1175    pub fn view_mut(&mut self) -> StridedViewMut<'_, T> {
1176        let ptr = if self.is_empty() {
1177            empty_mut_ptr()
1178        } else {
1179            unsafe { self.data.as_mut_ptr().offset(self.offset) }
1180        };
1181        StridedViewMut {
1182            ptr,
1183            data: &mut self.data,
1184            meta: self.meta.clone(),
1185            offset: self.offset,
1186        }
1187    }
1188
1189    /// Permute dimensions (metadata-only reorder, no data copy).
1190    ///
1191    /// Returns a new array with reordered dims and strides.
1192    /// The underlying data buffer is not touched.
1193    pub fn permuted(self, perm: &[usize]) -> Result<Self> {
1194        let rank = self.dims().len();
1195        if perm.len() != rank {
1196            return Err(StridedError::RankMismatch(perm.len(), rank));
1197        }
1198        let mut seen: tinyvec::TinyVec<[bool; INLINE_RANK]> = tinyvec::TinyVec::new();
1199        seen.resize(rank, false);
1200        for &p in perm {
1201            if p >= rank {
1202                return Err(StridedError::InvalidAxis { axis: p, rank });
1203            }
1204            if seen[p] {
1205                return Err(StridedError::InvalidAxis { axis: p, rank });
1206            }
1207            seen[p] = true;
1208        }
1209        let new_dims: AxisVec = perm.iter().map(|&p| self.dims()[p]).collect();
1210        let new_strides: StrideVec = perm.iter().map(|&p| self.strides()[p]).collect();
1211        Ok(Self {
1212            data: self.data,
1213            meta: ViewMeta::from_slices(&new_dims, &new_strides),
1214            offset: self.offset,
1215        })
1216    }
1217
1218    /// Consume the array and return the backing `Vec<T>`.
1219    pub fn into_data(self) -> Vec<T> {
1220        self.data
1221    }
1222
1223    /// Iterate over all elements in memory order.
1224    pub fn iter(&self) -> std::slice::Iter<'_, T> {
1225        self.data.iter()
1226    }
1227
1228    /// Mutable iteration over all elements in memory order.
1229    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
1230        self.data.iter_mut()
1231    }
1232}
1233
1234impl<T: Default> StridedArray<T> {
1235    /// Create a column-major tensor reusing an existing buffer.
1236    ///
1237    /// If `buf` has at least `product(dims)` elements, it is truncated and
1238    /// zeroed.  Otherwise a fresh buffer is allocated.
1239    pub fn col_major_from_buffer(mut buf: Vec<T>, dims: &[usize]) -> Self {
1240        let total: usize = dims.iter().product();
1241        if buf.len() >= total {
1242            buf.truncate(total);
1243        } else {
1244            buf.resize_with(total, T::default);
1245        }
1246        // Zero the reused region
1247        for v in buf.iter_mut() {
1248            *v = T::default();
1249        }
1250        let strides = col_major_strides(dims);
1251        Self {
1252            data: buf,
1253            meta: ViewMeta::from_slices(dims, &strides),
1254            offset: 0,
1255        }
1256    }
1257}
1258
1259impl<T: Copy> StridedArray<T> {
1260    /// Create a column-major tensor with **uninitialized** data.
1261    ///
1262    /// # Safety
1263    /// Caller must write every element before reading.
1264    pub unsafe fn col_major_uninit(dims: &[usize]) -> Self {
1265        let total: usize = dims.iter().product();
1266        let mut data = Vec::with_capacity(total);
1267        data.set_len(total);
1268        let strides = col_major_strides(dims);
1269        Self {
1270            data,
1271            meta: ViewMeta::from_slices(dims, &strides),
1272            offset: 0,
1273        }
1274    }
1275
1276    /// Reuse an existing buffer as a column-major tensor **without zeroing**.
1277    ///
1278    /// # Safety
1279    /// Caller must write every element before reading.
1280    pub unsafe fn col_major_from_buffer_uninit(mut buf: Vec<T>, dims: &[usize]) -> Self {
1281        let total: usize = dims.iter().product();
1282        if buf.capacity() < total {
1283            buf.reserve(total - buf.len());
1284        }
1285        buf.set_len(total);
1286        let strides = col_major_strides(dims);
1287        Self {
1288            data: buf,
1289            meta: ViewMeta::from_slices(dims, &strides),
1290            offset: 0,
1291        }
1292    }
1293}
1294
1295impl<T: Copy> StridedArray<T> {
1296    /// Get an element by multi-dimensional index.
1297    ///
1298    /// # Panics
1299    ///
1300    /// Panics if `indices.len()` is not the array's rank, an index is out of
1301    /// bounds, or the indexed offset does not fit `isize`.
1302    pub fn get(&self, indices: &[usize]) -> T {
1303        self.view().get(indices)
1304    }
1305
1306    /// Set an element by multi-dimensional index.
1307    ///
1308    /// # Panics
1309    ///
1310    /// Panics if `indices.len()` is not the array's rank, an index is out of
1311    /// bounds, or the indexed offset does not fit `isize`.
1312    pub fn set(&mut self, indices: &[usize], value: T) {
1313        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1314        self.data[offset as usize] = value;
1315    }
1316}
1317
1318impl<T: Copy> Index<&[usize]> for StridedArray<T> {
1319    type Output = T;
1320
1321    fn index(&self, indices: &[usize]) -> &T {
1322        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1323        &self.data[offset as usize]
1324    }
1325}
1326
1327impl<T: Copy> IndexMut<&[usize]> for StridedArray<T> {
1328    fn index_mut(&mut self, indices: &[usize]) -> &mut T {
1329        let offset = indexed_offset(self.dims(), self.strides(), indices, self.offset);
1330        &mut self.data[offset as usize]
1331    }
1332}
1333
1334// ============================================================================
1335// Tests
1336// ============================================================================
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341
1342    #[test]
1343    fn empty_views_use_dangling_base_pointers_for_extreme_offsets() {
1344        let dims = [0usize];
1345        let strides = [1isize];
1346        let data: [f64; 0] = [];
1347        let view = StridedView::<f64>::new(&data, &dims, &strides, isize::MAX).unwrap();
1348        assert_eq!(view.ptr(), empty_const_ptr());
1349
1350        let mut data: [f64; 0] = [];
1351        let view = StridedViewMut::new(&mut data, &dims, &strides, isize::MAX).unwrap();
1352        assert_eq!(view.ptr(), empty_const_ptr());
1353        assert_eq!(view.as_mut_ptr(), empty_mut_ptr());
1354
1355        let array =
1356            StridedArray::<f64>::from_parts(Vec::new(), &dims, &strides, isize::MAX).unwrap();
1357        assert_eq!(array.view().ptr(), empty_const_ptr());
1358
1359        let mut array =
1360            StridedArray::<f64>::from_parts(Vec::new(), &dims, &strides, isize::MAX).unwrap();
1361        let view = array.view_mut();
1362        assert_eq!(view.ptr(), empty_const_ptr());
1363        assert_eq!(view.as_mut_ptr(), empty_mut_ptr());
1364    }
1365    use num_complex::Complex64;
1366
1367    #[test]
1368    fn test_col_major_strides() {
1369        assert_eq!(col_major_strides(&[3, 4]).as_slice(), &[1, 3]);
1370        assert_eq!(col_major_strides(&[2, 3, 4]).as_slice(), &[1, 2, 6]);
1371    }
1372
1373    #[test]
1374    fn test_row_major_strides() {
1375        assert_eq!(row_major_strides(&[3, 4]).as_slice(), &[4, 1]);
1376        assert_eq!(row_major_strides(&[2, 3, 4]).as_slice(), &[12, 4, 1]);
1377    }
1378
1379    #[test]
1380    fn test_strided_view_new() {
1381        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1382        let view = StridedView::<f64>::new(&data, &[2, 3], &[3, 1], 0).unwrap();
1383        assert_eq!(view.ndim(), 2);
1384        assert_eq!(view.dims(), &[2, 3]);
1385        assert_eq!(view.strides(), &[3, 1]);
1386        assert_eq!(view.len(), 6);
1387    }
1388
1389    #[test]
1390    fn test_strided_view_get() {
1391        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1392        let view = StridedView::<f64>::new(&data, &[2, 3], &[3, 1], 0).unwrap();
1393        assert_eq!(view.get(&[0, 0]), 1.0);
1394        assert_eq!(view.get(&[0, 1]), 2.0);
1395        assert_eq!(view.get(&[0, 2]), 3.0);
1396        assert_eq!(view.get(&[1, 0]), 4.0);
1397        assert_eq!(view.get(&[1, 2]), 6.0);
1398    }
1399
1400    #[test]
1401    fn test_strided_view_col_major() {
1402        // Column-major: strides [1, 2] for 2x3
1403        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1404        let view = StridedView::<f64>::new(&data, &[2, 3], &[1, 2], 0).unwrap();
1405        assert_eq!(view.get(&[0, 0]), 1.0); // data[0]
1406        assert_eq!(view.get(&[1, 0]), 2.0); // data[1]
1407        assert_eq!(view.get(&[0, 1]), 3.0); // data[2]
1408        assert_eq!(view.get(&[1, 1]), 4.0); // data[3]
1409        assert_eq!(view.get(&[0, 2]), 5.0); // data[4]
1410        assert_eq!(view.get(&[1, 2]), 6.0); // data[5]
1411    }
1412
1413    #[test]
1414    fn test_strided_view_permute() {
1415        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1416        let view = StridedView::<f64>::new(&data, &[2, 3], &[3, 1], 0).unwrap();
1417        let perm = view.permute(&[1, 0]).unwrap();
1418        assert_eq!(perm.dims(), &[3, 2]);
1419        assert_eq!(perm.strides(), &[1, 3]);
1420        assert_eq!(perm.get(&[0, 0]), 1.0);
1421        assert_eq!(perm.get(&[1, 0]), 2.0);
1422        assert_eq!(perm.get(&[0, 1]), 4.0);
1423    }
1424
1425    #[test]
1426    fn test_strided_view_transpose_2d() {
1427        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1428        let view = StridedView::<f64>::new(&data, &[2, 3], &[3, 1], 0).unwrap();
1429        let t = view.transpose_2d().unwrap();
1430        assert_eq!(t.dims(), &[3, 2]);
1431        assert_eq!(t.get(&[0, 0]), 1.0);
1432        assert_eq!(t.get(&[1, 0]), 2.0);
1433        assert_eq!(t.get(&[0, 1]), 4.0);
1434    }
1435
1436    #[test]
1437    fn test_strided_view_conj() {
1438        let data = vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)];
1439        let view = StridedView::<Complex64>::new(&data, &[2], &[1], 0).unwrap();
1440        let c = view.conj();
1441        assert_eq!(c.get(&[0]), Complex64::new(1.0, -2.0));
1442        assert_eq!(c.get(&[1]), Complex64::new(3.0, -4.0));
1443    }
1444
1445    #[test]
1446    fn test_strided_view_adjoint_2d() {
1447        let data = vec![
1448            Complex64::new(1.0, 2.0),
1449            Complex64::new(3.0, 4.0),
1450            Complex64::new(5.0, 6.0),
1451            Complex64::new(7.0, 8.0),
1452        ];
1453        // 2x2 row-major
1454        let view = StridedView::<Complex64>::new(&data, &[2, 2], &[2, 1], 0).unwrap();
1455        let adj = view.adjoint_2d().unwrap();
1456        assert_eq!(adj.dims(), &[2, 2]);
1457        // Adjoint: conj + transpose
1458        assert_eq!(adj.get(&[0, 0]), Complex64::new(1.0, -2.0));
1459        assert_eq!(adj.get(&[1, 0]), Complex64::new(3.0, -4.0));
1460        assert_eq!(adj.get(&[0, 1]), Complex64::new(5.0, -6.0));
1461    }
1462
1463    #[test]
1464    fn test_strided_view_broadcast() {
1465        let data = vec![1.0, 2.0, 3.0];
1466        let view = StridedView::<f64>::new(&data, &[1, 3], &[3, 1], 0).unwrap();
1467        let broad = view.broadcast(&[4, 3]).unwrap();
1468        assert_eq!(broad.dims(), &[4, 3]);
1469        for i in 0..4 {
1470            assert_eq!(broad.get(&[i, 0]), 1.0);
1471            assert_eq!(broad.get(&[i, 1]), 2.0);
1472            assert_eq!(broad.get(&[i, 2]), 3.0);
1473        }
1474    }
1475
1476    #[test]
1477    fn test_strided_view_mut() {
1478        let mut data = vec![0.0; 6];
1479        {
1480            let mut view = StridedViewMut::<f64>::new(&mut data, &[2, 3], &[3, 1], 0).unwrap();
1481            view.set(&[0, 0], 1.0);
1482            view.set(&[1, 2], 6.0);
1483        }
1484        assert_eq!(data[0], 1.0);
1485        assert_eq!(data[5], 6.0);
1486    }
1487
1488    #[test]
1489    fn test_strided_view_mut_as_view() {
1490        let mut data = vec![1.0, 2.0, 3.0];
1491        let vm = StridedViewMut::<f64>::new(&mut data, &[3], &[1], 0).unwrap();
1492        let v = vm.as_view();
1493        assert_eq!(v.get(&[0]), 1.0);
1494        assert_eq!(v.get(&[2]), 3.0);
1495    }
1496
1497    #[test]
1498    fn test_strided_tensor_col_major() {
1499        let t = StridedArray::<f64>::from_fn_col_major(&[2, 3], |idx| (idx[0] * 3 + idx[1]) as f64);
1500        assert_eq!(t.dims(), &[2, 3]);
1501        assert_eq!(t.strides(), &[1, 2]); // column-major
1502        assert_eq!(t.get(&[0, 0]), 0.0);
1503        assert_eq!(t.get(&[1, 0]), 3.0);
1504        assert_eq!(t.get(&[0, 1]), 1.0);
1505        assert_eq!(t.get(&[1, 2]), 5.0);
1506    }
1507
1508    #[test]
1509    fn test_strided_tensor_row_major() {
1510        let t = StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1]) as f64);
1511        assert_eq!(t.dims(), &[2, 3]);
1512        assert_eq!(t.strides(), &[3, 1]); // row-major
1513        assert_eq!(t.get(&[0, 0]), 0.0);
1514        assert_eq!(t.get(&[0, 1]), 1.0);
1515        assert_eq!(t.get(&[1, 0]), 3.0);
1516        assert_eq!(t.get(&[1, 2]), 5.0);
1517    }
1518
1519    #[test]
1520    fn test_strided_tensor_view() {
1521        let t =
1522            StridedArray::<f64>::from_fn_col_major(&[2, 3], |idx| (idx[0] * 10 + idx[1]) as f64);
1523        let v = t.view();
1524        assert_eq!(v.get(&[0, 0]), 0.0);
1525        assert_eq!(v.get(&[1, 0]), 10.0);
1526        assert_eq!(v.get(&[0, 2]), 2.0);
1527    }
1528
1529    #[test]
1530    fn test_strided_tensor_view_mut() {
1531        let mut t = StridedArray::<f64>::col_major(&[2, 3]);
1532        {
1533            let mut vm = t.view_mut();
1534            vm.set(&[1, 2], 42.0);
1535        }
1536        assert_eq!(t.get(&[1, 2]), 42.0);
1537    }
1538
1539    #[test]
1540    fn test_strided_tensor_index() {
1541        let t =
1542            StridedArray::<f64>::from_fn_row_major(&[3, 4], |idx| (idx[0] * 10 + idx[1]) as f64);
1543        assert_eq!(t[&[0usize, 0] as &[usize]], 0.0);
1544        assert_eq!(t[&[2usize, 3] as &[usize]], 23.0);
1545    }
1546
1547    #[test]
1548    fn test_strided_tensor_index_mut() {
1549        let mut t = StridedArray::<f64>::row_major(&[2, 3]);
1550        t[&[1usize, 2] as &[usize]] = 99.0;
1551        assert_eq!(t.get(&[1, 2]), 99.0);
1552    }
1553
1554    #[test]
1555    fn test_validate_bounds_ok() {
1556        assert!(validate_bounds(6, &[2, 3], &[3, 1], 0).is_ok());
1557        assert!(validate_bounds(6, &[2, 3], &[1, 2], 0).is_ok());
1558    }
1559
1560    #[test]
1561    fn test_validate_bounds_out_of_range() {
1562        assert!(validate_bounds(5, &[2, 3], &[3, 1], 0).is_err());
1563    }
1564
1565    #[test]
1566    fn test_validate_bounds_empty() {
1567        assert!(validate_bounds(0, &[0, 3], &[3, 1], 0).is_ok());
1568    }
1569
1570    #[test]
1571    fn test_validate_bounds_with_offset() {
1572        assert!(validate_bounds(7, &[2, 3], &[3, 1], 1).is_ok());
1573        assert!(validate_bounds(6, &[2, 3], &[3, 1], 1).is_err());
1574    }
1575
1576    #[test]
1577    fn test_strided_tensor_3d() {
1578        let t = StridedArray::<f64>::from_fn_col_major(&[2, 3, 4], |idx| {
1579            (idx[0] * 100 + idx[1] * 10 + idx[2]) as f64
1580        });
1581        assert_eq!(t.ndim(), 3);
1582        assert_eq!(t.strides(), &[1, 2, 6]); // column-major
1583        assert_eq!(t.get(&[0, 0, 0]), 0.0);
1584        assert_eq!(t.get(&[1, 0, 0]), 100.0);
1585        assert_eq!(t.get(&[0, 1, 0]), 10.0);
1586        assert_eq!(t.get(&[0, 0, 1]), 1.0);
1587        assert_eq!(t.get(&[1, 2, 3]), 123.0);
1588    }
1589
1590    #[test]
1591    fn test_diagonal_view_2d() {
1592        // A[i,i] shape=[3,3] row-major strides=[3,1]
1593        // diagonal: shape=[3] strides=[4] (3+1)
1594        let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
1595        let view = StridedView::<f64>::new(&data, &[3, 3], &[3, 1], 0).unwrap();
1596        let diag = view.diagonal_view(&[(0, 1)]).unwrap();
1597        assert_eq!(diag.dims(), &[3]);
1598        assert_eq!(diag.strides(), &[4]);
1599        assert_eq!(diag.get(&[0]), 0.0); // A[0,0]
1600        assert_eq!(diag.get(&[1]), 4.0); // A[1,1]
1601        assert_eq!(diag.get(&[2]), 8.0); // A[2,2]
1602    }
1603
1604    #[test]
1605    fn test_diagonal_view_3d_adjacent() {
1606        // A[i,i,j] shape=[2,2,3] row-major strides=[6,3,1]
1607        // diagonal over (0,1): shape=[2,3] strides=[9,1] (6+3)
1608        let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
1609        let view = StridedView::<f64>::new(&data, &[2, 2, 3], &[6, 3, 1], 0).unwrap();
1610        let diag = view.diagonal_view(&[(0, 1)]).unwrap();
1611        assert_eq!(diag.dims(), &[2, 3]);
1612        assert_eq!(diag.strides(), &[9, 1]);
1613        assert_eq!(diag.get(&[0, 0]), 0.0);
1614        assert_eq!(diag.get(&[0, 2]), 2.0);
1615        assert_eq!(diag.get(&[1, 0]), 9.0);
1616    }
1617
1618    #[test]
1619    fn test_diagonal_view_3d_non_adjacent() {
1620        // A[i,j,i] shape=[2,3,2] row-major strides=[6,2,1]
1621        // diagonal over (0,2): shape=[2,3] strides=[7,2] (6+1)
1622        let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
1623        let view = StridedView::<f64>::new(&data, &[2, 3, 2], &[6, 2, 1], 0).unwrap();
1624        let diag = view.diagonal_view(&[(0, 2)]).unwrap();
1625        assert_eq!(diag.dims(), &[2, 3]);
1626        assert_eq!(diag.strides(), &[7, 2]);
1627        assert_eq!(diag.get(&[0, 0]), 0.0);
1628        assert_eq!(diag.get(&[0, 1]), 2.0);
1629        assert_eq!(diag.get(&[1, 0]), 7.0);
1630        assert_eq!(diag.get(&[1, 2]), 11.0);
1631    }
1632
1633    #[test]
1634    fn test_diagonal_view_two_pairs() {
1635        // A[i,j,i,j] shape=[2,3,2,3] -> A_diag[i,j] shape=[2,3]
1636        let data: Vec<f64> = (0..36).map(|x| x as f64).collect();
1637        let view = StridedView::<f64>::new(&data, &[2, 3, 2, 3], &[18, 6, 3, 1], 0).unwrap();
1638        let diag = view.diagonal_view(&[(0, 2), (1, 3)]).unwrap();
1639        assert_eq!(diag.dims(), &[2, 3]);
1640        assert_eq!(diag.strides(), &[21, 7]);
1641        assert_eq!(diag.get(&[0, 0]), 0.0);
1642        assert_eq!(diag.get(&[1, 1]), 28.0);
1643    }
1644
1645    #[test]
1646    fn test_diagonal_view_pairs_sharing_a_high_axis_fuse_all_three() {
1647        // Axis 2 is tied to both axis 0 and axis 1, so all three axes are one
1648        // diagonal: A[i,i,i] with stride 4 + 2 + 1. Folding axis 2 into both
1649        // survivors separately would reach offset 8 of a length-8 backing.
1650        let data: Vec<f64> = (0..8).map(|x| x as f64).collect();
1651        let view = StridedView::<f64>::new(&data, &[2, 2, 2], &[4, 2, 1], 0).unwrap();
1652        let diag = view.diagonal_view(&[(0, 2), (1, 2)]).unwrap();
1653        assert_eq!(diag.dims(), &[2]);
1654        assert_eq!(diag.strides(), &[7]);
1655        assert_eq!(diag.get(&[1]), 7.0);
1656    }
1657
1658    #[test]
1659    fn test_diagonal_view_repeated_pair_is_idempotent() {
1660        // Tying the same two axes twice is the same constraint as once; the
1661        // stride contribution must not be counted twice.
1662        let data: Vec<f64> = (0..8).map(|x| x as f64).collect();
1663        let view = StridedView::<f64>::new(&data, &[2, 2, 2], &[4, 2, 1], 0).unwrap();
1664        let once = view.diagonal_view(&[(0, 1)]).unwrap();
1665        let twice = view.diagonal_view(&[(0, 1), (1, 0)]).unwrap();
1666        assert_eq!(twice.dims(), once.dims());
1667        assert_eq!(twice.strides(), once.strides());
1668        assert_eq!(twice.strides(), &[6, 1]);
1669    }
1670
1671    #[test]
1672    fn test_diagonal_view_chained_pairs_are_order_independent() {
1673        // tensor4all/strided-rs#329: [(0, 1), (1, 2)] and [(1, 2), (0, 1)]
1674        // both describe A[i,i,i]. The second pair's low axis is the first
1675        // pair's removed axis, and its contribution must still reach axis 0.
1676        let data: Vec<f64> = (0..27).map(|x| x as f64).collect();
1677        let view = StridedView::<f64>::new(&data, &[3, 3, 3], &[1, 3, 9], 0).unwrap();
1678        for pairs in [
1679            [(0, 1), (1, 2)],
1680            [(1, 2), (0, 1)],
1681            [(2, 1), (1, 0)],
1682            [(0, 2), (2, 1)],
1683        ] {
1684            let diag = view.diagonal_view(&pairs).unwrap();
1685            assert_eq!(diag.dims(), &[3], "{pairs:?}");
1686            assert_eq!(diag.strides(), &[13], "{pairs:?}");
1687            assert_eq!(diag.get(&[2]), 26.0, "{pairs:?}");
1688        }
1689    }
1690
1691    #[test]
1692    fn test_diagonal_view_chained_pairs_keep_free_axes_in_order() {
1693        // A[i,j,i,i] with a chain through axis 2: the free axis j stays second
1694        // and the fused dim is the minimum over the whole class.
1695        let dims = [3usize, 4, 2, 3];
1696        let strides = [1isize, 3, 12, 24];
1697        let data: Vec<f64> = (0..72).map(|x| x as f64).collect();
1698        let view = StridedView::<f64>::new(&data, &dims, &strides, 0).unwrap();
1699        let a = view.diagonal_view(&[(2, 3), (0, 2)]).unwrap();
1700        let b = view.diagonal_view(&[(0, 2), (2, 3)]).unwrap();
1701        for diag in [a, b] {
1702            assert_eq!(diag.dims(), &[2, 4]);
1703            assert_eq!(diag.strides(), &[37, 3]);
1704            assert_eq!(diag.get(&[1, 2]), 43.0);
1705        }
1706    }
1707
1708    #[test]
1709    fn test_diagonal_view_fuses_a_shared_low_axis_in_bounds() {
1710        // A[i,i,i] is [(0, 1), (0, 2)]: axis 0 carries both fused strides and
1711        // the result stays inside the backing.
1712        let data: Vec<f64> = (0..8).map(|x| x as f64).collect();
1713        let view = StridedView::<f64>::new(&data, &[2, 2, 2], &[4, 2, 1], 0).unwrap();
1714        let diag = view.diagonal_view(&[(0, 1), (0, 2)]).unwrap();
1715        assert_eq!(diag.dims(), &[2]);
1716        assert_eq!(diag.strides(), &[7]);
1717        assert_eq!(diag.get(&[0]), 0.0);
1718        assert_eq!(diag.get(&[1]), 7.0);
1719    }
1720
1721    #[test]
1722    fn test_custom_type_without_element_op_apply() {
1723        // Custom Copy type that does NOT implement ElementOpApply.
1724        // Should work with Identity views (StridedView<T> default).
1725        #[derive(Debug, Clone, Copy, PartialEq)]
1726        struct MyScalar(f64);
1727
1728        impl Default for MyScalar {
1729            fn default() -> Self {
1730                MyScalar(0.0)
1731            }
1732        }
1733
1734        // Create array with custom type
1735        let data = vec![MyScalar(1.0), MyScalar(2.0), MyScalar(3.0), MyScalar(4.0)];
1736        let arr = StridedArray::from_parts(data, &[2, 2], &[2, 1], 0).unwrap();
1737
1738        // Access via Identity view (default)
1739        assert_eq!(arr.get(&[0, 0]), MyScalar(1.0));
1740        assert_eq!(arr.get(&[1, 0]), MyScalar(3.0));
1741
1742        // Create view and access elements
1743        let view: StridedView<MyScalar> = arr.view();
1744        assert_eq!(view.get(&[0, 1]), MyScalar(2.0));
1745
1746        // Permute view (metadata-only, no ElementOpApply needed)
1747        let perm = view.permute(&[1, 0]).unwrap();
1748        assert_eq!(perm.get(&[0, 0]), MyScalar(1.0));
1749        assert_eq!(perm.get(&[0, 1]), MyScalar(3.0)); // transposed
1750    }
1751
1752    #[test]
1753    #[should_panic(expected = "wrong number of indices")]
1754    fn test_index_rejects_a_short_index() {
1755        let arr = StridedArray::from_parts(vec![0.0f64; 4], &[2, 2], &[2, 1], 0).unwrap();
1756        // Without the rank check this reads an element the index does not name.
1757        let _ = arr[&[1][..]];
1758    }
1759
1760    #[test]
1761    #[should_panic(expected = "wrong number of indices")]
1762    fn test_view_get_rejects_a_short_index() {
1763        let data = vec![0.0f64; 4];
1764        let view = StridedView::<f64>::new(&data, &[2, 2], &[2, 1], 0).unwrap();
1765        let _ = view.get(&[1]);
1766    }
1767
1768    #[test]
1769    #[should_panic(expected = "out of bounds")]
1770    fn test_view_set_rejects_an_out_of_bounds_index() {
1771        let mut data = vec![0.0f64; 4];
1772        let mut view = StridedViewMut::<f64>::new(&mut data, &[2, 2], &[2, 1], 0).unwrap();
1773        view.set(&[2, 0], 1.0);
1774    }
1775}