miniextendr_macros/r_macro/
grammar.rs1use proc_macro2::{Delimiter, Group, Punct, Spacing, Span, TokenStream, TokenTree};
34use syn::Error;
35
36pub(crate) fn validate(tokens: &TokenStream) -> Result<(), Error> {
39 let flat: Vec<TokenTree> = flatten_top_level(tokens);
40 validate_sequence(&flat)
41}
42
43fn validate_sequence(seq: &[TokenTree]) -> Result<(), Error> {
46 if seq.is_empty() {
47 return Ok(());
48 }
49
50 check_trailing_binary_op(seq)?;
52
53 check_consecutive_binary_ops(seq)?;
55
56 let mut i = 0;
58 while i < seq.len() {
59 let tt = &seq[i];
60 if let TokenTree::Group(g) = tt {
61 validate_group(g, seq, i)?;
62 }
63 i += 1;
64 }
65
66 Ok(())
67}
68
69fn validate_group(g: &Group, outer_seq: &[TokenTree], pos: usize) -> Result<(), Error> {
74 let inner: Vec<TokenTree> = flatten_top_level(&g.stream());
75
76 match g.delimiter() {
77 Delimiter::Parenthesis => {
78 let is_call = pos > 0 && {
87 match &outer_seq[pos - 1] {
88 TokenTree::Ident(_) => true,
89 TokenTree::Group(prev) => #[allow(non_exhaustive_omitted_patterns)] match prev.delimiter() {
Delimiter::Parenthesis | Delimiter::Bracket => true,
_ => false,
}matches!(
90 prev.delimiter(),
91 Delimiter::Parenthesis | Delimiter::Bracket
92 ),
93 _ => false,
94 }
95 };
96
97 if !is_call {
98 if inner.is_empty()
101 && pos > 0
102 && let Some(op_span) = preceding_binary_op_span(outer_seq, pos - 1)
103 {
104 return Err(Error::new(
105 op_span,
106 "R syntax error: binary operator followed by empty parentheses `()` — \
107 expected an operand",
108 ));
109 }
110 }
111
112 for arg in split_by_comma(&inner) {
114 validate_sequence(arg)?;
115 }
116
117 validate_control_flow_keyword(outer_seq, pos, g.span())?;
119 }
120
121 Delimiter::Bracket => {
122 for arg in split_by_comma(&inner) {
123 validate_sequence(arg)?;
124 }
125 }
126
127 Delimiter::Brace => {
128 for stmt in split_by_semicolon(&inner) {
130 if !stmt.is_empty() {
131 validate_sequence(stmt)?;
132 }
133 }
134 }
135
136 Delimiter::None => {
137 validate_sequence(&inner)?;
138 }
139 }
140
141 Ok(())
142}
143
144fn validate_control_flow_keyword(
151 seq: &[TokenTree],
152 paren_pos: usize,
153 paren_span: Span,
154) -> Result<(), Error> {
155 if paren_pos == 0 {
156 return Ok(());
157 }
158 let kw = match &seq[paren_pos - 1] {
160 TokenTree::Ident(id) => id.to_string(),
161 _ => return Ok(()),
162 };
163
164 match kw.as_str() {
165 "if" | "while" => {
166 let g = match &seq[paren_pos] {
169 TokenTree::Group(g) => g,
170 _ => return Ok(()),
171 };
172 let inner: Vec<TokenTree> = flatten_top_level(&g.stream());
173 if inner.is_empty() {
174 return Err(Error::new(
175 paren_span,
176 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("R syntax error: `{0}` condition is empty — `{0} ()` is not valid R",
kw))
})format!(
177 "R syntax error: `{kw}` condition is empty — \
178 `{kw} ()` is not valid R"
179 ),
180 ));
181 }
182 if paren_pos + 1 >= seq.len() {
184 return Err(Error::new(
185 paren_span,
186 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("R syntax error: `{0} (...)` has no body — expected a consequent expression after the condition",
kw))
})format!(
187 "R syntax error: `{kw} (...)` has no body — \
188 expected a consequent expression after the condition"
189 ),
190 ));
191 }
192 }
193 "for" => {
194 let g = match &seq[paren_pos] {
197 TokenTree::Group(g) => g,
198 _ => return Ok(()),
199 };
200 let inner: Vec<TokenTree> = flatten_top_level(&g.stream());
201 if inner.is_empty() {
202 return Err(Error::new(
203 paren_span,
204 "R syntax error: `for` loop variable list is empty — \
205 expected `for (ident in seq)`",
206 ));
207 }
208 let has_in = inner
210 .iter()
211 .any(|t| #[allow(non_exhaustive_omitted_patterns)] match t {
TokenTree::Ident(id) if id == "in" => true,
_ => false,
}matches!(t, TokenTree::Ident(id) if id == "in"));
212 if !has_in {
213 return Err(Error::new(
214 paren_span,
215 "R syntax error: `for` loop missing `in` — \
216 expected `for (ident in seq)`",
217 ));
218 }
219 if paren_pos + 1 >= seq.len() {
221 return Err(Error::new(
222 paren_span,
223 "R syntax error: `for (ident in seq)` has no body — \
224 expected a loop body after the `for` header",
225 ));
226 }
227 }
228 "function" => {
229 }
232 _ => {}
233 }
234
235 Ok(())
236}
237
238fn check_trailing_binary_op(seq: &[TokenTree]) -> Result<(), Error> {
247 if seq.is_empty() {
248 return Ok(());
249 }
250
251 let logical_end = seq
253 .iter()
254 .rposition(|t| !is_semicolon(t))
255 .unwrap_or(seq.len().saturating_sub(1));
256
257 let span = match &seq[logical_end] {
262 TokenTree::Punct(p) => {
263 let ch = p.as_char();
264 if ch == '-' && logical_end > 0 && punct_char_at(seq, logical_end - 1) == Some('<') {
270 return Ok(()); }
272 if ch == '>' && logical_end > 0 && punct_char_at(seq, logical_end - 1) == Some('-') {
273 return Ok(()); }
275 if is_pure_binary_op(p) {
276 p.span()
277 } else {
278 return Ok(());
279 }
280 }
281 _ => return Ok(()),
282 };
283
284 Err(Error::new(
285 span,
286 "R syntax error: expression ends with a binary operator — \
287 expected a right-hand operand after the operator",
288 ))
289}
290
291fn check_consecutive_binary_ops(seq: &[TokenTree]) -> Result<(), Error> {
304 let puncts: Vec<(usize, char, Spacing, Span)> = seq
312 .iter()
313 .enumerate()
314 .filter_map(|(i, t)| {
315 if let TokenTree::Punct(p) = t {
316 Some((i, p.as_char(), p.spacing(), p.span()))
317 } else {
318 None
319 }
320 })
321 .collect();
322
323 for w in puncts.windows(2) {
326 let (i0, c0, spacing0, span0) = w[0];
327 let (i1, c1, _spacing1, span1) = w[1];
328
329 if i1 != i0 + 1 {
332 continue;
333 }
334
335 if spacing0 == Spacing::Joint {
341 continue;
342 }
343
344 let non_unary_binary = |c: char| #[allow(non_exhaustive_omitted_patterns)] match c {
'*' | '/' | '^' | '%' | '@' | '$' => true,
_ => false,
}matches!(c, '*' | '/' | '^' | '%' | '@' | '$');
347
348 if non_unary_binary(c0) && non_unary_binary(c1) {
349 let _ = span1; return Err(Error::new(
353 span0,
354 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("R syntax error: consecutive binary operators `{0}` and `{1}` — expected an operand between them",
c0, c1))
})format!(
355 "R syntax error: consecutive binary operators `{c0}` and `{c1}` — \
356 expected an operand between them"
357 ),
358 ));
359 }
360 }
361
362 Ok(())
363}
364
365fn flatten_top_level(ts: &TokenStream) -> Vec<TokenTree> {
370 ts.clone().into_iter().collect()
371}
372
373fn split_by_comma(seq: &[TokenTree]) -> Vec<&[TokenTree]> {
374 let mut parts: Vec<&[TokenTree]> = Vec::new();
375 let mut start = 0;
376 for (i, tt) in seq.iter().enumerate() {
377 if is_comma(tt) {
378 parts.push(&seq[start..i]);
379 start = i + 1;
380 }
381 }
382 parts.push(&seq[start..]);
383 parts
384}
385
386fn split_by_semicolon(seq: &[TokenTree]) -> Vec<&[TokenTree]> {
387 let mut parts: Vec<&[TokenTree]> = Vec::new();
388 let mut start = 0;
389 for (i, tt) in seq.iter().enumerate() {
390 if is_semicolon(tt) {
391 parts.push(&seq[start..i]);
392 start = i + 1;
393 }
394 }
395 parts.push(&seq[start..]);
396 parts
397}
398
399fn is_comma(tt: &TokenTree) -> bool {
400 #[allow(non_exhaustive_omitted_patterns)] match tt {
TokenTree::Punct(p) if p.as_char() == ',' => true,
_ => false,
}matches!(tt, TokenTree::Punct(p) if p.as_char() == ',')
401}
402
403fn is_semicolon(tt: &TokenTree) -> bool {
404 #[allow(non_exhaustive_omitted_patterns)] match tt {
TokenTree::Punct(p) if p.as_char() == ';' => true,
_ => false,
}matches!(tt, TokenTree::Punct(p) if p.as_char() == ';')
405}
406
407fn is_pure_binary_op(p: &Punct) -> bool {
410 #[allow(non_exhaustive_omitted_patterns)] match p.as_char() {
'*' | '/' | '^' | '%' | '@' | '$' => true,
_ => false,
}matches!(p.as_char(), '*' | '/' | '^' | '%' | '@' | '$')
413}
414
415fn punct_char_at(seq: &[TokenTree], idx: usize) -> Option<char> {
416 match &seq[idx] {
417 TokenTree::Punct(p) => Some(p.as_char()),
418 _ => None,
419 }
420}
421
422fn preceding_binary_op_span(seq: &[TokenTree], pos: usize) -> Option<Span> {
425 let p = match &seq[pos] {
426 TokenTree::Punct(p) => p,
427 _ => return None,
428 };
429 if is_pure_binary_op(p) {
433 Some(p.span())
434 } else {
435 None
436 }
437}
438
439#[cfg(test)]
444mod tests {
445 use super::*;
446 use proc_macro2::TokenStream;
447
448 fn ok(src: &str) {
449 let ts: TokenStream = src.parse().expect("not a valid Rust token stream");
450 assert!(
451 validate(&ts).is_ok(),
452 "expected Ok for {src:?}, got: {:?}",
453 validate(&ts)
454 );
455 }
456
457 fn err(src: &str) {
458 let ts: TokenStream = src.parse().expect("not a valid Rust token stream");
459 assert!(validate(&ts).is_err(), "expected Err for {src:?}, got Ok");
460 }
461
462 #[test]
465 fn trailing_multiply() {
466 err("x *");
467 }
468
469 #[test]
470 fn trailing_divide() {
471 err("x /");
472 }
473
474 #[test]
475 fn trailing_caret() {
476 err("x ^");
477 }
478
479 #[test]
482 fn arithmetic_expression() {
483 ok("1L + 2L");
484 }
485
486 #[test]
487 fn assignment_arrow() {
488 ok(".x <- 41L + 1L");
490 }
491
492 #[test]
493 fn forward_arrow_assignment() {
494 ok("41L + 1L -> .y");
495 }
496
497 #[test]
498 fn semicolon_sequence() {
499 ok("a <- 7L ; a * 6L");
500 }
501
502 #[test]
503 fn multi_statement_with_env_form() {
504 ok("local_val <- 7L ; local_val * 6L");
507 }
508
509 #[test]
510 fn trailing_semicolon_ok() {
511 ok("x <- 1L ;");
512 }
513
514 #[test]
515 fn nchar_call() {
516 ok(r#"nchar("hello")"#);
517 }
518
519 #[test]
520 fn index_with_empty_first_arg() {
521 ok("x[,1]");
524 }
525
526 #[test]
527 fn empty_call_args_are_valid_missing_args() {
528 ok("f(, x)");
531 ok("f(x,,y)");
532 ok("matrix(, 2, 2)");
533 }
534
535 #[test]
536 fn double_star_is_r_power_synonym() {
537 ok("2 ** 3");
539 }
540
541 #[test]
542 fn empty_function_call() {
543 ok("f()");
545 }
546
547 #[test]
548 fn percent_in_percent() {
549 ok("x % in % y");
551 }
552
553 #[test]
554 fn tilde_formula() {
555 ok("y ~ x");
556 }
557
558 #[test]
559 fn logical_operators() {
560 ok("x && y");
561 ok("x || y");
562 }
563
564 #[test]
567 fn if_else_valid() {
568 ok("if (x > 0) x else - x");
569 }
570
571 #[test]
572 fn while_valid() {
573 ok("while (i < 10) i <- i + 1L");
574 }
575
576 #[test]
577 fn for_valid() {
578 ok("for (i in 1:10) print(i)");
579 }
580
581 #[test]
584 fn if_empty_condition() {
585 err("if () x");
586 }
587
588 #[test]
589 fn if_no_body() {
590 err("if (x)");
591 }
592
593 #[test]
594 fn for_missing_in() {
595 err("for (x) {}");
596 }
597
598 #[test]
599 fn for_empty() {
600 err("for () {}");
601 }
602
603 #[test]
606 fn double_star() {
607 err("x * * y");
608 }
609
610 #[test]
611 fn double_slash() {
612 err("x / / y");
613 }
614
615 #[test]
616 fn trailing_empty_call_arg_ok() {
617 ok("f(x,)");
618 }
619
620 #[test]
621 fn empty_paren_after_binary_at() {
622 err("x @ ()");
624 }
625}
626
627