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};
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 {
34        tokens,
35        pos: 0,
36        errors: Vec::new(),
37        allow_macro_redeclaration,
38        last_statement_continued: false,
39        last_colon_body_continued: false,
40        open_if_indents: Vec::new(),
41    };
42    let program = parser.parse_program();
43    if parser.errors.is_empty() {
44        ParseOutput {
45            program: Some(program),
46            errors: Vec::new(),
47        }
48    } else {
49        ParseOutput {
50            program: None,
51            errors: parser.errors,
52        }
53    }
54}
55
56mod declarations;
57mod definitions;
58mod expressions;
59mod statements;
60
61struct Parser<'a> {
62    tokens: &'a [Token],
63    pos: usize,
64    errors: Vec<OpyError>,
65    allow_macro_redeclaration: bool,
66    last_statement_continued: bool,
67    last_colon_body_continued: bool,
68    /// Columns of the `if` statements whose branches are being parsed.
69    open_if_indents: Vec<u32>,
70}
71
72fn is_identifier(text: &str) -> bool {
73    !text.is_empty()
74        && text.chars().enumerate().all(|(index, ch)| {
75            if index == 0 {
76                ch.is_ascii_alphabetic() || ch == '_'
77            } else {
78                ch.is_ascii_alphanumeric() || ch == '_'
79            }
80        })
81}
82
83fn unquote_annotation_arg(text: &str) -> String {
84    text.strip_prefix('"')
85        .and_then(|value| value.strip_suffix('"'))
86        .unwrap_or(text)
87        .to_string()
88}
89
90impl Parser<'_> {
91    fn peek(&self) -> &Token {
92        &self.tokens[self.pos.min(self.tokens.len() - 1)]
93    }
94
95    fn peek_kind(&self) -> TokenKind {
96        self.peek().kind
97    }
98
99    fn peek_at(&self, offset: usize) -> &Token {
100        &self.tokens[(self.pos + offset).min(self.tokens.len() - 1)]
101    }
102
103    fn advance(&mut self) -> Token {
104        let token = self.tokens[self.pos.min(self.tokens.len() - 1)].clone();
105        if self.pos < self.tokens.len() - 1 {
106            self.pos += 1;
107        }
108        token
109    }
110
111    fn skip_newlines(&mut self) {
112        while self.peek_kind() == TokenKind::Newline {
113            self.advance();
114        }
115    }
116
117    fn skip_expression_newlines(&mut self) {
118        if self.peek_kind() != TokenKind::Newline {
119            return;
120        }
121        let previous = self.tokens[..self.pos]
122            .iter()
123            .rev()
124            .find(|token| !matches!(token.kind, TokenKind::Indent(_)));
125        let previous_allows_continuation = previous.is_some_and(|token| {
126            matches!(
127                token.kind,
128                TokenKind::LParen
129                    | TokenKind::LBracket
130                    | TokenKind::LBrace
131                    | TokenKind::Comma
132                    | TokenKind::Colon
133                    | TokenKind::Assign
134                    | TokenKind::Plus
135                    | TokenKind::Minus
136                    | TokenKind::Star
137                    | TokenKind::Slash
138                    | TokenKind::Percent
139                    | TokenKind::DoubleStar
140                    | TokenKind::Eq
141                    | TokenKind::Ne
142                    | TokenKind::Lt
143                    | TokenKind::Le
144                    | TokenKind::Gt
145                    | TokenKind::Ge
146            ) || (token.kind == TokenKind::Ident
147                && matches!(token.text.as_str(), "and" | "or" | "in" | "not" | "if"))
148        });
149        let mut next = self.pos;
150        while self.tokens[next].kind == TokenKind::Newline {
151            next += 1;
152        }
153        let inside_delimiter_group = self.inside_delimiter_group();
154        let next_allows_continuation = matches!(
155            self.tokens[next].kind,
156            TokenKind::Plus
157                | TokenKind::Minus
158                | TokenKind::Star
159                | TokenKind::Slash
160                | TokenKind::Percent
161                | TokenKind::DoubleStar
162                | TokenKind::Eq
163                | TokenKind::Ne
164                | TokenKind::Lt
165                | TokenKind::Le
166                | TokenKind::Gt
167                | TokenKind::Ge
168        ) || (inside_delimiter_group
169            && matches!(
170                self.tokens[next].kind,
171                TokenKind::LParen
172                    | TokenKind::LBracket
173                    | TokenKind::Dot
174                    | TokenKind::RParen
175                    | TokenKind::RBracket
176                    | TokenKind::RBrace
177            ))
178            || (self.tokens[next].kind == TokenKind::Ident
179                && matches!(
180                    self.tokens[next].text.as_str(),
181                    "and" | "or" | "in" | "not" | "else"
182                ))
183            || (inside_delimiter_group
184                && self.tokens[next].kind == TokenKind::Ident
185                && matches!(self.tokens[next].text.as_str(), "if" | "for"));
186        if previous_allows_continuation || next_allows_continuation {
187            self.skip_newlines();
188        }
189    }
190
191    fn is_ident(&self, text: &str) -> bool {
192        self.peek_kind() == TokenKind::Ident && self.peek().text == text
193    }
194
195    fn expect_ident(&mut self, what: &str) -> Result<String, ()> {
196        if self.peek_kind() == TokenKind::Ident {
197            Ok(self.advance().text)
198        } else {
199            self.error_at_current(format!("expected {what}"));
200            Err(())
201        }
202    }
203
204    fn expect(&mut self, kind: TokenKind, what: &str) -> Result<Token, ()> {
205        if self.peek_kind() == kind {
206            Ok(self.advance())
207        } else {
208            self.error_at_current(format!("expected {what}"));
209            Err(())
210        }
211    }
212
213    fn error_at_current(&mut self, message: String) {
214        let span = self.peek().span;
215        self.errors.push(OpyError::at("parse-error", message, span));
216    }
217
218    // ---- program ----
219
220    fn parse_program(&mut self) -> Program {
221        let mut declarations = Vec::new();
222        let mut rules = Vec::new();
223        let mut top_level = Vec::new();
224        loop {
225            self.skip_newlines();
226            if self.peek_kind() == TokenKind::Eof {
227                break;
228            }
229            let rule_prefix = if self.peek_kind() == TokenKind::RulePrefixMarker {
230                Some(self.advance().text)
231            } else {
232                None
233            };
234            let declaration_count = declarations.len();
235            let rule_count = rules.len();
236            let ok = self.parse_top_level(&mut declarations, &mut rules, rule_prefix);
237            if ok {
238                if declarations.len() > declaration_count {
239                    top_level.push(TopLevel::Declaration(
240                        declarations
241                            .last()
242                            .expect("declaration was appended")
243                            .clone(),
244                    ));
245                } else if rules.len() > rule_count {
246                    top_level.push(TopLevel::Rule(
247                        rules.last().expect("rule was appended").clone(),
248                    ));
249                }
250            }
251            if !ok {
252                self.recover_line();
253            }
254        }
255        Program {
256            declarations,
257            rules,
258            top_level,
259            settings: None,
260        }
261    }
262
263    fn parse_top_level(
264        &mut self,
265        declarations: &mut Vec<Decl>,
266        rules: &mut Vec<RuleEntry>,
267        rule_prefix: Option<String>,
268    ) -> bool {
269        let token = self.peek();
270        if token.kind == TokenKind::Ident {
271            match token.text.as_str() {
272                "rule" => return self.parse_rule(rules, rule_prefix),
273                "def" => return self.parse_def(rules, rule_prefix),
274                "globalvar" => return self.parse_variable(declarations, true),
275                "playervar" => return self.parse_variable(declarations, false),
276                "subroutine" => return self.parse_subroutine(declarations),
277                "enum" => return self.parse_enum(declarations),
278                "macro" => return self.parse_macro(declarations),
279                _ => {}
280            }
281        }
282        self.error_at_current(format!(
283            "expected a top-level declaration (rule/def/globalvar/playervar/subroutine/enum/macro) but found '{}'",
284            token.text
285        ));
286        false
287    }
288
289    /// Skip to the end of the current line (error recovery).
290    fn recover_line(&mut self) {
291        while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
292            self.advance();
293        }
294    }
295
296    /// The indentation of the next non-empty line, which must exceed
297    /// `line_indent` (an indented block follows the colon).
298    fn block_indent(&mut self, line_indent: u32) -> Option<u32> {
299        self.skip_newlines();
300        if self.peek_kind() == TokenKind::Eof {
301            self.error_at_current("expected an indented block".to_string());
302            return None;
303        }
304        let indent = self.peek().span.start.col;
305        if indent <= line_indent {
306            self.error_at_current("expected an indented block after ':'".to_string());
307            return None;
308        }
309        Some(indent)
310    }
311
312    fn expect_statement_end(&mut self, what: &str) -> Result<(), ()> {
313        let continued_line = self
314            .tokens
315            .get(self.pos.saturating_sub(1))
316            .is_some_and(|previous| self.peek().span.start.line > previous.span.end.line);
317        self.last_statement_continued = continued_line;
318        if matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) || continued_line {
319            Ok(())
320        } else {
321            self.error_at_current(format!("expected the end of {what}"));
322            Err(())
323        }
324    }
325}
326
327/// Parse one f-string expression fragment and shift its local token spans
328/// into the original source file.
329pub(crate) fn parse_expression_fragment(
330    text: &str,
331    file: u32,
332    origin: Position,
333) -> Result<Expr, OpyError> {
334    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
335        file_id: file,
336        text,
337    })?;
338    for token in &mut tokens {
339        token.span = shift_span(token.span, origin);
340    }
341    let mut parser = Parser {
342        tokens: &tokens,
343        pos: 0,
344        allow_macro_redeclaration: false,
345        errors: Vec::new(),
346        last_statement_continued: false,
347        last_colon_body_continued: false,
348        open_if_indents: Vec::new(),
349    };
350    let expression = parser.parse_expr().map_err(|()| {
351        parser.errors.first().cloned().unwrap_or_else(|| {
352            OpyError::at(
353                "parse-error",
354                "invalid f-string expression",
355                Span::new(file, origin, origin),
356            )
357        })
358    })?;
359    if parser.peek_kind() != TokenKind::Eof {
360        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
361    }
362    parser.errors.into_iter().next().map_or(Ok(expression), Err)
363}
364
365fn shift_span(span: Span, origin: Position) -> Span {
366    fn shift(position: Position, origin: Position) -> Position {
367        Position::new(
368            origin.line + position.line.saturating_sub(1),
369            if position.line == 1 {
370                origin.col + position.col.saturating_sub(1)
371            } else {
372                position.col
373            },
374        )
375    }
376    Span::new(
377        span.file,
378        shift(span.start, origin),
379        shift(span.end, origin),
380    )
381}
382
383fn decode_string_escape(character: char) -> char {
384    match character {
385        'n' => '\n',
386        't' => '\t',
387        'r' => '\r',
388        '\\' => '\\',
389        '"' => '"',
390        '\'' => '\'',
391        other => other,
392    }
393}
394
395fn is_string_modifier(text: &str) -> bool {
396    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use crate::lexer::{LexInput, lex};
403
404    fn parse_ok(text: &str) -> Program {
405        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
406        let output = parse(&tokens);
407        assert!(
408            output.errors.is_empty(),
409            "unexpected errors: {:?}",
410            output.errors
411        );
412        output.program.unwrap()
413    }
414
415    fn parse_err(text: &str) -> Vec<OpyError> {
416        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
417        parse(&tokens).errors
418    }
419
420    #[test]
421    fn parses_basic_rule() {
422        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
423        assert_eq!(program.rules.len(), 1);
424        let RuleEntry::Rule(rule) = &program.rules[0] else {
425            panic!("expected rule");
426        };
427        assert_eq!(rule.name, "setup");
428        assert_eq!(rule.event.name, "global");
429        assert_eq!(rule.actions.len(), 1);
430    }
431
432    #[test]
433    fn parses_power_augmented_assignment() {
434        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
435        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
436        let program =
437            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
438        let RuleEntry::Rule(rule) = &program.rules[0] else {
439            panic!("expected rule");
440        };
441        let Stmt::Assign {
442            value,
443            target: assigned_target,
444            ..
445        } = &rule.actions[1]
446        else {
447            panic!("expected an assignment");
448        };
449        let Expr::Binary {
450            op, left, right, ..
451        } = value
452        else {
453            panic!("expected a binary modification, got {value:?}");
454        };
455        assert_eq!(op, "**");
456        assert!(matches!(&**left, Expr::Name { .. }));
457        assert!(matches!(
458            assigned_target,
459            Expr::Name { name, .. } if name == "a"
460        ));
461        assert!(matches!(right.as_ref(), Expr::Number { .. }));
462    }
463
464    #[test]
465    fn parses_postfix_increment_and_decrement_as_modifications() {
466        let program =
467            parse_ok("globalvar value\nrule \"r\":\n    @Event global\n    value++\n    value--\n");
468        let RuleEntry::Rule(rule) = &program.rules[0] else {
469            panic!("expected a rule");
470        };
471        for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
472            let Stmt::Assign { target, value, .. } = statement else {
473                panic!("expected a postfix assignment");
474            };
475            let Expr::Binary {
476                op, left, right, ..
477            } = value
478            else {
479                panic!("expected a synthetic modification value");
480            };
481            assert_eq!(op, expected_op);
482            let Expr::Name {
483                name: left_name, ..
484            } = left.as_ref()
485            else {
486                panic!("expected the target to be the modification's left operand");
487            };
488            let Expr::Name {
489                name: target_name, ..
490            } = target
491            else {
492                panic!("expected a name target");
493            };
494            assert_eq!(left_name, target_name);
495            assert!(
496                matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
497            );
498        }
499    }
500
501    #[test]
502    fn rejects_prefix_increment_and_embedded_postfix_forms() {
503        for source in [
504            "globalvar value\nrule \"r\":\n    @Event global\n    ++value\n",
505            "globalvar value\nrule \"r\":\n    @Event global\n    value++++\n",
506        ] {
507            let errors = parse_err(source);
508            assert!(!errors.is_empty());
509            assert!(errors.iter().all(|error| error.code == "parse-error"));
510            assert!(errors.iter().all(|error| error.span.is_some()));
511        }
512    }
513
514    #[test]
515    fn preserves_consecutive_unary_minus_expressions() {
516        let source = concat!(
517            "globalvar value = 0\n",
518            "globalvar B = 1\n",
519            "rule \"r\":\n",
520            "    @Event global\n",
521            "    value = --1\n",
522            "    value = --B\n",
523            "    value = B--1\n",
524        );
525        parse_ok(source);
526    }
527
528    #[test]
529    fn parses_control_flow() {
530        let program = parse_ok(
531            "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",
532        );
533        let RuleEntry::Rule(rule) = &program.rules[0] else {
534            panic!();
535        };
536        assert!(matches!(rule.actions[0], Stmt::For { .. }));
537        let Stmt::For { body, .. } = &rule.actions[0] else {
538            panic!();
539        };
540        let Stmt::If {
541            branches, r#else, ..
542        } = &body[0]
543        else {
544            panic!();
545        };
546        assert_eq!(branches.len(), 2);
547        assert!(r#else.is_some());
548        let Stmt::While { body, .. } = &rule.actions[1] else {
549            panic!();
550        };
551        assert_eq!(body.len(), 2);
552    }
553
554    #[test]
555    fn parses_source_statement_surface() {
556        let program = parse_ok(concat!(
557            "globalvar value\n",
558            "rule \"r\":\n",
559            "    @Event global\n",
560            "    del value[1]\n",
561            "    value min= 2\n",
562            "    value max= 3\n",
563            "    while value < 4:\n",
564            "        continue\n",
565            "    goto RULE_START\n",
566            "    goto target\n",
567            "    goto loc + value\n",
568            "    target:\n",
569        ));
570        let RuleEntry::Rule(rule) = &program.rules[0] else {
571            panic!("expected rule");
572        };
573        assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
574        for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
575            let Stmt::Assign { value, .. } = statement else {
576                panic!("expected augmented assignment");
577            };
578            assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
579        }
580        let Stmt::While { body, .. } = &rule.actions[3] else {
581            panic!("expected while");
582        };
583        assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
584        assert!(matches!(
585            &rule.actions[4],
586            Stmt::Goto {
587                label: None,
588                offset: None,
589                rule_start: true,
590                ..
591            }
592        ));
593        assert!(matches!(
594            &rule.actions[5],
595            Stmt::Goto {
596                label: Some(label),
597                offset: None,
598                rule_start: false,
599                ..
600            } if label == "target"
601        ));
602        assert!(matches!(
603            &rule.actions[6],
604            Stmt::Goto {
605                label: None,
606                offset: Some(_),
607                rule_start: false,
608                ..
609            }
610        ));
611        assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
612    }
613
614    #[test]
615    fn rejects_invalid_source_statement_forms() {
616        for source in [
617            "rule \"r\":\n    @Event global\n    del value\n",
618            "rule \"r\":\n    @Event global\n    goto\n",
619            "rule \"r\":\n    @Event global\n    goto loc\n",
620            "rule \"r\":\n    @Event global\n    goto target extra\n",
621            "rule \"r\":\n    @Event global\n    continue now\n",
622            "rule \"r\":\n    @Event global\n    A = 1; A = 2\n",
623        ] {
624            let errors = parse_err(source);
625            assert!(!errors.is_empty(), "invalid form parsed: {source}");
626            assert!(errors.iter().all(|error| error.code == "parse-error"));
627            assert!(errors.iter().all(|error| error.span.is_some()));
628        }
629    }
630
631    #[test]
632    fn rejects_continued_inline_if_without_else() {
633        let errors = parse_err(concat!(
634            "globalvar value\n",
635            "macro nextHero():\n",
636            "    value = 1\\\n",
637            "    if value == 0: value = 2\\\n",
638            "    value = 3\\\n",
639        ));
640        assert!(errors.iter().any(|error| {
641            error.code == "parse-error" && error.message == "Found 'if', but no 'else'"
642        }));
643    }
644
645    #[test]
646    fn parses_syntax_constructs() {
647        let program = parse_ok(
648            "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",
649        );
650        let RuleEntry::Rule(rule) = &program.rules[0] else {
651            panic!("expected rule");
652        };
653        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
654        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
655    }
656
657    #[test]
658    fn rejects_incomplete_do_while_and_dictionary_entries() {
659        let errors = parse_err(
660            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
661        );
662        assert!(!errors.is_empty());
663        assert!(errors.iter().all(|error| error.code == "parse-error"));
664    }
665
666    #[test]
667    fn parses_explicit_enum_member_values() {
668        let program = parse_ok("enum EventType:\n    BUFF = 0\n    DEBUFF\n    MECH = 2\n");
669        let Decl::Enum { members, .. } = &program.declarations[0] else {
670            panic!("expected enum");
671        };
672        assert_eq!(
673            members
674                .iter()
675                .map(|(name, _)| name.as_str())
676                .collect::<Vec<_>>(),
677            ["BUFF", "DEBUFF", "MECH"]
678        );
679    }
680
681    #[test]
682    fn parses_multi_line_array() {
683        let program = parse_ok(
684            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
685        );
686        let RuleEntry::Rule(rule) = &program.rules[0] else {
687            panic!();
688        };
689        let Stmt::Assign { value, .. } = &rule.actions[0] else {
690            panic!();
691        };
692        let Expr::Array { elements, .. } = value else {
693            panic!("expected array, got {value:?}");
694        };
695        assert_eq!(elements.len(), 2);
696    }
697
698    #[test]
699    fn missing_colon_is_a_structured_error() {
700        let errors = parse_err("rule \"x\"\n    @Event global\n");
701        assert!(!errors.is_empty());
702        assert_eq!(errors[0].code, "parse-error");
703        assert!(errors[0].span.is_some());
704    }
705
706    #[test]
707    fn def_and_macro_parse() {
708        let program = parse_ok(
709            "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
710        );
711        assert_eq!(program.declarations.len(), 3);
712        assert!(matches!(program.declarations[1], Decl::Constant { .. }));
713        assert!(matches!(program.declarations[2], Decl::Macro { .. }));
714        let Decl::Macro { args, body, .. } = &program.declarations[2] else {
715            panic!();
716        };
717        assert_eq!(args, &vec!["value".to_string()]);
718        assert_eq!(body.len(), 1);
719    }
720
721    #[test]
722    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
723        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
724        let errors = parse_err(text);
725        assert_eq!(
726            errors
727                .iter()
728                .filter(|error| error.code == "macro-redeclaration")
729                .count(),
730            2
731        );
732
733        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
734        let output = parse_with_options(&tokens, true);
735        assert!(
736            output.errors.is_empty(),
737            "unexpected errors: {:?}",
738            output.errors
739        );
740        assert!(output.program.is_some());
741    }
742
743    #[test]
744    fn multiple_errors_are_reported() {
745        let errors =
746            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
747        assert!(!errors.is_empty());
748    }
749
750    #[test]
751    fn precedence_parses_python_like() {
752        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
753        let RuleEntry::Rule(rule) = &program.rules[0] else {
754            panic!();
755        };
756        let Stmt::Assign { value, .. } = &rule.actions[0] else {
757            panic!();
758        };
759        let Expr::Binary {
760            op, left, right, ..
761        } = value
762        else {
763            panic!();
764        };
765        assert_eq!(op, "+");
766        let Expr::Binary { op: inner, .. } = right.as_ref() else {
767            panic!();
768        };
769        assert_eq!(inner, "*");
770        assert!(matches!(left.as_ref(), Expr::Number { .. }));
771    }
772
773    #[test]
774    fn parses_right_associative_conditional_expressions() {
775        let program = parse_ok(
776            "rule \"r\":\n    @Event global\n    debug(1 if true else 2 if false else 3)\n",
777        );
778        let RuleEntry::Rule(rule) = &program.rules[0] else {
779            panic!("expected a rule");
780        };
781        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
782            panic!("expected an expression statement");
783        };
784        let Expr::Call { args, .. } = expr else {
785            panic!("expected a call");
786        };
787        let Expr::Conditional {
788            then_value,
789            condition,
790            else_value,
791            span,
792        } = &args[0].value
793        else {
794            panic!("expected a conditional expression");
795        };
796        assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
797        assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
798        assert!(matches!(
799            else_value.as_ref(),
800            Expr::Conditional { then_value, condition, else_value, .. }
801                if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
802                    && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
803                    && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
804        ));
805        assert_eq!(span.start.line, 3);
806        assert_eq!(span.start.col, 11);
807    }
808
809    #[test]
810    fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
811        let program = parse_ok(
812            "rule \"r\":\n    @Event global\n    debug((1 if true else 2) if false else 3)\n",
813        );
814        let RuleEntry::Rule(rule) = &program.rules[0] else {
815            panic!("expected a rule");
816        };
817        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
818            panic!("expected an expression statement");
819        };
820        let Expr::Call { args, .. } = expr else {
821            panic!("expected a call");
822        };
823        assert!(matches!(
824            &args[0].value,
825            Expr::Conditional {
826                then_value,
827                condition,
828                else_value,
829                ..
830            } if matches!(then_value.as_ref(), Expr::Conditional { .. })
831                && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
832                && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
833        ));
834
835        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 if true)\n");
836        assert_eq!(errors[0].code, "parse-error");
837        assert!(errors[0].message.contains("expected `else`"));
838    }
839
840    #[test]
841    fn parses_receiver_calls() {
842        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
843        // member access followed by call arguments (#104).
844        let program =
845            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
846        let RuleEntry::Rule(rule) = &program.rules[0] else {
847            panic!("expected rule");
848        };
849        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
850            panic!("expected expression statement, got {:?}", rule.actions[0]);
851        };
852        let Expr::ReceiverCall {
853            receiver,
854            name,
855            args,
856            ..
857        } = &expr
858        else {
859            panic!("expected receiver call, got {expr:?}");
860        };
861        assert_eq!(name, "setMoveSpeed");
862        assert!(
863            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
864            "receiver must be the eventPlayer name"
865        );
866        assert_eq!(args.len(), 1);
867        assert!(args[0].keyword.is_none(), "positional argument");
868        assert!(matches!(&args[0].value, Expr::Number { .. }));
869    }
870
871    #[test]
872    fn parses_keyword_arguments_with_name_spans() {
873        // `name = expr` call arguments are keyword arguments carrying the
874        // name token's exact span (issue #110); comparisons stay positional.
875        let program =
876            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
877        let RuleEntry::Rule(rule) = &program.rules[0] else {
878            panic!("expected rule");
879        };
880        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
881            panic!("expected expression statement");
882        };
883        let Expr::Call { args, .. } = expr else {
884            panic!("expected a call, got {expr:?}");
885        };
886        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
887        assert_eq!(keyword, "time");
888        assert_eq!(span.start.line, 3);
889        assert!(matches!(&args[0].value, Expr::Number { .. }));
890
891        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
892            panic!("expected expression statement");
893        };
894        let Expr::Call { args, .. } = expr else {
895            panic!("expected a call, got {expr:?}");
896        };
897        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
898        assert!(matches!(&args[0].value, Expr::Binary { .. }));
899    }
900
901    #[test]
902    fn adjacent_string_literals_concatenate_and_preserve_span() {
903        let program = parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\" \"two\")\n");
904        let RuleEntry::Rule(rule) = &program.rules[0] else {
905            panic!("expected rule");
906        };
907        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
908            panic!("expected expression statement");
909        };
910        let Expr::Call { args, .. } = expr else {
911            panic!("expected call");
912        };
913        let Expr::String { value, span } = &args[0].value else {
914            panic!("expected concatenated string");
915        };
916        assert_eq!(value, "onetwo");
917        assert_eq!(span.start.line, 3);
918        assert_eq!(span.start.col, 11);
919        assert_eq!(span.end.col, 22);
920    }
921
922    #[test]
923    fn multiline_adjacent_string_literals_concatenate_inside_group() {
924        let program =
925            parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\"\n        \"two\")\n");
926        let RuleEntry::Rule(rule) = &program.rules[0] else {
927            panic!("expected rule");
928        };
929        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
930            panic!("expected expression statement");
931        };
932        let Expr::Call { args, .. } = expr else {
933            panic!("expected call");
934        };
935        assert!(matches!(
936            &args[0].value,
937            Expr::String { value, .. } if value == "onetwo"
938        ));
939    }
940
941    #[test]
942    fn newline_outside_group_keeps_adjacent_literals_as_statements() {
943        let program = parse_ok("rule \"r\":\n    @Event global\n    \"one\"\n    \"two\"\n");
944        let RuleEntry::Rule(rule) = &program.rules[0] else {
945            panic!("expected rule");
946        };
947        assert_eq!(rule.actions.len(), 2);
948    }
949
950    #[test]
951    fn non_name_keyword_lhs_is_a_parse_error() {
952        // `f(1 = 2)` is not a call argument form; rejected explicitly.
953        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
954        assert!(!errors.is_empty());
955        assert_eq!(errors[0].code, "parse-error");
956    }
957
958    #[test]
959    fn parses_member_call_on_call_result() {
960        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
961        // followed by call arguments on a call result stays a receiver call.
962        let program = parse_ok(
963            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
964        );
965        let RuleEntry::Rule(rule) = &program.rules[0] else {
966            panic!("expected rule");
967        };
968        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
969            panic!("expected expression statement");
970        };
971        let Expr::ReceiverCall {
972            receiver,
973            name,
974            args,
975            ..
976        } = &expr
977        else {
978            panic!("expected receiver call, got {expr:?}");
979        };
980        assert_eq!(name, "setStatusEffect");
981        assert!(
982            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
983            "receiver must be the preceding call"
984        );
985        assert_eq!(args.len(), 2);
986    }
987
988    #[test]
989    fn member_without_call_is_not_a_call() {
990        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
991        // access; only a following `(` turns it into a receiver call.
992        let program =
993            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
994        let RuleEntry::Rule(rule) = &program.rules[0] else {
995            panic!("expected rule");
996        };
997        let Stmt::Assign { value, .. } = &rule.actions[0] else {
998            panic!("expected assignment");
999        };
1000        assert!(matches!(
1001            &value,
1002            Expr::Member { member, .. } if member == "moveSpeed"
1003        ));
1004    }
1005
1006    #[test]
1007    fn parses_advanced_rule_annotations_with_source_arguments() {
1008        let program = parse_ok(
1009            "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",
1010        );
1011        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1012            panic!("expected subroutine");
1013        };
1014        assert_eq!(annotations.len(), 2);
1015        let RuleEntry::Rule(rule) = &program.rules[1] else {
1016            panic!("expected rule");
1017        };
1018        assert!(rule.disabled);
1019        assert!(rule.delimiter);
1020        assert_eq!(rule.new_page.as_deref(), Some("Page"));
1021        assert_eq!(rule.annotations.len(), 7);
1022        assert_eq!(rule.annotations[1].args[0].text, "1");
1023        assert_eq!(rule.annotations[2].args[0].text, "dmon");
1024    }
1025
1026    #[test]
1027    fn decodes_unicode_escapes_in_names_and_ordinary_strings() {
1028        let program = parse_ok(
1029            r#"subroutine helper
1030def helper():
1031    @Name "helper\ufeffname"
1032    pass
1033rule "pa\ufeffssed":
1034    @Event global
1035    debug("pa\ufeffssed")
1036    debug(f"pa\ufeffssed {1}")
1037"#,
1038        );
1039        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
1040            panic!("expected subroutine");
1041        };
1042        assert_eq!(annotations[0].args[0].text, "\"helper\u{feff}name\"");
1043
1044        let RuleEntry::Rule(rule) = &program.rules[1] else {
1045            panic!("expected rule");
1046        };
1047        assert_eq!(rule.name, "pa\u{feff}ssed");
1048        assert_eq!(rule.name_span.start.col, 7);
1049        assert_eq!(rule.name_span.end.col, 19);
1050        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
1051            panic!("expected debug expression");
1052        };
1053        let Expr::Call { args, .. } = expr else {
1054            panic!("expected debug call");
1055        };
1056        assert!(matches!(
1057            &args[0].value,
1058            Expr::String { value, .. } if value == "pa\u{feff}ssed"
1059        ));
1060        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
1061            panic!("expected formatted debug expression");
1062        };
1063        let Expr::Call { args, .. } = expr else {
1064            panic!("expected formatted debug call");
1065        };
1066        assert!(matches!(
1067            &args[0].value,
1068            Expr::StringModifier {
1069                format_text: Some(text), ..
1070            } if text == "pa\u{feff}ssed {0}"
1071        ));
1072    }
1073}