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