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