Skip to main content

ferray_core/array/
arc.rs

1// ferray-core: ArcArray<T, D> — reference-counted with copy-on-write (REQ-3, REQ-4)
2
3use std::sync::Arc;
4
5use crate::dimension::Dimension;
6use crate::dtype::Element;
7use crate::layout::MemoryLayout;
8
9use super::ArrayFlags;
10use super::owned::Array;
11use super::view::ArrayView;
12
13/// A reference-counted N-dimensional array with copy-on-write semantics.
14///
15/// Multiple `ArcArray` instances can share the same underlying buffer.
16/// When a mutation is requested and the reference count is greater than 1,
17/// the buffer is cloned first (copy-on-write). Views derived from an
18/// `ArcArray` observe the data at creation time; subsequent mutations to
19/// the source (which trigger a `CoW` clone) do not affect existing views.
20pub struct ArcArray<T: Element, D: Dimension> {
21    /// Shared data buffer. Using Arc<Vec<T>> + shape/strides for `CoW` support.
22    data: Arc<Vec<T>>,
23    /// Shape of this array.
24    dim: D,
25    /// Strides in element counts.
26    strides: Vec<isize>,
27    /// Offset into the data buffer (for views into sub-regions).
28    offset: usize,
29}
30
31impl<T: Element, D: Dimension> ArcArray<T, D> {
32    /// Create an `ArcArray` from an owned `Array`.
33    ///
34    /// Preserves the original memory layout (C or Fortran) when possible.
35    /// Non-contiguous arrays are converted to C-contiguous.
36    pub fn from_owned(arr: Array<T, D>) -> Self {
37        let dim = arr.dim.clone();
38        let original_strides: Vec<isize> = arr.inner.strides().to_vec();
39
40        if arr.inner.is_standard_layout() {
41            // C-contiguous: take data directly, preserve strides
42            let data = arr.inner.into_raw_vec_and_offset().0;
43            Self {
44                data: Arc::new(data),
45                dim,
46                strides: original_strides,
47                offset: 0,
48            }
49        } else {
50            // Non-standard layout: check if F-contiguous
51            let shape = dim.as_slice();
52            let is_f = crate::layout::detect_layout(shape, &original_strides)
53                == crate::layout::MemoryLayout::Fortran;
54
55            if is_f {
56                // Fortran-contiguous: data is contiguous, just different stride order.
57                // Extract the raw vec — for F-order the data is contiguous in memory.
58                let data = arr.inner.into_raw_vec_and_offset().0;
59                Self {
60                    data: Arc::new(data),
61                    dim,
62                    strides: original_strides,
63                    offset: 0,
64                }
65            } else {
66                // Truly non-contiguous (e.g., strided slice): make C-contiguous
67                let contiguous = arr.inner.as_standard_layout().into_owned();
68                let data = contiguous.into_raw_vec_and_offset().0;
69                let strides = compute_c_strides(shape);
70                Self {
71                    data: Arc::new(data),
72                    dim,
73                    strides,
74                    offset: 0,
75                }
76            }
77        }
78    }
79
80    /// Shape as a slice.
81    #[inline]
82    pub fn shape(&self) -> &[usize] {
83        self.dim.as_slice()
84    }
85
86    /// Number of dimensions.
87    #[inline]
88    pub fn ndim(&self) -> usize {
89        self.dim.ndim()
90    }
91
92    /// Total number of elements.
93    #[inline]
94    pub fn size(&self) -> usize {
95        self.dim.size()
96    }
97
98    /// Whether the array has zero elements.
99    #[inline]
100    pub fn is_empty(&self) -> bool {
101        self.size() == 0
102    }
103
104    /// Strides as a slice.
105    #[inline]
106    pub fn strides(&self) -> &[isize] {
107        &self.strides
108    }
109
110    /// Memory layout.
111    pub fn layout(&self) -> MemoryLayout {
112        crate::layout::detect_layout(self.dim.as_slice(), &self.strides)
113    }
114
115    /// Return a reference to the dimension descriptor.
116    #[inline]
117    pub const fn dim(&self) -> &D {
118        &self.dim
119    }
120
121    /// Number of shared references to the underlying buffer.
122    pub fn ref_count(&self) -> usize {
123        Arc::strong_count(&self.data)
124    }
125
126    /// Whether this is the sole owner of the data (refcount == 1).
127    pub fn is_unique(&self) -> bool {
128        Arc::strong_count(&self.data) == 1
129    }
130
131    /// Get a slice of the data for this array.
132    pub fn as_slice(&self) -> &[T] {
133        &self.data[self.offset..self.offset + self.size()]
134    }
135
136    /// Raw pointer to the first element.
137    #[inline]
138    pub fn as_ptr(&self) -> *const T {
139        self.as_slice().as_ptr()
140    }
141
142    /// Create an immutable view of the data.
143    ///
144    /// The view borrows from this `ArcArray` and will see the data as it
145    /// exists at creation time. If the `ArcArray` is later mutated (triggering
146    /// a `CoW` clone), the view continues to see the old data.
147    pub fn view(&self) -> ArrayView<'_, T, D> {
148        let nd_dim = self.dim.to_ndarray_dim();
149        let slice = self.as_slice();
150        let nd_view = ndarray::ArrayView::from_shape(nd_dim, slice)
151            .expect("ArcArray data should be consistent with shape");
152        ArrayView::from_ndarray(nd_view)
153    }
154
155    /// Ensure exclusive ownership of the data buffer (`CoW`).
156    ///
157    /// If the reference count is > 1, this clones the buffer so that
158    /// mutations will not affect other holders.
159    fn make_unique(&mut self) {
160        if Arc::strong_count(&self.data) > 1 {
161            let slice = &self.data[self.offset..self.offset + self.size()];
162            self.data = Arc::new(slice.to_vec());
163            self.offset = 0;
164        }
165    }
166
167    /// Get a mutable slice of the data, performing a `CoW` clone if necessary.
168    pub fn as_slice_mut(&mut self) -> &mut [T] {
169        self.make_unique();
170        let size = self.size();
171        let offset = self.offset;
172        Arc::get_mut(&mut self.data)
173            .expect("make_unique should ensure refcount == 1")
174            .get_mut(offset..offset + size)
175            .expect("offset + size should be in bounds")
176    }
177
178    /// Apply a function to each element, performing `CoW` if needed.
179    pub fn mapv_inplace(&mut self, f: impl Fn(T) -> T) {
180        self.make_unique();
181        let size = self.size();
182        let offset = self.offset;
183        let data = Arc::get_mut(&mut self.data).expect("unique after make_unique");
184        for elem in &mut data[offset..offset + size] {
185            *elem = f(elem.clone());
186        }
187    }
188
189    /// Convert to an owned `Array`, cloning if shared.
190    pub fn into_owned(self) -> Array<T, D> {
191        let data: Vec<T> = if self.offset == 0 && self.data.len() == self.size() {
192            match Arc::try_unwrap(self.data) {
193                Ok(v) => v,
194                Err(arc) => arc[..].to_vec(),
195            }
196        } else {
197            self.data[self.offset..self.offset + self.size()].to_vec()
198        };
199        Array::from_vec(self.dim, data).expect("data should match shape")
200    }
201
202    /// Deep copy — always creates a new independent buffer.
203    #[must_use]
204    pub fn copy(&self) -> Self {
205        let data = self.as_slice().to_vec();
206        Self {
207            data: Arc::new(data),
208            dim: self.dim.clone(),
209            strides: self.strides.clone(),
210            offset: 0,
211        }
212    }
213
214    /// Array flags.
215    pub fn flags(&self) -> ArrayFlags {
216        let layout = self.layout();
217        ArrayFlags {
218            c_contiguous: layout.is_c_contiguous(),
219            f_contiguous: layout.is_f_contiguous(),
220            owndata: true, // ArcArray conceptually owns (shared ownership)
221            writeable: true,
222            // ArcArray wraps an Arc<ndarray::ArrayD<T>> which goes
223            // through the Vec<T> allocator path — always aligned (#345).
224            aligned: true,
225        }
226    }
227}
228
229impl<T: Element, D: Dimension> Clone for ArcArray<T, D> {
230    fn clone(&self) -> Self {
231        Self {
232            data: Arc::clone(&self.data),
233            dim: self.dim.clone(),
234            strides: self.strides.clone(),
235            offset: self.offset,
236        }
237    }
238}
239
240impl<T: Element, D: Dimension> From<Array<T, D>> for ArcArray<T, D> {
241    fn from(arr: Array<T, D>) -> Self {
242        Self::from_owned(arr)
243    }
244}
245
246/// Compute C-contiguous strides for a given shape.
247fn compute_c_strides(shape: &[usize]) -> Vec<isize> {
248    let ndim = shape.len();
249    if ndim == 0 {
250        return vec![];
251    }
252    let mut strides = vec![0isize; ndim];
253    strides[ndim - 1] = 1;
254    for i in (0..ndim - 1).rev() {
255        strides[i] = strides[i + 1] * shape[i + 1] as isize;
256    }
257    strides
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::dimension::{Ix1, Ix2};
264
265    #[test]
266    fn arc_from_owned() {
267        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
268        let arc = ArcArray::from_owned(arr);
269        assert_eq!(arc.shape(), &[3]);
270        assert_eq!(arc.as_slice(), &[1.0, 2.0, 3.0]);
271        assert_eq!(arc.ref_count(), 1);
272    }
273
274    #[test]
275    fn arc_clone_shares() {
276        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
277        let arc1 = ArcArray::from_owned(arr);
278        let arc2 = arc1.clone();
279        assert_eq!(arc1.ref_count(), 2);
280        assert_eq!(arc2.ref_count(), 2);
281        assert_eq!(arc1.as_ptr(), arc2.as_ptr());
282    }
283
284    #[test]
285    fn arc_cow_on_mutation() {
286        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
287        let arc1 = ArcArray::from_owned(arr);
288        let mut arc2 = arc1.clone();
289
290        // Before mutation, they share data
291        assert_eq!(arc1.as_ptr(), arc2.as_ptr());
292        assert_eq!(arc1.ref_count(), 2);
293
294        // Mutate arc2 — this triggers CoW
295        arc2.as_slice_mut()[0] = 99.0;
296
297        // After mutation, data is separate
298        assert_ne!(arc1.as_ptr(), arc2.as_ptr());
299        assert_eq!(arc1.as_slice(), &[1.0, 2.0, 3.0]);
300        assert_eq!(arc2.as_slice(), &[99.0, 2.0, 3.0]);
301        assert_eq!(arc1.ref_count(), 1);
302        assert_eq!(arc2.ref_count(), 1);
303    }
304
305    #[test]
306    fn arc_view_sees_old_data_after_cow() {
307        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
308        let mut arc = ArcArray::from_owned(arr);
309        let arc_clone = arc.clone();
310
311        // Create a view from the clone (borrows the shared data)
312        let view = arc_clone.view();
313        assert_eq!(view.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
314
315        // Mutate the original arc — triggers CoW
316        arc.as_slice_mut()[0] = 99.0;
317
318        // The view still sees the old data
319        assert_eq!(view.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
320        // But arc has the new data
321        assert_eq!(arc.as_slice(), &[99.0, 2.0, 3.0]);
322    }
323
324    #[test]
325    fn arc_unique_no_clone() {
326        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
327        let mut arc = ArcArray::from_owned(arr);
328        let ptr_before = arc.as_ptr();
329
330        // Sole owner — mutation should NOT clone
331        arc.as_slice_mut()[0] = 99.0;
332        assert_eq!(arc.as_ptr(), ptr_before);
333        assert_eq!(arc.as_slice(), &[99.0, 2.0, 3.0]);
334    }
335
336    #[test]
337    fn arc_into_owned() {
338        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0; 6]).unwrap();
339        let arc = ArcArray::from_owned(arr);
340        let owned = arc.into_owned();
341        assert_eq!(owned.shape(), &[2, 3]);
342    }
343
344    #[test]
345    fn arc_mapv_inplace() {
346        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
347        let mut arc = ArcArray::from_owned(arr);
348        arc.mapv_inplace(|x| x * 2.0);
349        assert_eq!(arc.as_slice(), &[2.0, 4.0, 6.0]);
350    }
351
352    #[test]
353    fn arc_copy_is_independent() {
354        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
355        let arc = ArcArray::from_owned(arr);
356        let copy = arc.copy();
357        assert_ne!(arc.as_ptr(), copy.as_ptr());
358        assert_eq!(arc.ref_count(), 1); // original not shared with copy
359        assert_eq!(copy.ref_count(), 1);
360    }
361
362    // ----- non-contiguous source coverage (#129) -----
363
364    #[test]
365    fn arc_from_owned_after_transpose_is_standard_layout() {
366        // Transpose produces an F-contiguous view; going through
367        // `as_standard_layout` in manipulation::transpose materializes
368        // a standard-layout copy, and wrapping that in ArcArray must
369        // yield a usable shared array whose `as_slice` succeeds.
370        use crate::dimension::Ix2;
371        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
372            .unwrap();
373        let transposed = crate::manipulation::transpose(&arr, None).unwrap();
374        assert_eq!(transposed.shape(), &[3, 2]);
375        let arc = ArcArray::<f64, crate::dimension::IxDyn>::from_owned(transposed);
376        // ArcArray::as_slice returns the underlying contiguous buffer
377        // in row-major order; the transposed data should be
378        // [1, 4, 2, 5, 3, 6] after as_standard_layout.
379        assert_eq!(arc.as_slice(), &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
380    }
381
382    #[test]
383    fn arc_from_owned_with_broadcast_to_materializes_data() {
384        // broadcast_to produces a stride-0 view that, after
385        // `as_standard_layout`, becomes a proper contiguous array
386        // with duplicated rows. ArcArray should accept it.
387        use crate::dimension::Ix1;
388        let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
389        let b = crate::manipulation::broadcast_to(&a, &[2, 3]).unwrap();
390        assert_eq!(b.shape(), &[2, 3]);
391        let arc = ArcArray::<f64, crate::dimension::IxDyn>::from_owned(b);
392        // Both rows should be `[1, 2, 3]`.
393        assert_eq!(arc.as_slice(), &[1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
394    }
395}