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