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