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_ptr()`], [`SEXP::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()`], [`SEXP::is_nil()`], [`SEXP::is_null_ptr()`]
100 #[inline]
101 pub fn nil() -> Self {
102 unsafe { R_NilValue }
103 }
104
105 /// Is this SEXP a C null pointer (address `0x0`)?
106 ///
107 /// This answers a pointer question, not an R question. An SEXP that R
108 /// hands you (a `.Call()` argument, a list element, an attribute) is never
109 /// a C null pointer: R's `NULL` is the `R_NilValue` singleton, a real
110 /// object at a non-zero address. A C null pointer only shows up where C
111 /// code uses address zero for "nothing here": an uninitialised static or
112 /// field ([`SEXP::null()`]), an ALTREP `data1`/`data2` slot that was never
113 /// set, or an FFI function that returns `NULL` on failure.
114 ///
115 /// To ask "is this R's `NULL`?", use [`SEXP::is_nil()`].
116 ///
117 /// ```
118 /// use miniextendr_api::SEXP;
119 ///
120 /// let unset = SEXP::null();
121 /// assert!(unset.is_null_ptr());
122 /// ```
123 ///
124 /// There is no `SEXP::is_null()`: the name read like a test for R's
125 /// `NULL`, so it is gone on purpose.
126 ///
127 /// ```compile_fail
128 /// use miniextendr_api::SEXP;
129 ///
130 /// let unset = SEXP::null();
131 /// assert!(unset.is_null());
132 /// ```
133 ///
134 /// See also: [`SEXP::is_nil()`], [`SEXP::is_null_or_nil()`], [`SEXP::null()`]
135 #[inline]
136 #[must_use]
137 pub const fn is_null_ptr(self) -> bool {
138 self.0.is_null()
139 }
140
141 /// Is this SEXP R's `NULL` (`R_NilValue`)?
142 ///
143 /// This is the check for an R value: an optional argument that defaults
144 /// to `NULL`, an absent attribute, an empty list slot. It compares the
145 /// address against the `R_NilValue` singleton and never dereferences
146 /// `self`, so it is safe on a stale SEXP. A C null pointer is not R's
147 /// `NULL`; for that, use [`SEXP::is_null_ptr()`].
148 ///
149 /// ```
150 /// use miniextendr_api::SEXP;
151 ///
152 /// // `x` is an argument whose R default is `NULL`: `f()` and `f(NULL)`
153 /// // both arrive as `R_NilValue`.
154 /// fn given(x: SEXP) -> bool {
155 /// !x.is_nil()
156 /// }
157 /// # let _ = given;
158 /// ```
159 ///
160 /// See also: [`SEXP::nil()`], [`SEXP::is_null_ptr()`], [`SEXP::is_null_or_nil()`]
161 #[inline]
162 #[must_use]
163 pub fn is_nil(self) -> bool {
164 // Pointer comparison, not a type check: reading TYPEOF would crash on
165 // a freed SEXP during cleanup.
166 std::ptr::addr_eq(self.0, unsafe { R_NilValue.0 })
167 }
168
169 /// Is this SEXP either a C null pointer or R's `NULL`?
170 ///
171 /// For a slot that can hold either kind of "nothing": unset (address
172 /// zero) or set to `R_NilValue`, such as an external pointer's protected
173 /// value. For an R value alone, [`SEXP::is_nil()`] is the right check.
174 ///
175 /// ```
176 /// use miniextendr_api::SEXP;
177 ///
178 /// // A slot that was never set counts as empty.
179 /// assert!(SEXP::null().is_null_or_nil());
180 /// ```
181 ///
182 /// See also: [`SEXP::is_nil()`], [`SEXP::is_null_ptr()`]
183 #[inline]
184 #[must_use]
185 pub fn is_null_or_nil(self) -> bool {
186 self.is_null_ptr() || self.is_nil()
187 }
188
189 /// Get the raw pointer.
190 #[inline]
191 pub const fn as_ptr(self) -> *mut SEXPREC {
192 self.0
193 }
194
195 /// Create from a raw pointer.
196 #[inline]
197 pub const fn from_ptr(ptr: *mut SEXPREC) -> Self {
198 Self(ptr)
199 }
200
201 // region: String construction
202
203 /// Create a CHARSXP from a Rust `&str` (UTF-8).
204 #[inline]
205 pub fn charsxp(s: &str) -> SEXP {
206 let len: i32 = s.len().try_into().expect("string exceeds i32::MAX bytes");
207 unsafe { Rf_mkCharLenCE(s.as_ptr().cast(), len, CE_UTF8) }
208 }
209
210 /// R's `NA_character_` singleton.
211 #[inline]
212 pub fn na_string() -> SEXP {
213 unsafe { R_NaString }
214 }
215
216 /// R's empty string `""` singleton.
217 #[inline]
218 pub fn blank_string() -> SEXP {
219 unsafe { R_BlankString }
220 }
221
222 /// Create an R symbol (SYMSXP) from a CHARSXP.
223 ///
224 /// Equivalent to `Rf_installChar(charsxp)`. The symbol is interned
225 /// in R's global symbol table and never garbage collected.
226 #[inline]
227 pub fn install_char(charsxp: SEXP) -> SEXP {
228 unsafe { Rf_installChar(charsxp) }
229 }
230
231 /// Create an R symbol (SYMSXP) from a Rust `&str`.
232 ///
233 /// Combines `SEXP::charsxp()` + `Rf_installChar` into one call.
234 /// The symbol is interned and never garbage collected.
235 #[inline]
236 pub fn symbol(name: &str) -> SEXP {
237 Self::install_char(Self::charsxp(name))
238 }
239
240 // endregion
241
242 // region: Scalar construction
243
244 /// Create a length-1 integer vector.
245 #[inline]
246 pub fn scalar_integer(x: i32) -> SEXP {
247 unsafe { Rf_ScalarInteger(x) }
248 }
249
250 /// Create a length-1 real vector.
251 #[inline]
252 pub fn scalar_real(x: f64) -> SEXP {
253 unsafe { Rf_ScalarReal(x) }
254 }
255
256 /// Create a length-1 logical vector.
257 ///
258 /// Produces only `TRUE` or `FALSE`; a `bool` cannot represent R's `NA`.
259 /// For an NA logical, use [`scalar_logical_raw`](Self::scalar_logical_raw)
260 /// with `NA_LOGICAL` (`i32::MIN`).
261 #[inline]
262 pub fn scalar_logical(x: bool) -> SEXP {
263 unsafe { Rf_ScalarLogical(if x { 1 } else { 0 }) }
264 }
265
266 /// Create a length-1 logical vector from raw i32 (0=FALSE, 1=TRUE, NA_LOGICAL=NA).
267 #[inline]
268 /// Accepts 0 (FALSE), 1 (TRUE), or `NA_LOGICAL` (`i32::MIN`) for NA.
269 /// Prefer [`scalar_logical`](Self::scalar_logical) for non-NA values.
270 pub fn scalar_logical_raw(x: i32) -> SEXP {
271 unsafe { Rf_ScalarLogical(x) }
272 }
273
274 /// Create a length-1 raw vector.
275 #[inline]
276 pub fn scalar_raw(x: u8) -> SEXP {
277 unsafe { Rf_ScalarRaw(x) }
278 }
279
280 /// Create a length-1 complex vector.
281 #[inline]
282 pub fn scalar_complex(x: Rcomplex) -> SEXP {
283 unsafe { Rf_ScalarComplex(x) }
284 }
285
286 /// Create a length-1 character vector from a CHARSXP.
287 #[inline]
288 pub fn scalar_string(charsxp: SEXP) -> SEXP {
289 unsafe { Rf_ScalarString(charsxp) }
290 }
291
292 /// Create a length-1 character vector from a Rust `&str`.
293 #[inline]
294 pub fn scalar_string_from_str(s: &str) -> SEXP {
295 Self::scalar_string(Self::charsxp(s))
296 }
297
298 // Unchecked scalar constructors — skip the `with_r_thread` check.
299 // Use only inside ALTREP callbacks, `with_r_unwind_protect`, or `with_r_thread` blocks
300 // where the R-thread invariant is already established (see `#[r_ffi_checked]` docs).
301
302 /// Create a length-1 integer vector (unchecked — no thread routing).
303 ///
304 /// # Safety
305 ///
306 /// Must be called from the R main thread.
307 #[inline]
308 pub unsafe fn scalar_integer_unchecked(x: i32) -> SEXP {
309 unsafe { Rf_ScalarInteger_unchecked(x) }
310 }
311
312 /// Create a length-1 real vector (unchecked — no thread routing).
313 ///
314 /// # Safety
315 ///
316 /// Must be called from the R main thread.
317 #[inline]
318 pub unsafe fn scalar_real_unchecked(x: f64) -> SEXP {
319 unsafe { Rf_ScalarReal_unchecked(x) }
320 }
321
322 /// Create a length-1 logical vector from raw i32 (unchecked — no thread routing).
323 ///
324 /// Accepts 0 (FALSE), 1 (TRUE), or `NA_LOGICAL` (`i32::MIN`) for NA.
325 ///
326 /// # Safety
327 ///
328 /// Must be called from the R main thread.
329 #[inline]
330 pub unsafe fn scalar_logical_raw_unchecked(x: i32) -> SEXP {
331 unsafe { Rf_ScalarLogical_unchecked(x) }
332 }
333
334 /// Create a length-1 raw vector (unchecked — no thread routing).
335 ///
336 /// # Safety
337 ///
338 /// Must be called from the R main thread.
339 #[inline]
340 pub unsafe fn scalar_raw_unchecked(x: u8) -> SEXP {
341 unsafe { Rf_ScalarRaw_unchecked(x) }
342 }
343
344 /// Create a length-1 complex vector (unchecked — no thread routing).
345 ///
346 /// # Safety
347 ///
348 /// Must be called from the R main thread.
349 #[inline]
350 pub unsafe fn scalar_complex_unchecked(x: Rcomplex) -> SEXP {
351 unsafe { Rf_ScalarComplex_unchecked(x) }
352 }
353
354 /// Create a length-1 character vector from a CHARSXP (unchecked — no thread routing).
355 ///
356 /// # Safety
357 ///
358 /// Must be called from the R main thread.
359 #[inline]
360 pub unsafe fn scalar_string_unchecked(charsxp: SEXP) -> SEXP {
361 unsafe { Rf_ScalarString_unchecked(charsxp) }
362 }
363
364 // endregion
365
366 // region: Vector allocation
367
368 /// Allocate a fresh R vector of the given type and length.
369 ///
370 /// Direct wrapper over `Rf_allocVector`. For typed allocations, prefer
371 /// helpers like [`SEXP::alloc_list`], [`SEXP::alloc_strsxp`], or wrap the
372 /// result in [`OwnedProtect`](crate::gc_protect::OwnedProtect) immediately
373 /// — the returned SEXP is unprotected.
374 ///
375 /// # Safety
376 ///
377 /// Must be called from the R main thread. The returned SEXP is unprotected;
378 /// any subsequent allocation may collect it.
379 #[inline]
380 pub unsafe fn alloc(ty: SEXPTYPE, n: R_xlen_t) -> SEXP {
381 unsafe { Rf_allocVector(ty, n) }
382 }
383
384 /// Allocate an R list (VECSXP) of length `n`. Unprotected.
385 ///
386 /// Equivalent to `Rf_allocVector(VECSXP, n)`. Elements are initialised to `R_NilValue`.
387 ///
388 /// # Safety
389 ///
390 /// Must be called from the R main thread. The returned SEXP is unprotected —
391 /// wrap it in [`OwnedProtect`](crate::gc_protect::OwnedProtect) before any
392 /// other allocation that could trigger GC.
393 #[inline]
394 pub unsafe fn alloc_list(n: R_xlen_t) -> SEXP {
395 unsafe { Rf_allocVector(SEXPTYPE::VECSXP, n) }
396 }
397
398 /// Allocate an R character vector (STRSXP) of length `n`. Unprotected.
399 ///
400 /// Equivalent to `Rf_allocVector(STRSXP, n)`. Elements are initialised to `R_BlankString`.
401 ///
402 /// # Safety
403 ///
404 /// Must be called from the R main thread. The returned SEXP is unprotected —
405 /// wrap it in [`OwnedProtect`](crate::gc_protect::OwnedProtect) before any
406 /// other allocation that could trigger GC.
407 #[inline]
408 pub unsafe fn alloc_strsxp(n: R_xlen_t) -> SEXP {
409 unsafe { Rf_allocVector(SEXPTYPE::STRSXP, n) }
410 }
411
412 // endregion
413
414 // region: R global symbols and singletons
415
416 /// R's `names` attribute symbol.
417 #[inline]
418 pub fn names_symbol() -> SEXP {
419 unsafe { R_NamesSymbol }
420 }
421
422 /// R's `dim` attribute symbol.
423 #[inline]
424 pub fn dim_symbol() -> SEXP {
425 unsafe { R_DimSymbol }
426 }
427
428 /// R's `dimnames` attribute symbol.
429 #[inline]
430 pub fn dimnames_symbol() -> SEXP {
431 unsafe { R_DimNamesSymbol }
432 }
433
434 /// R's `class` attribute symbol.
435 #[inline]
436 pub fn class_symbol() -> SEXP {
437 unsafe { R_ClassSymbol }
438 }
439
440 /// R's `levels` attribute symbol (factors).
441 #[inline]
442 pub fn levels_symbol() -> SEXP {
443 unsafe { R_LevelsSymbol }
444 }
445
446 /// R's `tsp` attribute symbol (time series).
447 #[inline]
448 pub fn tsp_symbol() -> SEXP {
449 unsafe { R_TspSymbol }
450 }
451
452 /// R's base namespace environment.
453 #[inline]
454 pub fn base_namespace() -> SEXP {
455 unsafe { R_BaseNamespace }
456 }
457
458 /// R's missing argument sentinel.
459 #[inline]
460 pub fn missing_arg() -> SEXP {
461 unsafe { R_MissingArg }
462 }
463
464 // endregion
465
466 // region: ALTREP data slot access
467
468 /// Get the raw SEXP in the ALTREP data1 slot.
469 ///
470 /// # Safety
471 ///
472 /// - `self` must be a valid ALTREP SEXP
473 /// - Must be called from the R main thread
474 #[inline]
475 pub unsafe fn altrep_data1_raw(self) -> SEXP {
476 unsafe { R_altrep_data1(self) }
477 }
478
479 /// Get the raw SEXP in the ALTREP data1 slot (unchecked — no thread routing).
480 ///
481 /// # Safety
482 ///
483 /// - `self` must be a valid ALTREP SEXP
484 /// - Must be called from the R main thread
485 #[inline]
486 pub unsafe fn altrep_data1_raw_unchecked(self) -> SEXP {
487 unsafe { R_altrep_data1_unchecked(self) }
488 }
489
490 /// Set the ALTREP data1 slot.
491 ///
492 /// # Safety
493 ///
494 /// - `self` must be a valid ALTREP SEXP
495 /// - Must be called from the R main thread
496 #[inline]
497 pub unsafe fn set_altrep_data1(self, v: SEXP) {
498 unsafe { R_set_altrep_data1(self, v) }
499 }
500
501 /// Get the raw SEXP in the ALTREP data2 slot.
502 ///
503 /// # Safety
504 ///
505 /// - `self` must be a valid ALTREP SEXP
506 /// - Must be called from the R main thread
507 #[inline]
508 pub unsafe fn altrep_data2_raw(self) -> SEXP {
509 unsafe { R_altrep_data2(self) }
510 }
511
512 /// Get the raw SEXP in the ALTREP data2 slot (unchecked — no thread routing).
513 ///
514 /// # Safety
515 ///
516 /// - `self` must be a valid ALTREP SEXP
517 /// - Must be called from the R main thread
518 #[inline]
519 pub unsafe fn altrep_data2_raw_unchecked(self) -> SEXP {
520 unsafe { R_altrep_data2_unchecked(self) }
521 }
522
523 /// Set the ALTREP data2 slot.
524 ///
525 /// # Safety
526 ///
527 /// - `self` must be a valid ALTREP SEXP
528 /// - Must be called from the R main thread
529 #[inline]
530 pub unsafe fn set_altrep_data2(self, v: SEXP) {
531 unsafe { R_set_altrep_data2(self, v) }
532 }
533
534 /// Set the ALTREP data2 slot (unchecked — no thread routing).
535 ///
536 /// # Safety
537 ///
538 /// - `self` must be a valid ALTREP SEXP
539 /// - Must be called from the R main thread
540 #[inline]
541 pub unsafe fn set_altrep_data2_unchecked(self, v: SEXP) {
542 unsafe { R_set_altrep_data2_unchecked(self, v) }
543 }
544
545 // endregion
546}
547
548impl Default for SEXP {
549 #[inline]
550 fn default() -> Self {
551 Self::null()
552 }
553}
554
555impl From<*mut SEXPREC> for SEXP {
556 #[inline]
557 fn from(ptr: *mut SEXPREC) -> Self {
558 Self(ptr)
559 }
560}
561
562impl From<SEXP> for *mut SEXPREC {
563 #[inline]
564 fn from(sexp: SEXP) -> Self {
565 sexp.0
566 }
567}