miniextendr_api/expression.rs
1//! Safe wrappers for R expression evaluation.
2//!
3//! This module provides ergonomic types for building and evaluating R function
4//! calls from Rust, handling GC protection and error propagation automatically.
5//!
6//! # Types
7//!
8//! | Type | Purpose |
9//! |------|---------|
10//! | [`RSymbol`] | Interned R symbol (SYMSXP) |
11//! | [`RCall`] | Builder for R function calls (LANGSXP) |
12//! | [`REnv`] | Well-known R environments |
13//!
14//! # Example
15//!
16//! ```ignore
17//! use miniextendr_api::expression::{RCall, REnv};
18//!
19//! unsafe {
20//! // Call paste0("hello", " world") in the base environment
21//! let result = RCall::new("paste0")
22//! .arg(Rf_mkString(c"hello".as_ptr()))
23//! .arg(Rf_mkString(c" world".as_ptr()))
24//! .eval(REnv::base().as_sexp())?;
25//! }
26//! ```
27
28use crate::ffi::{
29 self, PairListExt, R_BaseEnv, R_EmptyEnv, R_GlobalEnv, R_tryEvalSilent, Rf_install, Rf_protect,
30 Rf_unprotect, SEXP, SexpExt,
31};
32use std::ffi::{CStr, CString};
33
34// region: RSymbol
35
36/// A safe wrapper around R symbols (SYMSXP).
37///
38/// R symbols are interned strings used as variable and function names.
39/// They are never garbage collected, so `RSymbol` does not need GC protection.
40///
41/// # Example
42///
43/// ```ignore
44/// let sym = RSymbol::new("paste0");
45/// // sym.as_sexp() is a SYMSXP that can be used in call construction
46/// ```
47pub struct RSymbol {
48 sexp: SEXP,
49}
50
51impl RSymbol {
52 /// Create or retrieve an interned R symbol.
53 ///
54 /// # Safety
55 ///
56 /// Must be called from the R main thread.
57 ///
58 /// # Panics
59 ///
60 /// Panics if `name` contains a null byte.
61 #[inline]
62 pub unsafe fn new(name: &str) -> Self {
63 let c_name = CString::new(name).expect("symbol name must not contain null bytes");
64 RSymbol {
65 sexp: unsafe { Rf_install(c_name.as_ptr()) },
66 }
67 }
68
69 /// Create a symbol from a C string literal.
70 ///
71 /// This avoids the allocation needed by [`new`](Self::new) when you have
72 /// a `&CStr` available (e.g., from `c"name"` literals).
73 ///
74 /// # Safety
75 ///
76 /// Must be called from the R main thread.
77 #[inline]
78 pub unsafe fn from_cstr(name: &CStr) -> Self {
79 RSymbol {
80 sexp: unsafe { Rf_install(name.as_ptr()) },
81 }
82 }
83
84 /// Get the underlying SEXP.
85 #[inline]
86 pub fn as_sexp(&self) -> SEXP {
87 self.sexp
88 }
89}
90// endregion
91
92// region: REnv
93
94/// Handle to a well-known R environment.
95///
96/// Provides access to R's standard environments without raw FFI calls.
97pub struct REnv {
98 sexp: SEXP,
99}
100
101impl REnv {
102 /// The global environment (`R_GlobalEnv`).
103 ///
104 /// # Safety
105 ///
106 /// Must be called from the R main thread.
107 #[inline]
108 pub unsafe fn global() -> Self {
109 REnv {
110 sexp: unsafe { R_GlobalEnv },
111 }
112 }
113
114 /// The base environment (`R_BaseEnv`).
115 ///
116 /// # Safety
117 ///
118 /// Must be called from the R main thread.
119 #[inline]
120 pub unsafe fn base() -> Self {
121 REnv {
122 sexp: unsafe { R_BaseEnv },
123 }
124 }
125
126 /// The empty environment (`R_EmptyEnv`).
127 ///
128 /// # Safety
129 ///
130 /// Must be called from the R main thread.
131 #[inline]
132 pub unsafe fn empty() -> Self {
133 REnv {
134 sexp: unsafe { R_EmptyEnv },
135 }
136 }
137
138 /// The base namespace (`SEXP::base_namespace()`).
139 ///
140 /// Unlike [`base()`](Self::base) which is the base *environment* (exported
141 /// functions visible to users), this is the base *namespace* (includes
142 /// internal helpers). Rarely needed — prefer [`base()`](Self::base) unless
143 /// you specifically need unexported base internals.
144 ///
145 /// # Safety
146 ///
147 /// Must be called from the R main thread.
148 #[inline]
149 pub fn base_namespace() -> Self {
150 REnv {
151 sexp: SEXP::base_namespace(),
152 }
153 }
154
155 /// A package's namespace environment.
156 ///
157 /// Finds the namespace for a loaded package. Use this to evaluate functions
158 /// that live in a specific package (e.g., `slot()` from `methods`).
159 ///
160 /// This is a safe wrapper around `R_FindNamespace` — it uses
161 /// `R_tryEvalSilent` internally so that a missing namespace returns
162 /// `Err` instead of longjmping through Rust frames.
163 ///
164 /// # Safety
165 ///
166 /// Must be called from the R main thread.
167 ///
168 /// # Errors
169 ///
170 /// Returns `Err` if the package namespace is not found (package not loaded).
171 pub unsafe fn package_namespace(name: &str) -> Result<Self, String> {
172 unsafe {
173 let name_sexp = SEXP::scalar_string_from_str(name);
174 Rf_protect(name_sexp);
175 let result = RCall::new("getNamespace").arg(name_sexp).eval(R_BaseEnv);
176 Rf_unprotect(1);
177 result.map(|sexp| REnv { sexp })
178 }
179 }
180
181 /// The current execution environment.
182 ///
183 /// Returns the environment of the innermost active closure on R's call
184 /// stack, or the global environment if no closure is active.
185 ///
186 /// Useful when you need to evaluate an expression in the caller's context
187 /// rather than a fixed well-known environment.
188 ///
189 /// # Safety
190 ///
191 /// Must be called from the R main thread.
192 #[inline]
193 pub unsafe fn caller() -> Self {
194 REnv {
195 sexp: unsafe { ffi::R_GetCurrentEnv() },
196 }
197 }
198
199 /// Wrap an arbitrary environment SEXP.
200 ///
201 /// # Safety
202 ///
203 /// `sexp` must be a valid ENVSXP.
204 #[inline]
205 pub unsafe fn from_sexp(sexp: SEXP) -> Self {
206 REnv { sexp }
207 }
208
209 /// Get the underlying SEXP.
210 #[inline]
211 pub fn as_sexp(&self) -> SEXP {
212 self.sexp
213 }
214}
215// endregion
216
217// region: RCall
218
219/// Builder for constructing and evaluating R function calls.
220///
221/// `RCall` constructs a LANGSXP (R language object) from a function name or
222/// SEXP and a sequence of arguments (optionally named). It handles GC
223/// protection during construction and evaluation.
224///
225/// # Example
226///
227/// ```ignore
228/// use miniextendr_api::expression::RCall;
229/// use miniextendr_api::ffi;
230///
231/// unsafe {
232/// // seq_len(10)
233/// let result = RCall::new("seq_len")
234/// .arg(ffi::Rf_ScalarInteger(10))
235/// .eval_base()?;
236///
237/// // paste(x, collapse = ", ")
238/// let result = RCall::new("paste")
239/// .arg(some_sexp)
240/// .named_arg("collapse", ffi::Rf_mkString(c", ".as_ptr()))
241/// .eval_base()?;
242/// }
243/// ```
244pub struct RCall {
245 /// Function symbol or SEXP.
246 fun: SEXP,
247 /// Arguments as (optional_name, value) pairs.
248 args: Vec<(Option<CString>, SEXP)>,
249}
250
251impl RCall {
252 /// Start building a call to a named R function.
253 ///
254 /// The function is looked up via `Rf_install`, which returns an interned symbol.
255 ///
256 /// # Safety
257 ///
258 /// Must be called from the R main thread.
259 ///
260 /// # Panics
261 ///
262 /// Panics if `fun_name` contains a null byte.
263 #[inline]
264 pub unsafe fn new(fun_name: &str) -> Self {
265 let c_name = CString::new(fun_name).expect("function name must not contain null bytes");
266 RCall {
267 fun: unsafe { Rf_install(c_name.as_ptr()) },
268 args: Vec::new(),
269 }
270 }
271
272 /// Start building a call to a function given as a C string literal.
273 ///
274 /// More efficient than [`new`](Self::new) when a `&CStr` is available.
275 ///
276 /// # Safety
277 ///
278 /// Must be called from the R main thread.
279 #[inline]
280 pub unsafe fn from_cstr(fun_name: &CStr) -> Self {
281 RCall {
282 fun: unsafe { Rf_install(fun_name.as_ptr()) },
283 args: Vec::new(),
284 }
285 }
286
287 /// Start building a call with a function SEXP (closure, builtin, etc.).
288 ///
289 /// # Safety
290 ///
291 /// `fun` must be a valid SEXP representing a callable R object.
292 #[inline]
293 pub unsafe fn from_sexp(fun: SEXP) -> Self {
294 RCall {
295 fun,
296 args: Vec::new(),
297 }
298 }
299
300 /// Add a positional argument.
301 #[inline]
302 pub fn arg(mut self, value: SEXP) -> Self {
303 self.args.push((None, value));
304 self
305 }
306
307 /// Add a named argument.
308 ///
309 /// # Panics
310 ///
311 /// Panics if `name` contains a null byte.
312 #[inline]
313 pub fn named_arg(mut self, name: &str, value: SEXP) -> Self {
314 let c_name = CString::new(name).expect("argument name must not contain null bytes");
315 self.args.push((Some(c_name), value));
316 self
317 }
318
319 /// Build the LANGSXP without evaluating it.
320 ///
321 /// The returned SEXP is **unprotected**. The caller must protect it if
322 /// further allocations will occur before use.
323 ///
324 /// # Safety
325 ///
326 /// Must be called from the R main thread. All argument SEXPs must still
327 /// be valid (protected or otherwise reachable by R's GC).
328 pub unsafe fn build(&self) -> SEXP {
329 unsafe {
330 // Build the argument pairlist from back to front using Rf_cons.
331 // We protect intermediate results as we go.
332 let mut n_protect: i32 = 0;
333
334 let mut tail = SEXP::nil();
335 for (name, value) in self.args.iter().rev() {
336 tail = value.cons(tail);
337 Rf_protect(tail);
338 n_protect += 1;
339
340 if let Some(c_name) = name {
341 tail.set_tag(Rf_install(c_name.as_ptr()));
342 }
343 }
344
345 // Prepend the function as LANGSXP head
346 let call = self.fun.lcons(tail);
347 Rf_protect(call);
348 n_protect += 1;
349
350 // Clean up all intermediate protections; caller is responsible
351 // for protecting the returned call.
352 Rf_unprotect(n_protect);
353 call
354 }
355 }
356
357 /// Evaluate the call in the given environment.
358 ///
359 /// Uses `R_tryEvalSilent` so that R errors are captured as `Err(String)`
360 /// rather than causing a longjmp through Rust frames.
361 ///
362 /// # Safety
363 ///
364 /// - Must be called from the R main thread.
365 /// - `env` must be a valid ENVSXP.
366 /// - All argument SEXPs must still be valid.
367 ///
368 /// # Returns
369 ///
370 /// - `Ok(SEXP)` with the result (unprotected — caller should protect if needed)
371 /// - `Err(String)` with the R error message on failure
372 pub unsafe fn eval(&self, env: SEXP) -> Result<SEXP, String> {
373 unsafe {
374 let call = self.build();
375 Rf_protect(call);
376
377 let mut error_occurred: std::os::raw::c_int = 0;
378 let result = R_tryEvalSilent(call, env, &mut error_occurred);
379
380 Rf_unprotect(1); // call
381
382 if error_occurred != 0 {
383 Err(get_r_error_message())
384 } else {
385 Ok(result)
386 }
387 }
388 }
389
390 /// Evaluate in `R_BaseEnv`.
391 ///
392 /// # Safety
393 ///
394 /// Same as [`eval`](Self::eval).
395 #[inline]
396 pub unsafe fn eval_base(&self) -> Result<SEXP, String> {
397 unsafe { self.eval(R_BaseEnv) }
398 }
399}
400// endregion
401
402// region: Error message extraction
403
404/// Extract the most recent R error message.
405///
406/// Uses `geterrmessage()` which is public R API (unlike `R_curErrorBuf`
407/// which is non-API). Falls back to a generic message if extraction fails.
408unsafe fn get_r_error_message() -> String {
409 unsafe {
410 // Call geterrmessage() — a public R function that returns the last
411 // error message as a character(1) string.
412 let call = ffi::Rf_lang1(Rf_install(c"geterrmessage".as_ptr()));
413 Rf_protect(call);
414
415 let mut err: std::os::raw::c_int = 0;
416 let msg_sexp = R_tryEvalSilent(call, R_BaseEnv, &mut err);
417
418 if err != 0 || msg_sexp.is_null() {
419 Rf_unprotect(1); // call
420 return "R error occurred (could not retrieve message)".to_string();
421 }
422
423 Rf_protect(msg_sexp);
424
425 // geterrmessage() returns character(1)
426 let result = if ffi::Rf_xlength(msg_sexp) > 0 {
427 let charsxp = msg_sexp.string_elt(0);
428 if !charsxp.is_null() {
429 let ptr = charsxp.r_char();
430 if !ptr.is_null() {
431 let msg = CStr::from_ptr(ptr).to_string_lossy().into_owned();
432 msg.trim_end().to_string()
433 } else {
434 "R error occurred".to_string()
435 }
436 } else {
437 "R error occurred".to_string()
438 }
439 } else {
440 "R error occurred".to_string()
441 };
442
443 Rf_unprotect(2); // call + msg_sexp
444 result
445 }
446}
447// endregion
448
449// region: Tests
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 // These tests verify compilation and basic invariants.
456 // Full integration tests require the R runtime.
457
458 #[test]
459 fn rcall_arg_accumulation() {
460 // Verify the builder pattern accumulates args correctly.
461 // We can't call R functions without an R runtime, but we can
462 // check that the Vec grows as expected.
463 let call = RCall {
464 fun: SEXP(std::ptr::null_mut()),
465 args: Vec::new(),
466 };
467 let call = call
468 .arg(SEXP(std::ptr::null_mut()))
469 .arg(SEXP(std::ptr::null_mut()));
470 assert_eq!(call.args.len(), 2);
471 assert!(call.args[0].0.is_none());
472 assert!(call.args[1].0.is_none());
473 }
474
475 #[test]
476 fn rcall_named_arg() {
477 let call = RCall {
478 fun: SEXP(std::ptr::null_mut()),
479 args: Vec::new(),
480 };
481 let call = call.named_arg("collapse", SEXP(std::ptr::null_mut()));
482 assert_eq!(call.args.len(), 1);
483 assert_eq!(
484 call.args[0].0.as_ref().unwrap(),
485 &CString::new("collapse").unwrap()
486 );
487 }
488
489 #[test]
490 fn renv_types_are_sized() {
491 // Just verify types compile and are sized
492 fn assert_sized<T: Sized>() {}
493 assert_sized::<RSymbol>();
494 assert_sized::<RCall>();
495 assert_sized::<REnv>();
496 }
497
498 #[test]
499 fn renv_constructors_compile() {
500 // Verify all REnv constructor signatures compile.
501 // Actual testing requires the R runtime.
502 fn assert_env_fn<F: FnOnce() -> REnv>(_f: F) {}
503 fn assert_env_result_fn<F: FnOnce() -> Result<REnv, String>>(_f: F) {}
504
505 assert_env_fn(|| unsafe { REnv::global() });
506 assert_env_fn(|| unsafe { REnv::base() });
507 assert_env_fn(|| unsafe { REnv::empty() });
508 assert_env_fn(REnv::base_namespace);
509 assert_env_fn(|| unsafe { REnv::caller() });
510 assert_env_result_fn(|| unsafe { REnv::package_namespace("base") });
511 }
512}
513// endregion