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