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 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    pub fn copy(&self) -> Self {
204        let data = self.as_slice().to_vec();
205        Self {
206            data: Arc::new(data),
207            dim: self.dim.clone(),
208            strides: self.strides.clone(),
209            offset: 0,
210        }
211    }
212
213    /// Array flags.
214    pub fn flags(&self) -> ArrayFlags {
215        let layout = self.layout();
216        ArrayFlags {
217            c_contiguous: layout.is_c_contiguous(),
218            f_contiguous: layout.is_f_contiguous(),
219            owndata: true, // ArcArray conceptually owns (shared ownership)
220            writeable: true,
221        }
222    }
223}
224
225impl<T: Element, D: Dimension> Clone for ArcArray<T, D> {
226    fn clone(&self) -> Self {
227        Self {
228            data: Arc::clone(&self.data),
229            dim: self.dim.clone(),
230            strides: self.strides.clone(),
231            offset: self.offset,
232        }
233    }
234}
235
236impl<T: Element, D: Dimension> From<Array<T, D>> for ArcArray<T, D> {
237    fn from(arr: Array<T, D>) -> Self {
238        Self::from_owned(arr)
239    }
240}
241
242/// Compute C-contiguous strides for a given shape.
243fn compute_c_strides(shape: &[usize]) -> Vec<isize> {
244    let ndim = shape.len();
245    if ndim == 0 {
246        return vec![];
247    }
248    let mut strides = vec![0isize; ndim];
249    strides[ndim - 1] = 1;
250    for i in (0..ndim - 1).rev() {
251        strides[i] = strides[i + 1] * shape[i + 1] as isize;
252    }
253    strides
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::dimension::{Ix1, Ix2};
260
261    #[test]
262    fn arc_from_owned() {
263        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
264        let arc = ArcArray::from_owned(arr);
265        assert_eq!(arc.shape(), &[3]);
266        assert_eq!(arc.as_slice(), &[1.0, 2.0, 3.0]);
267        assert_eq!(arc.ref_count(), 1);
268    }
269
270    #[test]
271    fn arc_clone_shares() {
272        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
273        let arc1 = ArcArray::from_owned(arr);
274        let arc2 = arc1.clone();
275        assert_eq!(arc1.ref_count(), 2);
276        assert_eq!(arc2.ref_count(), 2);
277        assert_eq!(arc1.as_ptr(), arc2.as_ptr());
278    }
279
280    #[test]
281    fn arc_cow_on_mutation() {
282        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
283        let arc1 = ArcArray::from_owned(arr);
284        let mut arc2 = arc1.clone();
285
286        // Before mutation, they share data
287        assert_eq!(arc1.as_ptr(), arc2.as_ptr());
288        assert_eq!(arc1.ref_count(), 2);
289
290        // Mutate arc2 — this triggers CoW
291        arc2.as_slice_mut()[0] = 99.0;
292
293        // After mutation, data is separate
294        assert_ne!(arc1.as_ptr(), arc2.as_ptr());
295        assert_eq!(arc1.as_slice(), &[1.0, 2.0, 3.0]);
296        assert_eq!(arc2.as_slice(), &[99.0, 2.0, 3.0]);
297        assert_eq!(arc1.ref_count(), 1);
298        assert_eq!(arc2.ref_count(), 1);
299    }
300
301    #[test]
302    fn arc_view_sees_old_data_after_cow() {
303        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
304        let mut arc = ArcArray::from_owned(arr);
305        let arc_clone = arc.clone();
306
307        // Create a view from the clone (borrows the shared data)
308        let view = arc_clone.view();
309        assert_eq!(view.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
310
311        // Mutate the original arc — triggers CoW
312        arc.as_slice_mut()[0] = 99.0;
313
314        // The view still sees the old data
315        assert_eq!(view.as_slice().unwrap(), &[1.0, 2.0, 3.0]);
316        // But arc has the new data
317        assert_eq!(arc.as_slice(), &[99.0, 2.0, 3.0]);
318    }
319
320    #[test]
321    fn arc_unique_no_clone() {
322        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
323        let mut arc = ArcArray::from_owned(arr);
324        let ptr_before = arc.as_ptr();
325
326        // Sole owner — mutation should NOT clone
327        arc.as_slice_mut()[0] = 99.0;
328        assert_eq!(arc.as_ptr(), ptr_before);
329        assert_eq!(arc.as_slice(), &[99.0, 2.0, 3.0]);
330    }
331
332    #[test]
333    fn arc_into_owned() {
334        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0; 6]).unwrap();
335        let arc = ArcArray::from_owned(arr);
336        let owned = arc.into_owned();
337        assert_eq!(owned.shape(), &[2, 3]);
338    }
339
340    #[test]
341    fn arc_mapv_inplace() {
342        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
343        let mut arc = ArcArray::from_owned(arr);
344        arc.mapv_inplace(|x| x * 2.0);
345        assert_eq!(arc.as_slice(), &[2.0, 4.0, 6.0]);
346    }
347
348    #[test]
349    fn arc_copy_is_independent() {
350        let arr = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
351        let arc = ArcArray::from_owned(arr);
352        let copy = arc.copy();
353        assert_ne!(arc.as_ptr(), copy.as_ptr());
354        assert_eq!(arc.ref_count(), 1); // original not shared with copy
355        assert_eq!(copy.ref_count(), 1);
356    }
357
358    // ----- non-contiguous source coverage (#129) -----
359
360    #[test]
361    fn arc_from_owned_after_transpose_is_standard_layout() {
362        // Transpose produces an F-contiguous view; going through
363        // `as_standard_layout` in manipulation::transpose materializes
364        // a standard-layout copy, and wrapping that in ArcArray must
365        // yield a usable shared array whose `as_slice` succeeds.
366        use crate::dimension::Ix2;
367        let arr = Array::<f64, Ix2>::from_vec(Ix2::new([2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
368            .unwrap();
369        let transposed = crate::manipulation::transpose(&arr, None).unwrap();
370        assert_eq!(transposed.shape(), &[3, 2]);
371        let arc = ArcArray::<f64, crate::dimension::IxDyn>::from_owned(transposed);
372        // ArcArray::as_slice returns the underlying contiguous buffer
373        // in row-major order; the transposed data should be
374        // [1, 4, 2, 5, 3, 6] after as_standard_layout.
375        assert_eq!(arc.as_slice(), &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
376    }
377
378    #[test]
379    fn arc_from_owned_with_broadcast_to_materializes_data() {
380        // broadcast_to produces a stride-0 view that, after
381        // `as_standard_layout`, becomes a proper contiguous array
382        // with duplicated rows. ArcArray should accept it.
383        use crate::dimension::Ix1;
384        let a = Array::<f64, Ix1>::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
385        let b = crate::manipulation::broadcast_to(&a, &[2, 3]).unwrap();
386        assert_eq!(b.shape(), &[2, 3]);
387        let arc = ArcArray::<f64, crate::dimension::IxDyn>::from_owned(b);
388        // Both rows should be `[1, 2, 3]`.
389        assert_eq!(arc.as_slice(), &[1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
390    }
391}