miniextendr_macros/
r_macro.rs1pub(crate) mod grammar;
35pub(crate) mod lowering;
36
37use proc_macro2::TokenStream;
38use quote::quote;
39use syn::{Error, Expr};
40
41pub(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
51fn expand_inner(input: TokenStream) -> Result<TokenStream, Error> {
56 let (env_expr, tail_tokens) = parse_r_macro_input(input)?;
58
59 grammar::validate(&tail_tokens)?;
61
62 let env_ts: TokenStream = match env_expr {
64 Some(ref e) => {
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&e, &mut _s);
_s
}quote! { #e },
65 None => {
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "miniextendr_api");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "sys");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "R_GlobalEnv");
_s
}quote! { ::miniextendr_api::sys::R_GlobalEnv },
66 };
67
68 let expanded = if let Some(lowered) = lowering::try_lower(&tail_tokens, &env_ts) {
71 lowered
72 } else {
73 {
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_ident(&mut _s, "unsafe");
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Brace,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "miniextendr_api");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "expression");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "r_eval_str");
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "core");
::quote::__private::push_colon2(&mut _s);
::quote::__private::push_ident(&mut _s, "stringify");
::quote::__private::push_bang(&mut _s);
::quote::__private::push_group(&mut _s,
::quote::__private::Delimiter::Parenthesis,
{
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&tail_tokens, &mut _s);
_s
});
::quote::__private::push_comma(&mut _s);
::quote::ToTokens::to_tokens(&env_ts, &mut _s);
::quote::__private::push_comma(&mut _s);
_s
});
_s
});
_s
}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
86fn parse_r_macro_input(input: TokenStream) -> Result<(Option<TokenStream>, TokenStream), Error> {
101 let tokens: Vec<proc_macro2::TokenTree> = input.clone().into_iter().collect();
103
104 let has_env_prefix = tokens.len() >= 2
108 && #[allow(non_exhaustive_omitted_patterns)] match &tokens[0] {
proc_macro2::TokenTree::Ident(id) if id == "env" => true,
_ => false,
}matches!(&tokens[0], proc_macro2::TokenTree::Ident(id) if id == "env")
109 && #[allow(non_exhaustive_omitted_patterns)] match &tokens[1] {
proc_macro2::TokenTree::Punct(p) if
p.as_char() == ':' && p.spacing() == proc_macro2::Spacing::Alone =>
true,
_ => false,
}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 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 let after_env_colon = &tokens[2..];
126
127 let semi_idx = after_env_colon
129 .iter()
130 .position(|t| #[allow(non_exhaustive_omitted_patterns)] match t {
proc_macro2::TokenTree::Punct(p) if p.as_char() == ';' => true,
_ => false,
}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 let env_expr: Expr = syn::parse2(env_tokens).map_err(|e| {
159 Error::new(
160 e.span(),
161 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("r!(env: ...) — invalid env expression: {0}",
e))
})format!("r!(env: ...) — invalid env expression: {e}"),
162 )
163 })?;
164
165 Ok((Some({
let mut _s = ::quote::__private::TokenStream::new();
::quote::ToTokens::to_tokens(&env_expr, &mut _s);
_s
}quote! { #env_expr }), tail_tokens))
166}
167
168#[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 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