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