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}