Repository: extendr/extendr
Pull request #238 (2021-07-22 17:55:07)

Calling r functions in extension packages
Allow more flexible expressions in call! macros and update the Function
wrapper to accept primitives.


andy-thomason: There are many ways we can optimise here when the time comes.

We could even generate stubs for R libraries at compile time with something like:

```
mod base {
   include_R_namespace!("base");
}

mod stats {
   include_R_namespace!("stats");
}


...

base::sum(...);
rstats::dbeta(...);
```
andy-thomason: If there are no objections, I'll merge.
clauswilke: Yes, go ahead.

Patch:
diff --git a/README.md b/README.md
index 1f115cac1..55033db9c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # extendr - A safe and user friendly R extension interface using Rust.
 
 [![Github Actions Build Status](https://github.com/extendr/extendr/workflows/Tests/badge.svg)](https://github.com/extendr/extendr/actions)
-[![Crates.io](http://meritbadge.herokuapp.com/extendr-api)](https://crates.io/crates/extendr-api)
+[![Crates.io](https://img.shields.io/crates/v/extendr-api.svg)](https://crates.io/crates/extendr-api)
 [![Documentation](https://docs.rs/extendr-api/badge.svg)](https://docs.rs/extendr-api)
 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 
diff --git a/extendr-api/README.md b/extendr-api/README.md
index a728bd3d0..29885433e 100644
--- a/extendr-api/README.md
+++ b/extendr-api/README.md
@@ -180,7 +180,7 @@ use extendr_api::prelude::*;
 test! {
 
     // As one R! macro call
-    let confint1 = R!(confint(lm(weight ~ group - 1, PlantGrowth)))?;
+    let confint1 = R!("confint(lm(weight ~ group - 1, PlantGrowth))")?;
 
     // As many parameterized calls.
     let formula = call!("~", sym!(weight), lang!("-", sym!(group), 1))?;
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index 7d6599d64..c0a7a6f1b 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -1,3 +1,4 @@
+use crate as extendr_api;
 use crate::*;
 
 /// Get a global variable from global_env() and ancestors.
diff --git a/extendr-api/src/lang_macros.rs b/extendr-api/src/lang_macros.rs
index 00362ecdb..30595d107 100644
--- a/extendr-api/src/lang_macros.rs
+++ b/extendr-api/src/lang_macros.rs
@@ -92,31 +92,6 @@ macro_rules! append_lang {
     };
 }
 
-/// The call! macro calls an R function with Rust parameters.
-/// Equivalent to `lang!(sym, params).eval()`
-/// This returns a Rust Result.
-///
-/// Example:
-/// ```
-/// use extendr_api::prelude::*;
-/// test! {
-///     let vec = call!("c", 1.0, 2.0, 3.0).unwrap();
-///     assert_eq!(vec, r!([1., 2., 3.]));
-///
-///     let list = call!("list", a=1, b=2).unwrap();
-///     assert_eq!(list.len(), 2);
-///
-///     let three = call!("+", 1, 2).unwrap();
-///     assert_eq!(three, r!(3));
-/// }
-/// ```
-#[macro_export]
-macro_rules! call {
-    ($($toks: tt)*) => {
-        lang!($($toks)*).eval()
-    }
-}
-
 /// A macro for constructing R langage objects.
 ///
 /// Example:
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 531887db6..f97198359 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -178,10 +178,10 @@
 //! test! {
 //!
 //!     // As one R! macro call
-//!     let confint1 = R!(confint(lm(weight ~ group - 1, PlantGrowth)))?;
+//!     let confint1 = R!("confint(lm(weight ~ group - 1, PlantGrowth))")?;
 //!    
 //!     // As many parameterized calls.
-//!     let formula = call!("~", sym!(weight), lang!("-", sym!(group), 1))?;
+//!     let formula = lang!("~", sym!(weight), lang!("-", sym!(group), 1.0)).set_class(["formula"])?;
 //!     let plant_growth = global!(PlantGrowth)?;
 //!     let model = call!("lm", formula, plant_growth)?;
 //!     let confint2 = call!("confint", model)?;
@@ -657,12 +657,12 @@ mod tests {
                 // General integer types.
                 assert_eq!(new_owned(wrap__return_u8()), Robj::from(123));
                 assert_eq!(new_owned(wrap__return_u16()), Robj::from(123));
-                assert_eq!(new_owned(wrap__return_u32()), Robj::from(123));
-                assert_eq!(new_owned(wrap__return_u64()), Robj::from(123));
+                assert_eq!(new_owned(wrap__return_u32()), Robj::from(123.));
+                assert_eq!(new_owned(wrap__return_u64()), Robj::from(123.));
                 assert_eq!(new_owned(wrap__return_i8()), Robj::from(123));
                 assert_eq!(new_owned(wrap__return_i16()), Robj::from(123));
                 assert_eq!(new_owned(wrap__return_i32()), Robj::from(123));
-                assert_eq!(new_owned(wrap__return_i64()), Robj::from(123));
+                assert_eq!(new_owned(wrap__return_i64()), Robj::from(123.));
 
                 // Floating point types.
                 assert_eq!(new_owned(wrap__return_f32()), Robj::from(123.));
@@ -758,12 +758,12 @@ mod tests {
         //
 
         test! {
-            let txt_con = R!(textConnection("test_con", open = "w")).unwrap();
+            let txt_con = R!(r#"textConnection("test_con", open = "w")"#).unwrap();
             call!("sink", &txt_con).unwrap();
             rprintln!("Hello world");
             call!("sink").unwrap();
             call!("close", &txt_con).unwrap();
-            let result = R!(test_con).unwrap();
+            let result = R!("test_con").unwrap();
             assert_eq!(result, r!("Hello world"));
         }
     }
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index 19f29e37a..ccc2896c0 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -26,7 +26,7 @@ pub use super::wrapper::symbol::{
     spec_symbol, triple_colon_symbol, tsp_symbol, unbound_value,
 };
 
-pub use crate::{append, append_lang, append_with_name, args, call, lang, make_lang};
+pub use crate::{append, append_lang, append_with_name, args, lang, make_lang};
 pub use crate::{
     data_frame, factor, global, list, r, reprint, reprintln, rprint, rprintln, sym, test, var,
 };
@@ -52,7 +52,7 @@ pub use super::robj_ndarray::*;
 #[cfg(feature = "ndarray")]
 pub use ndarray::*;
 
-pub use extendr_macros::{extendr, extendr_module, pairlist, Rraw, R};
+pub use extendr_macros::{call, extendr, extendr_module, pairlist, Rraw, R};
 
 pub use super::iter::{Int, Logical, Real, StrIter};
 
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 5fc0bcfa0..8bb389f19 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -150,6 +150,12 @@ macro_rules! impl_real_tvv {
 
 impl_real_tvv!(f64);
 impl_real_tvv!(f32);
+
+// Since these types might exceeds the max or min of R's 32bit integer, we need
+// to return as REALSXP
+impl_real_tvv!(i64);
+impl_real_tvv!(u32);
+impl_real_tvv!(u64);
 impl_real_tvv!(usize);
 
 macro_rules! impl_integer_tvv {
@@ -193,11 +199,8 @@ macro_rules! impl_integer_tvv {
 impl_integer_tvv!(i8);
 impl_integer_tvv!(i16);
 impl_integer_tvv!(i32);
-impl_integer_tvv!(i64);
 impl_integer_tvv!(u8);
 impl_integer_tvv!(u16);
-impl_integer_tvv!(u32);
-impl_integer_tvv!(u64);
 
 macro_rules! impl_str_tvv {
     ($t: ty) => {
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 63d298b0e..9441a176b 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -828,7 +828,7 @@ impl Robj {
     /// use extendr_api::prelude::*;
     /// test! {
     ///
-    ///    let array = R!(array(data = c(1, 2, 3, 4), dim = c(2, 2), dimnames = list(c("x", "y"), c("a","b")))).unwrap();
+    ///    let array = R!(r#"array(data = c(1, 2, 3, 4), dim = c(2, 2), dimnames = list(c("x", "y"), c("a","b")))"#).unwrap();
     ///    let dim : Vec<_> = array.dim().unwrap().collect();
     ///    assert_eq!(dim, vec![2, 2]);
     /// }
@@ -845,7 +845,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///    let array = R!(array(data = c(1, 2, 3, 4), dim = c(2, 2), dimnames = list(c("x", "y"), c("a","b")))).unwrap();
+    ///    let array = R!(r#"array(data = c(1, 2, 3, 4), dim = c(2, 2), dimnames = list(c("x", "y"), c("a","b")))"#).unwrap();
     ///    let names : Vec<_> = array.dimnames().unwrap().collect();
     ///    assert_eq!(names, vec![r!(["x", "y"]), r!(["a", "b"])]);
     /// }
diff --git a/extendr-api/src/robj/operators.rs b/extendr-api/src/robj/operators.rs
index 27386d0f1..5d0660249 100644
--- a/extendr-api/src/robj/operators.rs
+++ b/extendr-api/src/robj/operators.rs
@@ -1,3 +1,4 @@
+use crate as extendr_api;
 use crate::*;
 use std::ops::{Add, Div, Mul, Sub};
 
@@ -20,7 +21,7 @@ impl Robj {
         T: AsRef<str>,
     {
         let symbol: Symbol = Symbol::from_string(symbol.as_ref());
-        call!("$", self, symbol)
+        call!("`$`", self, symbol)
     }
 
     /// Do the equivalent of `x[y]`
@@ -36,7 +37,7 @@ impl Robj {
     where
         T: Into<Robj>,
     {
-        call!("[", self, rhs.into())
+        call!("`[`", self, rhs.into())
     }
 
     /// Do the equivalent of `x[[y]]`
@@ -52,7 +53,7 @@ impl Robj {
     where
         T: Into<Robj>,
     {
-        call!("[[", self, rhs.into())
+        call!("`[[`", self, rhs.into())
     }
 
     /// Do the equivalent of x ~ y
@@ -69,24 +70,24 @@ impl Robj {
     where
         T: Into<Robj>,
     {
-        call!("~", self, rhs.into())
+        call!("`~`", self, rhs.into())
     }
 
     /// Do the equivalent of x :: y
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    /// let base = r!(Symbol::from_string("base"));
-    /// let env = r!(Symbol::from_string(".getNamespace"));
-    /// let base_env = base.double_colon(env).unwrap();
-    /// assert_eq!(base_env.is_function(), true);
+    ///     let base = r!(Symbol::from_string("base"));
+    ///     let list = r!(Symbol::from_string("list"));
+    ///     let base_list = base.double_colon(list).unwrap();
+    ///     assert_eq!(base_list.is_function(), true);
     /// }
     /// ```
     pub fn double_colon<T>(&self, rhs: T) -> Result<Robj>
     where
         T: Into<Robj>,
     {
-        call!("::", self, rhs.into())
+        call!("`::`", self, rhs.into())
     }
 
     /// Do the equivalent of x(a, b, c)
@@ -99,13 +100,13 @@ impl Robj {
     /// }
     /// ```
     pub fn call(&self, args: Pairlist) -> Result<Robj> {
-        if self.rtype() != RType::Function {
-            return Err(Error::ExpectedFunction(self.clone()));
-        }
-
-        unsafe {
-            let call = new_owned(Rf_lcons(self.get(), args.get()));
-            call.eval()
+        if self.is_function() {
+            unsafe {
+                let call = new_owned(Rf_lcons(self.get(), args.get()));
+                call.eval()
+            }
+        } else {
+            Err(Error::ExpectedFunction(self.clone()))
         }
     }
 }
@@ -138,7 +139,7 @@ where
     /// }
     /// ```
     fn add(self, rhs: Rhs) -> Self::Output {
-        call!("+", self, rhs.into()).expect("Robj add failed")
+        call!("`+`", self, rhs.into()).expect("Robj add failed")
     }
 }
 
@@ -170,7 +171,7 @@ where
     /// }
     /// ```
     fn sub(self, rhs: Rhs) -> Self::Output {
-        call!("-", self, rhs.into()).expect("Robj subtract failed")
+        call!("`-`", self, rhs.into()).expect("Robj subtract failed")
     }
 }
 
@@ -202,7 +203,7 @@ where
     /// }
     /// ```
     fn mul(self, rhs: Rhs) -> Self::Output {
-        call!("*", self, rhs.into()).expect("Robj multiply failed")
+        call!("`*`", self, rhs.into()).expect("Robj multiply failed")
     }
 }
 
@@ -234,7 +235,7 @@ where
     /// }
     /// ```
     fn div(self, rhs: Rhs) -> Self::Output {
-        call!("/", self, rhs.into()).expect("Robj divide failed")
+        call!("`/`", self, rhs.into()).expect("Robj divide failed")
     }
 }
 
diff --git a/extendr-api/src/robj/rinternals.rs b/extendr-api/src/robj/rinternals.rs
index 90e8fb0f7..39041e55f 100644
--- a/extendr-api/src/robj/rinternals.rs
+++ b/extendr-api/src/robj/rinternals.rs
@@ -295,7 +295,7 @@ impl Robj {
         unsafe { Rf_isFrame(self.get()) != 0 }
     }
 
-    /// Return true if this is a function.
+    /// Return true if this is a function or a primitive (CLOSXP, BUILTINSXP or SPECIALSXP)
     pub fn is_function(&self) -> bool {
         unsafe { Rf_isFunction(self.get()) != 0 }
     }
diff --git a/extendr-api/src/robj/try_from_robj.rs b/extendr-api/src/robj/try_from_robj.rs
index cbb9e387f..b9362fff2 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -149,6 +149,20 @@ impl TryFrom<Robj> for Vec<String> {
     }
 }
 
+// The following fails because T could be Option<U>
+//
+// impl<T : TryFrom<Robj>> TryFrom<Robj> for Option<T> {
+//     type Error = Error;
+
+//     fn try_from(robj: Robj) -> Result<Self> {
+//         if robj.is_na() {
+//             Ok(None)
+//         } else {
+//             Ok(Some(T::try_from(robj)?))
+//         }
+//     }
+// }
+
 macro_rules! impl_option {
     ($type : ty) => {
         impl TryFrom<Robj> for Option<$type> {
diff --git a/extendr-api/src/wrapper/environment.rs b/extendr-api/src/wrapper/environment.rs
index de63f90c0..20ac1e2c5 100644
--- a/extendr-api/src/wrapper/environment.rs
+++ b/extendr-api/src/wrapper/environment.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate as extendr_api;
 
 #[derive(Debug, PartialEq, Clone)]
 pub struct Environment {
diff --git a/extendr-api/src/wrapper/function.rs b/extendr-api/src/wrapper/function.rs
index 765f48c4d..8cd843be2 100644
--- a/extendr-api/src/wrapper/function.rs
+++ b/extendr-api/src/wrapper/function.rs
@@ -4,15 +4,23 @@ use super::*;
 /// ```
 /// use extendr_api::prelude::*;
 /// test! {
-///     let expr = R!("function(a = 1, b) {c <- a + b}").unwrap();
+///     // Closures are functions.
+///     let expr = R!("function(a = 1, b) {c <- a + b}")?;
 ///     let func = expr.as_function().unwrap();
 ///
 ///     let expected_formals = Pairlist::from_pairs(vec![("a", r!(1.0)), ("b", missing_arg().into())]);
 ///     let expected_body = lang!(
 ///         "{", lang!("<-", sym!(c), lang!("+", sym!(a), sym!(b))));
-///     assert_eq!(func.formals().as_pairlist().unwrap(), expected_formals);
-///     assert_eq!(func.body(), expected_body);
-///     assert_eq!(func.environment(), global_env());
+///     assert_eq!(func.formals().unwrap(), expected_formals);
+///     assert_eq!(func.body().unwrap(), expected_body);
+///     assert_eq!(func.environment().unwrap(), global_env());
+///
+///     // Primitives can also be functions.
+///     let expr = R!("`~`")?;
+///     let func = expr.as_function().unwrap();
+///     assert_eq!(func.formals(), None);
+///     assert_eq!(func.body(), None);
+///     assert_eq!(func.environment(), None);
 /// }
 /// ```
 #[derive(Debug, PartialEq, Clone)]
@@ -47,37 +55,54 @@ impl Function {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let function = R!(function(a, b) a + b).unwrap().as_function().unwrap();
+    ///     let function = R!("function(a, b) a + b").unwrap().as_function().unwrap();
     ///     assert_eq!(function.call(pairlist!(a=1, b=2)).unwrap(), r!(3));
     /// }
     /// ```
     pub fn call(&self, args: Pairlist) -> Result<Robj> {
-        self.robj.call(args)
+        unsafe {
+            let call = new_owned(Rf_lcons(self.get(), args.get()));
+            call.eval()
+        }
     }
 
-    /// Get the formal arguments of the function.
-    pub fn formals(&self) -> Pairlist {
+    /// Get the formal arguments of the function or None if it is a primitive.
+    pub fn formals(&self) -> Option<Pairlist> {
         unsafe {
-            let sexp = self.robj.get();
-            new_owned(FORMALS(sexp)).try_into().unwrap()
+            if self.rtype() == RType::Function {
+                let sexp = self.robj.get();
+                Some(new_owned(FORMALS(sexp)).try_into().unwrap())
+            } else {
+                None
+            }
         }
     }
 
-    /// Get the body of the function.
-    pub fn body(&self) -> Robj {
+    /// Get the body of the function or None if it is a primitive.
+    pub fn body(&self) -> Option<Robj> {
         unsafe {
-            let sexp = self.robj.get();
-            new_owned(BODY(sexp))
+            if self.rtype() == RType::Function {
+                let sexp = self.robj.get();
+                Some(new_owned(BODY(sexp)))
+            } else {
+                None
+            }
         }
     }
 
-    /// Get the environment of the function.
-    pub fn environment(&self) -> Environment {
+    /// Get the environment of the function or None if it is a primitive.
+    pub fn environment(&self) -> Option<Environment> {
         unsafe {
-            let sexp = self.robj.get();
-            new_owned(CLOENV(sexp))
-                .try_into()
-                .expect("Should be an environment")
+            if self.rtype() == RType::Function {
+                let sexp = self.robj.get();
+                Some(
+                    new_owned(CLOENV(sexp))
+                        .try_into()
+                        .expect("Should be an environment"),
+                )
+            } else {
+                None
+            }
         }
     }
 }
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index 56fd5e8a7..2ce4c22fe 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -100,7 +100,12 @@ macro_rules! make_conversions {
 }
 
 make_conversions!(Pairlist, ExpectedPairlist, is_pairlist, "Not a pairlist");
-make_conversions!(Function, ExpectedFunction, is_function, "Not a function");
+make_conversions!(
+    Function,
+    ExpectedFunction,
+    is_function,
+    "Not a function or primitive."
+);
 make_conversions!(Raw, ExpectedRaw, is_raw, "Not a raw object");
 
 make_conversions!(
diff --git a/extendr-api/src/wrapper/pairlist.rs b/extendr-api/src/wrapper/pairlist.rs
index 3d7feadb5..57e32e64f 100644
--- a/extendr-api/src/wrapper/pairlist.rs
+++ b/extendr-api/src/wrapper/pairlist.rs
@@ -8,6 +8,11 @@ pub struct Pairlist {
 }
 
 impl Pairlist {
+    pub fn new() -> Self {
+        let robj = Robj::from(());
+        Self { robj }
+    }
+
     /// Convert an iterator of names and values to a pairlist object.
     /// ```
     /// use extendr_api::prelude::*;
@@ -19,7 +24,7 @@ impl Pairlist {
     ///     // Use "" to indicate the absense of the name
     ///     let unnamed_pairlist = Pairlist::from_pairs([("", "a"), ("", "b")]);
     ///     assert_eq!(call!("names", unnamed_pairlist)?, r!(NULL));
-    ///     let unnamed_pairlist_r = R!(pairlist("a", "b"))?.as_pairlist().unwrap();
+    ///     let unnamed_pairlist_r = R!(r#"pairlist("a", "b")"#)?.as_pairlist().unwrap();
     ///     assert_eq!(unnamed_pairlist_r.names().collect::<Vec<_>>(), vec!["", ""]);
     /// }
     /// ```
@@ -77,6 +82,12 @@ impl Pairlist {
     }
 }
 
+impl Default for wrapper::pairlist::Pairlist {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 /// Generate paits of names and values.
 /// ```
 /// use extendr_api::prelude::*;
diff --git a/extendr-macros/src/call.rs b/extendr-macros/src/call.rs
new file mode 100644
index 000000000..3b4284596
--- /dev/null
+++ b/extendr-macros/src/call.rs
@@ -0,0 +1,68 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse::ParseStream, punctuated::Punctuated, Token};
+use syn::{parse_macro_input, parse_quote, Expr, ExprAssign, ExprPath, LitStr};
+
+#[derive(Debug)]
+struct Call {
+    caller: LitStr,
+    pairs: Punctuated<Expr, Token![,]>,
+}
+
+// Custom parser for a call eg. call!("xyz", a=1, b, c)
+impl syn::parse::Parse for Call {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut res = Self {
+            caller: input.parse::<LitStr>()?,
+            pairs: Punctuated::new(),
+        };
+
+        while !input.is_empty() {
+            input.parse::<Token![,]>()?;
+            res.pairs.push(input.parse::<Expr>()?);
+        }
+        Ok(res)
+    }
+}
+
+pub fn call(item: TokenStream) -> TokenStream {
+    // Get a [Call] object from the input token stream.
+    // This consists of a literal string followed by named or unnamed arguments
+    // as in the pairlist macro.
+    let call = parse_macro_input!(item as Call);
+
+    // Convert the pairs into tuples of ("name", Robj::from(value))
+    let pairs = call
+        .pairs
+        .iter()
+        .map(|e| {
+            if let Expr::Assign(ExprAssign { left, right, .. }) = e {
+                if let Expr::Path(ExprPath { path, .. }) = &**left {
+                    if let Some(ident) = path.get_ident() {
+                        let s = ident.to_string();
+                        return parse_quote!( (#s, extendr_api::Robj::from(#right)) );
+                    }
+                }
+            }
+            parse_quote!( ("", extendr_api::Robj::from(#e)) )
+        })
+        .collect::<Vec<Expr>>();
+
+    // Use eval_string to convert the literal string into a callable object.
+    let caller = &call.caller;
+    let caller = quote!(eval_string(#caller));
+
+    // Use the "call" method of Robj to call the function or primitive.
+    // This will error if the object is not callable.
+    let res = if pairs.is_empty() {
+        quote!(
+            (#caller).and_then(|caller| caller.call(Pairlist::new()))
+        )
+    } else {
+        quote!(
+            (#caller).and_then(|caller| caller.call(Pairlist::from_pairs(&[# ( #pairs ),*])))
+        )
+    };
+
+    TokenStream::from(res)
+}
diff --git a/extendr-macros/src/lib.rs b/extendr-macros/src/lib.rs
index 68ce64529..2bea7b989 100644
--- a/extendr-macros/src/lib.rs
+++ b/extendr-macros/src/lib.rs
@@ -56,6 +56,7 @@
 
 #[allow(non_snake_case)]
 mod R;
+mod call;
 mod extendr_function;
 mod extendr_impl;
 mod extendr_module;
@@ -112,6 +113,19 @@ pub fn pairlist(item: TokenStream) -> TokenStream {
     pairlist::pairlist(item)
 }
 
+/// Call a function or primitive defined by a text expression with arbitrary parameters.
+/// This currently works by parsing and evaluating the string in R, but will probably acquire
+/// some shortcuts for simple expessions, for example by caching symbols and constant values.
+///
+/// ```ignore
+///     assert_eq!(call!("`+`", 1, 2), r!(3));
+///     assert_eq!(call!("list", 1, 2), r!([r!(1), r!(2)]));
+/// ```
+#[proc_macro]
+pub fn call(item: TokenStream) -> TokenStream {
+    call::call(item)
+}
+
 /// Execute R code by parsing and evaluating tokens.
 ///
 /// ```ignore


Test patch:
diff --git a/extendr-api/src/robj/tests.rs b/extendr-api/src/robj/tests.rs
index 8678bdff6..328b6cd29 100644
--- a/extendr-api/src/robj/tests.rs
+++ b/extendr-api/src/robj/tests.rs
@@ -225,15 +225,25 @@ fn test_to_robj() {
         assert_eq!(Robj::from(true), Robj::from([Bool::from(true)]));
         //assert_eq!(Robj::from(1_u8), Robj::from(1));
         assert_eq!(Robj::from(1_u16), Robj::from(1));
-        assert_eq!(Robj::from(1_u32), Robj::from(1));
-        assert_eq!(Robj::from(1_u64), Robj::from(1));
+        assert_eq!(Robj::from(1_u32), Robj::from(1.));
+        assert_eq!(Robj::from(1_u64), Robj::from(1.));
+        assert_eq!(Robj::from(1_usize), Robj::from(1.));
         assert_eq!(Robj::from(1_i8), Robj::from(1));
         assert_eq!(Robj::from(1_i16), Robj::from(1));
         assert_eq!(Robj::from(1_i32), Robj::from(1));
-        assert_eq!(Robj::from(1_i64), Robj::from(1));
+        assert_eq!(Robj::from(1_i64), Robj::from(1.));
         assert_eq!(Robj::from(1.0_f32), Robj::from(1.));
         assert_eq!(Robj::from(1.0_f64), Robj::from(1.));
 
+        // check large values
+        assert_eq!(Robj::from(i64::MAX), Robj::from(i64::MAX as f64));
+        assert_eq!(Robj::from(i64::MIN), Robj::from(i64::MIN as f64));
+
+        // check NaN and Inf
+        assert_eq!(Robj::from(f64::NAN), R!("NaN")?);
+        assert_eq!(Robj::from(f64::INFINITY), R!("Inf")?);
+        assert_eq!(Robj::from(-f64::INFINITY), R!("-Inf")?);
+
         let ab = Robj::from(vec!["a", "b"]);
         let ab2 = Robj::from(vec!["a".to_string(), "b".to_string()]);
         assert_eq!(ab, ab2);
diff --git a/extendr-api/tests/call_tests.rs b/extendr-api/tests/call_tests.rs
new file mode 100644
index 000000000..ff1ee0a57
--- /dev/null
+++ b/extendr-api/tests/call_tests.rs
@@ -0,0 +1,27 @@
+use extendr_api::prelude::*;
+
+#[test]
+fn formula_test() {
+    test! {
+        // As one R! macro call
+        let confint1 = R!("confint(lm(weight ~ group - 1, PlantGrowth))")?;
+
+        // As many parameterized calls.
+        let formula = lang!("~", sym!(weight), lang!("-", sym!(group), 1.0)).set_class(["formula"])?;
+        let plant_growth = global!(PlantGrowth)?;
+        let model = call!("lm", formula, plant_growth)?;
+        let confint2 = call!("confint", model)?;
+
+        assert_eq!(confint1.as_real_vector(), confint2.as_real_vector());
+    }
+}
+
+#[test]
+fn special_test() {
+    test! {
+        assert_eq!(call!("`+`", 1, 2)?, r!(3));
+        let add = R!("`+`")?;
+        assert!(add.is_primitive());
+        assert_eq!(add.call(pairlist!(1, 2))?, r!(3));
+    }
+}
diff --git a/extendr-api/tests/extendr_macro.rs b/extendr-api/tests/extendr_macro.rs
index 828ce7f2f..5e3bc8b58 100644
--- a/extendr-api/tests/extendr_macro.rs
+++ b/extendr-api/tests/extendr_macro.rs
@@ -106,3 +106,17 @@ fn tests_with_unsuccessful_outcomes() {
         }
     }
 }
+
+#[test]
+fn test_call_macro() {
+    test! {
+        let vec = call!("c", 1.0, 2.0, 3.0).unwrap();
+        assert_eq!(vec, r!([1., 2., 3.]));
+
+        let list = call!("list", a=1, b=2).unwrap();
+        assert_eq!(list.len(), 2);
+
+        let three = call!("`+`", 1, 2).unwrap();
+        assert_eq!(three, r!(3));
+    }
+}