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