Skip to main content

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