Repository: extendr/extendr
Pull request #203 (2021-04-01 23:05:13)

R! as a procedural macro.
This PR upgrades the R! macro to take parameters and quoted strings.

The quoted strings are to make the code parseable by R, even with data.frame
and backticks.

The parameters substitute Rust expressions into an environment which is then used
to execute the expression.

Examples:

R!(1) -> 1.0 as before

R!("1") -> 1.0 instead of "1.0"

R!("1 + {{ 1.0 }}") -> 2.0 where the 1.0 is any Rust expression convertable to and R object.

R!(r"
   x <- {{ (0..4).collect_robj() }}
   x
") -> [0, 1, 2, 3]

Later we should be able to bake in the parsing and maybe even compile R expressions into Rust.

At the moment, there is some overhead for creating the parameters, the environment and so on.


clauswilke: I'm not sure I like the idea that the macro takes either raw expressions or quoted strings. How do I then create an R string? I think it'd be better to have two separate macros, one that takes raw expressions and one that takes strings, e.g. `R!` and `R_str!`.
andy-thomason: > How do I then create an R string?

We can use raw strings 

```
R!(r#"
    print("hello")
"#)
```

I want to discourage people from using the unquoted variety as they are a support nightmare.
Also, the unquoted form does not support parameters.

`R!(x <- {{expr()}})` does not work.
`R!("x <- {{expr()}}")` does work.

I would like to drop the unquoted form, but will implement both for now if that is desirable.

I'll keep the quoted version short, say Rp!("") for "parameterised".


clauswilke: I'm Ok with dropping the unquoted form altogether (at least in the long run). Is there a way to trigger a compiler warning when it is encountered, so people could be alerted to the fact that what they're doing is deprecated and no longer recommended? 
clauswilke: > I'm Ok with dropping the unquoted form altogether (at least in the long run).

However, then I think all the examples should be changed accordingly.
andy-thomason: It seems that we are waiting on a new feature to stabilise for warnings.

https://github.com/rust-lang/rust/issues/54140

I'd be happy if we just change all the R! documentation to R!("") or R!(r#""#)

andy-thomason: The problem is that the token stream we are given is opaque and does not contain the original text.

This is why we can't tell data.frame from data . frame
clauswilke: > I'd be happy if we just change all the `R!` documentation to `R!("")` or `R!(r#""#)`

Works for me. Probably there should also be a statement in the changelog that unquoted `R!()` is discouraged and may be disabled in the future.

> The problem is that the token stream we are given is opaque and does not contain the original text. This is why we can't tell `data.frame` from `data . frame`

Yes, I had realized that as well, and I think this will probably never change. So better to just work with strings always. On the flip side, in rextendr, we also take Rust code in a string.

andy-thomason: > On the flip side, in rextendr, we also take Rust code in a string.

Important to have respect for each language :)
andy-thomason: Way past our bed time here. Have a happy remainder of your Easter Sunday!

I'll merge this tomorrow if there are no objections before then.

Andy.
clauswilke: > Just curious, can we use the actual `{{` in the R expression?

I think this is why we want a parameterized and a non-parameterized version of the macro. We could use the R `{{` in the non-parameterized version.
yutannihilation: Ah, makes some sense, thanks.
andy-thomason: Thanks for the feedback.

I've added a Rraw! variant to cope with Yuta's example.


Patch:
diff --git a/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index 5714e81c5..8bef740d6 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -203,6 +203,7 @@ pub fn parse(code: &str) -> Result<Robj> {
 }
 
 /// Parse a string into an R executable object and run it.
+/// Used by the R! macro.
 /// ```
 /// use extendr_api::prelude::*;
 /// test! {
@@ -223,6 +224,37 @@ pub fn eval_string(code: &str) -> Result<Robj> {
     })
 }
 
+/// Parse a string into an R executable object and run it using
+///   parameters param.0, param.1, ...
+///
+/// Used by the R! macro.
+/// ```
+/// use extendr_api::prelude::*;
+/// test! {
+///    let res = eval_string_with_params("param.0", &[&r!(3.)]).unwrap();
+///    assert_eq!(res, r!(3.));
+/// }
+/// ```
+pub fn eval_string_with_params(code: &str, values: &[&Robj]) -> Result<Robj> {
+    single_threaded(|| {
+        let env = Environment::new(global_env());
+        for (i, &v) in values.into_iter().enumerate() {
+            let key = Symbol::from_string(format!("param.{}", i));
+            env.set_local(key, v);
+        }
+
+        let expr = parse(code)?;
+        let mut res = Robj::from(());
+        if let Some(expr) = expr.as_expression() {
+            for lang in expr.values() {
+                res = lang.eval_with_env(&env)?
+            }
+        }
+
+        Ok(res)
+    })
+}
+
 /// Find a function or primitive that may be in a namespace.
 /// ```
 /// use extendr_api::prelude::*;
