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