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}