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