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