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::gc_protect::{OwnedProtect, ProtectScope};
29use crate::sexp_ext::PairListExt;
30use crate::sys::{
31 self, ParseStatus, R_BaseEnv, R_EmptyEnv, R_GlobalEnv, R_ParseVector, R_tryEvalSilent,
32 Rf_install,
33};
34use crate::{SEXP, SexpExt};
35use std::ffi::{CStr, CString};
36
37// region: RSymbol
38
39/// A safe wrapper around R symbols (SYMSXP).
40///
41/// R symbols are interned strings used as variable and function names.
42/// They are never garbage collected, so `RSymbol` does not need GC protection.
43///
44/// # Example
45///
46/// ```ignore
47/// let sym = RSymbol::new("paste0");
48/// // sym.as_sexp() is a SYMSXP that can be used in call construction
49/// ```
50pub struct RSymbol {
51 sexp: SEXP,
52}
53
54impl RSymbol {
55 /// Create or retrieve an interned R symbol.
56 ///
57 /// # Safety
58 ///
59 /// Must be called from the R main thread.
60 ///
61 /// # Panics
62 ///
63 /// Panics if `name` contains a null byte.
64 #[inline]
65 pub unsafe fn new(name: &str) -> Self {
66 let c_name = CString::new(name).expect("symbol name must not contain null bytes");
67 RSymbol {
68 sexp: unsafe { Rf_install(c_name.as_ptr()) },
69 }
70 }
71
72 /// Create a symbol from a C string literal.
73 ///
74 /// This avoids the allocation needed by [`new`](Self::new) when you have
75 /// a `&CStr` available (e.g., from `c"name"` literals).
76 ///
77 /// # Safety
78 ///
79 /// Must be called from the R main thread.
80 #[inline]
81 pub unsafe fn from_cstr(name: &CStr) -> Self {
82 RSymbol {
83 sexp: unsafe { Rf_install(name.as_ptr()) },
84 }
85 }
86
87 /// Get the underlying SEXP.
88 #[inline]
89 pub fn as_sexp(&self) -> SEXP {
90 self.sexp
91 }
92}
93// endregion
94
95// region: REnv
96
97/// Handle to a well-known R environment.
98///
99/// Provides access to R's standard environments without raw FFI calls.
100pub struct REnv {
101 sexp: SEXP,
102}
103
104impl REnv {
105 /// The global environment (`R_GlobalEnv`).
106 ///
107 /// # Safety
108 ///
109 /// Must be called from the R main thread.
110 #[inline]
111 pub unsafe fn global() -> Self {
112 REnv {
113 sexp: unsafe { R_GlobalEnv },
114 }
115 }
116
117 /// The base environment (`R_BaseEnv`).
118 ///
119 /// # Safety
120 ///
121 /// Must be called from the R main thread.
122 #[inline]
123 pub unsafe fn base() -> Self {
124 REnv {
125 sexp: unsafe { R_BaseEnv },
126 }
127 }
128
129 /// The empty environment (`R_EmptyEnv`).
130 ///
131 /// # Safety
132 ///
133 /// Must be called from the R main thread.
134 #[inline]
135 pub unsafe fn empty() -> Self {
136 REnv {
137 sexp: unsafe { R_EmptyEnv },
138 }
139 }
140
141 /// The base namespace (`SEXP::base_namespace()`).
142 ///
143 /// Unlike [`base()`](Self::base) which is the base *environment* (exported
144 /// functions visible to users), this is the base *namespace* (includes
145 /// internal helpers). Rarely needed — prefer [`base()`](Self::base) unless
146 /// you specifically need unexported base internals.
147 ///
148 /// # Safety
149 ///
150 /// Must be called from the R main thread.
151 #[inline]
152 pub fn base_namespace() -> Self {
153 REnv {
154 sexp: SEXP::base_namespace(),
155 }
156 }
157
158 /// A package's namespace environment.
159 ///
160 /// Finds the namespace for a loaded package. Use this to evaluate functions
161 /// that live in a specific package (e.g., `slot()` from `methods`).
162 ///
163 /// This is a safe wrapper around `R_FindNamespace` — it uses
164 /// `R_tryEvalSilent` internally so that a missing namespace returns
165 /// `Err` instead of longjmping through Rust frames.
166 ///
167 /// # Safety
168 ///
169 /// Must be called from the R main thread.
170 ///
171 /// # Errors
172 ///
173 /// Returns `Err` if the package namespace is not found (package not loaded).
174 pub unsafe fn package_namespace(name: &str) -> Result<Self, String> {
175 unsafe {
176 let name_sexp = OwnedProtect::new(SEXP::scalar_string_from_str(name));
177 RCall::new("getNamespace")
178 .arg(name_sexp.get())
179 .eval(R_BaseEnv)
180 .map(|sexp| REnv { sexp })
181 }
182 }
183
184 /// The current execution environment.
185 ///
186 /// Returns the environment of the innermost active closure on R's call
187 /// stack, or the global environment if no closure is active.
188 ///
189 /// Useful when you need to evaluate an expression in the caller's context
190 /// rather than a fixed well-known environment.
191 ///
192 /// # Safety
193 ///
194 /// Must be called from the R main thread.
195 #[inline]
196 pub unsafe fn caller() -> Self {
197 REnv {
198 sexp: unsafe { sys::R_GetCurrentEnv() },
199 }
200 }
201
202 /// Wrap an arbitrary environment SEXP.
203 ///
204 /// # Safety
205 ///
206 /// `sexp` must be a valid ENVSXP.
207 #[inline]
208 pub unsafe fn from_sexp(sexp: SEXP) -> Self {
209 REnv { sexp }
210 }
211
212 /// Get the underlying SEXP.
213 #[inline]
214 pub fn as_sexp(&self) -> SEXP {
215 self.sexp
216 }
217}
218// endregion
219
220// region: RCall
221
222/// Builder for constructing and evaluating R function calls.
223///
224/// `RCall` constructs a LANGSXP (R language object) from a function name or
225/// SEXP and a sequence of arguments (optionally named). It handles GC
226/// protection during construction and evaluation.
227///
228/// # Example
229///
230/// ```ignore
231/// use miniextendr_api::expression::RCall;
232/// use miniextendr_api::sys;
233///
234/// unsafe {
235/// // seq_len(10)
236/// let result = RCall::new("seq_len")
237/// .arg(SEXP::scalar_integer(10))
238/// .eval_base()?;
239///
240/// // paste(x, collapse = ", ")
241/// let result = RCall::new("paste")
242/// .arg(some_sexp)
243/// .named_arg("collapse", sys::Rf_mkString(c", ".as_ptr()))
244/// .eval_base()?;
245/// }
246/// ```
247pub struct RCall {
248 /// Function symbol or SEXP.
249 fun: SEXP,
250 /// Arguments as (optional_name, value) pairs.
251 args: Vec<(Option<CString>, SEXP)>,
252}
253
254impl RCall {
255 /// Start building a call to a named R function.
256 ///
257 /// The function is looked up via `Rf_install`, which returns an interned symbol.
258 ///
259 /// # Safety
260 ///
261 /// Must be called from the R main thread.
262 ///
263 /// # Panics
264 ///
265 /// Panics if `fun_name` contains a null byte.
266 #[inline]
267 pub unsafe fn new(fun_name: &str) -> Self {
268 let c_name = CString::new(fun_name).expect("function name must not contain null bytes");
269 RCall {
270 fun: unsafe { Rf_install(c_name.as_ptr()) },
271 args: Vec::new(),
272 }
273 }
274
275 /// Start building a call to a function given as a C string literal.
276 ///
277 /// More efficient than [`new`](Self::new) when a `&CStr` is available.
278 ///
279 /// # Safety
280 ///
281 /// Must be called from the R main thread.
282 #[inline]
283 pub unsafe fn from_cstr(fun_name: &CStr) -> Self {
284 RCall {
285 fun: unsafe { Rf_install(fun_name.as_ptr()) },
286 args: Vec::new(),
287 }
288 }
289
290 /// Start building a call with a function SEXP (closure, builtin, etc.).
291 ///
292 /// # Safety
293 ///
294 /// `fun` must be a valid SEXP representing a callable R object.
295 #[inline]
296 pub unsafe fn from_sexp(fun: SEXP) -> Self {
297 RCall {
298 fun,
299 args: Vec::new(),
300 }
301 }
302
303 /// Add a positional argument.
304 #[inline]
305 pub fn arg(mut self, value: SEXP) -> Self {
306 self.args.push((None, value));
307 self
308 }
309
310 /// Add a named argument.
311 ///
312 /// # Panics
313 ///
314 /// Panics if `name` contains a null byte.
315 #[inline]
316 pub fn named_arg(mut self, name: &str, value: SEXP) -> Self {
317 let c_name = CString::new(name).expect("argument name must not contain null bytes");
318 self.args.push((Some(c_name), value));
319 self
320 }
321
322 /// Build the LANGSXP without evaluating it.
323 ///
324 /// The returned SEXP is **unprotected**. The caller must protect it if
325 /// further allocations will occur before use.
326 ///
327 /// # Safety
328 ///
329 /// Must be called from the R main thread. All argument SEXPs must still
330 /// be valid (protected or otherwise reachable by R's GC).
331 pub unsafe fn build(&self) -> SEXP {
332 unsafe {
333 // Build the argument pairlist from back to front using Rf_cons.
334 // ProtectScope tracks all intermediate cons cells and the final
335 // LANGSXP head, then unprotects them all on drop. The returned
336 // call is unprotected — caller protects it if needed.
337 let scope = ProtectScope::new();
338
339 let mut tail = SEXP::nil();
340 for (name, value) in self.args.iter().rev() {
341 tail = scope.protect_raw(value.cons(tail));
342 if let Some(c_name) = name {
343 tail.set_tag(Rf_install(c_name.as_ptr()));
344 }
345 }
346
347 // Prepend the function as LANGSXP head.
348 // ProtectScope drops here; the call is unprotected on return
349 // (callers re-protect via OwnedProtect before invoking eval).
350 scope.protect_raw(self.fun.lcons(tail))
351 }
352 }
353
354 /// Evaluate the call in the given environment.
355 ///
356 /// Uses `R_tryEvalSilent` so that R errors are captured as `Err(String)`
357 /// rather than causing a longjmp through Rust frames.
358 ///
359 /// # Safety
360 ///
361 /// - Must be called from the R main thread.
362 /// - `env` must be a valid ENVSXP.
363 /// - All argument SEXPs must still be valid.
364 ///
365 /// # Returns
366 ///
367 /// - `Ok(SEXP)` with the result (unprotected — caller should protect if needed)
368 /// - `Err(String)` with the R error message on failure
369 pub unsafe fn eval(&self, env: SEXP) -> Result<SEXP, String> {
370 unsafe {
371 let call = OwnedProtect::new(self.build());
372
373 let mut error_occurred: std::os::raw::c_int = 0;
374 let result = R_tryEvalSilent(call.get(), env, &mut error_occurred);
375
376 if error_occurred != 0 {
377 Err(get_r_error_message())
378 } else {
379 Ok(result)
380 }
381 }
382 }
383
384 /// Evaluate in `R_BaseEnv`.
385 ///
386 /// # Safety
387 ///
388 /// Same as [`eval`](Self::eval).
389 #[inline]
390 pub unsafe fn eval_base(&self) -> Result<SEXP, String> {
391 unsafe { self.eval(R_BaseEnv) }
392 }
393
394 /// Start building a namespaced call: `pkg::fun(args…)`.
395 ///
396 /// Looks up `pkg::fun_name` in the base environment and uses the resolved
397 /// function closure as the call target. This respects R's namespace lookup
398 /// rules (exported + non-exported via `::` / `:::`).
399 ///
400 /// This is the runtime counterpart of the lowered `pkg::fn(args…)` form
401 /// in `r!(pkg::fn(args…))`.
402 ///
403 /// # Safety
404 ///
405 /// Must be called from the R main thread.
406 ///
407 /// # Panics
408 ///
409 /// Panics if `pkg` or `fun_name` contain a null byte.
410 ///
411 /// # Errors
412 ///
413 /// Returns `Err(String)` if the namespace lookup fails (package not loaded
414 /// or function not found).
415 pub unsafe fn namespaced(pkg: &str, fun_name: &str) -> Result<Self, String> {
416 unsafe {
417 // Build (:: pkg fun_name) as a LANGSXP and evaluate it to get the closure.
418 let ns_op = Rf_install(c"::".as_ptr());
419 let pkg_sym = {
420 let c = CString::new(pkg).expect("pkg name must not contain null bytes");
421 Rf_install(c.as_ptr())
422 };
423 let fun_sym = {
424 let c = CString::new(fun_name).expect("fun name must not contain null bytes");
425 Rf_install(c.as_ptr())
426 };
427
428 // Build `(:: pkg fun_name)` pairlist from back to front.
429 let scope = crate::gc_protect::ProtectScope::new();
430 let nil = crate::sys::R_NilValue;
431 let fun_cons = scope.protect_raw(fun_sym.cons(nil));
432 let pkg_cons = scope.protect_raw(pkg_sym.cons(fun_cons));
433 let ns_call = scope.protect_raw(ns_op.lcons(pkg_cons));
434
435 // Evaluate to resolve the function closure.
436 let mut err: std::os::raw::c_int = 0;
437 let fun_sexp = R_tryEvalSilent(ns_call, R_BaseEnv, &mut err);
438 if err != 0 {
439 return Err(get_r_error_message());
440 }
441
442 // fun_sexp is the closure; it is reachable via R_GlobalEnv bindings,
443 // so we don't need explicit protection for the RCall builder lifetime.
444 Ok(RCall {
445 fun: fun_sexp,
446 args: Vec::new(),
447 })
448 }
449 }
450}
451
452/// Build and evaluate `target$name` — the R `$` extraction operator.
453///
454/// This is a convenience wrapper that avoids hand-rolling
455/// `Rf_install("$") + Rf_lang3(...) + R_tryEvalSilent(...)` ladders.
456/// Equivalent to:
457///
458/// ```ignore
459/// RCall::new("$")
460/// .arg(target)
461/// .arg(SEXP::scalar_string_from_str(name))
462/// .eval_base()
463/// ```
464///
465/// but uses the more direct LANGSXP form internally and protects all
466/// intermediate allocations via RAII.
467///
468/// # Safety
469///
470/// - Must be called from the R main thread.
471/// - `target` must be a valid SEXP (typically a list, environment, or S4
472/// object that supports `$` extraction).
473///
474/// # Returns
475///
476/// - `Ok(SEXP)` with the extracted value (unprotected — caller should protect if needed).
477/// - `Err(String)` with the R error message if `$` extraction fails or the
478/// evaluation errors.
479pub unsafe fn dollar_extract(target: SEXP, name: &str) -> Result<SEXP, String> {
480 unsafe {
481 let name_sexp = OwnedProtect::new(SEXP::scalar_string_from_str(name));
482 RCall::new("$").arg(target).arg(name_sexp.get()).eval_base()
483 }
484}
485// endregion
486
487// region: r_eval_str (runtime string parse + eval)
488
489/// Parse a string of R source and evaluate it in `env`.
490///
491/// This is the runtime workhorse behind the [`r_str!`](crate::r_str) and
492/// [`r!`](crate::r) macros. It performs the full
493/// `R_ParseVector` → check status → `Rf_eval` ladder with correct GC
494/// protection on every intermediate SEXP, so callers never have to hand-roll
495/// `OwnedProtect` around the parse tree.
496///
497/// Only the **last** top-level expression's value is returned (matching R's
498/// `eval(parse(text = ...))` semantics): each parsed expression is evaluated in
499/// order so that side effects (assignments, `library()`, …) take effect, and
500/// the value of the final one is returned. An empty / whitespace-only string
501/// yields `R_NilValue`.
502///
503/// # Safety
504///
505/// - Must be called from (or routed to) the R main thread. The parse and eval
506/// FFI calls go through the checked `#[r_ffi_checked]` variants, which
507/// serialize onto the R thread via `with_r_thread`, so calling from a
508/// worker thread is sound — but the returned SEXP must not outlive the R
509/// session.
510/// - `env` must be a valid ENVSXP.
511///
512/// # Returns
513///
514/// - `Ok(SEXP)` with the value of the last expression (**unprotected** — the
515/// caller should protect it if further allocations will occur before use).
516/// - `Err(String)` if parsing fails (syntax error / incomplete input) or if
517/// evaluation raises an R error. The error is captured via
518/// `R_tryEvalSilent` + `geterrmessage()`, so it never longjmps through Rust
519/// frames.
520///
521/// # Example
522///
523/// ```ignore
524/// use miniextendr_api::expression::r_eval_str;
525/// use miniextendr_api::sys::R_GlobalEnv;
526///
527/// unsafe {
528/// let three = r_eval_str("1L + 2L", R_GlobalEnv)?;
529/// // three is an INTSXP holding 3
530/// }
531/// ```
532pub unsafe fn r_eval_str(code: &str, env: SEXP) -> Result<SEXP, String> {
533 unsafe {
534 // 1. Wrap the source in a length-1 STRSXP. scalar_string_from_str
535 // allocates a CHARSXP + STRSXP; protect it across the parse, which
536 // allocates again.
537 let code_sexp = OwnedProtect::new(SEXP::scalar_string_from_str(code));
538
539 // 2. Parse. R_ParseVector returns an EXPRSXP (a vector of expressions).
540 // Protect it across the subsequent VECTOR_ELT / Rf_eval allocations.
541 let mut status = ParseStatus::PARSE_NULL;
542 let parsed = R_ParseVector(code_sexp.get(), -1, &mut status, sys::R_NilValue);
543
544 match status {
545 ParseStatus::PARSE_OK => {}
546 ParseStatus::PARSE_INCOMPLETE => {
547 return Err(format!(
548 "incomplete R expression (unbalanced delimiter?): {code}"
549 ));
550 }
551 ParseStatus::PARSE_ERROR => {
552 return Err(format!("R syntax error while parsing: {code}"));
553 }
554 ParseStatus::PARSE_EOF => {
555 return Err(format!("unexpected end of input while parsing: {code}"));
556 }
557 ParseStatus::PARSE_NULL => {
558 return Err(format!("R_ParseVector returned PARSE_NULL for: {code}"));
559 }
560 }
561
562 let parsed = OwnedProtect::new(parsed);
563
564 // 3. Evaluate each parsed expression in order; return the value of the
565 // last one. An empty EXPRSXP (blank source) yields R_NilValue.
566 let n = parsed.get().xlength();
567 let mut result = sys::R_NilValue;
568 for i in 0..n {
569 // VECTOR_ELT borrows from `parsed` (still protected). The element
570 // is part of the protected EXPRSXP, so it stays reachable.
571 let expr = parsed.get().vector_elt(i);
572
573 let mut error_occurred: std::os::raw::c_int = 0;
574 result = R_tryEvalSilent(expr, env, &mut error_occurred);
575 if error_occurred != 0 {
576 return Err(get_r_error_message());
577 }
578 }
579
580 Ok(result)
581 }
582}
583
584/// Parse and evaluate a string of R source in `R_GlobalEnv`.
585///
586/// Convenience wrapper over [`r_eval_str`] for the common case. See that
587/// function for safety and return semantics.
588///
589/// # Safety
590///
591/// Same as [`r_eval_str`].
592#[inline]
593pub unsafe fn r_eval_str_global(code: &str) -> Result<SEXP, String> {
594 unsafe { r_eval_str(code, R_GlobalEnv) }
595}
596// endregion
597
598// region: Error message extraction
599
600/// Extract the most recent R error message.
601///
602/// Uses `geterrmessage()` which is public R API (unlike `R_curErrorBuf`
603/// which is non-API). Falls back to a generic message if extraction fails.
604unsafe fn get_r_error_message() -> String {
605 unsafe {
606 // Call geterrmessage() — a public R function that returns the last
607 // error message as a character(1) string.
608 let call = OwnedProtect::new(sys::Rf_lang1(Rf_install(c"geterrmessage".as_ptr())));
609
610 let mut err: std::os::raw::c_int = 0;
611 let msg_sexp = R_tryEvalSilent(call.get(), R_BaseEnv, &mut err);
612
613 if err != 0 || msg_sexp.is_null() {
614 return "R error occurred (could not retrieve message)".to_string();
615 }
616
617 let _msg_guard = OwnedProtect::new(msg_sexp);
618
619 // geterrmessage() returns character(1)
620 if msg_sexp.xlength() > 0 {
621 let charsxp = msg_sexp.string_elt(0);
622 if let Some(msg) = crate::from_r::charsxp_to_string_lossy(charsxp) {
623 return msg.trim_end().to_string();
624 }
625 }
626 "R error occurred".to_string()
627 }
628}
629// endregion
630
631// region: Tests
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 // These tests verify compilation and basic invariants.
638 // Full integration tests require the R runtime.
639
640 #[test]
641 fn rcall_arg_accumulation() {
642 // Verify the builder pattern accumulates args correctly.
643 // We can't call R functions without an R runtime, but we can
644 // check that the Vec grows as expected.
645 let call = RCall {
646 fun: SEXP(std::ptr::null_mut()),
647 args: Vec::new(),
648 };
649 let call = call
650 .arg(SEXP(std::ptr::null_mut()))
651 .arg(SEXP(std::ptr::null_mut()));
652 assert_eq!(call.args.len(), 2);
653 assert!(call.args[0].0.is_none());
654 assert!(call.args[1].0.is_none());
655 }
656
657 #[test]
658 fn rcall_named_arg() {
659 let call = RCall {
660 fun: SEXP(std::ptr::null_mut()),
661 args: Vec::new(),
662 };
663 let call = call.named_arg("collapse", SEXP(std::ptr::null_mut()));
664 assert_eq!(call.args.len(), 1);
665 assert_eq!(
666 call.args[0].0.as_ref().unwrap(),
667 &CString::new("collapse").unwrap()
668 );
669 }
670
671 #[test]
672 fn renv_types_are_sized() {
673 // Just verify types compile and are sized
674 fn assert_sized<T: Sized>() {}
675 assert_sized::<RSymbol>();
676 assert_sized::<RCall>();
677 assert_sized::<REnv>();
678 }
679
680 #[test]
681 fn renv_constructors_compile() {
682 // Verify all REnv constructor signatures compile.
683 // Actual testing requires the R runtime.
684 fn assert_env_fn<F: FnOnce() -> REnv>(_f: F) {}
685 fn assert_env_result_fn<F: FnOnce() -> Result<REnv, String>>(_f: F) {}
686
687 assert_env_fn(|| unsafe { REnv::global() });
688 assert_env_fn(|| unsafe { REnv::base() });
689 assert_env_fn(|| unsafe { REnv::empty() });
690 assert_env_fn(REnv::base_namespace);
691 assert_env_fn(|| unsafe { REnv::caller() });
692 assert_env_result_fn(|| unsafe { REnv::package_namespace("base") });
693 }
694}
695// endregion