miniextendr_macros/r_macro.rs
1//! Implementation of the `r!` proc-macro.
2//!
3//! Parses the optional `env: <expr> ;` head, validates the R token tail with
4//! the conservative grammar checker in [`grammar`], then either:
5//!
6//! 1. **Lowers** the tail to `RCall`-based `Rf_lang*` construction when the
7//! tail is a single call of the lowerable subset (see [`lowering`]), or
8//! 2. **Falls back** to the `r_eval_str` string path for everything else.
9//!
10//! The fallback path produces code byte-identical to the old `macro_rules! r`
11//! expansion — `::core::stringify!` rather than `TokenStream::to_string()` so
12//! spacing normalisation is identical.
13//!
14//! ```rust,ignore
15//! // Lowered: r!(c(1L, 2L, 3L))
16//! unsafe {
17//! let __r_scope = ::miniextendr_api::gc_protect::ProtectScope::new();
18//! // ... protect each arg ...
19//! ::miniextendr_api::expression::RCall::new("c")
20//! .arg(__r_arg_0)
21//! ...
22//! .eval(::miniextendr_api::sys::R_GlobalEnv)
23//! }
24//!
25//! // Fallback: r!(1L + 2L)
26//! unsafe {
27//! ::miniextendr_api::expression::r_eval_str(
28//! ::core::stringify!(1L + 2L),
29//! ::miniextendr_api::sys::R_GlobalEnv,
30//! )
31//! }
32//! ```
33
34pub(crate) mod grammar;
35pub(crate) mod lowering;
36
37use proc_macro2::TokenStream;
38use quote::quote;
39use syn::{Error, Expr};
40
41// region: Public entry point
42
43/// Parse and validate `r!(...)` input, returning the expanded `TokenStream`.
44pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
45 match expand_inner(input.into()) {
46 Ok(ts) => ts.into(),
47 Err(e) => e.to_compile_error().into(),
48 }
49}
50
51// endregion
52
53// region: Parser + expansion
54
55fn expand_inner(input: TokenStream) -> Result<TokenStream, Error> {
56 // Parse the input as `[env: <Expr> ;] <tail_tokens>`.
57 let (env_expr, tail_tokens) = parse_r_macro_input(input)?;
58
59 // Conservative grammar validation on the tail.
60 grammar::validate(&tail_tokens)?;
61
62 // Build the environment expression used in both paths.
63 let env_ts: TokenStream = match env_expr {
64 Some(ref e) => quote! { #e },
65 None => quote! { ::miniextendr_api::sys::R_GlobalEnv },
66 };
67
68 // Attempt to lower the tail to an RCall-based expansion.
69 // Falls back to the stringify + r_eval_str path when not applicable.
70 let expanded = if let Some(lowered) = lowering::try_lower(&tail_tokens, &env_ts) {
71 lowered
72 } else {
73 quote! {
74 unsafe {
75 ::miniextendr_api::expression::r_eval_str(
76 ::core::stringify!(#tail_tokens),
77 #env_ts,
78 )
79 }
80 }
81 };
82
83 Ok(expanded)
84}
85
86// endregion
87
88// region: Input parser
89
90/// Parse the `r!(...)` input into an optional `env` expression and the tail token stream.
91///
92/// Grammar: `[env: <Expr> ;] <tail_tokens...>`
93///
94/// The `env:` head is identified by the leading `env` ident followed by `:`.
95/// The `<Expr>` is parsed greedily up to the first **top-level** `;` token
96/// (matching the `macro_rules!` contract: `env: $env:expr; $($code:tt)+` where
97/// `$env:expr` consumes greedily until the `;`).
98///
99/// After the `;`, the remaining tokens form the tail.
100fn parse_r_macro_input(input: TokenStream) -> Result<(Option<TokenStream>, TokenStream), Error> {
101 // Collect all tokens for lookahead.
102 let tokens: Vec<proc_macro2::TokenTree> = input.clone().into_iter().collect();
103
104 // Detect `env:` prefix: first token is `env` ident, second is `:` punct.
105 // The `:` must be Alone-spaced — a Joint `:` is the first half of `::`,
106 // i.e. R code like `env::foo` (namespace access), not the env head.
107 let has_env_prefix = tokens.len() >= 2
108 && matches!(&tokens[0], proc_macro2::TokenTree::Ident(id) if id == "env")
109 && matches!(&tokens[1], proc_macro2::TokenTree::Punct(p)
110 if p.as_char() == ':' && p.spacing() == proc_macro2::Spacing::Alone);
111
112 if !has_env_prefix {
113 // No env head — entire input is the R tail.
114 if tokens.is_empty() {
115 return Err(Error::new(
116 proc_macro2::Span::call_site(),
117 "r!() requires at least one R token",
118 ));
119 }
120 return Ok((None, input));
121 }
122
123 // Skip `env` + `:` (tokens[0] and tokens[1]); the remainder is `<Expr> ; <tail>`.
124 // We need to split at the first TOP-LEVEL `;`.
125 let after_env_colon = &tokens[2..];
126
127 // Find the first top-level `;` token.
128 let semi_idx = after_env_colon
129 .iter()
130 .position(|t| matches!(t, proc_macro2::TokenTree::Punct(p) if p.as_char() == ';'));
131
132 let semi_idx = semi_idx.ok_or_else(|| {
133 Error::new(
134 proc_macro2::Span::call_site(),
135 "r!(env: ...) missing `;` separator — expected `r!(env: <expr>; <R code>)`",
136 )
137 })?;
138
139 let env_tokens: TokenStream = after_env_colon[..semi_idx].iter().cloned().collect();
140 let tail_tokens: TokenStream = after_env_colon[semi_idx + 1..].iter().cloned().collect();
141
142 if env_tokens.is_empty() {
143 return Err(Error::new(
144 proc_macro2::Span::call_site(),
145 "r!(env: ...) has an empty `env` expression — expected `r!(env: <expr>; <R code>)`",
146 ));
147 }
148
149 if tail_tokens.is_empty() {
150 return Err(Error::new(
151 proc_macro2::Span::call_site(),
152 "r!(env: ...; <code>) has no R code after the `;` separator",
153 ));
154 }
155
156 // Parse the env expression string via syn to get a proper `Expr`.
157 // We use syn::parse2 on the env_tokens.
158 let env_expr: Expr = syn::parse2(env_tokens).map_err(|e| {
159 Error::new(
160 e.span(),
161 format!("r!(env: ...) — invalid env expression: {e}"),
162 )
163 })?;
164
165 Ok((Some(quote! { #env_expr }), tail_tokens))
166}
167
168// endregion
169
170// region: Unit tests
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn parse(src: &str) -> (Option<TokenStream>, TokenStream) {
177 parse_r_macro_input(src.parse().unwrap()).unwrap()
178 }
179
180 #[test]
181 fn env_head_is_split_off() {
182 let (env, tail) = parse("env: e; x + 1");
183 assert_eq!(env.unwrap().to_string(), "e");
184 assert_eq!(tail.to_string(), "x + 1");
185 }
186
187 #[test]
188 fn double_colon_is_r_code_not_env_head() {
189 // `env::foo()` is R namespace access on a package named `env`,
190 // not the `env: <expr> ;` head.
191 let (env, tail) = parse("env::foo()");
192 assert!(env.is_none());
193 assert_eq!(tail.to_string(), "env :: foo ()");
194 }
195
196 #[test]
197 fn env_head_without_semicolon_errors() {
198 let err = parse_r_macro_input("env: e".parse().unwrap()).unwrap_err();
199 assert!(err.to_string().contains("missing `;`"));
200 }
201}
202
203// endregion