1use crate::cst::{
11 Annotation, AnnotationArg, CallArg, Decl, DictEntry, Event, Expr, IfBranch, Program, Rule,
12 RuleEntry, Stmt, SwitchArm, TopLevel,
13};
14use crate::diag::{OpyError, Position, Span};
15use crate::lexer::{Token, TokenKind, decode_string_escape, is_identifier};
16
17#[derive(Debug, Default)]
19pub struct ParseOutput {
20 pub program: Option<Program>,
22 pub errors: Vec<OpyError>,
24}
25
26pub fn parse(tokens: &[Token]) -> ParseOutput {
28 parse_with_options(tokens, false)
29}
30
31pub fn parse_with_options(tokens: &[Token], allow_macro_redeclaration: bool) -> ParseOutput {
33 let mut parser = Parser::new(tokens, allow_macro_redeclaration);
34 let program = parser.parse_program();
35 if parser.errors.is_empty() {
36 ParseOutput {
37 program: Some(program),
38 errors: Vec::new(),
39 }
40 } else {
41 ParseOutput {
42 program: None,
43 errors: parser.errors,
44 }
45 }
46}
47
48mod declarations;
49mod definitions;
50mod expressions;
51mod statements;
52
53struct Parser<'a> {
54 tokens: &'a [Token],
55 pos: usize,
56 errors: Vec<OpyError>,
57 allow_macro_redeclaration: bool,
58 last_statement_continued: bool,
59 last_colon_body_continued: bool,
60 open_if_indents: Vec<u32>,
62 workshop_source_reported: bool,
66}
67
68impl<'a> Parser<'a> {
69 fn new(tokens: &'a [Token], allow_macro_redeclaration: bool) -> Self {
70 Self {
71 tokens,
72 pos: 0,
73 errors: Vec::new(),
74 allow_macro_redeclaration,
75 last_statement_continued: false,
76 last_colon_body_continued: false,
77 open_if_indents: Vec::new(),
78 workshop_source_reported: false,
79 }
80 }
81}
82
83fn is_opy_word(text: &str) -> bool {
87 matches!(
88 text,
89 "and"
90 | "or"
91 | "not"
92 | "in"
93 | "lambda"
94 | "if"
95 | "elif"
96 | "else"
97 | "for"
98 | "while"
99 | "do"
100 | "switch"
101 | "del"
102 | "goto"
103 | "continue"
104 | "break"
105 | "return"
106 | "pass"
107 | "case"
108 | "default"
109 )
110}
111
112const BRACE_BLOCKS: &str = "a `{`/`}` brace block; OPY blocks open with `:` and indentation";
114
115fn is_binary_operator(kind: TokenKind) -> bool {
116 matches!(
117 kind,
118 TokenKind::Plus
119 | TokenKind::Minus
120 | TokenKind::Star
121 | TokenKind::Slash
122 | TokenKind::Percent
123 | TokenKind::DoubleStar
124 | TokenKind::Eq
125 | TokenKind::Ne
126 | TokenKind::Lt
127 | TokenKind::Le
128 | TokenKind::Gt
129 | TokenKind::Ge
130 )
131}
132
133fn unquote_annotation_arg(text: &str) -> String {
134 text.strip_prefix('"')
135 .and_then(|value| value.strip_suffix('"'))
136 .unwrap_or(text)
137 .to_string()
138}
139
140impl Parser<'_> {
141 fn peek(&self) -> &Token {
142 &self.tokens[self.pos.min(self.tokens.len() - 1)]
143 }
144
145 fn peek_kind(&self) -> TokenKind {
146 self.peek().kind
147 }
148
149 fn peek_at(&self, offset: usize) -> &Token {
150 &self.tokens[(self.pos + offset).min(self.tokens.len() - 1)]
151 }
152
153 fn advance(&mut self) -> Token {
154 let token = self.tokens[self.pos.min(self.tokens.len() - 1)].clone();
155 if self.pos < self.tokens.len() - 1 {
156 self.pos += 1;
157 }
158 token
159 }
160
161 fn skip_newlines(&mut self) {
162 while self.peek_kind() == TokenKind::Newline {
163 self.advance();
164 }
165 }
166
167 fn skip_expression_newlines(&mut self) {
168 if self.peek_kind() != TokenKind::Newline {
169 return;
170 }
171 let previous = self.tokens[..self.pos]
172 .iter()
173 .rev()
174 .find(|token| !matches!(token.kind, TokenKind::Indent(_)));
175 let previous_allows_continuation = previous.is_some_and(|token| {
176 matches!(
177 token.kind,
178 TokenKind::LParen
179 | TokenKind::LBracket
180 | TokenKind::LBrace
181 | TokenKind::Comma
182 | TokenKind::Colon
183 | TokenKind::Assign
184 ) || is_binary_operator(token.kind)
185 || (token.kind == TokenKind::Ident
186 && matches!(token.text.as_str(), "and" | "or" | "in" | "not" | "if"))
187 });
188 let mut next = self.pos;
189 while self.tokens[next].kind == TokenKind::Newline {
190 next += 1;
191 }
192 let inside_delimiter_group = self.inside_delimiter_group();
193 let next_allows_continuation = is_binary_operator(self.tokens[next].kind)
194 || (inside_delimiter_group
195 && matches!(
196 self.tokens[next].kind,
197 TokenKind::LParen
198 | TokenKind::LBracket
199 | TokenKind::Dot
200 | TokenKind::RParen
201 | TokenKind::RBracket
202 | TokenKind::RBrace
203 ))
204 || (self.tokens[next].kind == TokenKind::Ident
205 && matches!(
206 self.tokens[next].text.as_str(),
207 "and" | "or" | "in" | "not" | "else"
208 ))
209 || (inside_delimiter_group
210 && self.tokens[next].kind == TokenKind::Ident
211 && matches!(self.tokens[next].text.as_str(), "if" | "for"));
212 if previous_allows_continuation || next_allows_continuation {
213 self.skip_newlines();
214 }
215 }
216
217 fn is_ident(&self, text: &str) -> bool {
218 self.peek_kind() == TokenKind::Ident && self.peek().text == text
219 }
220
221 fn expect_ident(&mut self, what: &str) -> Result<String, ()> {
222 if self.peek_kind() == TokenKind::Ident {
223 Ok(self.advance().text)
224 } else {
225 self.error_at_current(format!("expected {what}"));
226 Err(())
227 }
228 }
229
230 fn expect(&mut self, kind: TokenKind, what: &str) -> Result<Token, ()> {
231 if self.peek_kind() == kind {
232 Ok(self.advance())
233 } else {
234 self.error_at_current(format!("expected {what}"));
235 Err(())
236 }
237 }
238
239 fn error_at_current(&mut self, message: String) {
240 let span = self.peek().span;
241 self.errors.push(OpyError::at("parse-error", message, span));
242 }
243
244 pub(super) fn workshop_construct(&self, top_level: bool) -> Option<(Span, String)> {
257 let mut end = self.pos;
258 while !matches!(self.tokens[end].kind, TokenKind::Newline | TokenKind::Eof) {
259 end += 1;
260 }
261 let line = &self.tokens[self.pos..end];
262 let first = line.first()?;
263 let last = line.last().expect("line is non-empty");
264 if last.kind == TokenKind::Semicolon {
266 return Some((
267 Span::new(first.span.file, first.span.start, last.span.end),
268 "a `;` statement terminator; OPY ends statements at the line".to_string(),
269 ));
270 }
271 if !top_level {
278 return None;
279 }
280 match first.kind {
281 TokenKind::RBrace => {
283 return Some((
284 first.span,
285 "a `}` only closes a Workshop brace block; OPY blocks end by dedenting"
286 .to_string(),
287 ));
288 }
289 TokenKind::LBrace => {
292 return Some((first.span, BRACE_BLOCKS.to_string()));
293 }
294 _ => {}
295 }
296 if first.kind != TokenKind::Ident {
297 return None;
298 }
299 if line
302 .get(1)
303 .is_some_and(|token| token.kind == TokenKind::LBrace)
304 && !is_opy_word(&first.text)
305 {
306 return Some((
307 Span::new(first.span.file, first.span.start, line[1].span.end),
308 BRACE_BLOCKS.to_string(),
309 ));
310 }
311 let mut words = 0;
314 while line
315 .get(words)
316 .is_some_and(|token| token.kind == TokenKind::Ident && !is_opy_word(&token.text))
317 {
318 words += 1;
319 }
320 if words >= 2
321 && line
322 .get(words)
323 .is_some_and(|token| token.kind == TokenKind::LParen)
324 {
325 let name = line[..words]
326 .iter()
327 .map(|token| token.text.as_str())
328 .collect::<Vec<_>>()
329 .join(" ");
330 return Some((
331 Span::new(first.span.file, first.span.start, line[words].span.end),
332 format!("a multi-word call name `{name}`; OPY calls use a single name"),
333 ));
334 }
335 None
336 }
337
338 pub(super) fn report_workshop_source(&mut self, span: Span, detail: &str) {
341 if self.workshop_source_reported {
342 return;
343 }
344 self.workshop_source_reported = true;
345 self.errors.push(OpyError::at(
346 "workshop-source",
347 format!("this looks like Workshop script, not OPY ({detail})"),
348 span,
349 ));
350 }
351
352 pub(super) fn skip_workshop_construct(&mut self) {
355 let mut depth = 0u32;
356 loop {
357 match self.peek_kind() {
358 TokenKind::Eof => break,
359 TokenKind::Newline if depth == 0 => break,
360 TokenKind::LBrace => depth += 1,
361 TokenKind::RBrace => depth = depth.saturating_sub(1),
362 _ => {}
363 }
364 self.advance();
365 }
366 }
367
368 pub(super) fn expect_block_colon(&mut self, colon_context: &str) -> Result<(), ()> {
371 if self.peek_kind() == TokenKind::LBrace {
372 let span = self.peek().span;
373 self.report_workshop_source(span, BRACE_BLOCKS);
374 self.skip_workshop_construct();
375 return Err(());
376 }
377 self.expect(TokenKind::Colon, colon_context).map(|_| ())
378 }
379
380 fn parse_program(&mut self) -> Program {
383 let mut declarations = Vec::new();
384 let mut rules = Vec::new();
385 let mut top_level = Vec::new();
386 loop {
387 self.skip_newlines();
388 if self.peek_kind() == TokenKind::Eof {
389 break;
390 }
391 let rule_prefix = if self.peek_kind() == TokenKind::RulePrefixMarker {
392 Some(self.advance().text)
393 } else {
394 None
395 };
396 let declaration_count = declarations.len();
397 let rule_count = rules.len();
398 let ok = self.parse_top_level(&mut declarations, &mut rules, rule_prefix);
399 if ok {
400 if declarations.len() > declaration_count {
401 top_level.push(TopLevel::Declaration(
402 declarations
403 .last()
404 .expect("declaration was appended")
405 .clone(),
406 ));
407 } else if rules.len() > rule_count {
408 top_level.push(TopLevel::Rule(
409 rules.last().expect("rule was appended").clone(),
410 ));
411 }
412 }
413 if !ok {
414 self.recover_line();
415 }
416 }
417 Program {
418 declarations,
419 rules,
420 top_level,
421 settings: None,
422 }
423 }
424
425 fn parse_top_level(
426 &mut self,
427 declarations: &mut Vec<Decl>,
428 rules: &mut Vec<RuleEntry>,
429 rule_prefix: Option<String>,
430 ) -> bool {
431 let token = self.peek();
432 if token.kind == TokenKind::Ident {
433 match token.text.as_str() {
434 "rule" => return self.parse_rule(rules, rule_prefix),
435 "def" => return self.parse_def(rules, rule_prefix),
436 "globalvar" => return self.parse_variable(declarations, true),
437 "playervar" => return self.parse_variable(declarations, false),
438 "subroutine" => return self.parse_subroutine(declarations),
439 "enum" => return self.parse_enum(declarations),
440 "macro" => return self.parse_macro(declarations),
441 _ => {}
442 }
443 }
444 if let Some((span, detail)) = self.workshop_construct(true) {
445 self.report_workshop_source(span, &detail);
446 self.skip_workshop_construct();
447 } else {
448 self.error_at_current(format!(
449 "expected a top-level declaration (rule/def/globalvar/playervar/subroutine/enum/macro) but found '{}'",
450 token.text
451 ));
452 }
453 false
454 }
455
456 fn recover_line(&mut self) {
458 while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
459 self.advance();
460 }
461 }
462
463 fn block_indent(&mut self, line_indent: u32) -> Option<u32> {
466 self.skip_newlines();
467 if self.peek_kind() == TokenKind::Eof {
468 self.error_at_current("expected an indented block".to_string());
469 return None;
470 }
471 let indent = self.peek().span.start.col;
472 if indent <= line_indent {
473 self.error_at_current("expected an indented block after ':'".to_string());
474 return None;
475 }
476 Some(indent)
477 }
478
479 pub(super) fn expect_block_indent(
480 &mut self,
481 line_indent: u32,
482 colon_context: &str,
483 ) -> Result<u32, ()> {
484 self.expect_block_colon(colon_context)?;
485 self.block_indent(line_indent).ok_or(())
486 }
487
488 fn expect_statement_end(&mut self, what: &str) -> Result<(), ()> {
489 let continued_line = self
490 .tokens
491 .get(self.pos.saturating_sub(1))
492 .is_some_and(|previous| self.peek().span.start.line > previous.span.end.line);
493 self.last_statement_continued = continued_line;
494 if matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) || continued_line {
495 Ok(())
496 } else {
497 self.error_at_current(format!("expected the end of {what}"));
498 Err(())
499 }
500 }
501}
502
503pub(crate) fn parse_expression_fragment(
506 text: &str,
507 file: u32,
508 origin: Position,
509) -> Result<Expr, OpyError> {
510 let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
511 file_id: file,
512 text,
513 })?;
514 for token in &mut tokens {
515 token.span = shift_span(token.span, origin);
516 }
517 let mut parser = Parser::new(&tokens, false);
518 let expression = parser.parse_expr().map_err(|()| {
519 parser.errors.first().cloned().unwrap_or_else(|| {
520 OpyError::at(
521 "parse-error",
522 "invalid f-string expression",
523 Span::new(file, origin, origin),
524 )
525 })
526 })?;
527 if parser.peek_kind() != TokenKind::Eof {
528 parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
529 }
530 parser.errors.into_iter().next().map_or(Ok(expression), Err)
531}
532
533fn shift_span(span: Span, origin: Position) -> Span {
534 Span::new(
535 span.file,
536 crate::diag::shift_position(span.start, origin),
537 crate::diag::shift_position(span.end, origin),
538 )
539}
540
541fn is_string_modifier(text: &str) -> bool {
542 matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use crate::lexer::{LexInput, lex};
549
550 fn parse_ok(text: &str) -> Program {
551 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
552 let output = parse(&tokens);
553 assert!(
554 output.errors.is_empty(),
555 "unexpected errors: {:?}",
556 output.errors
557 );
558 output.program.unwrap()
559 }
560
561 fn parse_err(text: &str) -> Vec<OpyError> {
562 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
563 parse(&tokens).errors
564 }
565
566 #[test]
567 fn parses_basic_rule() {
568 let program = parse_ok("rule \"setup\":\n @Event global\n disableInspector()\n");
569 assert_eq!(program.rules.len(), 1);
570 let RuleEntry::Rule(rule) = &program.rules[0] else {
571 panic!("expected rule");
572 };
573 assert_eq!(rule.name, "setup");
574 assert_eq!(rule.event.name, "global");
575 assert_eq!(rule.actions.len(), 1);
576 }
577
578 #[test]
579 fn parses_power_augmented_assignment() {
580 let program =
583 parse_ok("globalvar a\nrule \"r\":\n @Event global\n a = 2\n a **= 3\n");
584 let RuleEntry::Rule(rule) = &program.rules[0] else {
585 panic!("expected rule");
586 };
587 let Stmt::Assign {
588 value,
589 target: assigned_target,
590 ..
591 } = &rule.actions[1]
592 else {
593 panic!("expected an assignment");
594 };
595 let Expr::Binary {
596 op, left, right, ..
597 } = value
598 else {
599 panic!("expected a binary modification, got {value:?}");
600 };
601 assert_eq!(op, "**");
602 assert!(matches!(&**left, Expr::Name { .. }));
603 assert!(matches!(
604 assigned_target,
605 Expr::Name { name, .. } if name == "a"
606 ));
607 assert!(matches!(right.as_ref(), Expr::Number { .. }));
608 }
609
610 #[test]
611 fn parses_postfix_increment_and_decrement_as_modifications() {
612 let program =
613 parse_ok("globalvar value\nrule \"r\":\n @Event global\n value++\n value--\n");
614 let RuleEntry::Rule(rule) = &program.rules[0] else {
615 panic!("expected a rule");
616 };
617 for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
618 let Stmt::Assign { target, value, .. } = statement else {
619 panic!("expected a postfix assignment");
620 };
621 let Expr::Binary {
622 op, left, right, ..
623 } = value
624 else {
625 panic!("expected a synthetic modification value");
626 };
627 assert_eq!(op, expected_op);
628 let Expr::Name {
629 name: left_name, ..
630 } = left.as_ref()
631 else {
632 panic!("expected the target to be the modification's left operand");
633 };
634 let Expr::Name {
635 name: target_name, ..
636 } = target
637 else {
638 panic!("expected a name target");
639 };
640 assert_eq!(left_name, target_name);
641 assert!(
642 matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
643 );
644 }
645 }
646
647 #[test]
648 fn rejects_prefix_increment_and_embedded_postfix_forms() {
649 for source in [
650 "globalvar value\nrule \"r\":\n @Event global\n ++value\n",
651 "globalvar value\nrule \"r\":\n @Event global\n value++++\n",
652 ] {
653 let errors = parse_err(source);
654 assert!(!errors.is_empty());
655 assert!(errors.iter().all(|error| error.code == "parse-error"));
656 assert!(errors.iter().all(|error| error.span.is_some()));
657 }
658 }
659
660 #[test]
661 fn preserves_consecutive_unary_minus_expressions() {
662 let source = concat!(
663 "globalvar value = 0\n",
664 "globalvar B = 1\n",
665 "rule \"r\":\n",
666 " @Event global\n",
667 " value = --1\n",
668 " value = --B\n",
669 " value = B--1\n",
670 );
671 parse_ok(source);
672 }
673
674 #[test]
675 fn parses_control_flow() {
676 let program = parse_ok(
677 "globalvar index = 0\n\nrule \"r\":\n @Event global\n for index in range(3):\n if index == 0:\n debug(index)\n elif index == 1:\n debug(index)\n else:\n debug(index)\n while index < 3:\n index += 1\n wait()\n",
678 );
679 let RuleEntry::Rule(rule) = &program.rules[0] else {
680 panic!();
681 };
682 assert!(matches!(rule.actions[0], Stmt::For { .. }));
683 let Stmt::For { body, .. } = &rule.actions[0] else {
684 panic!();
685 };
686 let Stmt::If {
687 branches, r#else, ..
688 } = &body[0]
689 else {
690 panic!();
691 };
692 assert_eq!(branches.len(), 2);
693 assert!(r#else.is_some());
694 let Stmt::While { body, .. } = &rule.actions[1] else {
695 panic!();
696 };
697 assert_eq!(body.len(), 2);
698 }
699
700 #[test]
701 fn parses_source_statement_surface() {
702 let program = parse_ok(concat!(
703 "globalvar value\n",
704 "rule \"r\":\n",
705 " @Event global\n",
706 " del value[1]\n",
707 " value min= 2\n",
708 " value max= 3\n",
709 " while value < 4:\n",
710 " continue\n",
711 " goto RULE_START\n",
712 " goto target\n",
713 " goto loc + value\n",
714 " target:\n",
715 ));
716 let RuleEntry::Rule(rule) = &program.rules[0] else {
717 panic!("expected rule");
718 };
719 assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
720 for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
721 let Stmt::Assign { value, .. } = statement else {
722 panic!("expected augmented assignment");
723 };
724 assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
725 }
726 let Stmt::While { body, .. } = &rule.actions[3] else {
727 panic!("expected while");
728 };
729 assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
730 assert!(matches!(
731 &rule.actions[4],
732 Stmt::Goto {
733 label: None,
734 offset: None,
735 rule_start: true,
736 ..
737 }
738 ));
739 assert!(matches!(
740 &rule.actions[5],
741 Stmt::Goto {
742 label: Some(label),
743 offset: None,
744 rule_start: false,
745 ..
746 } if label == "target"
747 ));
748 assert!(matches!(
749 &rule.actions[6],
750 Stmt::Goto {
751 label: None,
752 offset: Some(_),
753 rule_start: false,
754 ..
755 }
756 ));
757 assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
758 }
759
760 #[test]
761 fn workshop_script_source_reports_one_diagnostic() {
762 let errors = parse_err(concat!(
766 "rule \"workshop style\" {\n",
767 " event {\n",
768 " Ongoing - Global;\n",
769 " }\n",
770 " actions {\n",
771 " Wait(1, Ignore Condition);\n",
772 " }\n",
773 "}\n",
774 ));
775 assert_eq!(errors.len(), 1, "expected one diagnostic, got {errors:?}");
776 let error = &errors[0];
777 assert_eq!(error.code, "workshop-source");
778 assert_eq!(
779 error.span.expect("a source span"),
780 Span::new(0, Position::new(1, 23), Position::new(1, 24))
781 );
782 }
783
784 #[test]
785 fn workshop_script_forms_report_once() {
786 for source in [
787 "rule(\"x\") {\n event {\n Ongoing - Global;\n }\n}\n",
789 "rule \"r\":\n @Event global\n Ongoing - Global;\n",
791 "actions {\n Wait(1);\n}\n",
793 "rule \"r\":\n @Event global\n if x == 1 {\n pass\n }\n",
795 "rule \"r\":\n @Event global\n Wait(1);\n}\n",
799 "rule \"r\":\n @Event global\n a = 1;\n b = 2;\n",
801 ] {
802 let errors = parse_err(source);
803 assert_eq!(
804 errors.len(),
805 1,
806 "expected one diagnostic for {source:?}, got {errors:?}"
807 );
808 assert_eq!(errors[0].code, "workshop-source");
809 }
810 }
811
812 #[test]
813 fn workshop_tells_only_apply_at_top_level() {
814 for source in [
818 "x y(1)\n", "x {}\n", "}\n",
822 ] {
823 let errors = parse_err(source);
824 assert_eq!(
825 errors.len(),
826 1,
827 "expected one diagnostic for {source:?}, got {errors:?}"
828 );
829 assert_eq!(errors[0].code, "workshop-source");
830 }
831 parse_ok(
834 "globalvar x\nglobalvar y\nrule \"r\":\n @Event global\n x y(1)\n x {}\n if {1: 2}: pass\n",
835 );
836 let errors = parse_err("rule \"r\":\n @Event global\n x = {\n a\n }\n");
839 assert!(!errors.is_empty(), "malformed dict parsed cleanly");
840 assert!(errors.iter().all(|error| error.code == "parse-error"));
841 }
842
843 #[test]
844 fn workshop_style_action_call_reports_once() {
845 let errors = parse_err("Set Player Variable(eventPlayer, score, 1)\n");
849 assert_eq!(errors.len(), 1, "expected one diagnostic, got {errors:?}");
850 let error = &errors[0];
851 assert_eq!(error.code, "workshop-source");
852 assert_eq!(
853 error.span.expect("a source span").start,
854 Position::new(1, 1)
855 );
856 }
857
858 #[test]
859 fn ordinary_syntax_errors_keep_their_diagnostics() {
860 let errors = parse_err("rule no_quotes:\n");
862 assert_eq!(errors.len(), 1);
863 assert_eq!(errors[0].code, "parse-error");
864 assert!(errors[0].message.contains("rule name string"));
866 assert_eq!(
867 errors[0].span.expect("a source span").start,
868 Position::new(1, 6)
869 );
870 parse_ok("globalvar x\nglobalvar y\nrule \"r\":\n @Event global\n x y\n");
872 }
873
874 #[test]
875 fn rejects_invalid_source_statement_forms() {
876 for source in [
877 "rule \"r\":\n @Event global\n del value\n",
878 "rule \"r\":\n @Event global\n goto\n",
879 "rule \"r\":\n @Event global\n goto loc\n",
880 "rule \"r\":\n @Event global\n goto target extra\n",
881 "rule \"r\":\n @Event global\n continue now\n",
882 "rule \"r\":\n @Event global\n A = 1; A = 2\n",
883 ] {
884 let errors = parse_err(source);
885 assert!(!errors.is_empty(), "invalid form parsed: {source}");
886 assert!(errors.iter().all(|error| error.code == "parse-error"));
887 assert!(errors.iter().all(|error| error.span.is_some()));
888 }
889 }
890
891 #[test]
892 fn rejects_continued_inline_if_without_else() {
893 let errors = parse_err(concat!(
894 "globalvar value\n",
895 "macro nextHero():\n",
896 " value = 1\\\n",
897 " if value == 0: value = 2\\\n",
898 " value = 3\\\n",
899 ));
900 assert!(errors.iter().any(|error| {
901 error.code == "parse-error" && error.message == "Found 'if', but no 'else'"
902 }));
903 }
904
905 #[test]
906 fn parses_syntax_constructs() {
907 let program = parse_ok(
908 "globalvar x\nrule \"r\":\n @Event global\n switch x:\n case 0x10:\n x = 1 in [1, 2]\n default:\n do:\n x = {\"x\": 1}[\"x\"]\n while x not in [2, 3]\n x = [value * 2 for value, index in [1, 2] if value > index]\n x = sorted([1, 2], key=lambda value: value)\n x = w\"wide\"\n",
909 );
910 let RuleEntry::Rule(rule) = &program.rules[0] else {
911 panic!("expected rule");
912 };
913 assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
914 assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
915 }
916
917 #[test]
918 fn rejects_incomplete_do_while_and_dictionary_entries() {
919 let errors = parse_err(
920 "rule \"r\":\n @Event global\n do:\n pass\n while\n x = {\"x\"}\n",
921 );
922 assert!(!errors.is_empty());
923 assert!(errors.iter().all(|error| error.code == "parse-error"));
924 }
925
926 #[test]
927 fn parses_explicit_enum_member_values() {
928 let program = parse_ok("enum EventType:\n BUFF = 0\n DEBUFF\n MECH = 2\n");
929 let Decl::Enum { members, .. } = &program.declarations[0] else {
930 panic!("expected enum");
931 };
932 assert_eq!(
933 members
934 .iter()
935 .map(|(name, _)| name.as_str())
936 .collect::<Vec<_>>(),
937 ["BUFF", "DEBUFF", "MECH"]
938 );
939 }
940
941 #[test]
942 fn parses_multi_line_array() {
943 let program = parse_ok(
944 "globalvar p\nrule \"r\":\n @Event global\n p = [\n vect(1, 0, 0),\n vect(2, 0, 0),\n ]\n",
945 );
946 let RuleEntry::Rule(rule) = &program.rules[0] else {
947 panic!();
948 };
949 let Stmt::Assign { value, .. } = &rule.actions[0] else {
950 panic!();
951 };
952 let Expr::Array { elements, .. } = value else {
953 panic!("expected array, got {value:?}");
954 };
955 assert_eq!(elements.len(), 2);
956 }
957
958 #[test]
959 fn missing_colon_is_a_structured_error() {
960 let errors = parse_err("rule \"x\"\n @Event global\n");
961 assert!(!errors.is_empty());
962 assert_eq!(errors[0].code, "parse-error");
963 assert!(errors[0].span.is_some());
964 }
965
966 #[test]
967 fn def_and_macro_parse() {
968 let program = parse_ok(
969 "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n print(\"hi\")\n\nmacro double(value):\n value + value\n",
970 );
971 assert_eq!(program.declarations.len(), 3);
972 assert!(matches!(program.declarations[1], Decl::Constant { .. }));
973 assert!(matches!(program.declarations[2], Decl::Macro { .. }));
974 let Decl::Macro { args, body, .. } = &program.declarations[2] else {
975 panic!();
976 };
977 assert_eq!(args, &vec!["value".to_string()]);
978 assert_eq!(body.len(), 1);
979 }
980
981 #[test]
982 fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
983 let text = "enum Kind:\n First\n First\nmacro helper():\n pass\nmacro helper():\n pass\n";
984 let errors = parse_err(text);
985 assert_eq!(
986 errors
987 .iter()
988 .filter(|error| error.code == "macro-redeclaration")
989 .count(),
990 2
991 );
992
993 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
994 let output = parse_with_options(&tokens, true);
995 assert!(
996 output.errors.is_empty(),
997 "unexpected errors: {:?}",
998 output.errors
999 );
1000 assert!(output.program.is_some());
1001 }
1002
1003 #[test]
1004 fn multiple_errors_are_reported() {
1005 let errors =
1006 parse_err("rule \"a\"\n bad statement here\nrule \"b\"\n @Event global\n");
1007 assert!(!errors.is_empty());
1008 }
1009
1010 #[test]
1011 fn precedence_parses_python_like() {
1012 let program = parse_ok("globalvar x\nrule \"r\":\n @Event global\n x = 1 + 2 * 3\n");
1013 let RuleEntry::Rule(rule) = &program.rules[0] else {
1014 panic!();
1015 };
1016 let Stmt::Assign { value, .. } = &rule.actions[0] else {
1017 panic!();
1018 };
1019 let Expr::Binary {
1020 op, left, right, ..
1021 } = value
1022 else {
1023 panic!();
1024 };
1025 assert_eq!(op, "+");
1026 let Expr::Binary { op: inner, .. } = right.as_ref() else {
1027 panic!();
1028 };
1029 assert_eq!(inner, "*");
1030 assert!(matches!(left.as_ref(), Expr::Number { .. }));
1031 }
1032
1033 #[test]
1034 fn parses_right_associative_conditional_expressions() {
1035 let program = parse_ok(
1036 "rule \"r\":\n @Event global\n debug(1 if true else 2 if false else 3)\n",
1037 );
1038 let RuleEntry::Rule(rule) = &program.rules[0] else {
1039 panic!("expected a rule");
1040 };
1041 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1042 panic!("expected an expression statement");
1043 };
1044 let Expr::Call { args, .. } = expr else {
1045 panic!("expected a call");
1046 };
1047 let Expr::Conditional {
1048 then_value,
1049 condition,
1050 else_value,
1051 span,
1052 } = &args[0].value
1053 else {
1054 panic!("expected a conditional expression");
1055 };
1056 assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
1057 assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
1058 assert!(matches!(
1059 else_value.as_ref(),
1060 Expr::Conditional { then_value, condition, else_value, .. }
1061 if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
1062 && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
1063 && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
1064 ));
1065 assert_eq!(span.start.line, 3);
1066 assert_eq!(span.start.col, 11);
1067 }
1068
1069 #[test]
1070 fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
1071 let program = parse_ok(
1072 "rule \"r\":\n @Event global\n debug((1 if true else 2) if false else 3)\n",
1073 );
1074 let RuleEntry::Rule(rule) = &program.rules[0] else {
1075 panic!("expected a rule");
1076 };
1077 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1078 panic!("expected an expression statement");
1079 };
1080 let Expr::Call { args, .. } = expr else {
1081 panic!("expected a call");
1082 };
1083 assert!(matches!(
1084 &args[0].value,
1085 Expr::Conditional {
1086 then_value,
1087 condition,
1088 else_value,
1089 ..
1090 } if matches!(then_value.as_ref(), Expr::Conditional { .. })
1091 && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
1092 && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
1093 ));
1094
1095 let errors = parse_err("rule \"r\":\n @Event global\n debug(1 if true)\n");
1096 assert_eq!(errors[0].code, "parse-error");
1097 assert!(errors[0].message.contains("expected `else`"));
1098 }
1099
1100 #[test]
1101 fn parses_receiver_calls() {
1102 let program =
1105 parse_ok("rule \"r\":\n @Event eachPlayer\n eventPlayer.setMoveSpeed(100)\n");
1106 let RuleEntry::Rule(rule) = &program.rules[0] else {
1107 panic!("expected rule");
1108 };
1109 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1110 panic!("expected expression statement, got {:?}", rule.actions[0]);
1111 };
1112 let Expr::ReceiverCall {
1113 receiver,
1114 name,
1115 args,
1116 ..
1117 } = &expr
1118 else {
1119 panic!("expected receiver call, got {expr:?}");
1120 };
1121 assert_eq!(name, "setMoveSpeed");
1122 assert!(
1123 matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
1124 "receiver must be the eventPlayer name"
1125 );
1126 assert_eq!(args.len(), 1);
1127 assert!(args[0].keyword.is_none(), "positional argument");
1128 assert!(matches!(&args[0].value, Expr::Number { .. }));
1129 }
1130
1131 #[test]
1132 fn parses_keyword_arguments_with_name_spans() {
1133 let program =
1136 parse_ok("rule \"r\":\n @Event global\n wait(time=1)\n debug(g == 1)\n");
1137 let RuleEntry::Rule(rule) = &program.rules[0] else {
1138 panic!("expected rule");
1139 };
1140 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1141 panic!("expected expression statement");
1142 };
1143 let Expr::Call { args, .. } = expr else {
1144 panic!("expected a call, got {expr:?}");
1145 };
1146 let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
1147 assert_eq!(keyword, "time");
1148 assert_eq!(span.start.line, 3);
1149 assert!(matches!(&args[0].value, Expr::Number { .. }));
1150
1151 let Stmt::Expr { expr, .. } = &rule.actions[1] else {
1152 panic!("expected expression statement");
1153 };
1154 let Expr::Call { args, .. } = expr else {
1155 panic!("expected a call, got {expr:?}");
1156 };
1157 assert!(args[0].keyword.is_none(), "comparisons are not keywords");
1158 assert!(matches!(&args[0].value, Expr::Binary { .. }));
1159 }
1160
1161 #[test]
1162 fn adjacent_string_literals_concatenate_and_preserve_span() {
1163 let program = parse_ok("rule \"r\":\n @Event global\n debug(\"one\" \"two\")\n");
1164 let RuleEntry::Rule(rule) = &program.rules[0] else {
1165 panic!("expected rule");
1166 };
1167 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1168 panic!("expected expression statement");
1169 };
1170 let Expr::Call { args, .. } = expr else {
1171 panic!("expected call");
1172 };
1173 let Expr::String { value, span } = &args[0].value else {
1174 panic!("expected concatenated string");
1175 };
1176 assert_eq!(value, "onetwo");
1177 assert_eq!(span.start.line, 3);
1178 assert_eq!(span.start.col, 11);
1179 assert_eq!(span.end.col, 22);
1180 }
1181
1182 #[test]
1183 fn multiline_adjacent_string_literals_concatenate_inside_group() {
1184 let program =
1185 parse_ok("rule \"r\":\n @Event global\n debug(\"one\"\n \"two\")\n");
1186 let RuleEntry::Rule(rule) = &program.rules[0] else {
1187 panic!("expected rule");
1188 };
1189 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1190 panic!("expected expression statement");
1191 };
1192 let Expr::Call { args, .. } = expr else {
1193 panic!("expected call");
1194 };
1195 assert!(matches!(
1196 &args[0].value,
1197 Expr::String { value, .. } if value == "onetwo"
1198 ));
1199 }
1200
1201 #[test]
1202 fn newline_outside_group_keeps_adjacent_literals_as_statements() {
1203 let program = parse_ok("rule \"r\":\n @Event global\n \"one\"\n \"two\"\n");
1204 let RuleEntry::Rule(rule) = &program.rules[0] else {
1205 panic!("expected rule");
1206 };
1207 assert_eq!(rule.actions.len(), 2);
1208 }
1209
1210 #[test]
1211 fn non_name_keyword_lhs_is_a_parse_error() {
1212 let errors = parse_err("rule \"r\":\n @Event global\n debug(1 = 2)\n");
1214 assert!(!errors.is_empty());
1215 assert_eq!(errors[0].code, "parse-error");
1216 }
1217
1218 #[test]
1219 fn parses_member_call_on_call_result() {
1220 let program = parse_ok(
1223 "rule \"r\":\n @Event eachPlayer\n getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
1224 );
1225 let RuleEntry::Rule(rule) = &program.rules[0] else {
1226 panic!("expected rule");
1227 };
1228 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1229 panic!("expected expression statement");
1230 };
1231 let Expr::ReceiverCall {
1232 receiver,
1233 name,
1234 args,
1235 ..
1236 } = &expr
1237 else {
1238 panic!("expected receiver call, got {expr:?}");
1239 };
1240 assert_eq!(name, "setStatusEffect");
1241 assert!(
1242 matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
1243 "receiver must be the preceding call"
1244 );
1245 assert_eq!(args.len(), 2);
1246 }
1247
1248 #[test]
1249 fn member_without_call_is_not_a_call() {
1250 let program =
1253 parse_ok("rule \"r\":\n @Event eachPlayer\n x = eventPlayer.moveSpeed\n");
1254 let RuleEntry::Rule(rule) = &program.rules[0] else {
1255 panic!("expected rule");
1256 };
1257 let Stmt::Assign { value, .. } = &rule.actions[0] else {
1258 panic!("expected assignment");
1259 };
1260 assert!(matches!(
1261 &value,
1262 Expr::Member { member, .. } if member == "moveSpeed"
1263 ));
1264 }
1265
1266 #[test]
1267 fn parses_advanced_rule_annotations_with_source_arguments() {
1268 let program = parse_ok(
1269 "subroutine helper\ndef helper():\n @Name \"renamed\"\n @SuppressWarnings unusedVariable\n pass\nrule \"r\":\n @Event eachPlayer\n @Team 1\n @Hero dmon\n @Disabled\n @Delimiter\n @NewPage \"Page\"\n @SuppressWarnings unusedVariable\n pass\n",
1270 );
1271 let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1272 panic!("expected subroutine");
1273 };
1274 assert_eq!(annotations.len(), 2);
1275 let RuleEntry::Rule(rule) = &program.rules[1] else {
1276 panic!("expected rule");
1277 };
1278 assert!(rule.disabled);
1279 assert!(rule.delimiter);
1280 assert_eq!(rule.new_page.as_deref(), Some("Page"));
1281 assert_eq!(rule.annotations.len(), 7);
1282 assert_eq!(rule.annotations[1].args[0].text, "1");
1283 assert_eq!(rule.annotations[2].args[0].text, "dmon");
1284 }
1285
1286 #[test]
1287 fn decodes_unicode_escapes_in_names_and_ordinary_strings() {
1288 let program = parse_ok(
1289 r#"subroutine helper
1290def helper():
1291 @Name "helper\ufeffname"
1292 pass
1293rule "pa\ufeffssed":
1294 @Event global
1295 debug("pa\ufeffssed")
1296 debug(f"pa\ufeffssed {1}")
1297"#,
1298 );
1299 let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1300 panic!("expected subroutine");
1301 };
1302 assert_eq!(annotations[0].args[0].text, "\"helper\u{feff}name\"");
1303
1304 let RuleEntry::Rule(rule) = &program.rules[1] else {
1305 panic!("expected rule");
1306 };
1307 assert_eq!(rule.name, "pa\u{feff}ssed");
1308 assert_eq!(rule.name_span.start.col, 7);
1309 assert_eq!(rule.name_span.end.col, 19);
1310 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1311 panic!("expected debug expression");
1312 };
1313 let Expr::Call { args, .. } = expr else {
1314 panic!("expected debug call");
1315 };
1316 assert!(matches!(
1317 &args[0].value,
1318 Expr::String { value, .. } if value == "pa\u{feff}ssed"
1319 ));
1320 let Stmt::Expr { expr, .. } = &rule.actions[1] else {
1321 panic!("expected formatted debug expression");
1322 };
1323 let Expr::Call { args, .. } = expr else {
1324 panic!("expected formatted debug call");
1325 };
1326 assert!(matches!(
1327 &args[0].value,
1328 Expr::StringModifier {
1329 format_text: Some(text), ..
1330 } if text == "pa\u{feff}ssed {0}"
1331 ));
1332 }
1333}