Skip to main content

ipfrs_tensorlogic/
slice_view.rs

1//! `TensorSliceView` — zero-copy logical views into tensor data via
2//! offset+stride descriptors, supporting slicing, broadcasting, and element
3//! access without data duplication.
4//!
5//! # Overview
6//!
7//! A [`TensorSliceView`] holds a flat `Vec<f64>` as the backing store and a
8//! [`ViewDescriptor`] that describes the logical shape, strides, and starting
9//! offset into that store.  All slice and broadcast operations return new
10//! descriptors that share the same flat buffer – no data is ever copied.
11//!
12//! # Key types
13//!
14//! | Type | Purpose |
15//! |------|---------|
16//! | [`SliceRange`] | A half-open `[start, stop)` range with a `step` |
17//! | [`ViewDescriptor`] | Offset + shape + strides descriptor for a view |
18//! | [`BroadcastShape`] | NumPy-style broadcast compatibility and stride computation |
19//! | [`SliceViewStats`] | Cumulative counters for views, slices, and broadcasts |
20//! | [`TensorSliceView`] | The main manager that owns the data and exposes the API |
21
22// ---------------------------------------------------------------------------
23// SliceRange
24// ---------------------------------------------------------------------------
25
26/// A half-open range `[start, stop)` with a positive `step`.
27///
28/// # Examples
29///
30/// ```
31/// use ipfrs_tensorlogic::slice_view::SliceRange;
32///
33/// let r = SliceRange { start: 0, stop: 10, step: 2 };
34/// assert_eq!(r.len(), 5);
35/// assert_eq!(r.indices(), vec![0, 2, 4, 6, 8]);
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SliceRange {
39    /// First index to include.
40    pub start: usize,
41    /// Exclusive upper bound.
42    pub stop: usize,
43    /// Step between successive indices; must be ≥ 1 for a non-empty range.
44    pub step: usize,
45}
46
47impl Default for SliceRange {
48    fn default() -> Self {
49        Self {
50            start: 0,
51            stop: 0,
52            step: 1,
53        }
54    }
55}
56
57impl SliceRange {
58    /// Create a new `SliceRange` with an explicit step.
59    #[must_use]
60    pub fn new(start: usize, stop: usize, step: usize) -> Self {
61        Self { start, stop, step }
62    }
63
64    /// Create a contiguous range `[start, stop)` with `step = 1`.
65    #[must_use]
66    pub fn contiguous(start: usize, stop: usize) -> Self {
67        Self {
68            start,
69            stop,
70            step: 1,
71        }
72    }
73
74    /// Number of elements produced by this range.
75    ///
76    /// Returns `0` if `step == 0` or `start >= stop`.
77    /// Otherwise uses ceiling division: `⌈(stop − start) / step⌉`.
78    #[must_use]
79    pub fn len(&self) -> usize {
80        if self.step == 0 || self.start >= self.stop {
81            return 0;
82        }
83        let span = self.stop - self.start;
84        span.div_ceil(self.step)
85    }
86
87    /// Returns `true` when the range contains no elements.
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.len() == 0
91    }
92
93    /// Materialise all indices: `start, start+step, start+2*step, …` while `< stop`.
94    #[must_use]
95    pub fn indices(&self) -> Vec<usize> {
96        if self.step == 0 || self.start >= self.stop {
97            return Vec::new();
98        }
99        let mut result = Vec::with_capacity(self.len());
100        let mut idx = self.start;
101        while idx < self.stop {
102            result.push(idx);
103            idx = match idx.checked_add(self.step) {
104                Some(v) => v,
105                None => break,
106            };
107        }
108        result
109    }
110}
111
112// ---------------------------------------------------------------------------
113// ViewDescriptor
114// ---------------------------------------------------------------------------
115
116/// Describes a logical view into a flat buffer via an offset, shape, and strides.
117///
118/// The flat index of element `(i₀, i₁, …, iₙ₋₁)` is:
119///
120/// ```text
121/// base_offset + Σ iₖ * strides[k]
122/// ```
123///
124/// # Examples
125///
126/// ```
127/// use ipfrs_tensorlogic::slice_view::ViewDescriptor;
128///
129/// // 2×3 row-major view starting at element 0
130/// let desc = ViewDescriptor {
131///     base_offset: 0,
132///     shape: vec![2, 3],
133///     strides: vec![3, 1],
134/// };
135/// assert_eq!(desc.flat_index(&[1, 2]), Some(5));
136/// assert!(desc.is_contiguous());
137/// ```
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ViewDescriptor {
140    /// Starting element index in the flat backing buffer.
141    pub base_offset: usize,
142    /// Logical shape of the view.
143    pub shape: Vec<usize>,
144    /// Logical strides (one per dimension).
145    pub strides: Vec<usize>,
146}
147
148impl ViewDescriptor {
149    /// Number of dimensions.
150    #[must_use]
151    pub fn ndim(&self) -> usize {
152        self.shape.len()
153    }
154
155    /// Total number of logical elements (product of `shape`; `1` for a scalar).
156    #[must_use]
157    pub fn total_elements(&self) -> usize {
158        self.shape.iter().product()
159    }
160
161    /// Compute the flat index for a multi-dimensional index tuple.
162    ///
163    /// Returns `None` if:
164    /// - `indices.len() != self.ndim()`, or
165    /// - any `indices[i] >= self.shape[i]`.
166    #[must_use]
167    pub fn flat_index(&self, indices: &[usize]) -> Option<usize> {
168        if indices.len() != self.ndim() {
169            return None;
170        }
171        let mut offset = self.base_offset;
172        for (dim, (&idx, (&dim_size, &stride))) in indices
173            .iter()
174            .zip(self.shape.iter().zip(self.strides.iter()))
175            .enumerate()
176        {
177            let _ = dim; // suppress unused warning
178            if idx >= dim_size {
179                return None;
180            }
181            offset = offset.checked_add(idx * stride)?;
182        }
183        Some(offset)
184    }
185
186    /// Returns `true` if the view is C (row-major) contiguous.
187    ///
188    /// A view is contiguous when `strides[ndim-1] == 1` and each preceding
189    /// stride equals the product of the following shape dimensions.
190    #[must_use]
191    pub fn is_contiguous(&self) -> bool {
192        let n = self.ndim();
193        if n == 0 {
194            return true;
195        }
196        if self.strides[n - 1] != 1 {
197            return false;
198        }
199        let mut expected = 1usize;
200        for i in (0..n).rev() {
201            if self.strides[i] != expected {
202                return false;
203            }
204            expected *= self.shape[i];
205        }
206        true
207    }
208}
209
210// ---------------------------------------------------------------------------
211// BroadcastShape
212// ---------------------------------------------------------------------------
213
214/// NumPy-style broadcast compatibility checker and stride generator.
215///
216/// # Examples
217///
218/// ```
219/// use ipfrs_tensorlogic::slice_view::BroadcastShape;
220///
221/// let bs = BroadcastShape { shape: vec![1, 3] };
222/// assert!(bs.compatible_with(&[4, 3]));
223///
224/// let strides = bs.broadcast_to(&[4, 3]).expect("example: should succeed in docs");
225/// assert_eq!(strides, vec![0, 1]);
226/// ```
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct BroadcastShape {
229    /// The source shape to be broadcast.
230    pub shape: Vec<usize>,
231}
232
233impl BroadcastShape {
234    /// Check NumPy-style broadcast compatibility with `other`.
235    ///
236    /// Dimensions are aligned from the right.  Each pair `(a, b)` must satisfy:
237    /// `a == b`, `a == 1`, or `b == 1`.
238    #[must_use]
239    pub fn compatible_with(&self, other: &[usize]) -> bool {
240        let self_ndim = self.shape.len();
241        let other_ndim = other.len();
242
243        for k in 0..self_ndim.max(other_ndim) {
244            let a = if k < self_ndim {
245                self.shape[self_ndim - 1 - k]
246            } else {
247                1
248            };
249            let b = if k < other_ndim {
250                other[other_ndim - 1 - k]
251            } else {
252                1
253            };
254            if a != b && a != 1 && b != 1 {
255                return false;
256            }
257        }
258        true
259    }
260
261    /// Compute the broadcast strides needed to view `self` as `target`.
262    ///
263    /// A source dimension of size `1` gets stride `0` (it is repeated along
264    /// that axis without touching memory).  Equal dimensions get the natural
265    /// row-major stride computed from the *source* shape.
266    ///
267    /// Returns `None` if the shapes are not compatible.
268    #[must_use]
269    pub fn broadcast_to(&self, target: &[usize]) -> Option<Vec<usize>> {
270        if !self.compatible_with(target) {
271            return None;
272        }
273
274        let target_ndim = target.len();
275        let src_ndim = self.shape.len();
276
277        // Compute natural row-major strides for the source shape.
278        let src_strides = row_major_strides(&self.shape);
279
280        let mut strides = vec![0usize; target_ndim];
281        for k in 0..target_ndim {
282            // Align from the right.
283            let src_k_rev = k; // k from the right in target
284            let src_k = if src_k_rev < src_ndim {
285                src_ndim - 1 - src_k_rev
286            } else {
287                // Source has no dimension here — treat as 1 → stride 0
288                let out_k = target_ndim - 1 - k;
289                strides[out_k] = 0;
290                continue;
291            };
292            let out_k = target_ndim - 1 - k;
293            let src_dim = self.shape[src_k];
294            let tgt_dim = target[out_k];
295
296            if src_dim == 1 && tgt_dim != 1 {
297                // Broadcast dimension: stride 0 repeats the single element.
298                strides[out_k] = 0;
299            } else {
300                // Equal dimension: use natural stride.
301                strides[out_k] = src_strides[src_k];
302            }
303        }
304
305        Some(strides)
306    }
307}
308
309// ---------------------------------------------------------------------------
310// SliceViewStats
311// ---------------------------------------------------------------------------
312
313/// Cumulative statistics for a [`TensorSliceView`].
314#[derive(Debug, Clone, Default)]
315pub struct SliceViewStats {
316    /// Total number of `TensorSliceView` instances created (via `new`).
317    pub total_views_created: u64,
318    /// Total number of `slice` calls performed.
319    pub total_slices: u64,
320    /// Total number of `broadcast_strides` calls performed.
321    pub total_broadcasts: u64,
322}
323
324// ---------------------------------------------------------------------------
325// Helpers
326// ---------------------------------------------------------------------------
327
328/// Compute row-major (C-order) strides for `shape`.
329///
330/// For a shape `[d₀, d₁, …, dₙ₋₁]` the strides are
331/// `[d₁·d₂·…·dₙ₋₁, d₂·…·dₙ₋₁, …, 1]`.
332#[must_use]
333fn row_major_strides(shape: &[usize]) -> Vec<usize> {
334    let n = shape.len();
335    if n == 0 {
336        return Vec::new();
337    }
338    let mut strides = vec![1usize; n];
339    for i in (0..n - 1).rev() {
340        strides[i] = strides[i + 1] * shape[i + 1];
341    }
342    strides
343}
344
345// ---------------------------------------------------------------------------
346// TensorSliceView
347// ---------------------------------------------------------------------------
348
349/// Zero-copy logical view manager over a flat `f64` data buffer.
350///
351/// All slice/broadcast operations produce new [`ViewDescriptor`]s that share
352/// the same backing `data` — no elements are ever duplicated.
353///
354/// # Examples
355///
356/// ```
357/// use ipfrs_tensorlogic::slice_view::{TensorSliceView, SliceRange};
358///
359/// // 2×3 tensor filled with 0..6
360/// let data: Vec<f64> = (0..6).map(|x| x as f64).collect();
361/// let mut view = TensorSliceView::new(data, vec![2, 3]);
362///
363/// assert_eq!(view.get(&[0, 0]), Some(0.0));
364/// assert_eq!(view.get(&[1, 2]), Some(5.0));
365///
366/// // Slice row 1 only
367/// let slice_desc = view.slice(0, SliceRange::contiguous(1, 2)).expect("example: should succeed in docs");
368/// assert_eq!(slice_desc.base_offset, 3);
369/// assert_eq!(slice_desc.shape, vec![1, 3]);
370/// ```
371#[derive(Debug)]
372pub struct TensorSliceView {
373    /// Flat backing buffer.
374    pub data: Vec<f64>,
375    /// Current logical view descriptor.
376    pub descriptor: ViewDescriptor,
377    /// Cumulative statistics (slices, broadcasts, views created).
378    pub stats: SliceViewStats,
379}
380
381impl TensorSliceView {
382    /// Create a new `TensorSliceView` over `data` with the given logical `shape`.
383    ///
384    /// Row-major strides are computed automatically and `base_offset` is set to `0`.
385    /// `stats.total_views_created` is incremented.
386    #[must_use]
387    pub fn new(data: Vec<f64>, shape: Vec<usize>) -> Self {
388        let strides = row_major_strides(&shape);
389        let descriptor = ViewDescriptor {
390            base_offset: 0,
391            shape,
392            strides,
393        };
394        let stats = SliceViewStats {
395            total_views_created: 1,
396            ..Default::default()
397        };
398        Self {
399            data,
400            descriptor,
401            stats,
402        }
403    }
404
405    /// Retrieve a single element by its multi-dimensional index.
406    ///
407    /// Returns `None` if the index is out of bounds for the logical shape *or*
408    /// if the computed flat index exceeds the backing buffer length.
409    #[must_use]
410    pub fn get(&self, indices: &[usize]) -> Option<f64> {
411        let flat = self.descriptor.flat_index(indices)?;
412        self.data.get(flat).copied()
413    }
414
415    /// Apply a slice along `dim` and return the resulting [`ViewDescriptor`].
416    ///
417    /// The `data` buffer is **not** modified; the new descriptor adjusts
418    /// `base_offset` and `strides[dim]` to reflect the slice.
419    ///
420    /// Returns `None` if:
421    /// - `dim >= self.descriptor.ndim()`, or
422    /// - `range.start >= self.descriptor.shape[dim]`.
423    ///
424    /// `stats.total_slices` is incremented on success.
425    pub fn slice(&mut self, dim: usize, range: SliceRange) -> Option<ViewDescriptor> {
426        let ndim = self.descriptor.ndim();
427        if dim >= ndim {
428            return None;
429        }
430        if range.start >= self.descriptor.shape[dim] {
431            return None;
432        }
433
434        let mut new_shape = self.descriptor.shape.clone();
435        let mut new_strides = self.descriptor.strides.clone();
436
437        // The new base offset is shifted by range.start * strides[dim].
438        let new_base_offset = self
439            .descriptor
440            .base_offset
441            .checked_add(range.start * self.descriptor.strides[dim])?;
442
443        // The length of this dimension becomes range.len() (may be 0 for empty slice).
444        new_shape[dim] = range.len();
445
446        // The stride along this dimension is scaled by range.step.
447        new_strides[dim] = self.descriptor.strides[dim].checked_mul(range.step.max(1))?;
448
449        let desc = ViewDescriptor {
450            base_offset: new_base_offset,
451            shape: new_shape,
452            strides: new_strides,
453        };
454
455        self.stats.total_slices += 1;
456        Some(desc)
457    }
458
459    /// Compute broadcast strides to view the current logical shape as `target_shape`.
460    ///
461    /// Uses [`BroadcastShape`] internally.  Returns `None` if the shapes are
462    /// incompatible.  `stats.total_broadcasts` is incremented on success.
463    pub fn broadcast_strides(&mut self, target_shape: &[usize]) -> Option<Vec<usize>> {
464        let bs = BroadcastShape {
465            shape: self.descriptor.shape.clone(),
466        };
467        let strides = bs.broadcast_to(target_shape)?;
468        self.stats.total_broadcasts += 1;
469        Some(strides)
470    }
471
472    /// Return a reference to the cumulative statistics.
473    #[must_use]
474    pub fn stats(&self) -> &SliceViewStats {
475        &self.stats
476    }
477}
478
479// ---------------------------------------------------------------------------
480// Tests
481// ---------------------------------------------------------------------------
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    // ── SliceRange::len ───────────────────────────────────────────────────────
488
489    #[test]
490    fn slice_range_len_step_zero_is_empty() {
491        let r = SliceRange {
492            start: 0,
493            stop: 10,
494            step: 0,
495        };
496        assert_eq!(r.len(), 0);
497    }
498
499    #[test]
500    fn slice_range_len_start_ge_stop_is_empty() {
501        let r = SliceRange {
502            start: 5,
503            stop: 5,
504            step: 1,
505        };
506        assert_eq!(r.len(), 0);
507        let r2 = SliceRange {
508            start: 6,
509            stop: 5,
510            step: 1,
511        };
512        assert_eq!(r2.len(), 0);
513    }
514
515    #[test]
516    fn slice_range_len_step_one() {
517        let r = SliceRange::contiguous(2, 7);
518        assert_eq!(r.len(), 5);
519    }
520
521    #[test]
522    fn slice_range_len_step_two_even() {
523        // [0,2,4,6,8] => 5 elements from [0,10) step 2
524        let r = SliceRange::new(0, 10, 2);
525        assert_eq!(r.len(), 5);
526    }
527
528    #[test]
529    fn slice_range_len_step_two_odd_span() {
530        // [0,2,4] => 3 elements from [0,5) step 2
531        let r = SliceRange::new(0, 5, 2);
532        assert_eq!(r.len(), 3);
533    }
534
535    #[test]
536    fn slice_range_len_step_larger_than_span() {
537        // step=10, span=5 → only start is included
538        let r = SliceRange::new(0, 5, 10);
539        assert_eq!(r.len(), 1);
540    }
541
542    #[test]
543    fn slice_range_len_step_three() {
544        // [1,4,7] => 3 elements from [1,9) step 3
545        let r = SliceRange::new(1, 9, 3);
546        assert_eq!(r.len(), 3);
547    }
548
549    // ── SliceRange::indices ───────────────────────────────────────────────────
550
551    #[test]
552    fn slice_range_indices_step_one() {
553        let r = SliceRange::contiguous(3, 6);
554        assert_eq!(r.indices(), vec![3, 4, 5]);
555    }
556
557    #[test]
558    fn slice_range_indices_step_two() {
559        let r = SliceRange::new(0, 10, 2);
560        assert_eq!(r.indices(), vec![0, 2, 4, 6, 8]);
561    }
562
563    #[test]
564    fn slice_range_indices_empty_step_zero() {
565        let r = SliceRange {
566            start: 0,
567            stop: 10,
568            step: 0,
569        };
570        assert!(r.indices().is_empty());
571    }
572
573    #[test]
574    fn slice_range_indices_start_ge_stop() {
575        let r = SliceRange::new(5, 3, 1);
576        assert!(r.indices().is_empty());
577    }
578
579    // ── ViewDescriptor::flat_index ────────────────────────────────────────────
580
581    #[test]
582    fn view_descriptor_flat_index_2d_correct() {
583        // 3×4 row-major: strides [4, 1]
584        let desc = ViewDescriptor {
585            base_offset: 0,
586            shape: vec![3, 4],
587            strides: vec![4, 1],
588        };
589        assert_eq!(desc.flat_index(&[0, 0]), Some(0));
590        assert_eq!(desc.flat_index(&[1, 2]), Some(6));
591        assert_eq!(desc.flat_index(&[2, 3]), Some(11));
592    }
593
594    #[test]
595    fn view_descriptor_flat_index_with_base_offset() {
596        let desc = ViewDescriptor {
597            base_offset: 10,
598            shape: vec![2, 2],
599            strides: vec![2, 1],
600        };
601        assert_eq!(desc.flat_index(&[0, 0]), Some(10));
602        assert_eq!(desc.flat_index(&[1, 1]), Some(13));
603    }
604
605    #[test]
606    fn view_descriptor_flat_index_none_wrong_ndim() {
607        let desc = ViewDescriptor {
608            base_offset: 0,
609            shape: vec![3, 4],
610            strides: vec![4, 1],
611        };
612        assert_eq!(desc.flat_index(&[0]), None);
613        assert_eq!(desc.flat_index(&[0, 0, 0]), None);
614    }
615
616    #[test]
617    fn view_descriptor_flat_index_none_out_of_bounds() {
618        let desc = ViewDescriptor {
619            base_offset: 0,
620            shape: vec![3, 4],
621            strides: vec![4, 1],
622        };
623        assert_eq!(desc.flat_index(&[3, 0]), None); // row out of bounds
624        assert_eq!(desc.flat_index(&[0, 4]), None); // col out of bounds
625    }
626
627    // ── ViewDescriptor::is_contiguous ─────────────────────────────────────────
628
629    #[test]
630    fn view_descriptor_is_contiguous_row_major() {
631        let desc = ViewDescriptor {
632            base_offset: 0,
633            shape: vec![2, 3, 4],
634            strides: vec![12, 4, 1],
635        };
636        assert!(desc.is_contiguous());
637    }
638
639    #[test]
640    fn view_descriptor_is_not_contiguous_non_unit_last_stride() {
641        let desc = ViewDescriptor {
642            base_offset: 0,
643            shape: vec![2, 3],
644            strides: vec![6, 2], // last stride should be 1
645        };
646        assert!(!desc.is_contiguous());
647    }
648
649    #[test]
650    fn view_descriptor_is_not_contiguous_broadcast_zero_stride() {
651        let desc = ViewDescriptor {
652            base_offset: 0,
653            shape: vec![3, 4],
654            strides: vec![0, 1], // broadcast zero stride
655        };
656        assert!(!desc.is_contiguous());
657    }
658
659    // ── TensorSliceView::new ──────────────────────────────────────────────────
660
661    #[test]
662    fn tensor_slice_view_new_correct_strides_1d() {
663        let data = vec![1.0, 2.0, 3.0];
664        let view = TensorSliceView::new(data, vec![3]);
665        assert_eq!(view.descriptor.strides, vec![1]);
666        assert_eq!(view.descriptor.base_offset, 0);
667        assert_eq!(view.stats.total_views_created, 1);
668    }
669
670    #[test]
671    fn tensor_slice_view_new_correct_strides_3d() {
672        let data = vec![0.0; 24];
673        let view = TensorSliceView::new(data, vec![2, 3, 4]);
674        // strides: [12, 4, 1]
675        assert_eq!(view.descriptor.strides, vec![12, 4, 1]);
676    }
677
678    // ── TensorSliceView::get ──────────────────────────────────────────────────
679
680    #[test]
681    fn tensor_slice_view_get_correct_element() {
682        // 2×3 tensor: [[0,1,2],[3,4,5]]
683        let data: Vec<f64> = (0..6).map(|x| x as f64).collect();
684        let view = TensorSliceView::new(data, vec![2, 3]);
685        assert_eq!(view.get(&[0, 0]), Some(0.0));
686        assert_eq!(view.get(&[0, 2]), Some(2.0));
687        assert_eq!(view.get(&[1, 0]), Some(3.0));
688        assert_eq!(view.get(&[1, 2]), Some(5.0));
689    }
690
691    #[test]
692    fn tensor_slice_view_get_none_out_of_bounds() {
693        let data: Vec<f64> = (0..6).map(|x| x as f64).collect();
694        let view = TensorSliceView::new(data, vec![2, 3]);
695        assert_eq!(view.get(&[2, 0]), None);
696        assert_eq!(view.get(&[0, 3]), None);
697    }
698
699    // ── TensorSliceView::slice ────────────────────────────────────────────────
700
701    #[test]
702    fn slice_on_dim0_updates_base_offset() {
703        // 3×4 tensor; slice dim 0: rows 1..3 → base_offset = 1*4 = 4
704        let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
705        let mut view = TensorSliceView::new(data, vec![3, 4]);
706        let desc = view
707            .slice(0, SliceRange::contiguous(1, 3))
708            .expect("test: should succeed");
709        assert_eq!(desc.base_offset, 4);
710        assert_eq!(desc.shape, vec![2, 4]);
711        assert_eq!(desc.strides, vec![4, 1]);
712        assert_eq!(view.stats.total_slices, 1);
713    }
714
715    #[test]
716    fn slice_with_step_multiplies_stride() {
717        // 1-D tensor of 10 elements; slice [0,10) step 3 → stride[0] = 3
718        let data: Vec<f64> = (0..10).map(|x| x as f64).collect();
719        let mut view = TensorSliceView::new(data, vec![10]);
720        let desc = view
721            .slice(0, SliceRange::new(0, 10, 3))
722            .expect("test: should succeed");
723        assert_eq!(desc.strides[0], 3);
724        assert_eq!(desc.shape[0], 4); // ⌈10/3⌉ = 4
725    }
726
727    #[test]
728    fn slice_returns_none_for_invalid_dim() {
729        let data = vec![1.0, 2.0, 3.0];
730        let mut view = TensorSliceView::new(data, vec![3]);
731        assert!(view.slice(1, SliceRange::contiguous(0, 1)).is_none());
732    }
733
734    #[test]
735    fn slice_returns_none_when_start_ge_shape_dim() {
736        let data = vec![1.0, 2.0, 3.0];
737        let mut view = TensorSliceView::new(data, vec![3]);
738        assert!(view.slice(0, SliceRange::contiguous(3, 4)).is_none());
739    }
740
741    #[test]
742    fn slice_stat_incremented() {
743        let data: Vec<f64> = (0..6).map(|x| x as f64).collect();
744        let mut view = TensorSliceView::new(data, vec![2, 3]);
745        let _ = view.slice(1, SliceRange::contiguous(0, 2));
746        let _ = view.slice(0, SliceRange::contiguous(0, 1));
747        assert_eq!(view.stats.total_slices, 2);
748    }
749
750    // ── BroadcastShape::compatible_with ──────────────────────────────────────
751
752    #[test]
753    fn broadcast_compatible_same_shape() {
754        let bs = BroadcastShape { shape: vec![3, 4] };
755        assert!(bs.compatible_with(&[3, 4]));
756    }
757
758    #[test]
759    fn broadcast_compatible_scalar_broadcasts_everywhere() {
760        let bs = BroadcastShape { shape: vec![1] };
761        assert!(bs.compatible_with(&[5, 7]));
762    }
763
764    #[test]
765    fn broadcast_compatible_leading_ones() {
766        // [1, 3] broadcasts to [4, 3]
767        let bs = BroadcastShape { shape: vec![1, 3] };
768        assert!(bs.compatible_with(&[4, 3]));
769    }
770
771    #[test]
772    fn broadcast_incompatible_mismatched_dims() {
773        let bs = BroadcastShape { shape: vec![2, 3] };
774        assert!(!bs.compatible_with(&[2, 4]));
775    }
776
777    // ── TensorSliceView::broadcast_strides ────────────────────────────────────
778
779    #[test]
780    fn broadcast_strides_compatible_returns_strides() {
781        // [1, 3] → [4, 3]: dim 0 has stride 0, dim 1 has stride 1
782        let data = vec![1.0, 2.0, 3.0];
783        let mut view = TensorSliceView::new(data, vec![1, 3]);
784        let strides = view
785            .broadcast_strides(&[4, 3])
786            .expect("test: should succeed");
787        assert_eq!(strides[0], 0); // broadcast dim
788        assert_eq!(strides[1], 1); // preserved dim
789        assert_eq!(view.stats.total_broadcasts, 1);
790    }
791
792    #[test]
793    fn broadcast_strides_incompatible_returns_none() {
794        let data = vec![1.0, 2.0];
795        let mut view = TensorSliceView::new(data, vec![2]);
796        assert!(view.broadcast_strides(&[3]).is_none());
797        assert_eq!(view.stats.total_broadcasts, 0);
798    }
799
800    #[test]
801    fn broadcast_strides_stat_incremented() {
802        let data = vec![0.0; 4];
803        let mut view = TensorSliceView::new(data, vec![1, 4]);
804        let _ = view.broadcast_strides(&[3, 4]);
805        let _ = view.broadcast_strides(&[5, 4]);
806        assert_eq!(view.stats.total_broadcasts, 2);
807    }
808
809    // ── Stats reference ───────────────────────────────────────────────────────
810
811    #[test]
812    fn stats_getter_returns_reference() {
813        let data = vec![0.0; 6];
814        let mut view = TensorSliceView::new(data, vec![2, 3]);
815        let _ = view.slice(0, SliceRange::contiguous(0, 1));
816        assert_eq!(view.stats().total_slices, 1);
817        assert_eq!(view.stats().total_views_created, 1);
818    }
819
820    // ── Combined workflow ─────────────────────────────────────────────────────
821
822    #[test]
823    fn combined_slice_and_get() {
824        // 3×4 tensor: row 2 starts at flat index 8
825        let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
826        let mut view = TensorSliceView::new(data, vec![3, 4]);
827        // Slice rows 1..=2 (i.e. [1,3))
828        let desc = view
829            .slice(0, SliceRange::contiguous(1, 3))
830            .expect("test: should succeed");
831        // Element [0, 2] in the new view corresponds to [1, 2] in original → flat 6
832        let flat = desc.flat_index(&[0, 2]).expect("test: should succeed");
833        assert_eq!(view.data[flat], 6.0);
834    }
835
836    #[test]
837    fn row_major_strides_scalar() {
838        assert_eq!(row_major_strides(&[]), Vec::<usize>::new());
839    }
840
841    #[test]
842    fn row_major_strides_1d() {
843        assert_eq!(row_major_strides(&[5]), vec![1]);
844    }
845
846    #[test]
847    fn row_major_strides_3d() {
848        assert_eq!(row_major_strides(&[2, 3, 4]), vec![12, 4, 1]);
849    }
850}