Skip to main content

miniextendr_api/
rarray.rs

1//! N-dimensional R arrays with const generic dimension count.
2//!
3//! This module provides [`RArray<T, NDIM>`], a wrapper around R arrays that
4//! tracks the number of dimensions at compile time.
5//!
6//! # Type Aliases
7//!
8//! | Alias | Type | R Equivalent |
9//! |-------|------|--------------|
10//! | [`RVector<T>`] | `RArray<T, 1>` | `vector` (with dim) |
11//! | [`RMatrix<T>`] | `RArray<T, 2>` | `matrix` |
12//! | [`RArray3D<T>`] | `RArray<T, 3>` | `array(..., dim=c(a,b,c))` |
13//!
14//! # Memory Layout
15//!
16//! R arrays are stored in **column-major** (Fortran) order. For a 2×3 matrix:
17//!
18//! ```text
19//! Logical layout:     Memory layout:
20//! [0,0] [0,1] [0,2]   [0,0] [1,0] [0,1] [1,1] [0,2] [1,2]
21//! [1,0] [1,1] [1,2]
22//! ```
23//!
24//! The [`get`][RArray::get] method handles index translation automatically.
25//!
26//! # Thread Safety
27//!
28//! **`RArray` is `!Send` and `!Sync`** - it cannot be transferred to or accessed
29//! from other threads. This is because the underlying R APIs (`DATAPTR_RO`, etc.)
30//! must be called on the R main thread.
31//!
32//! For functions that use `RArray`/`RMatrix` parameters, you must use
33//! `#[miniextendr(unsafe(main_thread))]` to ensure execution on the main thread.
34//!
35//! For worker-thread usability, use [`to_vec()`][RArray::to_vec] to copy data
36//! on the main thread, then pass the owned `Vec` to worker threads.
37//!
38//! # Performance
39//!
40//! For best performance, prefer slice-based and column-based access over per-element
41//! indexing:
42//!
43//! | Method | Speed | Use Case |
44//! |--------|-------|----------|
45//! | [`as_slice()`][RArray::as_slice] | Fastest | Full-buffer iteration, SIMD |
46//! | [`column()`][RMatrix::column] | Fast | Per-column operations (matrices) |
47//! | [`column_mut()`][RMatrix::column_mut] | Fast | Per-column mutation |
48//! | [`get()`][RArray::get] / [`get_rc()`][RMatrix::get_rc] | Slower | Single-element access |
49//!
50//! **Why?** Per-element methods like `get()` perform index translation and bounds
51//! checks on every call. For tight loops, this overhead dominates.
52//!
53//! ```ignore
54//! // Slow: per-element access
55//! for row in 0..nrow {
56//!     for col in 0..ncol {
57//!         let val = unsafe { matrix.get_rc(row, col) };
58//!     }
59//! }
60//!
61//! // Fast: slice-based iteration
62//! for val in unsafe { matrix.as_slice() } {
63//!     // ...
64//! }
65//!
66//! // Fast: column-wise iteration (columns are contiguous in R)
67//! for col in 0..ncol {
68//!     for val in unsafe { matrix.column(col) } {
69//!         // ...
70//!     }
71//! }
72//! ```
73//!
74//! # Example
75//!
76//! ```ignore
77//! use miniextendr_api::rarray::{RMatrix, RArray};
78//!
79//! // Must run on main thread due to RMatrix parameter
80//! #[miniextendr(unsafe(main_thread))]
81//! fn matrix_sum(m: RMatrix<f64>) -> f64 {
82//!     unsafe { m.as_slice().iter().sum() }
83//! }
84//! ```
85
86use crate::ffi::{self, RNativeType, SEXP, SEXPTYPE, SexpExt};
87use crate::from_r::{SexpError, SexpLengthError, SexpTypeError, TryFromSexp};
88use crate::into_r::IntoR;
89use core::marker::PhantomData;
90
91// region: Type aliases
92
93/// A 1-dimensional R vector with explicit dim attribute.
94pub type RVector<T> = RArray<T, 1>;
95
96/// A 2-dimensional R matrix.
97pub type RMatrix<T> = RArray<T, 2>;
98
99/// A 3-dimensional R array.
100pub type RArray3D<T> = RArray<T, 3>;
101// endregion
102
103// region: RArray
104
105/// An N-dimensional R array.
106///
107/// This type wraps an R array SEXP. The dimension count `NDIM` is tracked
108/// at compile time, but dimension sizes are read from the R object.
109///
110/// # Type Parameters
111///
112/// - `T`: The element type, must implement [`RNativeType`]
113/// - `NDIM`: The number of dimensions (compile-time constant)
114///
115/// # Thread Safety
116///
117/// This type is `!Send` and `!Sync` because its methods require access to
118/// R APIs that must run on the R main thread.
119#[derive(Clone, Copy)]
120#[repr(transparent)]
121pub struct RArray<T, const NDIM: usize> {
122    sexp: SEXP,
123    // PhantomData<*const T> keeps T in the type AND makes this !Send + !Sync
124    _marker: PhantomData<*const T>,
125}
126// endregion
127
128// region: Basic methods (no T bounds - available for all RArray types)
129
130impl<T, const NDIM: usize> RArray<T, NDIM> {
131    /// Create an RArray from a SEXP without validation.
132    ///
133    /// # Safety
134    ///
135    /// - The SEXP must be protected from GC
136    /// - The SEXP must have the correct type for `T`
137    /// - The SEXP must have exactly `NDIM` dimensions
138    #[inline]
139    pub const unsafe fn from_sexp_unchecked(sexp: SEXP) -> Self {
140        Self {
141            sexp,
142            _marker: PhantomData,
143        }
144    }
145
146    /// Get the underlying SEXP.
147    #[inline]
148    pub const fn as_sexp(&self) -> SEXP {
149        self.sexp
150    }
151
152    /// Consume and return the underlying SEXP.
153    #[inline]
154    pub fn into_inner(self) -> SEXP {
155        self.sexp
156    }
157
158    /// Get the dimensions as an array.
159    ///
160    /// # Safety
161    ///
162    /// The SEXP must be valid.
163    #[inline]
164    pub unsafe fn dims(&self) -> [usize; NDIM] {
165        unsafe { get_dims::<NDIM>(self.sexp) }
166    }
167
168    /// Get a specific dimension size.
169    ///
170    /// # Safety
171    ///
172    /// The SEXP must be valid.
173    ///
174    /// # Panics
175    ///
176    /// Panics if `dim >= NDIM`.
177    #[inline]
178    pub unsafe fn dim(&self, dim: usize) -> usize {
179        assert!(dim < NDIM, "dimension index out of bounds");
180        unsafe { self.dims()[dim] }
181    }
182
183    /// Get the total number of elements.
184    #[inline]
185    pub fn len(&self) -> usize {
186        self.sexp.len()
187    }
188
189    /// Check if the array is empty.
190    #[inline]
191    pub fn is_empty(&self) -> bool {
192        self.len() == 0
193    }
194
195    /// Convert N-dimensional indices to linear index (column-major).
196    ///
197    /// # Safety
198    ///
199    /// The SEXP must be valid (needed to read dims).
200    ///
201    /// # Panics
202    ///
203    /// Panics if any index is out of bounds.
204    #[inline]
205    pub unsafe fn linear_index(&self, indices: [usize; NDIM]) -> usize {
206        let dims = unsafe { self.dims() };
207        let mut linear = 0;
208        let mut stride = 1;
209        for i in 0..NDIM {
210            assert!(
211                indices[i] < dims[i],
212                "index {} out of bounds for dimension {} (size {})",
213                indices[i],
214                i,
215                dims[i]
216            );
217            linear += indices[i] * stride;
218            stride *= dims[i];
219        }
220        linear
221    }
222}
223// endregion
224
225// region: Native type methods (T: RNativeType - slice access, mutation, etc.)
226
227impl<T: RNativeType, const NDIM: usize> RArray<T, NDIM> {
228    /// Create an RArray from a SEXP, validating type and dimensions.
229    ///
230    /// # Safety
231    ///
232    /// The SEXP must be protected from GC for the lifetime of the returned RArray.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if:
237    /// - The SEXP type doesn't match `T::SEXP_TYPE`
238    /// - The dim attribute has wrong number of dimensions
239    #[inline]
240    pub unsafe fn from_sexp(sexp: SEXP) -> Result<Self, SexpError> {
241        // Type check
242        let actual = sexp.type_of();
243        if actual != T::SEXP_TYPE {
244            return Err(SexpTypeError {
245                expected: T::SEXP_TYPE,
246                actual,
247            }
248            .into());
249        }
250
251        // Validate dimensions count
252        let ndim = get_ndim(sexp);
253        if ndim != NDIM {
254            return Err(SexpLengthError {
255                expected: NDIM,
256                actual: ndim,
257            }
258            .into());
259        }
260
261        Ok(Self {
262            sexp,
263            _marker: PhantomData,
264        })
265    }
266
267    /// Get the data as a slice (column-major order).
268    ///
269    /// # Safety
270    ///
271    /// The SEXP must be protected and valid.
272    #[inline]
273    pub unsafe fn as_slice(&self) -> &[T] {
274        unsafe { self.sexp.as_slice() }
275    }
276
277    /// Get the data as a mutable slice (column-major order).
278    ///
279    /// # Safety
280    ///
281    /// - The SEXP must be protected and valid
282    /// - No other references to the data may exist
283    #[inline]
284    pub unsafe fn as_slice_mut(&mut self) -> &mut [T] {
285        unsafe {
286            let ptr = T::dataptr_mut(self.sexp);
287            crate::from_r::r_slice_mut(ptr, self.len())
288        }
289    }
290
291    /// Copy array data to an owned `Vec<T>`.
292    ///
293    /// This method copies the data, making it safe to use in worker threads
294    /// or pass to parallel computation. The copy is performed on the current
295    /// thread (which must be the R main thread).
296    ///
297    /// # Safety
298    ///
299    /// The SEXP must be protected and valid.
300    ///
301    /// # Example
302    ///
303    /// ```ignore
304    /// use miniextendr_api::rarray::RMatrix;
305    ///
306    /// #[miniextendr(unsafe(main_thread))]
307    /// fn process_matrix(m: RMatrix<f64>) -> f64 {
308    ///     // Copy data - Vec<f64> is Send and can be used in worker threads
309    ///     let data: Vec<f64> = unsafe { m.to_vec() };
310    ///     // Now data can be passed to parallel computation
311    ///     data.iter().sum()
312    /// }
313    /// ```
314    #[inline]
315    pub unsafe fn to_vec(&self) -> Vec<T>
316    where
317        T: Copy,
318    {
319        unsafe { self.as_slice().to_vec() }
320    }
321
322    /// Get an element by N-dimensional indices.
323    ///
324    /// # Safety
325    ///
326    /// The SEXP must be protected and valid.
327    ///
328    /// # Panics
329    ///
330    /// Panics if any index is out of bounds.
331    #[inline]
332    pub unsafe fn get(&self, indices: [usize; NDIM]) -> T
333    where
334        T: Copy,
335    {
336        let idx = unsafe { self.linear_index(indices) };
337        unsafe { *self.as_slice().get_unchecked(idx) }
338    }
339
340    /// Set an element by N-dimensional indices.
341    ///
342    /// # Safety
343    ///
344    /// - The SEXP must be protected and valid
345    /// - No other references to the data may exist
346    ///
347    /// # Panics
348    ///
349    /// Panics if any index is out of bounds.
350    #[inline]
351    pub unsafe fn set(&mut self, indices: [usize; NDIM], value: T)
352    where
353        T: Copy,
354    {
355        let idx = unsafe { self.linear_index(indices) };
356        unsafe {
357            *self.as_slice_mut().get_unchecked_mut(idx) = value;
358        }
359    }
360}
361// endregion
362
363// region: Matrix-specific methods (NDIM = 2)
364
365impl<T: RNativeType> RMatrix<T> {
366    /// Get the number of rows.
367    ///
368    /// # Safety
369    ///
370    /// The SEXP must be valid.
371    #[inline]
372    pub unsafe fn nrow(&self) -> usize {
373        unsafe { self.dim(0) }
374    }
375
376    /// Get the number of columns.
377    ///
378    /// # Safety
379    ///
380    /// The SEXP must be valid.
381    #[inline]
382    pub unsafe fn ncol(&self) -> usize {
383        unsafe { self.dim(1) }
384    }
385
386    /// Get an element by row and column.
387    ///
388    /// # Safety
389    ///
390    /// The SEXP must be protected and valid.
391    #[inline]
392    pub unsafe fn get_rc(&self, row: usize, col: usize) -> T
393    where
394        T: Copy,
395    {
396        unsafe { self.get([row, col]) }
397    }
398
399    /// Set an element by row and column.
400    ///
401    /// # Safety
402    ///
403    /// - The SEXP must be protected and valid
404    /// - No other references to the data may exist
405    #[inline]
406    pub unsafe fn set_rc(&mut self, row: usize, col: usize, value: T)
407    where
408        T: Copy,
409    {
410        unsafe { self.set([row, col], value) }
411    }
412
413    /// Get a column as a slice.
414    ///
415    /// # Safety
416    ///
417    /// The SEXP must be protected and valid.
418    #[inline]
419    pub unsafe fn column(&self, col: usize) -> &[T] {
420        let nrow = unsafe { self.nrow() };
421        let ncol = unsafe { self.ncol() };
422        assert!(col < ncol, "column index out of bounds");
423        let start = col * nrow;
424        unsafe { &self.as_slice()[start..start + nrow] }
425    }
426
427    /// Get a mutable column as a slice.
428    ///
429    /// Columns are contiguous in R's column-major layout, so this returns
430    /// a proper `&mut [T]` without any striding.
431    ///
432    /// # Safety
433    ///
434    /// The SEXP must be protected and valid.
435    ///
436    /// # Panics
437    ///
438    /// Panics if `col >= ncol`.
439    #[inline]
440    pub unsafe fn column_mut(&mut self, col: usize) -> &mut [T] {
441        let nrow = unsafe { self.nrow() };
442        let ncol = unsafe { self.ncol() };
443        assert!(col < ncol, "column index out of bounds");
444        let start = col * nrow;
445        unsafe { &mut self.as_slice_mut()[start..start + nrow] }
446    }
447}
448// endregion
449
450// region: Attribute access (equivalent to R's GET_*/SET_* macros)
451
452impl<T: RNativeType, const NDIM: usize> RArray<T, NDIM> {
453    // region: Attribute getters
454
455    /// Get an arbitrary attribute by symbol (unchecked internal helper).
456    ///
457    /// # Safety
458    ///
459    /// - The SEXP must be valid.
460    /// - `what` must be a valid symbol SEXP.
461    #[inline]
462    fn get_attr_opt(&self, name: SEXP) -> Option<SEXP> {
463        let attr = self.sexp.get_attr(name);
464        if attr.is_nil() { None } else { Some(attr) }
465    }
466
467    /// Get the `names` attribute if present.
468    ///
469    /// Equivalent to R's `GET_NAMES(x)`.
470    ///
471    /// # Safety
472    ///
473    /// The SEXP must be valid.
474    #[inline]
475    pub unsafe fn get_names(&self) -> Option<SEXP> {
476        // Safety: R_NamesSymbol is a known symbol
477        self.get_attr_opt(SEXP::names_symbol())
478    }
479
480    /// Get the `class` attribute if present.
481    ///
482    /// Equivalent to R's `GET_CLASS(x)`.
483    ///
484    /// # Safety
485    ///
486    /// The SEXP must be valid.
487    #[inline]
488    pub unsafe fn get_class(&self) -> Option<SEXP> {
489        // Safety: R_ClassSymbol is a known symbol
490        self.get_attr_opt(SEXP::class_symbol())
491    }
492
493    /// Get the `dimnames` attribute if present.
494    ///
495    /// Equivalent to R's `GET_DIMNAMES(x)`.
496    ///
497    /// # Safety
498    ///
499    /// The SEXP must be valid.
500    #[inline]
501    pub unsafe fn get_dimnames(&self) -> Option<SEXP> {
502        // Safety: R_DimNamesSymbol is a known symbol
503        self.get_attr_opt(SEXP::dimnames_symbol())
504    }
505
506    /// Get row names from the `dimnames` attribute.
507    ///
508    /// Equivalent to R's `GET_ROWNAMES(x)` / `Rf_GetRowNames(x)`.
509    ///
510    /// # Safety
511    ///
512    /// The SEXP must be valid.
513    #[inline]
514    pub unsafe fn get_rownames(&self) -> Option<SEXP> {
515        unsafe {
516            let rownames = ffi::Rf_GetRowNames(self.sexp);
517            if rownames.is_nil() {
518                None
519            } else {
520                Some(rownames)
521            }
522        }
523    }
524
525    /// Get column names from the `dimnames` attribute.
526    ///
527    /// Equivalent to R's `GET_COLNAMES(x)` / `Rf_GetColNames(x)`.
528    ///
529    /// # Safety
530    ///
531    /// The SEXP must be valid.
532    #[inline]
533    pub unsafe fn get_colnames(&self) -> Option<SEXP> {
534        unsafe {
535            let dimnames = self.sexp.get_dimnames();
536            if dimnames.is_nil() {
537                return None;
538            }
539            let colnames = ffi::Rf_GetColNames(dimnames);
540            if colnames.is_nil() {
541                None
542            } else {
543                Some(colnames)
544            }
545        }
546    }
547    // endregion
548
549    // region: Attribute setters
550
551    /// Set an arbitrary attribute by symbol (unchecked internal helper).
552    ///
553    /// # Safety
554    ///
555    /// Set the `names` attribute.
556    ///
557    /// Equivalent to R's `SET_NAMES(x, n)`.
558    ///
559    /// # Safety
560    ///
561    /// The SEXP must be valid and not shared.
562    #[inline]
563    pub unsafe fn set_names(&mut self, names: SEXP) {
564        self.sexp.set_names(names);
565    }
566
567    /// Set the `class` attribute.
568    ///
569    /// Equivalent to R's `SET_CLASS(x, n)`.
570    ///
571    /// # Safety
572    ///
573    /// The SEXP must be valid and not shared.
574    #[inline]
575    pub unsafe fn set_class(&mut self, class: SEXP) {
576        self.sexp.set_class(class);
577    }
578
579    /// Set the `dimnames` attribute.
580    ///
581    /// Equivalent to R's `SET_DIMNAMES(x, n)`.
582    ///
583    /// # Safety
584    ///
585    /// The SEXP must be valid and not shared.
586    #[inline]
587    pub unsafe fn set_dimnames(&mut self, dimnames: SEXP) {
588        self.sexp.set_dimnames(dimnames);
589    }
590    // endregion
591}
592// endregion
593
594// region: Construction helpers
595
596impl<T: RNativeType, const NDIM: usize> RArray<T, NDIM> {
597    /// Allocate a new R array with the given dimensions.
598    ///
599    /// The array is allocated. The closure receives a mutable slice to
600    /// initialize the data.
601    ///
602    /// # Safety
603    ///
604    /// Must be called from the R main thread (or via routed FFI).
605    /// The returned RArray holds an unprotected SEXP - caller must protect.
606    ///
607    /// # Example
608    ///
609    /// ```ignore
610    /// let matrix = unsafe {
611    ///     RMatrix::<f64>::new([3, 4], |slice| {
612    ///         for (i, v) in slice.iter_mut().enumerate() {
613    ///             *v = i as f64;
614    ///         }
615    ///     })
616    /// };
617    /// ```
618    pub unsafe fn new<F>(dims: [usize; NDIM], init: F) -> Self
619    where
620        F: FnOnce(&mut [T]),
621    {
622        let total_len: usize = dims
623            .iter()
624            .try_fold(1usize, |acc, &d| acc.checked_mul(d))
625            .expect("array total length overflows usize");
626
627        assert!(
628            total_len <= ffi::R_xlen_t::MAX as usize,
629            "array total length {total_len} exceeds R_xlen_t::MAX"
630        );
631
632        // Allocate the vector
633        let sexp = unsafe { ffi::Rf_allocVector(T::SEXP_TYPE, total_len as ffi::R_xlen_t) };
634
635        // Set dimensions
636        unsafe { set_dims::<NDIM>(sexp, &dims) };
637
638        // Initialize data
639        let ptr = unsafe { T::dataptr_mut(sexp) };
640        let slice = unsafe { crate::from_r::r_slice_mut(ptr, total_len) };
641        init(slice);
642
643        Self {
644            sexp,
645            _marker: PhantomData,
646        }
647    }
648
649    /// Allocate a new R array filled with zeros.
650    ///
651    /// # Safety
652    ///
653    /// Must be called from the R main thread (or via routed FFI).
654    /// The returned RArray holds an unprotected SEXP - caller must protect.
655    pub unsafe fn zeros(dims: [usize; NDIM]) -> Self
656    where
657        T: Default + Copy,
658    {
659        unsafe {
660            Self::new(dims, |slice| {
661                slice.fill(T::default());
662            })
663        }
664    }
665}
666// endregion
667
668// region: TryFromSexp implementation
669
670impl<T: RNativeType, const NDIM: usize> TryFromSexp for RArray<T, NDIM> {
671    type Error = SexpError;
672
673    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
674        unsafe { Self::from_sexp(sexp) }
675    }
676
677    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
678        unsafe { Self::from_sexp(sexp) }
679    }
680}
681// endregion
682
683// region: Direct coercion TryFromSexp implementations
684//
685// These implement TryFromSexp for RArray<T, NDIM> where T is not an R native type
686// but can be coerced from one. The RArray wraps the source SEXP directly (zero-copy).
687// Note: as_slice() is not available for coerced types - use to_vec_coerced() instead.
688
689use crate::coerce::TryCoerce;
690use crate::ffi::RLogical;
691
692/// Helper to validate all elements can be coerced.
693fn validate_coercion<S, T>(slice: &[S]) -> Result<(), SexpError>
694where
695    S: Copy + TryCoerce<T>,
696    <S as TryCoerce<T>>::Error: std::fmt::Debug,
697{
698    for &val in slice {
699        val.try_coerce()
700            .map_err(|e| SexpError::InvalidValue(format!("{e:?}")))?;
701    }
702    Ok(())
703}
704
705/// Implement `TryFromSexp for RArray<$target, NDIM>` by reading R's native `$source` type.
706///
707/// The RArray wraps the source SEXP directly. Use `to_vec_coerced()` to get coerced data.
708macro_rules! impl_rarray_try_from_sexp_coerce {
709    ($source:ty => $target:ty) => {
710        impl<const NDIM: usize> TryFromSexp for RArray<$target, NDIM> {
711            type Error = SexpError;
712
713            fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
714                // Check source type
715                let actual = sexp.type_of();
716                if actual != <$source as RNativeType>::SEXP_TYPE {
717                    return Err(SexpTypeError {
718                        expected: <$source as RNativeType>::SEXP_TYPE,
719                        actual,
720                    }
721                    .into());
722                }
723
724                // Validate dimensions count
725                let ndim = get_ndim(sexp);
726                if ndim != NDIM {
727                    return Err(SexpLengthError {
728                        expected: NDIM,
729                        actual: ndim,
730                    }
731                    .into());
732                }
733
734                // Validate all elements can be coerced
735                let slice: &[$source] = unsafe { sexp.as_slice() };
736                validate_coercion::<$source, $target>(slice)?;
737
738                Ok(Self {
739                    sexp,
740                    _marker: PhantomData,
741                })
742            }
743
744            unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
745                Self::try_from_sexp(sexp)
746            }
747        }
748
749        impl<const NDIM: usize> RArray<$target, NDIM> {
750            /// Copy array data to an owned `Vec`, coercing from the R native type.
751            ///
752            /// # Safety
753            ///
754            /// The SEXP must be protected and valid.
755            ///
756            /// # Panics
757            ///
758            /// Panics if any element fails to coerce (shouldn't happen if constructed via TryFromSexp).
759            #[inline]
760            pub unsafe fn to_vec_coerced(&self) -> Vec<$target> {
761                let slice: &[$source] = unsafe { self.sexp.as_slice() };
762                slice
763                    .iter()
764                    .copied()
765                    .map(|v| {
766                        <$source as TryCoerce<$target>>::try_coerce(v)
767                            .expect("coercion should succeed")
768                    })
769                    .collect()
770            }
771        }
772    };
773}
774
775// Integer coercions: R integer (i32) -> various Rust integer types
776impl_rarray_try_from_sexp_coerce!(i32 => i8);
777impl_rarray_try_from_sexp_coerce!(i32 => i16);
778impl_rarray_try_from_sexp_coerce!(i32 => i64);
779impl_rarray_try_from_sexp_coerce!(i32 => isize);
780impl_rarray_try_from_sexp_coerce!(i32 => u16);
781impl_rarray_try_from_sexp_coerce!(i32 => u32);
782impl_rarray_try_from_sexp_coerce!(i32 => u64);
783impl_rarray_try_from_sexp_coerce!(i32 => usize);
784
785// Float coercions: R numeric (f64) -> f32
786impl_rarray_try_from_sexp_coerce!(f64 => f32);
787
788// Logical coercions: R logical (RLogical) -> bool
789impl_rarray_try_from_sexp_coerce!(RLogical => bool);
790// endregion
791
792// region: IntoR implementation
793
794impl<T: RNativeType, const NDIM: usize> IntoR for RArray<T, NDIM> {
795    type Error = std::convert::Infallible;
796    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
797        Ok(self.into_sexp())
798    }
799    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
800        Ok(unsafe { self.into_sexp_unchecked() })
801    }
802    fn into_sexp(self) -> SEXP {
803        self.sexp
804    }
805
806    unsafe fn into_sexp_unchecked(self) -> SEXP {
807        self.sexp
808    }
809}
810// endregion
811
812// region: Helper functions
813
814/// Get number of dimensions from SEXP.
815fn get_ndim(sexp: SEXP) -> usize {
816    {
817        let dim_sexp = sexp.get_dim();
818        if dim_sexp.type_of() != SEXPTYPE::INTSXP {
819            // No dim attribute - treat as 1D
820            1
821        } else {
822            dim_sexp.len()
823        }
824    }
825}
826
827/// Get dimensions from SEXP as array.
828///
829/// # Safety
830///
831/// Caller must ensure SEXP has NDIM dimensions.
832unsafe fn get_dims<const NDIM: usize>(sexp: SEXP) -> [usize; NDIM] {
833    let mut dims = [0usize; NDIM];
834
835    unsafe {
836        let dim_sexp = sexp.get_dim();
837
838        if dim_sexp.type_of() != SEXPTYPE::INTSXP {
839            // No dim attribute - treat as 1D with length
840            if NDIM == 1 {
841                dims[0] = sexp.len();
842            }
843        } else {
844            let dim_slice: &[i32] = dim_sexp.as_slice();
845            for (i, &d) in dim_slice.iter().take(NDIM).enumerate() {
846                dims[i] = d as usize;
847            }
848        }
849    }
850
851    dims
852}
853
854/// Set dimensions on a SEXP.
855///
856/// # Safety
857///
858/// Must be called from R main thread.
859unsafe fn set_dims<const NDIM: usize>(sexp: SEXP, dims: &[usize; NDIM]) {
860    unsafe {
861        let (dim_sexp, dim_slice) = crate::into_r::alloc_r_vector::<i32>(NDIM);
862        ffi::Rf_protect(dim_sexp);
863
864        for (slot, &d) in dim_slice.iter_mut().zip(dims.iter()) {
865            *slot = i32::try_from(d).unwrap_or_else(|_| {
866                panic!("array dimension {d} exceeds i32::MAX");
867            });
868        }
869
870        sexp.set_dim(dim_sexp);
871        ffi::Rf_unprotect(1);
872    }
873}
874// endregion
875
876// region: Debug implementation
877
878impl<T: RNativeType, const NDIM: usize> std::fmt::Debug for RArray<T, NDIM> {
879    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
880        f.debug_struct("RArray")
881            .field("ndim", &NDIM)
882            .field("len", &self.len())
883            .field("sexp", &self.sexp)
884            .finish()
885    }
886}
887// endregion
888
889// region: Tests
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894
895    #[test]
896    fn matrix_is_array2() {
897        fn assert_matrix<T: RNativeType>(_: RMatrix<T>) {}
898        fn assert_array2<T: RNativeType>(_: RArray<T, 2>) {}
899
900        // These should compile - RMatrix<T> == RArray<T, 2>
901        let m: RMatrix<f64> = unsafe { RArray::from_sexp_unchecked(SEXP(std::ptr::null_mut())) };
902        assert_matrix(m);
903        assert_array2(m);
904    }
905
906    #[test]
907    fn size_equals_sexp() {
908        // RArray should be same size as SEXP (PhantomData is zero-sized)
909        assert_eq!(
910            std::mem::size_of::<RArray<f64, 2>>(),
911            std::mem::size_of::<SEXP>()
912        );
913    }
914
915    // Note: RArray is !Send and !Sync due to PhantomData<*const ()>.
916    // This is verified by the compiler - attempting to send RArray across
917    // threads will fail to compile.
918}
919// endregion