Skip to main content

miniextendr_api/
altrep_sexp.rs

1//! `AltrepSexp` — a `!Send + !Sync` wrapper for ALTREP vectors.
2//!
3//! R uses ALTREP (Alternative Representations) for common idioms like `1:N`,
4//! `seq_len(N)`, and `as.character(1:N)`. These vectors are lazily materialized:
5//! calling `DATAPTR_RO` triggers allocation, GC, and C callbacks inside R's
6//! runtime. This must only happen on the R main thread.
7//!
8//! This module provides two complementary tools:
9//!
10//! - **[`AltrepSexp`]** — a `!Send + !Sync` wrapper that holds an ALTREP SEXP
11//!   and prevents it from crossing thread boundaries at compile time.
12//! - **[`ensure_materialized`]** — a function that forces materialization if
13//!   the SEXP is ALTREP, returning a SEXP with a stable data pointer.
14//!
15//! Plain (non-ALTREP) SEXPs are `Send + Sync` and are unaffected by either.
16//!
17//! # How ALTREP flows through miniextendr
18//!
19//! | Parameter type | ALTREP handling |
20//! |---|---|
21//! | Typed (`Vec<i32>`, `&[f64]`) | Auto-materialized via `DATAPTR_RO` in `TryFromSexp` |
22//! | `SEXP` | Auto-materialized via [`ensure_materialized`] in `TryFromSexp` |
23//! | [`AltrepSexp`] | Wrapped without materializing. `!Send + !Sync`. |
24//! | `extern "C-unwind"` raw SEXP | No conversion — receives raw SEXP as-is |
25//!
26//! # Usage
27//!
28//! ```ignore
29//! use miniextendr_api::AltrepSexp;
30//!
31//! // As a #[miniextendr] parameter — accepts only ALTREP vectors:
32//! #[miniextendr]
33//! pub fn altrep_length(x: AltrepSexp) -> usize {
34//!     x.len()
35//! }
36//!
37//! // Manual wrapping:
38//! if let Some(altrep) = AltrepSexp::try_wrap(sexp) {
39//!     // Must materialize on R main thread before accessing data
40//!     let materialized: SEXP = unsafe { altrep.materialize() };
41//! }
42//!
43//! // Or use the convenience helper on any SEXP:
44//! let safe_sexp = unsafe { ensure_materialized(sexp) };
45//! ```
46
47use crate::from_r::r_slice;
48use crate::sys::{self};
49use crate::{R_xlen_t, Rcomplex, SEXP, SEXPTYPE, SexpExt};
50use std::marker::PhantomData;
51use std::rc::Rc;
52
53/// A SEXP known to be ALTREP. `!Send + !Sync` — must be materialized on the
54/// R main thread before data can be accessed or sent to other threads.
55///
56/// This type prevents ALTREP vectors from being accidentally sent to rayon
57/// or other worker threads where `DATAPTR_RO` would invoke R internals
58/// (undefined behavior).
59///
60/// # As a `#[miniextendr]` parameter
61///
62/// `AltrepSexp` implements [`TryFromSexp`](crate::from_r::TryFromSexp), so it
63/// can be used directly as a function parameter. It **only accepts ALTREP
64/// vectors** — non-ALTREP input produces an error.
65///
66/// ```ignore
67/// #[miniextendr]
68/// pub fn altrep_info(x: AltrepSexp) -> String {
69///     format!("{:?}, len={}", x.sexptype(), x.len())
70/// }
71/// ```
72///
73/// ```r
74/// altrep_info(1:10)          # OK — 1:10 is ALTREP
75/// altrep_info(c(1L, 2L, 3L)) # Error: "expected an ALTREP vector"
76/// ```
77///
78/// # Construction
79///
80/// - [`AltrepSexp::try_wrap`] — runtime check, returns `None` if not ALTREP
81/// - [`AltrepSexp::from_raw`] — unsafe, caller asserts `ALTREP(sexp) != 0`
82///
83/// # Materialization
84///
85/// All materialization methods must be called on the R main thread.
86///
87/// - [`AltrepSexp::materialize`] — forces R to materialize, returns plain SEXP
88/// - [`AltrepSexp::materialize_integer`] — materialize INTSXP and return `&[i32]`
89/// - [`AltrepSexp::materialize_real`] — materialize REALSXP and return `&[f64]`
90/// - [`AltrepSexp::materialize_logical`] — materialize LGLSXP and return `&[i32]`
91/// - [`AltrepSexp::materialize_raw`] — materialize RAWSXP and return `&[u8]`
92/// - [`AltrepSexp::materialize_complex`] — materialize CPLXSXP and return `&[Rcomplex]`
93/// - [`AltrepSexp::materialize_strings`] — materialize STRSXP to `Vec<Option<String>>`
94///
95/// # Thread safety
96///
97/// `AltrepSexp` is `!Send + !Sync` (via `PhantomData<Rc<()>>`). This is a
98/// compile-time guarantee: you cannot send an un-materialized ALTREP vector
99/// to another thread. Call one of the `materialize_*` methods first to get
100/// a `Send + Sync` slice or SEXP.
101pub struct AltrepSexp {
102    sexp: SEXP,
103    /// PhantomData<Rc<()>> makes this type !Send + !Sync.
104    _not_send: PhantomData<Rc<()>>,
105}
106
107impl AltrepSexp {
108    /// Wrap a SEXP that is known to be ALTREP.
109    ///
110    /// # Safety
111    ///
112    /// Caller must ensure `ALTREP(sexp)` is true (non-zero).
113    #[inline]
114    pub unsafe fn from_raw(sexp: SEXP) -> Self {
115        debug_assert!(sexp.is_altrep());
116        Self {
117            sexp,
118            _not_send: PhantomData,
119        }
120    }
121
122    /// Check a SEXP and wrap if ALTREP. Returns `None` if not ALTREP.
123    #[inline]
124    pub fn try_wrap(sexp: SEXP) -> Option<Self> {
125        if sexp.is_altrep() {
126            Some(Self {
127                sexp,
128                _not_send: PhantomData,
129            })
130        } else {
131            None
132        }
133    }
134
135    /// Force materialization and return the (now materialized) SEXP.
136    ///
137    /// For contiguous types (INTSXP, REALSXP, LGLSXP, RAWSXP, CPLXSXP),
138    /// calls `DATAPTR_RO` to trigger ALTREP materialization.
139    /// For STRSXP, iterates `STRING_ELT` to force element materialization.
140    ///
141    /// After this call, the SEXP's data pointer is stable and can be safely
142    /// accessed from any thread (the SEXP itself is still `Send + Sync`).
143    ///
144    /// # Safety
145    ///
146    /// Must be called on the R main thread.
147    pub unsafe fn materialize(self) -> SEXP {
148        let typ = self.sexp.type_of();
149        match typ {
150            SEXPTYPE::STRSXP => {
151                let n = self.sexp.xlength();
152                for i in 0..n {
153                    let _ = self.sexp.string_elt(i);
154                }
155            }
156            SEXPTYPE::INTSXP
157            | SEXPTYPE::REALSXP
158            | SEXPTYPE::LGLSXP
159            | SEXPTYPE::RAWSXP
160            | SEXPTYPE::CPLXSXP => {
161                let _ = unsafe { sys::DATAPTR_RO(self.sexp) };
162            }
163            _ => {} // non-vector types, nothing to materialize
164        }
165        self.sexp
166    }
167
168    /// Materialize and return a typed slice of `f64` (REALSXP).
169    ///
170    /// # Safety
171    ///
172    /// Must be called on the R main thread. The SEXP must be REALSXP.
173    pub unsafe fn materialize_real(&self) -> &[f64] {
174        let ptr = unsafe { sys::DATAPTR_RO(self.sexp) } as *const f64;
175        let len = self.sexp.len();
176        unsafe { r_slice(ptr, len) }
177    }
178
179    /// Materialize and return a typed slice of `i32` (INTSXP).
180    ///
181    /// # Safety
182    ///
183    /// Must be called on the R main thread. The SEXP must be INTSXP.
184    pub unsafe fn materialize_integer(&self) -> &[i32] {
185        let ptr = unsafe { sys::DATAPTR_RO(self.sexp) } as *const i32;
186        let len = self.sexp.len();
187        unsafe { r_slice(ptr, len) }
188    }
189
190    /// Materialize and return a typed slice of `i32` (LGLSXP, R's internal logical storage).
191    ///
192    /// # Safety
193    ///
194    /// Must be called on the R main thread. The SEXP must be LGLSXP.
195    pub unsafe fn materialize_logical(&self) -> &[i32] {
196        let ptr = unsafe { sys::DATAPTR_RO(self.sexp) } as *const i32;
197        let len = self.sexp.len();
198        unsafe { r_slice(ptr, len) }
199    }
200
201    /// Materialize and return a typed slice of `u8` (RAWSXP).
202    ///
203    /// # Safety
204    ///
205    /// Must be called on the R main thread. The SEXP must be RAWSXP.
206    pub unsafe fn materialize_raw(&self) -> &[u8] {
207        let ptr = unsafe { sys::DATAPTR_RO(self.sexp) } as *const u8;
208        let len = self.sexp.len();
209        unsafe { r_slice(ptr, len) }
210    }
211
212    /// Materialize and return a typed slice of `Rcomplex` (CPLXSXP).
213    ///
214    /// # Safety
215    ///
216    /// Must be called on the R main thread. The SEXP must be CPLXSXP.
217    pub unsafe fn materialize_complex(&self) -> &[Rcomplex] {
218        let ptr = unsafe { sys::DATAPTR_RO(self.sexp) } as *const Rcomplex;
219        let len = self.sexp.len();
220        unsafe { r_slice(ptr, len) }
221    }
222
223    /// Materialize strings into owned Rust data.
224    ///
225    /// Each element is `None` for `NA_character_`, or `Some(String)` otherwise.
226    ///
227    /// # Safety
228    ///
229    /// Must be called on the R main thread. The SEXP must be STRSXP.
230    pub unsafe fn materialize_strings(&self) -> Vec<Option<String>> {
231        use crate::from_r::charsxp_to_str;
232        let n = self.sexp.len();
233        let mut out = Vec::with_capacity(n);
234        for i in 0..n {
235            let elt = self.sexp.string_elt(i as R_xlen_t);
236            if elt == SEXP::na_string() {
237                out.push(None);
238            } else {
239                out.push(Some(unsafe { charsxp_to_str(elt) }.to_owned()));
240            }
241        }
242        out
243    }
244
245    /// Get the inner SEXP without materializing.
246    ///
247    /// # Safety
248    ///
249    /// The returned SEXP is still ALTREP. Do not call `DATAPTR_RO` on it
250    /// from a non-R thread.
251    #[inline]
252    pub unsafe fn as_raw(&self) -> SEXP {
253        self.sexp
254    }
255
256    /// Get the SEXPTYPE of the underlying vector.
257    #[inline]
258    pub fn sexptype(&self) -> SEXPTYPE {
259        self.sexp.type_of()
260    }
261
262    /// Get the length of the underlying vector.
263    #[inline]
264    pub fn len(&self) -> usize {
265        self.sexp.len()
266    }
267
268    /// Check if the underlying vector is empty.
269    #[inline]
270    pub fn is_empty(&self) -> bool {
271        self.len() == 0
272    }
273}
274
275/// Conversion from R SEXP to `AltrepSexp`.
276///
277/// Only succeeds if the input is an ALTREP vector (`ALTREP(sexp) != 0`).
278/// Non-ALTREP input produces `SexpError::InvalidValue`.
279///
280/// This is the inverse of [`TryFromSexp for SEXP`](crate::from_r::TryFromSexp),
281/// which accepts any SEXP but auto-materializes ALTREP.
282impl crate::from_r::TryFromSexp for AltrepSexp {
283    type Error = crate::from_r::SexpError;
284
285    #[inline]
286    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
287        AltrepSexp::try_wrap(sexp).ok_or_else(|| {
288            crate::from_r::SexpError::InvalidValue(
289                "expected an ALTREP vector but got a non-ALTREP SEXP".to_string(),
290            )
291        })
292    }
293
294    #[inline]
295    unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
296        Self::try_from_sexp(sexp)
297    }
298}
299
300impl std::fmt::Debug for AltrepSexp {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        f.debug_struct("AltrepSexp")
303            .field("sexptype", &self.sexptype())
304            .field("len", &self.len())
305            .finish()
306    }
307}
308
309/// If `sexp` is ALTREP, force materialization and return the SEXP.
310/// If not ALTREP, return as-is (no-op).
311///
312/// This is the main entry point for ensuring a SEXP is safe to access
313/// from non-R threads. After materialization, the data pointer is stable
314/// and the SEXP can be freely sent across threads.
315///
316/// Called automatically by `TryFromSexp for SEXP` — you only need to call
317/// this directly in `extern "C-unwind"` functions that receive raw SEXPs.
318///
319/// For contiguous types (INTSXP, REALSXP, LGLSXP, RAWSXP, CPLXSXP),
320/// calls `DATAPTR_RO` to trigger materialization. For STRSXP, iterates
321/// `STRING_ELT` to force each element to materialize.
322///
323/// # Safety
324///
325/// Must be called on the R main thread (materialization invokes R internals).
326#[inline]
327pub unsafe fn ensure_materialized(sexp: SEXP) -> SEXP {
328    if sexp.is_altrep() {
329        unsafe { AltrepSexp::from_raw(sexp).materialize() }
330    } else {
331        sexp
332    }
333}
334
335// Compile-time assertions: SEXP must remain Send + Sync.
336const _: () = {
337    fn _assert_send<T: Send>() {}
338    fn _assert_sync<T: Sync>() {}
339
340    fn _sexp_is_send_sync() {
341        _assert_send::<SEXP>();
342        _assert_sync::<SEXP>();
343    }
344};
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    /// Verify AltrepSexp is !Send and !Sync at compile time.
351    /// SEXP IS Send + Sync.
352    fn _assert_send_sync_properties() {
353        fn requires_send<T: Send>() {}
354        fn requires_sync<T: Sync>() {}
355
356        // These must NOT compile — uncomment to verify:
357        // requires_send::<AltrepSexp>();
358        // requires_sync::<AltrepSexp>();
359
360        // SEXP IS Send + Sync:
361        requires_send::<SEXP>();
362        requires_sync::<SEXP>();
363    }
364}