Repository: extendr/extendr (rust)

Issue:
Update impl_try_from_scalar! to check limits and rounding.
Currently, `impl_try_from_scalar!` uses "as" to convert between scalar types.

This doesn't do type checking.

We could maybe use TryFrom on scalar types to do the conversion.

We could add an error type of CouldNotConvert(Robj) in case this fails.

So if we do:

```
#[extendr(use_try_from = true)]
fn test_i32(val: i32) -> i32 {
    val
}


// Then this should fail with Error::CouldNotConvert
new_owned(wrap__test_i32(r!(1.5).get()))

```

Likewise for i16 and r!(32768)

Pull request:
Check the limits and the rounding of to-int conversions
Fix #223

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/Cargo.toml b/extendr-api/Cargo.toml
index 2ef2c12e7..22551b21e 100644
--- a/extendr-api/Cargo.toml
+++ b/extendr-api/Cargo.toml
@@ -14,7 +14,7 @@ license = "MIT"
 repository = "https://github.com/extendr/extendr"
 
 [dependencies]
-libR-sys = "0.2.0"
+libR-sys = "0.2.2"
 extendr-macros = { path = "../extendr-macros", version="0.2.0" }
 extendr-engine = { path = "../extendr-engine", version="0.2.0" }
 ndarray = { version = "0.13.1", optional = true }
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/build.rs b/extendr-api/build.rs
new file mode 100644
index 000000000..a06650e60
--- /dev/null
+++ b/extendr-api/build.rs
@@ -0,0 +1,18 @@
+use std::env;
+
+fn main() {
+    // The R version information is needed to handle the API differences
+    // between versions. `These DEP_R_R_VERSION_*` are provided by libR-sys
+    // (for more details, please refer to libR-sys's `build.rs`).
+    // The current approach is to add a config flag per feature. When there are
+    // too many features, we might need to consider switching to per-version
+    // config flags (e.g. `r410`).
+    let major = env::var("DEP_R_R_VERSION_MAJOR").unwrap();
+    let minor = env::var("DEP_R_R_VERSION_MINOR").unwrap();
+    // let patch = env::var("DEP_R_R_VERSION_PATCH").unwrap();
+
+    // R_NewEnv is available as of R 4.1.0
+    if &*major >= "4" && &*minor >= "1" {
+        println!("cargo:rustc-cfg=use_r_newenv");
+    }
+}
diff --git a/extendr-api/src/error.rs b/extendr-api/src/error.rs
index 517d13f27..e4a1df000 100644
--- a/extendr-api/src/error.rs
+++ b/extendr-api/src/error.rs
@@ -67,6 +67,8 @@ pub enum Error {
     OutOfRange(Robj),
     MustNotBeNA(Robj),
     ExpectedNonZeroLength(Robj),
+    ExpectedWholeNumber(Robj),
+    OutOfLimits(Robj),
     TypeMismatch(Robj),
     NamespaceNotFound(Robj),
 
@@ -128,6 +130,14 @@ impl std::fmt::Display for Error {
             Error::OutOfRange(_robj) => write!(f, "Out of range."),
             Error::MustNotBeNA(_robj) => write!(f, "Must not be NA."),
             Error::ExpectedNonZeroLength(_robj) => write!(f, "Expected non zero length"),
+            Error::ExpectedWholeNumber(robj) => {
+                write!(
+                    f,
+                    "Expected an integer or a float representing a whole number, got {:?}",
+                    robj
+                )
+            }
+            Error::OutOfLimits(robj) => write!(f, "The value is too big: {:?}", robj),
             Error::TypeMismatch(_robj) => write!(f, "Type mismatch"),
 
             Error::NamespaceNotFound(robj) => write!(f, "Namespace {:?} not found", robj),
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index 7d6599d64..e1d8733cf 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.
@@ -103,6 +104,33 @@ pub fn empty_env() -> Environment {
     unsafe { new_sys(R_EmptyEnv).try_into().unwrap() }
 }
 
+/// Create a new environment
+///
+/// ```
+/// use extendr_api::prelude::*;
+/// test! {
+///     let env: Environment = new_env(global_env(), true, 10).try_into().unwrap();
+///     env.set_local(sym!(x), "hello");
+///     assert_eq!(env.local(sym!(x)), Ok(r!("hello")));
+/// }
+/// ```
+#[cfg(use_r_newenv)]
+pub fn new_env(parent: Environment, hash: bool, capacity: i32) -> Environment {
+    unsafe {
+        let env = R_NewEnv(parent.robj.get(), hash as i32, capacity);
+        new_sys(env).try_into().unwrap()
+    }
+}
+
+// R_NewEnv is available as of R 4.1.0. For the older version, we call an R function `new.env()`.
+#[cfg(not(use_r_newenv))]
+pub fn new_env(parent: Environment, hash: bool, capacity: i32) -> Environment {
+    call!("new.env", hash, parent, capacity)
+        .unwrap()
+        .try_into()
+        .unwrap()
+}
+
 /// The base environment; formerly R_NilValue
 ///
 /// ```
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 0c9823f20..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)?;
@@ -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..5ea5e58c0 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -13,8 +13,8 @@ pub use super::error::{Error, Result};
 pub use super::functions::{
     base_env, base_namespace, blank_scalar_string, blank_string, current_env, empty_env,
     eval_string, eval_string_with_params, find_namespace, find_namespaced_function, global_env,
-    global_function, global_var, local_var, na_str, na_string, namespace_registry, nil_value,
-    parse, srcref,
+    global_function, global_var, local_var, na_str, na_string, namespace_registry, new_env,
+    nil_value, parse, srcref,
 };
 
 pub use super::wrapper::symbol::{
@@ -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/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..af67ee50b 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -2,54 +2,100 @@
 
 use super::*;
 
-macro_rules! impl_try_from_scalar {
-    ($t: ty) => {
+macro_rules! impl_try_from_scalar_integer {
+    ($t:ty) => {
         impl TryFrom<Robj> for $t {
             type Error = Error;
 
             fn try_from(robj: Robj) -> Result<Self> {
-                if let Some(v) = robj.as_integer_slice() {
-                    match v.len() {
-                        0 => Err(Error::ExpectedNonZeroLength(robj)),
-                        1 => {
-                            if !v[0].is_na() {
-                                Ok(v[0] as Self)
-                            } else {
-                                Err(Error::MustNotBeNA(robj))
-                            }
-                        }
-                        _ => Err(Error::ExpectedScalar(robj)),
+                // Check if the value is a scalar
+                match robj.len() {
+                    0 => return Err(Error::ExpectedNonZeroLength(robj)),
+                    1 => {}
+                    _ => return Err(Error::ExpectedScalar(robj)),
+                };
+
+                // Check if the value is not a missing value
+                if robj.is_na() {
+                    return Err(Error::MustNotBeNA(robj));
+                }
+
+                // If the conversion is int-to-int, check the limits. This
+                // needs to be done by `TryFrom` because the conversion by `as`
+                // is problematic when converting a negative value to unsigned
+                // integer types (e.g. `-1i32 as u8` becomes 255).
+                if let Some(v) = robj.as_integer() {
+                    if let Ok(v) = Self::try_from(v) {
+                        return Ok(v);
+                    } else {
+                        return Err(Error::OutOfLimits(robj));
                     }
-                } else if let Some(v) = robj.as_real_slice() {
-                    match v.len() {
-                        0 => Err(Error::ExpectedNonZeroLength(robj)),
-                        1 => {
-                            if !v[0].is_na() {
-                                Ok(v[0] as Self)
-                            } else {
-                                Err(Error::MustNotBeNA(robj))
-                            }
-                        }
-                        _ => Err(Error::ExpectedScalar(robj)),
+                }
+
+                // If the conversion is float-to-int, check if the value is
+                // integer-like (i.e., an integer, or a float representing a
+                // whole number). This needs to be down with `as`, as no
+                // `TryFrom` is implemented for float types. `FloatToInt` trait
+                // might eventually become available in future, though.
+                if let Some(v) = robj.as_real() {
+                    let result = v as Self;
+                    if ((result as f64 - v).abs() < f64::EPSILON) {
+                        return Ok(result);
+                    } else {
+                        return Err(Error::ExpectedWholeNumber(robj));
                     }
-                } else {
-                    Err(Error::ExpectedNumeric(robj))
                 }
+
+                Err(Error::ExpectedNumeric(robj))
+            }
+        }
+    };
+}
+
+macro_rules! impl_try_from_scalar_real {
+    ($t:ty) => {
+        impl TryFrom<Robj> for $t {
+            type Error = Error;
+
+            fn try_from(robj: Robj) -> Result<Self> {
+                // Check if the value is a scalar
+                match robj.len() {
+                    0 => return Err(Error::ExpectedNonZeroLength(robj)),
+                    1 => {}
+                    _ => return Err(Error::ExpectedScalar(robj)),
+                };
+
+                // Check if the value is not a missing value
+                if robj.is_na() {
+                    return Err(Error::MustNotBeNA(robj));
+                }
+
+                // <Robj>::as_xxx() methods can work only when the underlying
+                // SEXP is the corresponding type, so we cannot use as_real()
+                // directly on INTSXP.
+                if let Some(v) = robj.as_real() {
+                    return Ok(v as Self);
+                }
+                if let Some(v) = robj.as_integer() {
+                    return Ok(v as Self);
+                }
+
+                Err(Error::ExpectedNumeric(robj))
             }
         }
     };
 }
 
-impl_try_from_scalar!(u8);
-impl_try_from_scalar!(u16);
-impl_try_from_scalar!(u32);
-impl_try_from_scalar!(u64);
-impl_try_from_scalar!(i8);
-impl_try_from_scalar!(i16);
-impl_try_from_scalar!(i32);
-impl_try_from_scalar!(i64);
-impl_try_from_scalar!(f32);
-impl_try_from_scalar!(f64);
+impl_try_from_scalar_integer!(u8);
+impl_try_from_scalar_integer!(u16);
+impl_try_from_scalar_integer!(u32);
+impl_try_from_scalar_integer!(u64);
+impl_try_from_scalar_integer!(i8);
+impl_try_from_scalar_integer!(i16);
+impl_try_from_scalar_integer!(i32);
+impl_try_from_scalar_integer!(i64);
+impl_try_from_scalar_real!(f32);
+impl_try_from_scalar_real!(f64);
 
 impl TryFrom<Robj> for Bool {
     type Error = Error;
@@ -149,6 +195,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..9e5695526 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 {
@@ -33,15 +34,13 @@ impl Environment {
     /// }
     /// ```
     pub fn new_with_capacity(parent: Environment, capacity: usize) -> Self {
-        let robj = if capacity <= 5 {
+        if capacity <= 5 {
             // Unhashed envirnment
-            call!("new.env", FALSE, parent, 0).unwrap()
+            new_env(parent, false, 0)
         } else {
             // Hashed environment for larger hashmaps.
-            call!("new.env", TRUE, parent, capacity as i32 * 2 + 1).unwrap()
-        };
-        assert!(robj.is_environment());
-        Self { robj }
+            new_env(parent, true, capacity as i32 * 2 + 1)
+        }
     }
 
     /// Make an R environment object.
@@ -62,14 +61,14 @@ impl Environment {
     {
         single_threaded(|| {
             let dict_len = 29;
-            let robj = call!("new.env", TRUE, parent, dict_len).unwrap();
+            let env = new_env(parent, true, dict_len);
             for nv in names_and_values {
                 let (n, v) = nv.sym_pair();
                 if let Some(n) = n {
-                    unsafe { Rf_defineVar(n.get(), v.get(), robj.get()) }
+                    unsafe { Rf_defineVar(n.get(), v.get(), env.get()) }
                 }
             }
-            Environment { robj }
+            env
         })
     }
 
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-engine/Cargo.toml b/extendr-engine/Cargo.toml
index 7dc1c2c63..b2b270c01 100644
--- a/extendr-engine/Cargo.toml
+++ b/extendr-engine/Cargo.toml
@@ -14,7 +14,7 @@ license = "MIT"
 repository = "https://github.com/extendr/extendr"
 
 [dependencies]
-libR-sys = "0.2.0"
+libR-sys = "0.2.2"
 
 [features]
 default = []
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 328b6cd29..510875db6 100644
--- a/extendr-api/src/robj/tests.rs
+++ b/extendr-api/src/robj/tests.rs
@@ -142,6 +142,15 @@ fn test_try_from_robj() {
         assert_eq!(<f32>::try_from(Robj::from(1)), Ok(1.));
         assert_eq!(<f64>::try_from(Robj::from(1)), Ok(1.));
 
+        // conversion from non-integer-ish value to integer should fail
+        let robj = Robj::from(1.5);
+        assert_eq!(<i32>::try_from(robj.clone()), Err(Error::ExpectedWholeNumber(robj)));
+        // conversion from out-of-limit value should fail
+        let robj = Robj::from(32768);
+        assert_eq!(<i16>::try_from(robj.clone()), Err(Error::OutOfLimits(robj)));
+        let robj = Robj::from(-1);
+        assert_eq!(<u32>::try_from(robj.clone()), Err(Error::OutOfLimits(robj)));
+
         assert_eq!(<Vec::<i32>>::try_from(Robj::from(1)), Ok(vec![1]));
         assert_eq!(<Vec::<f64>>::try_from(Robj::from(1.)), Ok(vec![1.]));
 
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));
+    }
+}