Skip to main content

rssn_advanced/tensor/
view.rs

1//! Zero-copy tensor views over flat `f64` buffers.
2//!
3//! A `TensorView` pairs a raw read-only slice with its `Shape` and `Strides`,
4//! enabling element access via multi-dimensional indexing without allocation.
5//! A `TensorViewMut` provides the same over a mutable buffer.
6//!
7//! ## Safety
8//!
9//! All public indexing methods perform bounds checking by default.
10//! The `get_unchecked` family is `unsafe` and requires the caller to guarantee
11//! that the flat index does not exceed the buffer length.
12
13#![allow(clippy::elidable_lifetime_names)]
14#![allow(clippy::too_many_arguments)]
15
16use super::shape::{Shape, Strides};
17use smallvec::SmallVec;
18
19/// An immutable zero-copy view of a multi-dimensional `f64` tensor.
20///
21/// This struct is `Send` and `Sync` because all its components (`&[f64]`, `Shape`, `Strides`, and `usize`) are `Send` and `Sync`.
22pub struct TensorView<'a> {
23    /// Flat backing buffer of `f64` elements (the entire underlying buffer).
24    data: &'a [f64],
25    /// Logical shape (extents per dimension).
26    shape: Shape,
27    /// Element-offset per index step along each dimension.
28    strides: Strides,
29    /// Offset from the start of `data` to the first element of this view.
30    storage_offset: usize,
31}
32
33impl<'a> TensorView<'a> {
34    /// Constructs a new `TensorView`.
35    ///
36    /// # Panics
37    /// Panics if the buffer is smaller than the maximum addressable index implied by
38    /// the strides and storage offset.
39    #[must_use]
40    pub fn new(data: &'a [f64], shape: Shape, storage_offset: usize) -> Self {
41        let strides = shape.row_major_strides();
42        let max_idx = if shape.numel() > 0 {
43            strides.max_flat_index(&shape)
44        } else {
45            0
46        };
47        let required_len = storage_offset + max_idx + usize::from(shape.numel() > 0);
48        assert!(
49            data.len() >= required_len,
50            "Buffer too small for contiguous shape: need {} elements, got {}",
51            required_len,
52            data.len()
53        );
54
55        Self {
56            data,
57            shape,
58            strides,
59            storage_offset,
60        }
61    }
62
63    /// Constructs a new `TensorView` with default `storage_offset` of 0.
64    #[must_use]
65    pub fn new_default(data: &'a [f64], shape: Shape) -> Self {
66        Self::new(data, shape, 0)
67    }
68
69    /// Constructs a view with explicit (possibly non-contiguous) strides.
70    ///
71    /// # Panics
72    /// Panics if `shape.rank() != strides.as_slice().len()`, or if the backing
73    /// buffer is too small to safely access the largest flat index addressable by these strides.
74    #[must_use]
75    pub fn with_strides(
76        data: &'a [f64],
77        shape: Shape,
78        strides: Strides,
79        storage_offset: usize,
80    ) -> Self {
81        assert_eq!(
82            shape.rank(),
83            strides.as_slice().len(),
84            "Rank mismatch between shape and strides"
85        );
86
87        let max_idx = if shape.numel() > 0 {
88            strides.max_flat_index(&shape)
89        } else {
90            0
91        };
92        let required_len = storage_offset + max_idx + usize::from(shape.numel() > 0);
93        assert!(
94            data.len() >= required_len,
95            "Buffer too small for strided layout: need space for index {} (required size {}), got buffer of size {}",
96            max_idx,
97            required_len,
98            data.len()
99        );
100
101        Self {
102            data,
103            shape,
104            strides,
105            storage_offset,
106        }
107    }
108
109    /// Constructs a new `TensorView` with explicit strides and default `storage_offset` of 0.
110    #[must_use]
111    pub fn with_strides_default(data: &'a [f64], shape: Shape, strides: Strides) -> Self {
112        Self::with_strides(data, shape, strides, 0)
113    }
114
115    /// Returns a reference to the `Shape`.
116    #[must_use]
117    pub const fn shape(&self) -> &Shape {
118        &self.shape
119    }
120
121    /// Returns a reference to the `Strides`.
122    #[must_use]
123    pub const fn strides(&self) -> &Strides {
124        &self.strides
125    }
126
127    /// Returns the element at the multi-dimensional index.
128    ///
129    /// Returns `None` if any index component is out of bounds.
130    #[must_use]
131    pub fn get(&self, idx: &[usize]) -> Option<f64> {
132        if idx.len() != self.shape.rank() {
133            return None;
134        }
135        for (&i, &d) in idx.iter().zip(self.shape.dims()) {
136            if i >= d {
137                return None;
138            }
139        }
140        let flat = self.strides.flat_index(idx);
141        self.data.get(self.storage_offset + flat).copied()
142    }
143
144    /// Returns the element at the multi-dimensional index without bounds checking.
145    ///
146    /// # Safety
147    /// The caller must guarantee that `idx` is a valid multi-dimensional index
148    /// for this tensor view.
149    #[must_use]
150    pub unsafe fn get_unchecked(&self, idx: &[usize]) -> f64 {
151        let flat = self.strides.flat_index(idx);
152        // Explicit unsafe block for the call to get_unchecked
153        unsafe { *self.data.get_unchecked(self.storage_offset + flat) }
154    }
155
156    /// Returns the rank (number of dimensions).
157    #[must_use]
158    pub fn rank(&self) -> usize {
159        self.shape.rank()
160    }
161
162    /// Returns the total number of elements.
163    #[must_use]
164    pub fn numel(&self) -> usize {
165        self.shape.numel()
166    }
167
168    /// Returns `true` if this view has a contiguous layout.
169    #[must_use]
170    pub fn is_contiguous(&self) -> bool {
171        if self.shape.numel() <= 1 {
172            return true;
173        }
174        self.strides == self.shape.row_major_strides()
175    }
176
177    /// Returns the flat backing buffer (the entire underlying memory).
178    /// Use `as_slice()` to get the view's active segment.
179    #[must_use]
180    pub const fn as_raw_slice(&self) -> &[f64] {
181        self.data
182    }
183
184    /// Returns the active slice of this view.
185    ///
186    /// # Panics
187    /// Panics if the view is not contiguous.
188    #[must_use]
189    pub fn as_slice(&self) -> &[f64] {
190        assert!(
191            self.is_contiguous(),
192            "View is not contiguous, cannot convert to slice."
193        );
194        &self.data[self.storage_offset..self.storage_offset + self.numel()]
195    }
196
197    /// Iterates over all elements in row-major order.
198    ///
199    /// This is the fundamental building block for element-wise operations and
200    /// vectorized kernel dispatch.
201    #[must_use]
202    pub fn iter_elements(&self) -> ElementIter<'_> {
203        if self.is_contiguous() {
204            ElementIter::Contiguous(self.as_slice().iter())
205        } else {
206            let rank = self.shape.rank();
207            let idx = SmallVec::<[usize; 8]>::from_elem(0, rank);
208            let done = self.shape.dims().contains(&0);
209            ElementIter::Strided(StridedElementIter {
210                data: self.data,
211                strides: self.strides.clone(),
212                dims: self.shape.clone(),
213                idx,
214                done,
215                flat_offset: self.storage_offset,
216                elements_yielded: 0,
217            })
218        }
219    }
220}
221
222impl<'a, const N: usize> core::ops::Index<[usize; N]> for TensorView<'a> {
223    type Output = f64;
224
225    #[inline]
226    fn index(&self, index: [usize; N]) -> &Self::Output {
227        assert_eq!(N, self.shape.rank(), "Index rank mismatch");
228        for (&i, &d) in index.iter().zip(self.shape.dims()) {
229            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
230        }
231        let flat = self.strides.flat_index(&index);
232        &self.data[self.storage_offset + flat]
233    }
234}
235
236impl<'a> core::ops::Index<&[usize]> for TensorView<'a> {
237    type Output = f64;
238
239    #[inline]
240    fn index(&self, index: &[usize]) -> &Self::Output {
241        assert_eq!(index.len(), self.shape.rank(), "Index rank mismatch");
242        for (&i, &d) in index.iter().zip(self.shape.dims()) {
243            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
244        }
245        let flat = self.strides.flat_index(index);
246        &self.data[self.storage_offset + flat]
247    }
248}
249
250impl<'a> core::ops::Index<(usize,)> for TensorView<'a> {
251    type Output = f64;
252    #[inline]
253    fn index(&self, index: (usize,)) -> &Self::Output {
254        &self[[index.0]]
255    }
256}
257
258impl<'a> core::ops::Index<(usize, usize)> for TensorView<'a> {
259    type Output = f64;
260    #[inline]
261    fn index(&self, index: (usize, usize)) -> &Self::Output {
262        &self[[index.0, index.1]]
263    }
264}
265
266impl<'a> core::ops::Index<(usize, usize, usize)> for TensorView<'a> {
267    type Output = f64;
268    #[inline]
269    fn index(&self, index: (usize, usize, usize)) -> &Self::Output {
270        &self[[index.0, index.1, index.2]]
271    }
272}
273
274/// A mutable zero-copy view of a multi-dimensional `f64` tensor.
275///
276/// This struct is `Send` but NOT `Sync`. It is `Send` because `&mut [f64]` is `Send`.
277/// It is not `Sync` because `&mut [f64]` is not `Sync`.
278pub struct TensorViewMut<'a> {
279    data: &'a mut [f64],
280    shape: Shape,
281    strides: Strides,
282    storage_offset: usize,
283}
284
285impl<'a> TensorViewMut<'a> {
286    /// Constructs a new mutable `TensorViewMut`.
287    ///
288    /// # Panics
289    /// Panics if the buffer is smaller than the maximum addressable flat index.
290    #[must_use]
291    pub fn new(data: &'a mut [f64], shape: Shape, storage_offset: usize) -> Self {
292        let strides = shape.row_major_strides();
293        let max_idx = if shape.numel() > 0 {
294            strides.max_flat_index(&shape)
295        } else {
296            0
297        };
298        let required_len = storage_offset + max_idx + usize::from(shape.numel() > 0);
299        assert!(
300            data.len() >= required_len,
301            "Buffer too small for shape and offset: need {} elements, got {}",
302            required_len,
303            data.len(),
304        );
305
306        Self {
307            data,
308            shape,
309            strides,
310            storage_offset,
311        }
312    }
313
314    /// Constructs a new `TensorViewMut` with default `storage_offset` of 0.
315    #[must_use]
316    pub fn new_default(data: &'a mut [f64], shape: Shape) -> Self {
317        Self::new(data, shape, 0)
318    }
319
320    /// Returns a reference to the `Shape`.
321    #[must_use]
322    pub const fn shape(&self) -> &Shape {
323        &self.shape
324    }
325
326    /// Returns a reference to the `Strides`.
327    #[must_use]
328    pub const fn strides(&self) -> &Strides {
329        &self.strides
330    }
331
332    /// Returns `true` if this view has a contiguous layout.
333    #[must_use]
334    pub fn is_contiguous(&self) -> bool {
335        if self.shape.numel() <= 1 {
336            return true;
337        }
338        self.strides == self.shape.row_major_strides()
339    }
340
341    /// Returns the element at the multi-dimensional index mutably.
342    ///
343    /// Returns `None` if any index component is out of bounds.
344    pub fn get_mut(&mut self, idx: &[usize]) -> Option<&mut f64> {
345        if idx.len() != self.shape.rank() {
346            return None;
347        }
348        for (&i, &d) in idx.iter().zip(self.shape.dims()) {
349            if i >= d {
350                return None;
351            }
352        }
353        let flat = self.strides.flat_index(idx);
354        self.data.get_mut(self.storage_offset + flat)
355    }
356
357    /// Returns a mutable reference to the element at the multi-dimensional index
358    /// without bounds checking.
359    ///
360    /// # Safety
361    /// The caller must guarantee that `idx` is a valid multi-dimensional index
362    /// for this tensor view.
363    #[must_use]
364    pub unsafe fn get_unchecked_mut(&mut self, idx: &[usize]) -> &mut f64 {
365        let flat = self.strides.flat_index(idx);
366        unsafe { self.data.get_unchecked_mut(self.storage_offset + flat) }
367    }
368
369    /// Sets the element at the multi-dimensional index.
370    ///
371    /// Returns `false` if the index is out-of-bounds.
372    pub fn set(&mut self, idx: &[usize], val: f64) -> bool {
373        self.get_mut(idx).is_some_and(|slot| {
374            *slot = val;
375            true
376        })
377    }
378
379    /// Returns the flat backing buffer immutably (the entire underlying memory).
380    /// Use `as_slice()` to get the view's active segment.
381    #[must_use]
382    pub const fn as_raw_slice(&self) -> &[f64] {
383        self.data
384    }
385
386    /// Returns the flat backing buffer mutably (the entire underlying memory).
387    /// Use `as_slice_mut()` to get the view's active segment.
388    pub const fn as_raw_slice_mut(&mut self) -> &mut [f64] {
389        self.data
390    }
391
392    /// Returns the active slice of this view.
393    ///
394    /// # Panics
395    /// Panics if the view is not contiguous.
396    #[must_use]
397    pub fn as_slice(&self) -> &[f64] {
398        assert!(
399            self.is_contiguous(),
400            "View is not contiguous, cannot convert to slice."
401        );
402        &self.data[self.storage_offset..self.storage_offset + self.shape.numel()]
403    }
404
405    /// Returns the active mutable slice of this view.
406    ///
407    /// # Panics
408    /// Panics if the view is not contiguous.
409    pub fn as_slice_mut(&mut self) -> &mut [f64] {
410        assert!(
411            self.is_contiguous(),
412            "View is not contiguous, cannot convert to mutable slice."
413        );
414        let numel = self.shape.numel();
415        &mut self.data[self.storage_offset..self.storage_offset + numel]
416    }
417
418    /// Returns a read-only view of this mutable tensor.
419    #[must_use]
420    pub fn as_view(&self) -> TensorView<'_> {
421        TensorView {
422            data: self.data,
423            shape: self.shape.clone(),
424            strides: self.strides.clone(),
425            storage_offset: self.storage_offset,
426        }
427    }
428}
429
430impl<'a, const N: usize> core::ops::Index<[usize; N]> for TensorViewMut<'a> {
431    type Output = f64;
432
433    #[inline]
434    fn index(&self, index: [usize; N]) -> &Self::Output {
435        assert_eq!(N, self.shape.rank(), "Index rank mismatch");
436        for (&i, &d) in index.iter().zip(self.shape.dims()) {
437            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
438        }
439        let flat = self.strides.flat_index(&index);
440        &self.data[self.storage_offset + flat]
441    }
442}
443
444impl<'a, const N: usize> core::ops::IndexMut<[usize; N]> for TensorViewMut<'a> {
445    #[inline]
446    fn index_mut(&mut self, index: [usize; N]) -> &mut Self::Output {
447        assert_eq!(N, self.shape.rank(), "Index rank mismatch");
448        for (&i, &d) in index.iter().zip(self.shape.dims()) {
449            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
450        }
451        let flat = self.strides.flat_index(&index);
452        &mut self.data[self.storage_offset + flat]
453    }
454}
455
456impl<'a> core::ops::IndexMut<&[usize]> for TensorViewMut<'a> {
457    #[inline]
458    fn index_mut(&mut self, index: &[usize]) -> &mut Self::Output {
459        assert_eq!(index.len(), self.shape.rank(), "Index rank mismatch");
460        for (&i, &d) in index.iter().zip(self.shape.dims()) {
461            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
462        }
463        let flat = self.strides.flat_index(index);
464        &mut self.data[self.storage_offset + flat]
465    }
466}
467
468impl<'a> core::ops::Index<&[usize]> for TensorViewMut<'a> {
469    type Output = f64;
470
471    #[inline]
472    fn index(&self, index: &[usize]) -> &Self::Output {
473        assert_eq!(index.len(), self.shape.rank(), "Index rank mismatch");
474        for (&i, &d) in index.iter().zip(self.shape.dims()) {
475            assert!(i < d, "Index {i} out of bounds for dimension size {d}");
476        }
477        let flat = self.strides.flat_index(index);
478        &self.data[self.storage_offset + flat]
479    }
480}
481
482impl<'a> core::ops::Index<(usize,)> for TensorViewMut<'a> {
483    type Output = f64;
484    #[inline]
485    fn index(&self, index: (usize,)) -> &Self::Output {
486        &self[[index.0]]
487    }
488}
489
490impl<'a> core::ops::Index<(usize, usize)> for TensorViewMut<'a> {
491    type Output = f64;
492    #[inline]
493    fn index(&self, index: (usize, usize)) -> &Self::Output {
494        &self[[index.0, index.1]]
495    }
496}
497
498impl<'a> core::ops::Index<(usize, usize, usize)> for TensorViewMut<'a> {
499    type Output = f64;
500    #[inline]
501    fn index(&self, index: (usize, usize, usize)) -> &Self::Output {
502        &self[[index.0, index.1, index.2]]
503    }
504}
505
506impl<'a> core::ops::IndexMut<(usize,)> for TensorViewMut<'a> {
507    #[inline]
508    fn index_mut(&mut self, index: (usize,)) -> &mut Self::Output {
509        &mut self[[index.0]]
510    }
511}
512
513impl<'a> core::ops::IndexMut<(usize, usize)> for TensorViewMut<'a> {
514    #[inline]
515    fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output {
516        &mut self[[index.0, index.1]]
517    }
518}
519
520impl<'a> core::ops::IndexMut<(usize, usize, usize)> for TensorViewMut<'a> {
521    #[inline]
522    fn index_mut(&mut self, index: (usize, usize, usize)) -> &mut Self::Output {
523        &mut self[[index.0, index.1, index.2]]
524    }
525}
526
527/// Element-wise binary operation on two broadcast-compatible views into an output.
528///
529/// This is the core primitive for all binary tensor operators (+, -, *, /).
530/// Broadcasting is handled via the stride-zero convention: a dimension with
531/// size 1 has stride 0, so the same element is reused without copying.
532///
533/// # Safety
534/// To prevent undefined behavior, the caller must ensure that the output buffer window
535/// does not overlap with the backing storage buffers of `a` and `b` unless an exact
536/// in-place operation is intended and safe.
537///
538/// # Errors
539/// Returns `Err(String)` if the shapes are not broadcast-compatible or the
540/// output buffer is too small for the broadcast output shape.
541pub fn broadcast_elementwise<F>(
542    a: &TensorView<'_>,
543    b: &TensorView<'_>,
544    out: &mut TensorViewMut<'_>,
545    op: F,
546) -> Result<(), String>
547where
548    F: Fn(f64, f64) -> f64 + Copy,
549{
550    let out_shape = a.shape().broadcast_output(b.shape()).ok_or_else(|| {
551        format!(
552            "Shapes {} and {} are not broadcast-compatible",
553            a.shape(),
554            b.shape(),
555        )
556    })?;
557
558    if out.shape() != &out_shape {
559        return Err(format!(
560            "Output shape {} doesn't match expected broadcast shape {}",
561            out.shape(),
562            out_shape,
563        ));
564    }
565
566    let is_contiguous_fast_path = a.shape() == b.shape()
567        && a.shape() == out.shape()
568        && a.is_contiguous()
569        && b.is_contiguous()
570        && out.is_contiguous();
571
572    let a_slice = a.as_raw_slice();
573    let b_slice = b.as_raw_slice();
574
575    if is_contiguous_fast_path {
576        let out_slice = out.as_slice_mut();
577        let len = a.numel();
578
579        let a_sub = &a_slice[a.storage_offset..a.storage_offset + len];
580        let b_sub = &b_slice[b.storage_offset..b.storage_offset + len];
581        let out_sub = &mut out_slice[..len];
582
583        for i in 0..len {
584            out_sub[i] = op(a_sub[i], b_sub[i]);
585        }
586        return Ok(());
587    }
588
589    let rank = out_shape.rank();
590    let a_offset = rank.saturating_sub(a.rank());
591    let b_offset = rank.saturating_sub(b.rank());
592
593    let a_strides = a.strides().as_slice();
594    let b_strides = b.strides().as_slice();
595
596    let mut a_strides_padded = SmallVec::<[usize; 8]>::from_elem(0, rank);
597    let mut b_strides_padded = SmallVec::<[usize; 8]>::from_elem(0, rank);
598
599    // If an original dimension size is 1, its broadcast stride is 0
600    let a_dims = a.shape().dims();
601    for i in a_offset..rank {
602        if a_dims[i - a_offset] > 1 {
603            a_strides_padded[i] = a_strides[i - a_offset];
604        }
605    }
606
607    let b_dims = b.shape().dims();
608    for i in b_offset..rank {
609        if b_dims[i - b_offset] > 1 {
610            b_strides_padded[i] = b_strides[i - b_offset];
611        }
612    }
613
614    let out_strides_cloned = out.strides().clone();
615    let output_iter = OutputFlatIndexIterator::new(
616        out_shape,
617        out_strides_cloned.clone(),
618        a_strides_padded.clone(),
619        b_strides_padded.clone(),
620    );
621
622    let a_step = if rank > 0 {
623        a_strides_padded[rank - 1]
624    } else {
625        0
626    };
627    let b_step = if rank > 0 {
628        b_strides_padded[rank - 1]
629    } else {
630        0
631    };
632    let out_step = if rank > 0 {
633        out_strides_cloned.as_slice()[rank - 1]
634    } else {
635        0
636    };
637
638    let a_ptr = unsafe { a_slice.as_ptr().add(a.storage_offset) };
639    let b_ptr = unsafe { b_slice.as_ptr().add(b.storage_offset) };
640    let out_ptr = unsafe { out.as_raw_slice_mut().as_mut_ptr().add(out.storage_offset) };
641
642    // Debug assertions to catch physical overlap/aliasing in development
643    debug_assert!(
644        a_ptr.cast::<()>() != out_ptr as *const (),
645        "Tensor A and Output alias!"
646    );
647    debug_assert!(
648        b_ptr.cast::<()>() != out_ptr as *const (),
649        "Tensor B and Output alias!"
650    );
651
652    unsafe {
653        broadcast_elementwise_kernel(
654            output_iter,
655            a_ptr,
656            b_ptr,
657            out_ptr,
658            a_step,
659            b_step,
660            out_step,
661            op,
662        );
663    }
664
665    Ok(())
666}
667
668#[inline(always)]
669unsafe fn process_row_contiguous<F>(
670    a_ptr: *const f64,
671    b_ptr: *const f64,
672    out_ptr: *mut f64,
673    mut current_a: usize,
674    mut current_b: usize,
675    mut current_out: usize,
676    row_len: usize,
677    op: F,
678) where
679    F: Fn(f64, f64) -> f64,
680{
681    for _ in 0..row_len {
682        let va = unsafe { *a_ptr.add(current_a) };
683        let vb = unsafe { *b_ptr.add(current_b) };
684        unsafe { *out_ptr.add(current_out) = op(va, vb) };
685        current_a += 1;
686        current_b += 1;
687        current_out += 1;
688    }
689}
690
691#[inline(always)]
692#[allow(clippy::similar_names)]
693unsafe fn process_row_broadcast_a<OpF>(
694    a_ptr: *const f64,
695    b_ptr: *const f64,
696    out_ptr: *mut f64,
697    current_a_fixed: usize,
698    mut current_b: usize,
699    mut current_out: usize,
700    b_step: usize,
701    out_step: usize,
702    row_len: usize,
703    op: OpF,
704) where
705    OpF: Fn(f64, f64) -> f64,
706{
707    let va = unsafe { *a_ptr.add(current_a_fixed) };
708    for _ in 0..row_len {
709        let vb = unsafe { *b_ptr.add(current_b) };
710        unsafe { *out_ptr.add(current_out) = op(va, vb) };
711        current_b += b_step;
712        current_out += out_step;
713    }
714}
715
716#[inline(always)]
717#[allow(clippy::similar_names)]
718unsafe fn process_row_broadcast_b<OpF>(
719    a_ptr: *const f64,
720    b_ptr: *const f64,
721    out_ptr: *mut f64,
722    mut current_a: usize,
723    current_b_fixed: usize,
724    mut current_out: usize,
725    a_step: usize,
726    out_step: usize,
727    row_len: usize,
728    op: OpF,
729) where
730    OpF: Fn(f64, f64) -> f64,
731{
732    let vb = unsafe { *b_ptr.add(current_b_fixed) };
733    for _ in 0..row_len {
734        let va = unsafe { *a_ptr.add(current_a) };
735        unsafe { *out_ptr.add(current_out) = op(va, vb) };
736        current_a += a_step;
737        current_out += out_step;
738    }
739}
740
741#[inline(always)]
742#[allow(clippy::similar_names)]
743unsafe fn process_row_broadcast_ab<OpF>(
744    a_ptr: *const f64,
745    b_ptr: *const f64,
746    out_ptr: *mut f64,
747    current_a_fixed: usize,
748    current_b_fixed: usize,
749    mut current_out: usize,
750    out_step: usize,
751    row_len: usize,
752    op: OpF,
753) where
754    OpF: Fn(f64, f64) -> f64,
755{
756    let va = unsafe { *a_ptr.add(current_a_fixed) };
757    let vb = unsafe { *b_ptr.add(current_b_fixed) };
758    let result = op(va, vb);
759    for _ in 0..row_len {
760        unsafe { *out_ptr.add(current_out) = result };
761        current_out += out_step;
762    }
763}
764
765#[inline(always)]
766unsafe fn process_row_general<F>(
767    a_ptr: *const f64,
768    b_ptr: *const f64,
769    out_ptr: *mut f64,
770    mut current_a: usize,
771    mut current_b: usize,
772    mut current_out: usize,
773    a_step: usize,
774    b_step: usize,
775    out_step: usize,
776    row_len: usize,
777    op: F,
778) where
779    F: Fn(f64, f64) -> f64,
780{
781    for _ in 0..row_len {
782        let va = unsafe { *a_ptr.add(current_a) };
783        let vb = unsafe { *b_ptr.add(current_b) };
784        unsafe { *out_ptr.add(current_out) = op(va, vb) };
785        current_a += a_step;
786        current_b += b_step;
787        current_out += out_step;
788    }
789}
790
791#[inline(always)]
792unsafe fn broadcast_elementwise_kernel<F>(
793    output_iter: OutputFlatIndexIterator,
794    a_ptr: *const f64,
795    b_ptr: *const f64,
796    out_ptr: *mut f64,
797    a_step: usize,
798    b_step: usize,
799    out_step: usize,
800    op: F,
801) where
802    F: Fn(f64, f64) -> f64 + Copy,
803{
804    for (a_flat, b_flat, out_flat, row_len) in output_iter {
805        if a_step == 1 && b_step == 1 && out_step == 1 {
806            unsafe {
807                process_row_contiguous(
808                    a_ptr, b_ptr, out_ptr, a_flat, b_flat, out_flat, row_len, op,
809                );
810            }
811        } else if a_step == 0 && b_step == 0 {
812            unsafe {
813                process_row_broadcast_ab(
814                    a_ptr, b_ptr, out_ptr, a_flat, b_flat, out_flat, out_step, row_len, op,
815                );
816            }
817        } else if a_step == 0 {
818            unsafe {
819                process_row_broadcast_a(
820                    a_ptr, b_ptr, out_ptr, a_flat, b_flat, out_flat, b_step, out_step, row_len, op,
821                );
822            }
823        } else if b_step == 0 {
824            unsafe {
825                process_row_broadcast_b(
826                    a_ptr, b_ptr, out_ptr, a_flat, b_flat, out_flat, a_step, out_step, row_len, op,
827                );
828            }
829        } else {
830            unsafe {
831                process_row_general(
832                    a_ptr, b_ptr, out_ptr, a_flat, b_flat, out_flat, a_step, b_step, out_step,
833                    row_len, op,
834                );
835            }
836        }
837    }
838}
839
840/// An iterator over the elements of a tensor view.
841#[allow(clippy::large_enum_variant)]
842pub enum ElementIter<'a> {
843    /// An iterator for contiguous tensor views.
844    Contiguous(std::slice::Iter<'a, f64>),
845    /// An iterator for strided (non-contiguous) tensor views.
846    Strided(StridedElementIter<'a>),
847}
848
849impl<'a> Iterator for ElementIter<'a> {
850    type Item = f64;
851
852    #[inline]
853    fn next(&mut self) -> Option<Self::Item> {
854        match self {
855            Self::Contiguous(it) => it.next().copied(),
856            Self::Strided(it) => it.next(),
857        }
858    }
859
860    #[inline]
861    fn size_hint(&self) -> (usize, Option<usize>) {
862        match self {
863            Self::Contiguous(it) => it.size_hint(),
864            Self::Strided(it) => it.size_hint(),
865        }
866    }
867}
868
869/// An optimized element iterator over strided tensors.
870///
871/// Iteration tracking coordinates and memory offsets are modified incrementally
872/// using an amortized $O(1)$ technique, resolving performance penalties when
873/// performing element-by-element iteration.
874pub struct StridedElementIter<'a> {
875    data: &'a [f64],
876    strides: Strides,
877    dims: Shape,
878    idx: SmallVec<[usize; 8]>,
879    done: bool,
880    flat_offset: usize,
881    elements_yielded: usize,
882}
883
884impl<'a> Iterator for StridedElementIter<'a> {
885    type Item = f64;
886
887    /// Advances the iterator and returns the next element.
888    ///
889    /// Returns `None` when the iterator is exhausted.
890    #[inline]
891    fn next(&mut self) -> Option<Self::Item> {
892        if self.done {
893            return None;
894        }
895
896        let val = unsafe { *self.data.get_unchecked(self.flat_offset) };
897        self.elements_yielded += 1;
898
899        let rank = self.dims.rank();
900        let dims = self.dims.dims();
901        let strides_slice = self.strides.as_slice();
902
903        let mut carry = true;
904        for i in (0..rank).rev() {
905            self.idx[i] += 1;
906            self.flat_offset += strides_slice[i];
907
908            if self.idx[i] >= dims[i] {
909                // Rewind flat memory offset accumulated along this dimension axis
910                let steps_taken = self.idx[i];
911                self.flat_offset -= strides_slice[i] * steps_taken;
912                self.idx[i] = 0;
913            } else {
914                carry = false;
915                break;
916            }
917        }
918
919        if carry {
920            self.done = true;
921        }
922
923        Some(val)
924    }
925
926    #[inline]
927    fn size_hint(&self) -> (usize, Option<usize>) {
928        if self.done {
929            (0, Some(0))
930        } else {
931            let total_numel = self.dims.numel();
932            let remaining = total_numel.saturating_sub(self.elements_yielded);
933            (remaining, Some(remaining))
934        }
935    }
936}
937
938/// High-performance flat offset iterator resolving broadcast targets coordinate-by-coordinate.
939pub struct OutputFlatIndexIterator {
940    dims: Shape,
941    strides: Strides,
942    idx: SmallVec<[usize; 8]>,
943    done: bool,
944    a_strides_padded: SmallVec<[usize; 8]>,
945    b_strides_padded: SmallVec<[usize; 8]>,
946    current_a: usize,
947    current_b: usize,
948    current_out: usize,
949}
950
951impl OutputFlatIndexIterator {
952    /// Creates a new `OutputFlatIndexIterator`.
953    #[must_use]
954    pub fn new(
955        output_shape: Shape,
956        output_strides: Strides,
957        a_strides_padded: SmallVec<[usize; 8]>,
958        b_strides_padded: SmallVec<[usize; 8]>,
959    ) -> Self {
960        let rank = output_shape.rank();
961        let idx = SmallVec::<[usize; 8]>::from_elem(0, rank);
962        let done = output_shape.numel() == 0;
963
964        let mut current_a = 0;
965        let mut current_b = 0;
966        let mut current_out = 0;
967
968        if rank > 0 && !done {
969            let out_strides_slice = output_strides.as_slice();
970            for i in 0..rank {
971                current_a += idx[i] * a_strides_padded[i];
972                current_b += idx[i] * b_strides_padded[i];
973                current_out += idx[i] * out_strides_slice[i];
974            }
975        }
976
977        Self {
978            dims: output_shape,
979            strides: output_strides,
980            idx,
981            done,
982            a_strides_padded,
983            b_strides_padded,
984            current_a,
985            current_b,
986            current_out,
987        }
988    }
989}
990
991impl Iterator for OutputFlatIndexIterator {
992    type Item = (usize, usize, usize, usize);
993
994    #[inline]
995    fn next(&mut self) -> Option<Self::Item> {
996        if self.done {
997            return None;
998        }
999
1000        let rank = self.dims.rank();
1001        let dims = self.dims.dims();
1002        let out_strides_slice = self.strides.as_slice();
1003
1004        if rank == 0 {
1005            self.done = true;
1006            return Some((0, 0, 0, 1));
1007        }
1008
1009        let innermost_dim_idx = rank - 1;
1010        let row_len = dims[innermost_dim_idx];
1011
1012        let res = (self.current_a, self.current_b, self.current_out, row_len);
1013
1014        let mut carry = true;
1015        let mut i = rank;
1016
1017        while carry && i > 0 {
1018            i -= 1;
1019
1020            if i == innermost_dim_idx {
1021                self.idx[i] += row_len;
1022                self.current_a += self.a_strides_padded[i] * row_len;
1023                self.current_b += self.b_strides_padded[i] * row_len;
1024                self.current_out += out_strides_slice[i] * row_len;
1025            } else {
1026                self.idx[i] += 1;
1027                self.current_a += self.a_strides_padded[i];
1028                self.current_b += self.b_strides_padded[i];
1029                self.current_out += out_strides_slice[i];
1030            }
1031
1032            if self.idx[i] >= dims[i] {
1033                let steps_taken = self.idx[i];
1034                // Subtract the accumulated steps to rewind the offsets
1035                self.current_a -= self.a_strides_padded[i] * steps_taken;
1036                self.current_b -= self.b_strides_padded[i] * steps_taken;
1037                self.current_out -= out_strides_slice[i] * steps_taken;
1038                self.idx[i] = 0;
1039            } else {
1040                carry = false;
1041            }
1042        }
1043
1044        if carry {
1045            self.done = true;
1046        }
1047
1048        Some(res)
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055
1056    #[test]
1057    fn test_tensor_view_indexing() {
1058        let data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1059        let view = TensorView::new_default(&data, Shape::matrix(2, 3));
1060        assert_eq!(view.get(&[0, 0]), Some(0.0));
1061        assert_eq!(view.get(&[0, 2]), Some(2.0));
1062        assert_eq!(view.get(&[1, 1]), Some(4.0));
1063        assert_eq!(view.get(&[2, 0]), None); // out of bounds
1064
1065        assert_eq!(view[[0, 0]], 0.0);
1066        assert_eq!(view[[0, 2]], 2.0);
1067        assert_eq!(view[[1, 1]], 4.0);
1068    }
1069
1070    #[test]
1071    fn test_tensor_view_mut_indexing() {
1072        let mut data = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1073        let mut view_mut = TensorViewMut::new_default(&mut data, Shape::matrix(2, 3));
1074
1075        assert_eq!(view_mut[[1, 1]], 4.0);
1076        view_mut[[1, 1]] = 42.0;
1077        assert_eq!(view_mut[[1, 1]], 42.0);
1078
1079        assert!(view_mut.set(&[0, 1], 99.0));
1080        assert_eq!(view_mut[[0, 1]], 99.0);
1081    }
1082}