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