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 .span
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().span.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().span.start.line > previous.span.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(
560 text: &str,
561 file: u32,
562 origin: Position,
563) -> Result<Expr, OpyError> {
564 let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
565 file_id: file,
566 text,
567 })?;
568 for token in &mut tokens {
569 token.span = shift_span(token.span, origin);
570 }
571 let mut parser = Parser::new(&tokens, false);
572 let expression = parser.parse_expr().map_err(|()| {
573 parser.errors.first().cloned().unwrap_or_else(|| {
574 OpyError::at(
575 "parse-error",
576 "invalid f-string expression",
577 Span::new(file, origin, origin),
578 )
579 })
580 })?;
581 if parser.peek_kind() != TokenKind::Eof {
582 parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
583 }
584 parser.errors.into_iter().next().map_or(Ok(expression), Err)
585}
586
587fn shift_span(span: Span, origin: Position) -> Span {
588 Span::new(
589 span.file,
590 crate::diag::shift_position(span.start, origin),
591 crate::diag::shift_position(span.end, origin),
592 )
593}
594
595fn is_string_modifier(text: &str) -> bool {
596 matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::lexer::{LexInput, lex};
603
604 fn parse_ok(text: &str) -> Program {
605 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
606 let output = parse(&tokens);
607 assert!(
608 output.errors.is_empty(),
609 "unexpected errors: {:?}",
610 output.errors
611 );
612 output.program.unwrap()
613 }
614
615 fn parse_err(text: &str) -> Vec<OpyError> {
616 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
617 parse(&tokens).errors
618 }
619
620 #[test]
621 fn parses_basic_rule() {
622 let program = parse_ok("rule \"setup\":\n @Event global\n disableInspector()\n");
623 assert_eq!(program.rules.len(), 1);
624 let RuleEntry::Rule(rule) = &program.rules[0] else {
625 panic!("expected rule");
626 };
627 assert_eq!(rule.name, "setup");
628 assert_eq!(rule.event.name, "global");
629 assert_eq!(rule.actions.len(), 1);
630 }
631
632 #[test]
633 fn parses_power_augmented_assignment() {
634 let program =
637 parse_ok("globalvar a\nrule \"r\":\n @Event global\n a = 2\n a **= 3\n");
638 let RuleEntry::Rule(rule) = &program.rules[0] else {
639 panic!("expected rule");
640 };
641 let Stmt::Assign {
642 value,
643 target: assigned_target,
644 ..
645 } = &rule.actions[1]
646 else {
647 panic!("expected an assignment");
648 };
649 let Expr::Binary {
650 op, left, right, ..
651 } = value
652 else {
653 panic!("expected a binary modification, got {value:?}");
654 };
655 assert_eq!(op, "**");
656 assert!(matches!(&**left, Expr::Name { .. }));
657 assert!(matches!(
658 assigned_target,
659 Expr::Name { name, .. } if name == "a"
660 ));
661 assert!(matches!(right.as_ref(), Expr::Number { .. }));
662 }
663
664 #[test]
665 fn parses_postfix_increment_and_decrement_as_modifications() {
666 let program =
667 parse_ok("globalvar value\nrule \"r\":\n @Event global\n value++\n value--\n");
668 let RuleEntry::Rule(rule) = &program.rules[0] else {
669 panic!("expected a rule");
670 };
671 for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
672 let Stmt::Assign { target, value, .. } = statement else {
673 panic!("expected a postfix assignment");
674 };
675 let Expr::Binary {
676 op, left, right, ..
677 } = value
678 else {
679 panic!("expected a synthetic modification value");
680 };
681 assert_eq!(op, expected_op);
682 let Expr::Name {
683 name: left_name, ..
684 } = left.as_ref()
685 else {
686 panic!("expected the target to be the modification's left operand");
687 };
688 let Expr::Name {
689 name: target_name, ..
690 } = target
691 else {
692 panic!("expected a name target");
693 };
694 assert_eq!(left_name, target_name);
695 assert!(
696 matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
697 );
698 }
699 }
700
701 #[test]
702 fn rejects_prefix_increment_and_embedded_postfix_forms() {
703 for source in [
704 "globalvar value\nrule \"r\":\n @Event global\n ++value\n",
705 "globalvar value\nrule \"r\":\n @Event global\n value++++\n",
706 ] {
707 let errors = parse_err(source);
708 assert!(!errors.is_empty());
709 assert!(errors.iter().all(|error| error.code == "parse-error"));
710 assert!(errors.iter().all(|error| error.span.is_some()));
711 }
712 }
713
714 #[test]
715 fn preserves_consecutive_unary_minus_expressions() {
716 let source = concat!(
717 "globalvar value = 0\n",
718 "globalvar B = 1\n",
719 "rule \"r\":\n",
720 " @Event global\n",
721 " value = --1\n",
722 " value = --B\n",
723 " value = B--1\n",
724 );
725 parse_ok(source);
726 }
727
728 #[test]
729 fn parses_control_flow() {
730 let program = parse_ok(
731 "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",
732 );
733 let RuleEntry::Rule(rule) = &program.rules[0] else {
734 panic!();
735 };
736 assert!(matches!(rule.actions[0], Stmt::For { .. }));
737 let Stmt::For { body, .. } = &rule.actions[0] else {
738 panic!();
739 };
740 let Stmt::If {
741 branches, r#else, ..
742 } = &body[0]
743 else {
744 panic!();
745 };
746 assert_eq!(branches.len(), 2);
747 assert!(r#else.is_some());
748 let Stmt::While { body, .. } = &rule.actions[1] else {
749 panic!();
750 };
751 assert_eq!(body.len(), 2);
752 }
753
754 #[test]
755 fn parses_source_statement_surface() {
756 let program = parse_ok(concat!(
757 "globalvar value\n",
758 "rule \"r\":\n",
759 " @Event global\n",
760 " del value[1]\n",
761 " value min= 2\n",
762 " value max= 3\n",
763 " while value < 4:\n",
764 " continue\n",
765 " goto RULE_START\n",
766 " goto target\n",
767 " goto loc + value\n",
768 " target:\n",
769 ));
770 let RuleEntry::Rule(rule) = &program.rules[0] else {
771 panic!("expected rule");
772 };
773 assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
774 for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
775 let Stmt::Assign { value, .. } = statement else {
776 panic!("expected augmented assignment");
777 };
778 assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
779 }
780 let Stmt::While { body, .. } = &rule.actions[3] else {
781 panic!("expected while");
782 };
783 assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
784 assert!(matches!(
785 &rule.actions[4],
786 Stmt::Goto {
787 label: None,
788 offset: None,
789 rule_start: true,
790 ..
791 }
792 ));
793 assert!(matches!(
794 &rule.actions[5],
795 Stmt::Goto {
796 label: Some(label),
797 offset: None,
798 rule_start: false,
799 ..
800 } if label == "target"
801 ));
802 assert!(matches!(
803 &rule.actions[6],
804 Stmt::Goto {
805 label: None,
806 offset: Some(_),
807 rule_start: false,
808 ..
809 }
810 ));
811 assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
812 }
813
814 #[test]
815 fn workshop_script_source_reports_one_diagnostic() {
816 let errors = parse_err(concat!(
820 "rule \"workshop style\" {\n",
821 " event {\n",
822 " Ongoing - Global;\n",
823 " }\n",
824 " actions {\n",
825 " Wait(1, Ignore Condition);\n",
826 " }\n",
827 "}\n",
828 ));
829 assert_eq!(errors.len(), 1, "expected one diagnostic, got {errors:?}");
830 let error = &errors[0];
831 assert_eq!(error.code, "workshop-source");
832 assert_eq!(
833 error.span.expect("a source span"),
834 Span::new(0, Position::new(1, 23), Position::new(1, 24))
835 );
836 }
837
838 #[test]
839 fn workshop_script_forms_report_once() {
840 for source in [
841 "rule(\"x\") {\n event {\n Ongoing - Global;\n }\n}\n",
843 "rule \"r\":\n @Event global\n Ongoing - Global;\n",
845 "actions {\n Wait(1);\n}\n",
847 "rule \"r\":\n @Event global\n if x == 1 {\n pass\n }\n",
849 "rule \"r\":\n @Event global\n Wait(1);\n}\n",
853 "rule \"r\":\n @Event global\n a = 1;\n b = 2;\n",
855 ] {
856 let errors = parse_err(source);
857 assert_eq!(
858 errors.len(),
859 1,
860 "expected one diagnostic for {source:?}, got {errors:?}"
861 );
862 assert_eq!(errors[0].code, "workshop-source");
863 }
864 }
865
866 #[test]
867 fn workshop_tells_only_apply_at_top_level() {
868 for source in [
872 "x y(1)\n", "x {}\n", "}\n",
876 ] {
877 let errors = parse_err(source);
878 assert_eq!(
879 errors.len(),
880 1,
881 "expected one diagnostic for {source:?}, got {errors:?}"
882 );
883 assert_eq!(errors[0].code, "workshop-source");
884 }
885 parse_ok(
888 "globalvar x\nglobalvar y\nrule \"r\":\n @Event global\n x y(1)\n x {}\n if {1: 2}: pass\n",
889 );
890 let errors = parse_err("rule \"r\":\n @Event global\n x = {\n a\n }\n");
893 assert!(!errors.is_empty(), "malformed dict parsed cleanly");
894 assert!(errors.iter().all(|error| error.code == "parse-error"));
895 }
896
897 #[test]
898 fn workshop_style_action_call_reports_once() {
899 let errors = parse_err("Set Player Variable(eventPlayer, score, 1)\n");
903 assert_eq!(errors.len(), 1, "expected one diagnostic, got {errors:?}");
904 let error = &errors[0];
905 assert_eq!(error.code, "workshop-source");
906 assert_eq!(
907 error.span.expect("a source span").start,
908 Position::new(1, 1)
909 );
910 }
911
912 #[test]
913 fn ordinary_syntax_errors_keep_their_diagnostics() {
914 let errors = parse_err("rule no_quotes:\n");
916 assert_eq!(errors.len(), 1);
917 assert_eq!(errors[0].code, "parse-error");
918 assert!(errors[0].message.contains("rule name string"));
920 assert_eq!(
921 errors[0].span.expect("a source span").start,
922 Position::new(1, 6)
923 );
924 parse_ok("globalvar x\nglobalvar y\nrule \"r\":\n @Event global\n x y\n");
926 }
927
928 #[test]
929 fn rejects_invalid_source_statement_forms() {
930 for source in [
931 "rule \"r\":\n @Event global\n del value\n",
932 "rule \"r\":\n @Event global\n goto\n",
933 "rule \"r\":\n @Event global\n goto loc\n",
934 "rule \"r\":\n @Event global\n goto target extra\n",
935 "rule \"r\":\n @Event global\n continue now\n",
936 "rule \"r\":\n @Event global\n A = 1; A = 2\n",
937 ] {
938 let errors = parse_err(source);
939 assert!(!errors.is_empty(), "invalid form parsed: {source}");
940 assert!(errors.iter().all(|error| error.code == "parse-error"));
941 assert!(errors.iter().all(|error| error.span.is_some()));
942 }
943 }
944
945 #[test]
946 fn rejects_continued_inline_if_without_else() {
947 let errors = parse_err(concat!(
948 "globalvar value\n",
949 "macro nextHero():\n",
950 " value = 1\\\n",
951 " if value == 0: value = 2\\\n",
952 " value = 3\\\n",
953 ));
954 assert!(errors.iter().any(|error| {
955 error.code == "parse-error" && error.message == "Found 'if', but no 'else'"
956 }));
957 }
958
959 #[test]
960 fn parses_syntax_constructs() {
961 let program = parse_ok(
962 "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",
963 );
964 let RuleEntry::Rule(rule) = &program.rules[0] else {
965 panic!("expected rule");
966 };
967 assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
968 assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
969 }
970
971 #[test]
972 fn rejects_incomplete_do_while_and_dictionary_entries() {
973 let errors = parse_err(
974 "rule \"r\":\n @Event global\n do:\n pass\n while\n x = {\"x\"}\n",
975 );
976 assert!(!errors.is_empty());
977 assert!(errors.iter().all(|error| error.code == "parse-error"));
978 }
979
980 #[test]
981 fn parses_explicit_enum_member_values() {
982 let program = parse_ok("enum EventType:\n BUFF = 0\n DEBUFF\n MECH = 2\n");
983 let Decl::Enum { members, .. } = &program.declarations[0] else {
984 panic!("expected enum");
985 };
986 assert_eq!(
987 members
988 .iter()
989 .map(|(name, _)| name.as_str())
990 .collect::<Vec<_>>(),
991 ["BUFF", "DEBUFF", "MECH"]
992 );
993 }
994
995 #[test]
996 fn parses_multi_line_array() {
997 let program = parse_ok(
998 "globalvar p\nrule \"r\":\n @Event global\n p = [\n vect(1, 0, 0),\n vect(2, 0, 0),\n ]\n",
999 );
1000 let RuleEntry::Rule(rule) = &program.rules[0] else {
1001 panic!();
1002 };
1003 let Stmt::Assign { value, .. } = &rule.actions[0] else {
1004 panic!();
1005 };
1006 let Expr::Array { elements, .. } = value else {
1007 panic!("expected array, got {value:?}");
1008 };
1009 assert_eq!(elements.len(), 2);
1010 }
1011
1012 #[test]
1013 fn missing_colon_is_a_structured_error() {
1014 let errors = parse_err("rule \"x\"\n @Event global\n");
1015 assert!(!errors.is_empty());
1016 assert_eq!(errors[0].code, "parse-error");
1017 assert!(errors[0].span.is_some());
1018 }
1019
1020 #[test]
1021 fn def_and_macro_parse() {
1022 let program = parse_ok(
1023 "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n print(\"hi\")\n\nmacro double(value):\n value + value\n",
1024 );
1025 assert_eq!(program.declarations.len(), 3);
1026 assert!(matches!(program.declarations[1], Decl::Constant { .. }));
1027 assert!(matches!(program.declarations[2], Decl::Macro { .. }));
1028 let Decl::Macro { args, body, .. } = &program.declarations[2] else {
1029 panic!();
1030 };
1031 assert_eq!(args, &vec!["value".to_string()]);
1032 assert_eq!(body.len(), 1);
1033 }
1034
1035 #[test]
1036 fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
1037 let text = "enum Kind:\n First\n First\nmacro helper():\n pass\nmacro helper():\n pass\n";
1038 let errors = parse_err(text);
1039 assert_eq!(
1040 errors
1041 .iter()
1042 .filter(|error| error.code == "macro-redeclaration")
1043 .count(),
1044 2
1045 );
1046
1047 let tokens = lex(LexInput { file_id: 0, text }).unwrap();
1048 let output = parse_with_options(&tokens, true);
1049 assert!(
1050 output.errors.is_empty(),
1051 "unexpected errors: {:?}",
1052 output.errors
1053 );
1054 assert!(output.program.is_some());
1055 }
1056
1057 #[test]
1058 fn multiple_errors_are_reported() {
1059 let errors =
1060 parse_err("rule \"a\"\n bad statement here\nrule \"b\"\n @Event global\n");
1061 assert!(!errors.is_empty());
1062 }
1063
1064 #[test]
1065 fn precedence_parses_python_like() {
1066 let program = parse_ok("globalvar x\nrule \"r\":\n @Event global\n x = 1 + 2 * 3\n");
1067 let RuleEntry::Rule(rule) = &program.rules[0] else {
1068 panic!();
1069 };
1070 let Stmt::Assign { value, .. } = &rule.actions[0] else {
1071 panic!();
1072 };
1073 let Expr::Binary {
1074 op, left, right, ..
1075 } = value
1076 else {
1077 panic!();
1078 };
1079 assert_eq!(op, "+");
1080 let Expr::Binary { op: inner, .. } = right.as_ref() else {
1081 panic!();
1082 };
1083 assert_eq!(inner, "*");
1084 assert!(matches!(left.as_ref(), Expr::Number { .. }));
1085 }
1086
1087 #[test]
1088 fn parses_right_associative_conditional_expressions() {
1089 let program = parse_ok(
1090 "rule \"r\":\n @Event global\n debug(1 if true else 2 if false else 3)\n",
1091 );
1092 let RuleEntry::Rule(rule) = &program.rules[0] else {
1093 panic!("expected a rule");
1094 };
1095 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1096 panic!("expected an expression statement");
1097 };
1098 let Expr::Call { args, .. } = expr else {
1099 panic!("expected a call");
1100 };
1101 let Expr::Conditional {
1102 then_value,
1103 condition,
1104 else_value,
1105 span,
1106 } = &args[0].value
1107 else {
1108 panic!("expected a conditional expression");
1109 };
1110 assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
1111 assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
1112 assert!(matches!(
1113 else_value.as_ref(),
1114 Expr::Conditional { then_value, condition, else_value, .. }
1115 if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
1116 && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
1117 && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
1118 ));
1119 assert_eq!(span.start.line, 3);
1120 assert_eq!(span.start.col, 11);
1121 }
1122
1123 #[test]
1124 fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
1125 let program = parse_ok(
1126 "rule \"r\":\n @Event global\n debug((1 if true else 2) if false else 3)\n",
1127 );
1128 let RuleEntry::Rule(rule) = &program.rules[0] else {
1129 panic!("expected a rule");
1130 };
1131 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1132 panic!("expected an expression statement");
1133 };
1134 let Expr::Call { args, .. } = expr else {
1135 panic!("expected a call");
1136 };
1137 assert!(matches!(
1138 &args[0].value,
1139 Expr::Conditional {
1140 then_value,
1141 condition,
1142 else_value,
1143 ..
1144 } if matches!(then_value.as_ref(), Expr::Conditional { .. })
1145 && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
1146 && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
1147 ));
1148
1149 let errors = parse_err("rule \"r\":\n @Event global\n debug(1 if true)\n");
1150 assert_eq!(errors[0].code, "parse-error");
1151 assert!(errors[0].message.contains("expected `else`"));
1152 }
1153
1154 #[test]
1155 fn parses_receiver_calls() {
1156 let program =
1159 parse_ok("rule \"r\":\n @Event eachPlayer\n eventPlayer.setMoveSpeed(100)\n");
1160 let RuleEntry::Rule(rule) = &program.rules[0] else {
1161 panic!("expected rule");
1162 };
1163 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1164 panic!("expected expression statement, got {:?}", rule.actions[0]);
1165 };
1166 let Expr::ReceiverCall {
1167 receiver,
1168 name,
1169 args,
1170 ..
1171 } = &expr
1172 else {
1173 panic!("expected receiver call, got {expr:?}");
1174 };
1175 assert_eq!(name, "setMoveSpeed");
1176 assert!(
1177 matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
1178 "receiver must be the eventPlayer name"
1179 );
1180 assert_eq!(args.len(), 1);
1181 assert!(args[0].keyword.is_none(), "positional argument");
1182 assert!(matches!(&args[0].value, Expr::Number { .. }));
1183 }
1184
1185 #[test]
1186 fn parses_keyword_arguments_with_name_spans() {
1187 let program =
1190 parse_ok("rule \"r\":\n @Event global\n wait(time=1)\n debug(g == 1)\n");
1191 let RuleEntry::Rule(rule) = &program.rules[0] else {
1192 panic!("expected rule");
1193 };
1194 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1195 panic!("expected expression statement");
1196 };
1197 let Expr::Call { args, .. } = expr else {
1198 panic!("expected a call, got {expr:?}");
1199 };
1200 let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
1201 assert_eq!(keyword, "time");
1202 assert_eq!(span.start.line, 3);
1203 assert!(matches!(&args[0].value, Expr::Number { .. }));
1204
1205 let Stmt::Expr { expr, .. } = &rule.actions[1] else {
1206 panic!("expected expression statement");
1207 };
1208 let Expr::Call { args, .. } = expr else {
1209 panic!("expected a call, got {expr:?}");
1210 };
1211 assert!(args[0].keyword.is_none(), "comparisons are not keywords");
1212 assert!(matches!(&args[0].value, Expr::Binary { .. }));
1213 }
1214
1215 #[test]
1216 fn adjacent_string_literals_concatenate_and_preserve_span() {
1217 let program = parse_ok("rule \"r\":\n @Event global\n debug(\"one\" \"two\")\n");
1218 let RuleEntry::Rule(rule) = &program.rules[0] else {
1219 panic!("expected rule");
1220 };
1221 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1222 panic!("expected expression statement");
1223 };
1224 let Expr::Call { args, .. } = expr else {
1225 panic!("expected call");
1226 };
1227 let Expr::String { value, span } = &args[0].value else {
1228 panic!("expected concatenated string");
1229 };
1230 assert_eq!(value, "onetwo");
1231 assert_eq!(span.start.line, 3);
1232 assert_eq!(span.start.col, 11);
1233 assert_eq!(span.end.col, 22);
1234 }
1235
1236 #[test]
1237 fn multiline_adjacent_string_literals_concatenate_inside_group() {
1238 let program =
1239 parse_ok("rule \"r\":\n @Event global\n debug(\"one\"\n \"two\")\n");
1240 let RuleEntry::Rule(rule) = &program.rules[0] else {
1241 panic!("expected rule");
1242 };
1243 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1244 panic!("expected expression statement");
1245 };
1246 let Expr::Call { args, .. } = expr else {
1247 panic!("expected call");
1248 };
1249 assert!(matches!(
1250 &args[0].value,
1251 Expr::String { value, .. } if value == "onetwo"
1252 ));
1253 }
1254
1255 #[test]
1256 fn newline_outside_group_keeps_adjacent_literals_as_statements() {
1257 let program = parse_ok("rule \"r\":\n @Event global\n \"one\"\n \"two\"\n");
1258 let RuleEntry::Rule(rule) = &program.rules[0] else {
1259 panic!("expected rule");
1260 };
1261 assert_eq!(rule.actions.len(), 2);
1262 }
1263
1264 #[test]
1265 fn non_name_keyword_lhs_is_a_parse_error() {
1266 let errors = parse_err("rule \"r\":\n @Event global\n debug(1 = 2)\n");
1268 assert!(!errors.is_empty());
1269 assert_eq!(errors[0].code, "parse-error");
1270 }
1271
1272 #[test]
1273 fn parses_member_call_on_call_result() {
1274 let program = parse_ok(
1277 "rule \"r\":\n @Event eachPlayer\n getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
1278 );
1279 let RuleEntry::Rule(rule) = &program.rules[0] else {
1280 panic!("expected rule");
1281 };
1282 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1283 panic!("expected expression statement");
1284 };
1285 let Expr::ReceiverCall {
1286 receiver,
1287 name,
1288 args,
1289 ..
1290 } = &expr
1291 else {
1292 panic!("expected receiver call, got {expr:?}");
1293 };
1294 assert_eq!(name, "setStatusEffect");
1295 assert!(
1296 matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
1297 "receiver must be the preceding call"
1298 );
1299 assert_eq!(args.len(), 2);
1300 }
1301
1302 #[test]
1303 fn member_without_call_is_not_a_call() {
1304 let program =
1307 parse_ok("rule \"r\":\n @Event eachPlayer\n x = eventPlayer.moveSpeed\n");
1308 let RuleEntry::Rule(rule) = &program.rules[0] else {
1309 panic!("expected rule");
1310 };
1311 let Stmt::Assign { value, .. } = &rule.actions[0] else {
1312 panic!("expected assignment");
1313 };
1314 assert!(matches!(
1315 &value,
1316 Expr::Member { member, .. } if member == "moveSpeed"
1317 ));
1318 }
1319
1320 #[test]
1321 fn parses_advanced_rule_annotations_with_source_arguments() {
1322 let program = parse_ok(
1323 "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",
1324 );
1325 let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1326 panic!("expected subroutine");
1327 };
1328 assert_eq!(annotations.len(), 2);
1329 let RuleEntry::Rule(rule) = &program.rules[1] else {
1330 panic!("expected rule");
1331 };
1332 assert!(rule.disabled);
1333 assert!(rule.delimiter);
1334 assert_eq!(rule.new_page.as_deref(), Some("Page"));
1335 assert_eq!(rule.annotations.len(), 7);
1336 assert_eq!(rule.annotations[1].args[0].text, "1");
1337 assert_eq!(rule.annotations[2].args[0].text, "dmon");
1338 }
1339
1340 #[test]
1341 fn decodes_unicode_escapes_in_names_and_ordinary_strings() {
1342 let program = parse_ok(
1343 r#"subroutine helper
1344def helper():
1345 @Name "helper\ufeffname"
1346 pass
1347rule "pa\ufeffssed":
1348 @Event global
1349 debug("pa\ufeffssed")
1350 debug(f"pa\ufeffssed {1}")
1351"#,
1352 );
1353 let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1354 panic!("expected subroutine");
1355 };
1356 assert_eq!(annotations[0].args[0].text, "\"helper\u{feff}name\"");
1357
1358 let RuleEntry::Rule(rule) = &program.rules[1] else {
1359 panic!("expected rule");
1360 };
1361 assert_eq!(rule.name, "pa\u{feff}ssed");
1362 assert_eq!(rule.name_span.start.col, 7);
1363 assert_eq!(rule.name_span.end.col, 19);
1364 let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1365 panic!("expected debug expression");
1366 };
1367 let Expr::Call { args, .. } = expr else {
1368 panic!("expected debug call");
1369 };
1370 assert!(matches!(
1371 &args[0].value,
1372 Expr::String { value, .. } if value == "pa\u{feff}ssed"
1373 ));
1374 let Stmt::Expr { expr, .. } = &rule.actions[1] else {
1375 panic!("expected formatted debug expression");
1376 };
1377 let Expr::Call { args, .. } = expr else {
1378 panic!("expected formatted debug call");
1379 };
1380 assert!(matches!(
1381 &args[0].value,
1382 Expr::StringModifier {
1383 format_text: Some(text), ..
1384 } if text == "pa\u{feff}ssed {0}"
1385 ));
1386 }
1387}