Skip to main content

miniextendr_api/
sexp.rs

1//! The `SEXP` newtype and its inherent methods.
2//!
3//! `SEXP` is the central pointer type for R values. It's a thin newtype over
4//! `*mut SEXPREC` that implements `Send`/`Sync` for cross-thread plumbing
5//! (the pointed-to data must still only be touched on R's main thread).
6//!
7//! For the broader vocabulary of safe SEXP accessors, see [`crate::sexp_ext::SexpExt`].
8
9use crate::sexp_types::{CE_UTF8, R_xlen_t, Rcomplex, SEXPTYPE};
10// Pull in the extern bindings and module-level fns that the inherent methods
11// dispatch to. The type-level re-exports in `sys` (SEXP, SEXPREC, SEXPTYPE,
12// etc.) shadow but are identical to what we're defining locally — that's
13// fine because they're the same items.
14use crate::sys::{
15    R_BaseNamespace, R_BlankString, R_ClassSymbol, R_DimNamesSymbol, R_DimSymbol, R_LevelsSymbol,
16    R_MissingArg, R_NaString, R_NamesSymbol, R_NilValue, R_TspSymbol, R_altrep_data1,
17    R_altrep_data1_unchecked, R_altrep_data2, R_altrep_data2_unchecked, R_set_altrep_data1,
18    R_set_altrep_data2, R_set_altrep_data2_unchecked, Rf_ScalarComplex, Rf_ScalarComplex_unchecked,
19    Rf_ScalarInteger, Rf_ScalarInteger_unchecked, Rf_ScalarLogical, Rf_ScalarLogical_unchecked,
20    Rf_ScalarRaw, Rf_ScalarRaw_unchecked, Rf_ScalarReal, Rf_ScalarReal_unchecked, Rf_ScalarString,
21    Rf_ScalarString_unchecked, Rf_allocVector, Rf_installChar, Rf_mkCharLenCE,
22};
23
24#[repr(transparent)]
25#[derive(Debug)]
26/// Opaque underlying S-expression header type.
27pub struct SEXPREC(::std::os::raw::c_void);
28
29/// R's pointer type for S-expressions.
30///
31/// This is a newtype wrapper around `*mut SEXPREC` that implements Send and Sync.
32/// SEXP is just a handle (pointer) - the actual data it points to is managed by R's
33/// garbage collector and should only be accessed on R's main thread.
34///
35/// # Safety
36///
37/// While SEXP is Send+Sync (allowing it to be passed between threads), the data
38/// it points to must only be accessed on R's main thread. The miniextendr runtime
39/// enforces this through checked FFI wrappers and `with_r_thread` routing when
40/// worker dispatch is selected.
41///
42/// # Equality Semantics
43///
44/// IMPORTANT: The derived `PartialEq` compares **pointer equality**, not semantic equality.
45/// For proper R semantics (comparing object contents), use `R_compute_identical`.
46///
47/// ```ignore
48/// // Pointer equality (fast, often wrong for R semantics)
49/// if sexp1 == sexp2 { ... }  // Only true if same pointer
50///
51/// // Semantic equality (correct R semantics)
52/// if R_compute_identical(sexp1, sexp2, 16) != 0 { ... }
53/// ```
54///
55/// **Hash trait removed**: SEXP no longer implements `Hash` because proper hashing
56/// would require deep content inspection via `R_compute_identical`, which is too
57/// expensive for general use. If you need SEXP as a HashMap key, use pointer identity:
58///
59/// ```ignore
60/// // Store by pointer identity (common pattern for R symbol lookups)
61/// let mut map: HashMap<*mut SEXPREC, Value> = HashMap::new();
62/// map.insert(sexp.as_ptr(), value);
63/// ```
64#[repr(transparent)]
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct SEXP(pub *mut SEXPREC);
67
68// SAFETY: SEXP is just a pointer (memory address). Passing the address between
69// threads is safe. The actual data access is protected by miniextendr's runtime
70// which ensures R API calls happen on the main thread.
71unsafe impl Send for SEXP {}
72unsafe impl Sync for SEXP {}
73
74impl SEXP {
75    /// Create a C null pointer SEXP (0x0).
76    ///
77    /// This is **not** R's `NULL` value (`R_NilValue`). R's `NULL` is a real
78    /// heap-allocated singleton; a C null pointer is just address zero. Passing
79    /// `SEXP::null()` where R expects `R_NilValue` will corrupt R's GC state
80    /// and likely segfault.
81    ///
82    /// Use [`SEXP::nil()`] for R's `NULL`. Only use `null()` for low-level
83    /// pointer initialization, ALTREP Sum/Min/Max "can't compute" returns
84    /// (R checks `!= NULL`, not `!= R_NilValue`), or comparison against
85    /// uninitialized pointers.
86    ///
87    /// See also: [`SEXP::nil()`], [`SEXP::is_null()`], [`crate::SexpExt::is_nil()`]
88    #[inline]
89    pub const fn null() -> Self {
90        Self(std::ptr::null_mut())
91    }
92
93    /// Return R's `NULL` singleton (`R_NilValue`).
94    ///
95    /// This is **not** a C null pointer — it points to R's actual nil object
96    /// on the heap. Use this for `.Call()` return values, SEXP arguments to
97    /// R API functions, and any slot in R data structures.
98    ///
99    /// See also: [`SEXP::null()`], [`crate::SexpExt::is_nil()`], [`SEXP::is_null()`]
100    #[inline]
101    pub fn nil() -> Self {
102        unsafe { R_NilValue }
103    }
104
105    /// Check if this SEXP is a C null pointer (0x0).
106    ///
107    /// To check if an SEXP is R's `NULL` (`R_NilValue`), use
108    /// [`crate::SexpExt::is_nil()`] instead.
109    ///
110    /// See also: [`crate::SexpExt::is_nil()`], [`crate::SexpExt::is_null_or_nil()`]
111    #[inline]
112    pub const fn is_null(self) -> bool {
113        self.0.is_null()
114    }
115
116    /// Get the raw pointer.
117    #[inline]
118    pub const fn as_ptr(self) -> *mut SEXPREC {
119        self.0
120    }
121
122    /// Create from a raw pointer.
123    #[inline]
124    pub const fn from_ptr(ptr: *mut SEXPREC) -> Self {
125        Self(ptr)
126    }
127
128    // region: String construction
129
130    /// Create a CHARSXP from a Rust `&str` (UTF-8).
131    #[inline]
132    pub fn charsxp(s: &str) -> SEXP {
133        let len: i32 = s.len().try_into().expect("string exceeds i32::MAX bytes");
134        unsafe { Rf_mkCharLenCE(s.as_ptr().cast(), len, CE_UTF8) }
135    }
136
137    /// R's `NA_character_` singleton.
138    #[inline]
139    pub fn na_string() -> SEXP {
140        unsafe { R_NaString }
141    }
142
143    /// R's empty string `""` singleton.
144    #[inline]
145    pub fn blank_string() -> SEXP {
146        unsafe { R_BlankString }
147    }
148
149    /// Create an R symbol (SYMSXP) from a CHARSXP.
150    ///
151    /// Equivalent to `Rf_installChar(charsxp)`. The symbol is interned
152    /// in R's global symbol table and never garbage collected.
153    #[inline]
154    pub fn install_char(charsxp: SEXP) -> SEXP {
155        unsafe { Rf_installChar(charsxp) }
156    }
157
158    /// Create an R symbol (SYMSXP) from a Rust `&str`.
159    ///
160    /// Combines `SEXP::charsxp()` + `Rf_installChar` into one call.
161    /// The symbol is interned and never garbage collected.
162    #[inline]
163    pub fn symbol(name: &str) -> SEXP {
164        Self::install_char(Self::charsxp(name))
165    }
166
167    // endregion
168
169    // region: Scalar construction
170
171    /// Create a length-1 integer vector.
172    #[inline]
173    pub fn scalar_integer(x: i32) -> SEXP {
174        unsafe { Rf_ScalarInteger(x) }
175    }
176
177    /// Create a length-1 real vector.
178    #[inline]
179    pub fn scalar_real(x: f64) -> SEXP {
180        unsafe { Rf_ScalarReal(x) }
181    }
182
183    /// Create a length-1 logical vector.
184    ///
185    /// Produces only `TRUE` or `FALSE`; a `bool` cannot represent R's `NA`.
186    /// For an NA logical, use [`scalar_logical_raw`](Self::scalar_logical_raw)
187    /// with `NA_LOGICAL` (`i32::MIN`).
188    #[inline]
189    pub fn scalar_logical(x: bool) -> SEXP {
190        unsafe { Rf_ScalarLogical(if x { 1 } else { 0 }) }
191    }
192
193    /// Create a length-1 logical vector from raw i32 (0=FALSE, 1=TRUE, NA_LOGICAL=NA).
194    #[inline]
195    /// Accepts 0 (FALSE), 1 (TRUE), or `NA_LOGICAL` (`i32::MIN`) for NA.
196    /// Prefer [`scalar_logical`](Self::scalar_logical) for non-NA values.
197    pub fn scalar_logical_raw(x: i32) -> SEXP {
198        unsafe { Rf_ScalarLogical(x) }
199    }
200
201    /// Create a length-1 raw vector.
202    #[inline]
203    pub fn scalar_raw(x: u8) -> SEXP {
204        unsafe { Rf_ScalarRaw(x) }
205    }
206
207    /// Create a length-1 complex vector.
208    #[inline]
209    pub fn scalar_complex(x: Rcomplex) -> SEXP {
210        unsafe { Rf_ScalarComplex(x) }
211    }
212
213    /// Create a length-1 character vector from a CHARSXP.
214    #[inline]
215    pub fn scalar_string(charsxp: SEXP) -> SEXP {
216        unsafe { Rf_ScalarString(charsxp) }
217    }
218
219    /// Create a length-1 character vector from a Rust `&str`.
220    #[inline]
221    pub fn scalar_string_from_str(s: &str) -> SEXP {
222        Self::scalar_string(Self::charsxp(s))
223    }
224
225    // Unchecked scalar constructors — skip the `with_r_thread` check.
226    // Use only inside ALTREP callbacks, `with_r_unwind_protect`, or `with_r_thread` blocks
227    // where the R-thread invariant is already established (see `#[r_ffi_checked]` docs).
228
229    /// Create a length-1 integer vector (unchecked — no thread routing).
230    ///
231    /// # Safety
232    ///
233    /// Must be called from the R main thread.
234    #[inline]
235    pub unsafe fn scalar_integer_unchecked(x: i32) -> SEXP {
236        unsafe { Rf_ScalarInteger_unchecked(x) }
237    }
238
239    /// Create a length-1 real vector (unchecked — no thread routing).
240    ///
241    /// # Safety
242    ///
243    /// Must be called from the R main thread.
244    #[inline]
245    pub unsafe fn scalar_real_unchecked(x: f64) -> SEXP {
246        unsafe { Rf_ScalarReal_unchecked(x) }
247    }
248
249    /// Create a length-1 logical vector from raw i32 (unchecked — no thread routing).
250    ///
251    /// Accepts 0 (FALSE), 1 (TRUE), or `NA_LOGICAL` (`i32::MIN`) for NA.
252    ///
253    /// # Safety
254    ///
255    /// Must be called from the R main thread.
256    #[inline]
257    pub unsafe fn scalar_logical_raw_unchecked(x: i32) -> SEXP {
258        unsafe { Rf_ScalarLogical_unchecked(x) }
259    }
260
261    /// Create a length-1 raw vector (unchecked — no thread routing).
262    ///
263    /// # Safety
264    ///
265    /// Must be called from the R main thread.
266    #[inline]
267    pub unsafe fn scalar_raw_unchecked(x: u8) -> SEXP {
268        unsafe { Rf_ScalarRaw_unchecked(x) }
269    }
270
271    /// Create a length-1 complex vector (unchecked — no thread routing).
272    ///
273    /// # Safety
274    ///
275    /// Must be called from the R main thread.
276    #[inline]
277    pub unsafe fn scalar_complex_unchecked(x: Rcomplex) -> SEXP {
278        unsafe { Rf_ScalarComplex_unchecked(x) }
279    }
280
281    /// Create a length-1 character vector from a CHARSXP (unchecked — no thread routing).
282    ///
283    /// # Safety
284    ///
285    /// Must be called from the R main thread.
286    #[inline]
287    pub unsafe fn scalar_string_unchecked(charsxp: SEXP) -> SEXP {
288        unsafe { Rf_ScalarString_unchecked(charsxp) }
289    }
290
291    // endregion
292
293    // region: Vector allocation
294
295    /// Allocate a fresh R vector of the given type and length.
296    ///
297    /// Direct wrapper over `Rf_allocVector`. For typed allocations, prefer
298    /// helpers like [`SEXP::alloc_list`], [`SEXP::alloc_strsxp`], or wrap the
299    /// result in [`OwnedProtect`](crate::gc_protect::OwnedProtect) immediately
300    /// — the returned SEXP is unprotected.
301    ///
302    /// # Safety
303    ///
304    /// Must be called from the R main thread. The returned SEXP is unprotected;
305    /// any subsequent allocation may collect it.
306    #[inline]
307    pub unsafe fn alloc(ty: SEXPTYPE, n: R_xlen_t) -> SEXP {
308        unsafe { Rf_allocVector(ty, n) }
309    }
310
311    /// Allocate an R list (VECSXP) of length `n`. Unprotected.
312    ///
313    /// Equivalent to `Rf_allocVector(VECSXP, n)`. Elements are initialised to `R_NilValue`.
314    ///
315    /// # Safety
316    ///
317    /// Must be called from the R main thread. The returned SEXP is unprotected —
318    /// wrap it in [`OwnedProtect`](crate::gc_protect::OwnedProtect) before any
319    /// other allocation that could trigger GC.
320    #[inline]
321    pub unsafe fn alloc_list(n: R_xlen_t) -> SEXP {
322        unsafe { Rf_allocVector(SEXPTYPE::VECSXP, n) }
323    }
324
325    /// Allocate an R character vector (STRSXP) of length `n`. Unprotected.
326    ///
327    /// Equivalent to `Rf_allocVector(STRSXP, n)`. Elements are initialised to `R_BlankString`.
328    ///
329    /// # Safety
330    ///
331    /// Must be called from the R main thread. The returned SEXP is unprotected —
332    /// wrap it in [`OwnedProtect`](crate::gc_protect::OwnedProtect) before any
333    /// other allocation that could trigger GC.
334    #[inline]
335    pub unsafe fn alloc_strsxp(n: R_xlen_t) -> SEXP {
336        unsafe { Rf_allocVector(SEXPTYPE::STRSXP, n) }
337    }
338
339    // endregion
340
341    // region: R global symbols and singletons
342
343    /// R's `names` attribute symbol.
344    #[inline]
345    pub fn names_symbol() -> SEXP {
346        unsafe { R_NamesSymbol }
347    }
348
349    /// R's `dim` attribute symbol.
350    #[inline]
351    pub fn dim_symbol() -> SEXP {
352        unsafe { R_DimSymbol }
353    }
354
355    /// R's `dimnames` attribute symbol.
356    #[inline]
357    pub fn dimnames_symbol() -> SEXP {
358        unsafe { R_DimNamesSymbol }
359    }
360
361    /// R's `class` attribute symbol.
362    #[inline]
363    pub fn class_symbol() -> SEXP {
364        unsafe { R_ClassSymbol }
365    }
366
367    /// R's `levels` attribute symbol (factors).
368    #[inline]
369    pub fn levels_symbol() -> SEXP {
370        unsafe { R_LevelsSymbol }
371    }
372
373    /// R's `tsp` attribute symbol (time series).
374    #[inline]
375    pub fn tsp_symbol() -> SEXP {
376        unsafe { R_TspSymbol }
377    }
378
379    /// R's base namespace environment.
380    #[inline]
381    pub fn base_namespace() -> SEXP {
382        unsafe { R_BaseNamespace }
383    }
384
385    /// R's missing argument sentinel.
386    #[inline]
387    pub fn missing_arg() -> SEXP {
388        unsafe { R_MissingArg }
389    }
390
391    // endregion
392
393    // region: ALTREP data slot access
394
395    /// Get the raw SEXP in the ALTREP data1 slot.
396    ///
397    /// # Safety
398    ///
399    /// - `self` must be a valid ALTREP SEXP
400    /// - Must be called from the R main thread
401    #[inline]
402    pub unsafe fn altrep_data1_raw(self) -> SEXP {
403        unsafe { R_altrep_data1(self) }
404    }
405
406    /// Get the raw SEXP in the ALTREP data1 slot (unchecked — no thread routing).
407    ///
408    /// # Safety
409    ///
410    /// - `self` must be a valid ALTREP SEXP
411    /// - Must be called from the R main thread
412    #[inline]
413    pub unsafe fn altrep_data1_raw_unchecked(self) -> SEXP {
414        unsafe { R_altrep_data1_unchecked(self) }
415    }
416
417    /// Set the ALTREP data1 slot.
418    ///
419    /// # Safety
420    ///
421    /// - `self` must be a valid ALTREP SEXP
422    /// - Must be called from the R main thread
423    #[inline]
424    pub unsafe fn set_altrep_data1(self, v: SEXP) {
425        unsafe { R_set_altrep_data1(self, v) }
426    }
427
428    /// Get the raw SEXP in the ALTREP data2 slot.
429    ///
430    /// # Safety
431    ///
432    /// - `self` must be a valid ALTREP SEXP
433    /// - Must be called from the R main thread
434    #[inline]
435    pub unsafe fn altrep_data2_raw(self) -> SEXP {
436        unsafe { R_altrep_data2(self) }
437    }
438
439    /// Get the raw SEXP in the ALTREP data2 slot (unchecked — no thread routing).
440    ///
441    /// # Safety
442    ///
443    /// - `self` must be a valid ALTREP SEXP
444    /// - Must be called from the R main thread
445    #[inline]
446    pub unsafe fn altrep_data2_raw_unchecked(self) -> SEXP {
447        unsafe { R_altrep_data2_unchecked(self) }
448    }
449
450    /// Set the ALTREP data2 slot.
451    ///
452    /// # Safety
453    ///
454    /// - `self` must be a valid ALTREP SEXP
455    /// - Must be called from the R main thread
456    #[inline]
457    pub unsafe fn set_altrep_data2(self, v: SEXP) {
458        unsafe { R_set_altrep_data2(self, v) }
459    }
460
461    /// Set the ALTREP data2 slot (unchecked — no thread routing).
462    ///
463    /// # Safety
464    ///
465    /// - `self` must be a valid ALTREP SEXP
466    /// - Must be called from the R main thread
467    #[inline]
468    pub unsafe fn set_altrep_data2_unchecked(self, v: SEXP) {
469        unsafe { R_set_altrep_data2_unchecked(self, v) }
470    }
471
472    // endregion
473}
474
475impl Default for SEXP {
476    #[inline]
477    fn default() -> Self {
478        Self::null()
479    }
480}
481
482impl From<*mut SEXPREC> for SEXP {
483    #[inline]
484    fn from(ptr: *mut SEXPREC) -> Self {
485        Self(ptr)
486    }
487}
488
489impl From<SEXP> for *mut SEXPREC {
490    #[inline]
491    fn from(sexp: SEXP) -> Self {
492        sexp.0
493    }
494}