Skip to main content

opy_rs/
parser.rs

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