Skip to main content

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