Skip to main content

ferray_core/array/
view.rs

1// ferray-core: Immutable array view — ArrayView<'a, T, D> (REQ-3)
2
3use ndarray::ShapeBuilder;
4
5use crate::dimension::Dimension;
6use crate::dtype::Element;
7use crate::layout::MemoryLayout;
8
9use super::ArrayFlags;
10use super::owned::Array;
11
12/// An immutable, borrowed view into an existing array's data.
13///
14/// This is a zero-copy slice — no data is cloned. The lifetime `'a`
15/// ties this view to the source array.
16pub struct ArrayView<'a, T: Element, D: Dimension> {
17    pub(crate) inner: ndarray::ArrayView<'a, T, D::NdarrayDim>,
18    pub(crate) dim: D,
19}
20
21impl<'a, T: Element, D: Dimension> ArrayView<'a, T, D> {
22    /// Create from an ndarray view. Crate-internal.
23    pub(crate) fn from_ndarray(inner: ndarray::ArrayView<'a, T, D::NdarrayDim>) -> Self {
24        let dim = D::from_ndarray_dim(&inner.raw_dim());
25        Self { inner, dim }
26    }
27
28    /// Shape as a slice.
29    #[inline]
30    pub fn shape(&self) -> &[usize] {
31        self.inner.shape()
32    }
33
34    /// Number of dimensions.
35    #[inline]
36    pub fn ndim(&self) -> usize {
37        self.dim.ndim()
38    }
39
40    /// Total number of elements.
41    #[inline]
42    pub fn size(&self) -> usize {
43        self.inner.len()
44    }
45
46    /// Whether the view has zero elements.
47    #[inline]
48    pub fn is_empty(&self) -> bool {
49        self.inner.is_empty()
50    }
51
52    /// Strides as a slice.
53    #[inline]
54    pub fn strides(&self) -> &[isize] {
55        self.inner.strides()
56    }
57
58    /// Raw pointer to the first element.
59    #[inline]
60    pub fn as_ptr(&self) -> *const T {
61        self.inner.as_ptr()
62    }
63
64    /// Try to get a contiguous slice.
65    pub fn as_slice(&self) -> Option<&[T]> {
66        self.inner.as_slice()
67    }
68
69    /// Memory layout.
70    pub fn layout(&self) -> MemoryLayout {
71        if self.inner.is_standard_layout() {
72            MemoryLayout::C
73        } else {
74            let shape = self.dim.as_slice();
75            let strides: Vec<isize> = self.inner.strides().to_vec();
76            crate::layout::detect_layout(shape, &strides)
77        }
78    }
79
80    /// Return a reference to the internal dimension descriptor.
81    #[inline]
82    pub fn dim(&self) -> &D {
83        &self.dim
84    }
85
86    /// Convert this view into an owned array by cloning all elements.
87    pub fn to_owned(&self) -> Array<T, D> {
88        Array::from_ndarray(self.inner.to_owned())
89    }
90
91    /// Convert to a flat `Vec<T>` in logical (row-major) order.
92    ///
93    /// Unlike `as_slice()` which requires contiguous memory, this works on
94    /// strided/non-contiguous views by iterating elements in logical order.
95    pub fn to_vec_flat(&self) -> Vec<T> {
96        self.inner.iter().cloned().collect()
97    }
98
99    /// Array flags for this view.
100    pub fn flags(&self) -> ArrayFlags {
101        let layout = self.layout();
102        ArrayFlags {
103            c_contiguous: layout.is_c_contiguous(),
104            f_contiguous: layout.is_f_contiguous(),
105            owndata: false,
106            writeable: false,
107        }
108    }
109}
110
111use crate::dimension::IxDyn;
112
113impl<'a, T: Element> ArrayView<'a, T, IxDyn> {
114    /// Construct a dynamic-rank view from a raw pointer, shape, and strides.
115    ///
116    /// This is the primary escape hatch for crates that need to build views
117    /// with custom stride patterns (e.g., `ferray-stride-tricks`).
118    ///
119    /// # Safety
120    ///
121    /// The caller must ensure:
122    /// - `ptr` is valid for reads for the entire region described by `shape`
123    ///   and `strides`.
124    /// - The lifetime `'a` does not outlive the allocation that `ptr` points
125    ///   into.
126    /// - No mutable reference to the same memory region exists for the
127    ///   duration of `'a`.
128    /// - `strides` are given in units of elements (not bytes).
129    pub unsafe fn from_shape_ptr(ptr: *const T, shape: &[usize], strides: &[usize]) -> Self {
130        let nd_shape = ndarray::IxDyn(shape);
131        let nd_strides = ndarray::IxDyn(strides);
132        let nd_view =
133            unsafe { ndarray::ArrayView::from_shape_ptr(nd_shape.strides(nd_strides), ptr) };
134        Self::from_ndarray(nd_view)
135    }
136}
137
138impl<T: Element, D: Dimension> Clone for ArrayView<'_, T, D> {
139    fn clone(&self) -> Self {
140        Self {
141            inner: self.inner.clone(),
142            dim: self.dim.clone(),
143        }
144    }
145}
146
147// Create an ArrayView from an owned Array
148impl<T: Element, D: Dimension> Array<T, D> {
149    /// Create an immutable view of this array.
150    pub fn view(&self) -> ArrayView<'_, T, D> {
151        ArrayView::from_ndarray(self.inner.view())
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::dimension::Ix2;
159
160    #[test]
161    fn view_from_owned() {
162        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
163            .unwrap();
164        let v = arr.view();
165        assert_eq!(v.shape(), &[2, 3]);
166        assert_eq!(v.size(), 6);
167        assert!(!v.flags().owndata);
168        assert!(!v.flags().writeable);
169    }
170
171    #[test]
172    fn view_shares_data() {
173        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
174            .unwrap();
175        let v = arr.view();
176        // Same pointer
177        assert_eq!(arr.as_ptr(), v.as_ptr());
178    }
179
180    #[test]
181    fn view_to_owned() {
182        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
183            .unwrap();
184        let v = arr.view();
185        let owned = v.to_owned();
186        assert_eq!(owned.shape(), arr.shape());
187        assert_eq!(owned.as_slice().unwrap(), arr.as_slice().unwrap());
188        // But different allocations
189        assert_ne!(owned.as_ptr(), arr.as_ptr());
190    }
191}