@@ -238,13 +270,13 @@ pub fn find_namespaced_function(name: &str) -> Result<Language> {
     let mut iter = name.split("::");
     match (iter.next(), iter.next(), iter.next()) {
         (Some(key), None, None) => {
-            let gf = global_function(Symbol::from_str(key))?;
+            let gf = global_function(Symbol::from_string(key))?;
             Ok(Language::from_values(&[gf]))
         }
         (Some(ns), Some(key), None) => {
             let namespace = find_namespace(ns)?;
             Ok(Language::from_values(&[
-                namespace.local(Symbol::from_str(key))?
+                namespace.local(Symbol::from_string(key))?
             ]))
         }
         _ => Err(Error::NotFound(r!(name))),
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 375b0c034..dc9df4f9b 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -57,7 +57,7 @@
 //!     let real_vector = r!(vec![1.0, 2.0]);
 //!    
 //!     // An R function object.
-//!     let function = R!(function(x, y) { x + y })?;
+//!     let function = R!("function(x, y) { x + y }")?;
 //!    
 //!     // A named list using the list! macro.
 //!     let list = list!(a = 1, b = 2);
@@ -82,7 +82,7 @@
 //! test! {
 //!     // 1 ..= 100 is the same as 1:100
 //!     let res = r!(1 ..= 100);
-//!     assert_eq!(res, R!(1:100)?);
+//!     assert_eq!(res, R!("1:100")?);
 //!    
 //!     // Rust arrays are zero-indexed so it is more common to use 0 .. 100.
 //!     let res = r!(0 .. 100);
@@ -130,13 +130,34 @@
 //! ```
 //!
 //! The [R!] macro lets you embed R code in Rust
-//! but does not take variables.
+//! and takes Rust expressions in {{ }} pairs.
+//!
+//! The [Rraw!] macro will not expand the {{ }} pairs.
 //! ```
 //! use extendr_api::prelude::*;
 //! test! {
 //!     // The text "1 + 1" is parsed as R source code.
 //!     // The result is 1.0 + 1.0 in Rust.
-//!     assert_eq!(R!(1 + 1)?, r!(2.0));
+//!     assert_eq!(R!("1 + 1")?, r!(2.0));
+//!
+//!     let a = 1.0;
+//!     assert_eq!(R!("1 + {{a}}")?, r!(2.0));
+//!
+//!     assert_eq!(R!(r"
+//!         x <- {{ a }}
+//!         x + 1
+//!     ")?, r!(2.0));
+//!
+//!     assert_eq!(R!(r#"
+//!         x <- "hello"
+//!         x
+//!     "#)?, r!("hello"));
+//!
+//!     // Use the R meaning of {{ }} and do not expand.
+//!     assert_eq!(Rraw!(r"
+//!         x <- {{ 1 }}
+//!         x + 1
+//!     ")?, r!(2.0));
 //! }
 //! ```
 //!
@@ -190,7 +211,7 @@
 //!     // robj is an "Owned" object that controls the memory allocated.
 //!     let robj = r!([1, 2, 3]);
 //!    
-//!     // slice is a "borrowed" reference to the bytes in robj.
+//!     // Here slice is a "borrowed" reference to the bytes in robj.
 //!     // and cannot live longer than robj.
 //!     let slice = robj.as_integer_slice().ok_or("expected slice")?;
 //!     assert_eq!(slice.len(), 3);
@@ -787,4 +808,32 @@ mod tests {
             assert_eq!(pairlist!(), Pairlist::from(()));
         }
     }
+
+    #[test]
+    fn big_r_macro_works() {
+        test! {
+            assert_eq!(R!("1")?, r!(1.0));
+            assert_eq!(R!(r"1")?, r!(1.0));
+            assert_eq!(R!(r"
+                x <- 1
+                x
+            ")?, r!(1.0));
+            assert_eq!(R!(r"
+                x <- {{ 1.0 }}
+                x
+            ")?, r!(1.0));
+            assert_eq!(R!(r"
+                x <- {{ (0..4).collect_robj() }}
+                x
+            ")?, r!([0, 1, 2, 3]));
+            assert_eq!(R!(r#"
+                x <- "hello"
+                x
+            "#)?, r!("hello"));
+            assert_eq!(Rraw!(r"
+                x <- {{ 1 }}
+                x
+            ")?, r!(1.0));
+        }
+    }
 }
diff --git a/extendr-api/src/prelude.rs b/extendr-api/src/prelude.rs
index b7e8f27ff..19f29e37a 100644
--- a/extendr-api/src/prelude.rs
+++ b/extendr-api/src/prelude.rs
@@ -12,8 +12,9 @@ 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, find_namespace, find_namespaced_function, global_env, global_function, global_var,
-    local_var, na_str, na_string, namespace_registry, nil_value, parse, srcref,
+    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,
 };
 
 pub use super::wrapper::symbol::{
@@ -27,8 +28,7 @@ pub use super::wrapper::symbol::{
 
 pub use crate::{append, append_lang, append_with_name, args, call, lang, make_lang};
 pub use crate::{
-    data_frame, factor, global, list, pairlist, r, reprint, reprintln, rprint, rprintln, sym, test,
-    var, R,
+    data_frame, factor, global, list, r, reprint, reprintln, rprint, rprintln, sym, test, var,
 };
 
 pub use super::logical::Bool;
@@ -52,7 +52,7 @@ pub use super::robj_ndarray::*;
 #[cfg(feature = "ndarray")]
 pub use ndarray::*;
 
-pub use extendr_macros::{extendr, extendr_module};
+pub use extendr_macros::{extendr, extendr_module, pairlist, Rraw, R};
 
 pub use super::iter::{Int, Logical, Real, StrIter};
 
diff --git a/extendr-api/src/rmacros.rs b/extendr-api/src/rmacros.rs
index 463040b09..51f9cb2ae 100644
--- a/extendr-api/src/rmacros.rs
+++ b/extendr-api/src/rmacros.rs
@@ -33,29 +33,6 @@ macro_rules! r {
     };
 }
 
-/// Call inline R source code from Rust.
-///
-/// Multiline expressions and variables with embedded "." may not work.
-/// Note that the performance of this function is not good.
-///
-/// ```
-/// use extendr_api::prelude::*;
-/// test! {
-///
-/// let formula = R!(y ~ z + 1).unwrap();
-/// assert!(formula.inherits("formula"));
-///
-/// let function = R!(function(x,y) x+y).unwrap();
-/// assert!(function.is_function());
-/// }
-/// ```
-#[macro_export]
-macro_rules! R {
-    ($($t:tt)*) => {
-        eval_string(stringify!($($t)*))
-    };
-}
-
 /// Get a local variable from the calling function
 /// or a global variable if no such variable exists.
 ///
@@ -100,13 +77,13 @@ macro_rules! global {
 /// test! {
 ///
 /// let wombat = sym!(wombat);
-/// assert_eq!(wombat, r!(Symbol::from_str("wombat")));
+/// assert_eq!(wombat, r!(Symbol::from_string("wombat")));
 /// }
 /// ```
 #[macro_export]
 macro_rules! sym {
     ($($tokens: tt)*) => {
-        Robj::from(Symbol::from_str(stringify!($($tokens)*)))
+        Robj::from(Symbol::from_string(stringify!($($tokens)*)))
     };
 }
 
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index 7973137b5..876a41b9c 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -178,7 +178,7 @@ impl Robj {
     ///     assert_eq!(r!(NULL).rtype(), RType::Null);
     ///     assert_eq!(sym!(xyz).rtype(), RType::Symbol);
     ///     assert_eq!(r!(Pairlist::from_pairs(vec![("a", r!(1))])).rtype(), RType::Pairlist);
-    ///     assert_eq!(R!(function() {})?.rtype(), RType::Function);
+    ///     assert_eq!(R!("function() {}")?.rtype(), RType::Function);
     ///     assert_eq!(Environment::new(global_env()).rtype(), RType::Enviroment);
     ///     assert_eq!(lang!("+", 1, 2).rtype(), RType::Language);
     ///     assert_eq!(r!(Primitive::from_str("if")).rtype(), RType::Special);
@@ -621,9 +621,22 @@ impl Robj {
     /// }
     /// ```
     pub fn eval(&self) -> Result<Robj> {
+        self.eval_with_env(&global_env())
+    }
+
+    /// Evaluate the expression in R and return an error or an R object.
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///
+    ///    let add = lang!("+", 1, 2);
+    ///    assert_eq!(add.eval_with_env(&global_env()).unwrap(), r!(3));
+    /// }
+    /// ```
+    pub fn eval_with_env(&self, env: &Environment) -> Result<Robj> {
         single_threaded(|| unsafe {
             let mut error: raw::c_int = 0;
-            let res = R_tryEval(self.get(), R_GlobalEnv, &mut error as *mut raw::c_int);
+            let res = R_tryEval(self.get(), env.get(), &mut error as *mut raw::c_int);
             if error != 0 {
                 Err(Error::EvalError(self.clone()))
             } else {
@@ -873,7 +886,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///    let formula = R!(y ~ A * x + b).unwrap();
+    ///    let formula = R!("y ~ A * x + b").unwrap();
     ///    let class : Vec<_> = formula.class().unwrap().collect();
     ///    assert_eq!(class, ["formula"]);
     /// }
@@ -912,7 +925,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///    let formula = R!(y ~ A * x + b).unwrap();
+    ///    let formula = R!("y ~ A * x + b").unwrap();
     ///    assert_eq!(formula.inherits("formula"), true);
     /// }
     /// ```
diff --git a/extendr-api/src/robj/operators.rs b/extendr-api/src/robj/operators.rs
index 68bdb4840..27386d0f1 100644
--- a/extendr-api/src/robj/operators.rs
+++ b/extendr-api/src/robj/operators.rs
@@ -19,7 +19,7 @@ impl Robj {
     where
         T: AsRef<str>,
     {
-        let symbol: Symbol = Symbol::from_str(symbol.as_ref());
+        let symbol: Symbol = Symbol::from_string(symbol.as_ref());
         call!("$", self, symbol)
     }
 
@@ -59,8 +59,8 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let x = r!(Symbol::from_str("x"));
-    ///     let y = r!(Symbol::from_str("y"));
+    ///     let x = r!(Symbol::from_string("x"));
+    ///     let y = r!(Symbol::from_string("y"));
     ///     let tilde = x.tilde(y).unwrap();
     ///     assert_eq!(tilde.inherits("formula"), true);
     /// }
@@ -76,8 +76,8 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    /// let base = r!(Symbol::from_str("base"));
-    /// let env = r!(Symbol::from_str(".getNamespace"));
+    /// 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);
     /// }
@@ -93,7 +93,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let function = R!(function(a, b) a + b).unwrap();
+    ///     let function = R!("function(a, b) a + b").unwrap();
     ///     assert_eq!(function.is_function(), true);
     ///     assert_eq!(function.call(pairlist!(a=1, b=2)).unwrap(), r!(3));
     /// }
diff --git a/extendr-api/src/robj_ndarray.rs b/extendr-api/src/robj_ndarray.rs
index 77b9e72e0..3e770c95f 100644
--- a/extendr-api/src/robj_ndarray.rs
+++ b/extendr-api/src/robj_ndarray.rs
@@ -72,7 +72,7 @@ fn test_from_robj() {
             Ok(ArrayView1::<Bool>::from(&[Bool(1)][..]))
         );
 
-        let robj = R!(matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4))?;
+        let robj = R!("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4)")?;
         let mx = <ArrayView2<f64>>::from_robj(&robj)?;
         assert_eq!(mx[[0, 0]], 1.0);
         assert_eq!(mx[[1, 0]], 2.0);
@@ -91,7 +91,7 @@ fn test_from_robj() {
         assert_eq!(col0[3], 4.0);
 
         // check integer matrices
-        let robj = R!(matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4))?;
+        let robj = R!("matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4)")?;
         let mx = <ArrayView2<i32>>::from_robj(&robj)?;
         assert_eq!(mx[[0, 0]], 1);
         assert_eq!(mx[[1, 0]], 2);
@@ -103,7 +103,7 @@ fn test_from_robj() {
         assert_eq!(mx[[3, 1]], 8);
 
         // check logical matrices
-        let robj = R!(matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4))?;
+        let robj = R!("matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4)")?;
         let mx = <ArrayView2<Bool>>::from_robj(&robj)?;
         assert_eq!(mx[[0, 0]], TRUE);
         assert_eq!(mx[[1, 0]], TRUE);
diff --git a/extendr-api/src/wrapper/function.rs b/extendr-api/src/wrapper/function.rs
index af7a3074f..765f48c4d 100644
--- a/extendr-api/src/wrapper/function.rs
+++ b/extendr-api/src/wrapper/function.rs
@@ -4,7 +4,7 @@ use super::*;
 /// ```
 /// use extendr_api::prelude::*;
 /// test! {
-///     let expr = R!(function(a = 1, b) {c <- a + b}).unwrap();
+///     let expr = R!("function(a = 1, b) {c <- a + b}").unwrap();
 ///     let func = expr.as_function().unwrap();
 ///
 ///     let expected_formals = Pairlist::from_pairs(vec![("a", r!(1.0)), ("b", missing_arg().into())]);
diff --git a/extendr-api/src/wrapper/mod.rs b/extendr-api/src/wrapper/mod.rs
index 2c4de3e00..bd0c6111a 100644
--- a/extendr-api/src/wrapper/mod.rs
+++ b/extendr-api/src/wrapper/mod.rs
@@ -150,7 +150,7 @@ impl Robj {
     /// use extendr_api::prelude::*;
     /// test! {
     ///     let fred = sym!(fred);
-    ///     assert_eq!(fred.as_symbol(), Some(Symbol::from_str("fred")));
+    ///     assert_eq!(fred.as_symbol(), Some(Symbol::from_string("fred")));
     /// }
     /// ```
     pub fn as_symbol(&self) -> Option<Symbol> {
@@ -186,7 +186,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let call_to_xyz = r!(Language::from_values(&[r!(Symbol::from_str("xyz")), r!(1), r!(2)]));
+    ///     let call_to_xyz = r!(Language::from_values(&[r!(Symbol::from_string("xyz")), r!(1), r!(2)]));
     ///     assert_eq!(call_to_xyz.is_language(), true);
     ///     assert_eq!(call_to_xyz.len(), 3);
     ///     assert_eq!(format!("{:?}", call_to_xyz), r#"r!(Language::from_values([sym!(xyz), r!(1), r!(2)]))"#);
@@ -257,7 +257,7 @@ impl Robj {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let func = R!(function(a,b) a + b).unwrap();
+    ///     let func = R!("function(a,b) a + b").unwrap();
     ///     println!("{:?}", func.as_function());
     /// }
     /// ```
@@ -281,7 +281,7 @@ where
     R: Into<Robj>,
 {
     fn sym_pair(self) -> (Robj, Robj) {
-        (r!(Symbol::from_str(self.0.as_ref())), self.1.into())
+        (r!(Symbol::from_string(self.0.as_ref())), self.1.into())
     }
 }
 
@@ -292,6 +292,9 @@ where
     R: Clone,
 {
     fn sym_pair(self) -> (Robj, Robj) {
-        (r!(Symbol::from_str(self.0.as_ref())), self.1.clone().into())
+        (
+            r!(Symbol::from_string(self.0.as_ref())),
+            self.1.clone().into(),
+        )
     }
 }
diff --git a/extendr-api/src/wrapper/primitive.rs b/extendr-api/src/wrapper/primitive.rs
index f66daa2a2..8be4b2bf5 100644
--- a/extendr-api/src/wrapper/primitive.rs
+++ b/extendr-api/src/wrapper/primitive.rs
@@ -30,7 +30,7 @@ impl Primitive {
     pub fn from_str(val: &str) -> Result<Self> {
         single_threaded(|| unsafe {
             // Primitives have a special "SYMVALUE" entry in their symbol.
-            let sym = Symbol::from_str(val);
+            let sym = Symbol::from_string(val);
             let symvalue = new_owned(SYMVALUE(sym.get()));
             if symvalue.is_primitive() {
                 Ok(Primitive { robj: symvalue })
diff --git a/extendr-api/src/wrapper/symbol.rs b/extendr-api/src/wrapper/symbol.rs
index fa18b60e7..cf690b72f 100644
--- a/extendr-api/src/wrapper/symbol.rs
+++ b/extendr-api/src/wrapper/symbol.rs
@@ -5,7 +5,7 @@ use super::*;
 /// ```
 /// use extendr_api::prelude::*;
 /// test! {
-///     let chr = r!(Symbol::from_str("xyz"));
+///     let chr = r!(Symbol::from_string("xyz"));
 ///     assert_eq!(chr.as_symbol().unwrap().as_str(), "xyz");
 /// }
 /// ```
@@ -23,13 +23,14 @@ impl Symbol {
     /// ```
     /// use extendr_api::prelude::*;
     /// test! {
-    ///     let chr = r!(Symbol::from_str("xyz"));
+    ///     let chr = r!(Symbol::from_string("xyz"));
     ///     assert_eq!(chr, sym!(xyz));
-    ///     assert_eq!(Symbol::from_str(na_str()).is_unbound_value(), true);
-    ///     assert_eq!(Symbol::from_str("").is_unbound_value(), true);
+    ///     assert_eq!(Symbol::from_string(na_str()).is_unbound_value(), true);
+    ///     assert_eq!(Symbol::from_string("").is_unbound_value(), true);
     /// }
     /// ```
-    pub fn from_str(val: &str) -> Self {
+    pub fn from_string<S: AsRef<str>>(val: S) -> Self {
+        let val = val.as_ref();
         if val.is_empty() || val.is_na() {
             unbound_value()
         } else {
@@ -69,7 +70,7 @@ impl Symbol {
 impl From<&str> for Symbol {
     /// Convert a string to a symbol.
     fn from(name: &str) -> Self {
-        Symbol::from_str(name)
+        Symbol::from_string(name)
     }
 }
 
diff --git a/extendr-macros/Cargo.toml b/extendr-macros/Cargo.toml
index 38b2de365..2598659c9 100644
--- a/extendr-macros/Cargo.toml
+++ b/extendr-macros/Cargo.toml
@@ -19,3 +19,5 @@ proc_macro = true
 [dependencies]
 syn = { version = "1.0.22", features = ["full", "extra-traits"] }
 quote = "1.0.6"
+proc-macro2 = { version = "1.0" }
+
diff --git a/extendr-macros/src/R.rs b/extendr-macros/src/R.rs
new file mode 100644
index 000000000..195e248d8
--- /dev/null
+++ b/extendr-macros/src/R.rs
@@ -0,0 +1,97 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{parse_quote, punctuated::Punctuated, Expr, Token};
+
+#[allow(non_snake_case)]
+pub fn R(item: TokenStream, expand_params: bool) -> TokenStream {
+    // Check if the input is a string.
+    let lit = match syn::parse2::<syn::LitStr>(item.clone()) {
+        Ok(lit) => lit,
+        Err(_) => {
+            // If not a string, expand the tokens to make a string.
+            let src = format!("{}", item);
+            return quote!(eval_string(#src));
+        }
+    };
+
+    let mut src = lit.value();
+
+    let mut expressions: Punctuated<Expr, Token!(,)> = Punctuated::new();
+    if expand_params {
+        // Replace rust expressions in {{..}} with _expr0, _expr1, ...
+        while let Some(start) = src.find("{{") {
+            if let Some(end) = src[start + 2..].find("}}") {
+                if let Ok(param) = syn::parse_str::<Expr>(&src[start + 2..start + 2 + end]) {
+                    src = format!(
+                        "{} param.{} {}",
+                        &src[0..start],
+                        expressions.len(),
+                        &src[start + 2 + end + 2..]
+                    );
+                    expressions.push(parse_quote!(&extendr_api::Robj::from(#param)));
+                } else {
+                    return quote!(compile_error!("Not a valid rust expression."));
+                }
+            } else {
+                return quote!(compile_error!("Unterminated {{ block."));
+            }
+        }
+    }
+
+    if expressions.is_empty() {
+        quote!(eval_string(#src))
+    } else {
+        quote!(
+            {
+                let params = &[#expressions];
+                eval_string_with_params(#src, params)
+            }
+        )
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_r_macro() {
+        // Note: strip spaces to cover differences between compilers.
+
+        // Naked R!
+        assert_eq!(
+            format!("{}", R(quote!(data.frame), true)),
+            format!("{}", quote!(eval_string("data . frame")))
+        );
+
+        // Quoted R!
+        assert_eq!(
+            format!("{}", R(quote!("data.frame"), true)),
+            format!("{}", quote!(eval_string("data.frame")))
+        );
+
+        // Param R!
+        assert_eq!(
+            format!("{}", R(quote!("a <- {{1}}"), true)),
+            format!(
+                "{}",
+                quote!({
+                    let params = &[&extendr_api::Robj::from(1)];
+                    eval_string_with_params("a <-  param.0 ", params)
+                })
+            )
+        );
+
+        // Unquoted R!
+        assert_eq!(
+            format!("{}", R(quote!(r#""hello""#), true)),
+            format!("{}", quote!(eval_string("\"hello\"")))
+        );
+
+        // Rraw!
+        assert_eq!(
+            format!("{}", R(quote!("a <- {{1}}"), false)),
+            format!("{}", quote!(eval_string("a <- {{1}}")))
+        );
+    }
+}
diff --git a/extendr-macros/src/lib.rs b/extendr-macros/src/lib.rs
index 91e045a5e..68ce64529 100644
--- a/extendr-macros/src/lib.rs
+++ b/extendr-macros/src/lib.rs
@@ -54,6 +54,8 @@
 // * An output converstion from that type to an owned external pointer object.
 // * A finalizer for that type to free memory allocated.
 
+#[allow(non_snake_case)]
+mod R;
 mod extendr_function;
 mod extendr_impl;
 mod extendr_module;
@@ -109,3 +111,36 @@ pub fn extendr_module(item: TokenStream) -> TokenStream {
 pub fn pairlist(item: TokenStream) -> TokenStream {
     pairlist::pairlist(item)
 }
+
+/// Execute R code by parsing and evaluating tokens.
+///
+/// ```ignore
+///     R!("c(1, 2, 3)");
+///     R!("{{(0..3).collect_robj()}} + 1");
+///     R!(r#"
+///       print("hello")
+///     "#);
+/// ```
+#[proc_macro]
+#[allow(non_snake_case)]
+pub fn R(item: TokenStream) -> TokenStream {
+    R::R(item.into(), true).into()
+}
+
+/// Execute R code by parsing and evaluating tokens
+/// but without expanding parameters.
+///
+/// ```ignore
+/// // c.f. https://dplyr.tidyverse.org/articles/programming.html
+/// Rraw!(r#"
+/// var_summary <- function(data, var) {
+///   data %>%
+///     summarise(n = n(), min = min({{ var }}), max = max({{ var }}))
+/// }
+/// "#)
+/// ```
+#[proc_macro]
+#[allow(non_snake_case)]
+pub fn Rraw(item: TokenStream) -> TokenStream {
+    R::R(item.into(), false).into()
+}


Test patch:
diff --git a/extendr-api/src/robj/tests.rs b/extendr-api/src/robj/tests.rs
index 3dc7195b5..71ef39333 100644
--- a/extendr-api/src/robj/tests.rs
+++ b/extendr-api/src/robj/tests.rs
@@ -21,10 +21,10 @@ fn test_debug() {
         assert_eq!(format!("{:?}", r!(Raw::from_bytes(&[1, 2, 3]))), "r!(Raw::from_bytes([1, 2, 3]))");
 
         // Wrappers
-        assert_eq!(format!("{:?}", r!(Symbol::from_str("x"))), "sym!(x)");
+        assert_eq!(format!("{:?}", r!(Symbol::from_string("x"))), "sym!(x)");
         assert_eq!(format!("{:?}", r!(Character::from_str("x"))), "r!(Character::from_str(\"x\"))");
         assert_eq!(
-            format!("{:?}", r!(Language::from_values(&[r!(Symbol::from_str("x"))]))),
+            format!("{:?}", r!(Language::from_values(&[r!(Symbol::from_string("x"))]))),
             "r!(Language::from_values([sym!(x)]))"
         );
 
@@ -265,8 +265,8 @@ fn parse_test() {
     test! {
     let p = parse("print(1L);print(1L);")?;
     let q = r!(Expression::from_values(&[
-        r!(Language::from_values(&[r!(Symbol::from_str("print")), r!(1)])),
-        r!(Language::from_values(&[r!(Symbol::from_str("print")), r!(1)]))
+        r!(Language::from_values(&[r!(Symbol::from_string("print")), r!(1)])),
+        r!(Language::from_values(&[r!(Symbol::from_string("print")), r!(1)]))
     ]));
     assert_eq!(p, q);