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