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
53struct Parser<'a> {
54    tokens: &'a [Token],
55    pos: usize,
56    errors: Vec<OpyError>,
57    allow_macro_redeclaration: bool,
58}
59
60fn is_identifier(text: &str) -> bool {
61    !text.is_empty()
62        && text.chars().enumerate().all(|(index, ch)| {
63            if index == 0 {
64                ch.is_ascii_alphabetic() || ch == '_'
65            } else {
66                ch.is_ascii_alphanumeric() || ch == '_'
67            }
68        })
69}
70
71fn unquote_annotation_arg(text: &str) -> String {
72    text.strip_prefix('"')
73        .and_then(|value| value.strip_suffix('"'))
74        .unwrap_or(text)
75        .to_string()
76}
77
78impl Parser<'_> {
79    fn peek(&self) -> &Token {
80        &self.tokens[self.pos.min(self.tokens.len() - 1)]
81    }
82
83    fn peek_kind(&self) -> TokenKind {
84        self.peek().kind
85    }
86
87    fn peek_at(&self, offset: usize) -> &Token {
88        &self.tokens[(self.pos + offset).min(self.tokens.len() - 1)]
89    }
90
91    fn advance(&mut self) -> Token {
92        let token = self.tokens[self.pos.min(self.tokens.len() - 1)].clone();
93        if self.pos < self.tokens.len() - 1 {
94            self.pos += 1;
95        }
96        token
97    }
98
99    fn skip_newlines(&mut self) {
100        while self.peek_kind() == TokenKind::Newline {
101            self.advance();
102        }
103    }
104
105    fn skip_expression_newlines(&mut self) {
106        if self.peek_kind() != TokenKind::Newline {
107            return;
108        }
109        let previous = self.tokens[..self.pos]
110            .iter()
111            .rev()
112            .find(|token| !matches!(token.kind, TokenKind::Indent(_)));
113        let previous_allows_continuation = previous.is_some_and(|token| {
114            matches!(
115                token.kind,
116                TokenKind::LParen
117                    | TokenKind::LBracket
118                    | TokenKind::LBrace
119                    | TokenKind::Comma
120                    | TokenKind::Colon
121                    | TokenKind::Assign
122                    | TokenKind::Plus
123                    | TokenKind::Minus
124                    | TokenKind::Star
125                    | TokenKind::Slash
126                    | TokenKind::Percent
127                    | TokenKind::DoubleStar
128                    | TokenKind::Eq
129                    | TokenKind::Ne
130                    | TokenKind::Lt
131                    | TokenKind::Le
132                    | TokenKind::Gt
133                    | TokenKind::Ge
134            ) || (token.kind == TokenKind::Ident
135                && matches!(token.text.as_str(), "and" | "or" | "in" | "not" | "if"))
136        });
137        let mut next = self.pos;
138        while self.tokens[next].kind == TokenKind::Newline {
139            next += 1;
140        }
141        let inside_delimiter_group = self.inside_delimiter_group();
142        let next_allows_continuation = matches!(
143            self.tokens[next].kind,
144            TokenKind::Plus
145                | TokenKind::Minus
146                | TokenKind::Star
147                | TokenKind::Slash
148                | TokenKind::Percent
149                | TokenKind::DoubleStar
150                | TokenKind::Eq
151                | TokenKind::Ne
152                | TokenKind::Lt
153                | TokenKind::Le
154                | TokenKind::Gt
155                | TokenKind::Ge
156        ) || (inside_delimiter_group
157            && matches!(
158                self.tokens[next].kind,
159                TokenKind::LParen
160                    | TokenKind::LBracket
161                    | TokenKind::Dot
162                    | TokenKind::RParen
163                    | TokenKind::RBracket
164                    | TokenKind::RBrace
165            ))
166            || (self.tokens[next].kind == TokenKind::Ident
167                && matches!(
168                    self.tokens[next].text.as_str(),
169                    "and" | "or" | "in" | "not" | "else"
170                ))
171            || (inside_delimiter_group
172                && self.tokens[next].kind == TokenKind::Ident
173                && matches!(self.tokens[next].text.as_str(), "if" | "for"));
174        if previous_allows_continuation || next_allows_continuation {
175            self.skip_newlines();
176        }
177    }
178
179    fn is_ident(&self, text: &str) -> bool {
180        self.peek_kind() == TokenKind::Ident && self.peek().text == text
181    }
182
183    fn expect_ident(&mut self, what: &str) -> Result<String, ()> {
184        if self.peek_kind() == TokenKind::Ident {
185            Ok(self.advance().text)
186        } else {
187            self.error_at_current(format!("expected {what}"));
188            Err(())
189        }
190    }
191
192    fn expect(&mut self, kind: TokenKind, what: &str) -> Result<Token, ()> {
193        if self.peek_kind() == kind {
194            Ok(self.advance())
195        } else {
196            self.error_at_current(format!("expected {what}"));
197            Err(())
198        }
199    }
200
201    fn error_at_current(&mut self, message: String) {
202        let span = self.peek().span;
203        self.errors.push(OpyError::at("parse-error", message, span));
204    }
205
206    // ---- program ----
207
208    fn parse_program(&mut self) -> Program {
209        let mut declarations = Vec::new();
210        let mut rules = Vec::new();
211        let mut top_level = Vec::new();
212        loop {
213            self.skip_newlines();
214            if self.peek_kind() == TokenKind::Eof {
215                break;
216            }
217            let rule_prefix = if self.peek_kind() == TokenKind::RulePrefixMarker {
218                Some(self.advance().text)
219            } else {
220                None
221            };
222            let declaration_count = declarations.len();
223            let rule_count = rules.len();
224            let ok = self.parse_top_level(&mut declarations, &mut rules, rule_prefix);
225            if ok {
226                if declarations.len() > declaration_count {
227                    top_level.push(TopLevel::Declaration(
228                        declarations
229                            .last()
230                            .expect("declaration was appended")
231                            .clone(),
232                    ));
233                } else if rules.len() > rule_count {
234                    top_level.push(TopLevel::Rule(
235                        rules.last().expect("rule was appended").clone(),
236                    ));
237                }
238            }
239            if !ok {
240                self.recover_line();
241            }
242        }
243        Program {
244            declarations,
245            rules,
246            top_level,
247            settings: None,
248        }
249    }
250
251    fn parse_top_level(
252        &mut self,
253        declarations: &mut Vec<Decl>,
254        rules: &mut Vec<RuleEntry>,
255        rule_prefix: Option<String>,
256    ) -> bool {
257        let token = self.peek();
258        if token.kind == TokenKind::Ident {
259            match token.text.as_str() {
260                "rule" => return self.parse_rule(rules, rule_prefix),
261                "def" => return self.parse_def(rules, rule_prefix),
262                "globalvar" => return self.parse_variable(declarations, true),
263                "playervar" => return self.parse_variable(declarations, false),
264                "subroutine" => return self.parse_subroutine(declarations),
265                "enum" => return self.parse_enum(declarations),
266                "macro" => return self.parse_macro(declarations),
267                _ => {}
268            }
269        }
270        self.error_at_current(format!(
271            "expected a top-level declaration (rule/def/globalvar/playervar/subroutine/enum/macro) but found '{}'",
272            token.text
273        ));
274        false
275    }
276
277    /// Skip to the end of the current line (error recovery).
278    fn recover_line(&mut self) {
279        while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
280            self.advance();
281        }
282    }
283
284    fn expect_statement_end(&mut self, what: &str) -> Result<(), ()> {
285        let continued_line = self
286            .tokens
287            .get(self.pos.saturating_sub(1))
288            .is_some_and(|previous| self.peek().span.start.line > previous.span.end.line);
289        if matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) || continued_line {
290            Ok(())
291        } else {
292            self.error_at_current(format!("expected the end of {what}"));
293            Err(())
294        }
295    }
296
297    // ---- declarations ----
298
299    fn parse_variable(&mut self, declarations: &mut Vec<Decl>, global: bool) -> bool {
300        let start = self.advance(); // `globalvar`/`playervar`
301        // The name token follows the keyword; its span is the exact declared
302        // identifier occurrence (rename targets, not the keyword/statement).
303        let name_token = self.peek().clone();
304        let name = match self.expect_ident("a variable name after the keyword") {
305            Ok(name) => name,
306            Err(()) => return false,
307        };
308        let name_span = if name_token.kind == TokenKind::Ident {
309            name_token.span
310        } else {
311            start.span
312        };
313        let mut index = None;
314        let mut initializer = None;
315        if self.peek_kind() == TokenKind::Assign {
316            self.advance();
317            match self.parse_expr() {
318                Ok(expr) => initializer = Some(expr),
319                Err(()) => return false,
320            }
321        } else if self.peek_kind() == TokenKind::Number {
322            // `globalvar cakePos 100`: an explicit Workshop variable index.
323            let token = self.advance();
324            index = token.text.parse::<u32>().ok();
325            if index.is_none() {
326                self.errors.push(OpyError::at(
327                    "parse-error",
328                    format!(
329                        "invalid variable index '{}' (expected an integer)",
330                        token.text
331                    ),
332                    token.span,
333                ));
334                return false;
335            }
336        } else if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
337            self.error_at_current(
338                "expected '=', an integer index, or end of line after the variable name"
339                    .to_string(),
340            );
341            return false;
342        }
343        let end = self
344            .tokens
345            .get(self.pos.saturating_sub(1))
346            .map_or(start.span.end, |token| token.span.end);
347        let span = Span::new(start.span.file, start.span.start, end);
348        let decl = if global {
349            Decl::GlobalVariable {
350                name,
351                index,
352                span,
353                name_span,
354                initializer,
355            }
356        } else {
357            Decl::PlayerVariable {
358                name,
359                index,
360                span,
361                name_span,
362                initializer,
363            }
364        };
365        declarations.push(decl);
366        true
367    }
368
369    fn parse_subroutine(&mut self, declarations: &mut Vec<Decl>) -> bool {
370        let start = self.advance();
371        // The name token follows the `subroutine` keyword; its span is the
372        // exact declared identifier occurrence.
373        let name_token = self.peek().clone();
374        let name = match self.expect_ident("a subroutine name") {
375            Ok(name) => name,
376            Err(()) => return false,
377        };
378        let name_span = if name_token.kind == TokenKind::Ident {
379            name_token.span
380        } else {
381            start.span
382        };
383        let end = self
384            .tokens
385            .get(self.pos.saturating_sub(1))
386            .map_or(start.span.end, |token| token.span.end);
387        declarations.push(Decl::Subroutine {
388            name,
389            span: Span::new(start.span.file, start.span.start, end),
390            name_span,
391        });
392        true
393    }
394
395    fn parse_enum(&mut self, declarations: &mut Vec<Decl>) -> bool {
396        let start = self.advance();
397        let name = match self.expect_ident("an enum name") {
398            Ok(name) => name,
399            Err(()) => return false,
400        };
401        if self
402            .expect(TokenKind::Colon, "':' after the enum name")
403            .is_err()
404        {
405            return false;
406        }
407        let line_indent = start.span.start.col;
408        let body_indent = match self.block_indent(line_indent) {
409            Some(indent) => indent,
410            None => return false,
411        };
412        let mut members = Vec::new();
413        loop {
414            self.skip_newlines();
415            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
416                break;
417            }
418            if self.peek_kind() == TokenKind::Ident {
419                let member = self.advance();
420                let member_span = member.span;
421                if !self.allow_macro_redeclaration
422                    && members.iter().any(|(name, _)| name == &member.text)
423                {
424                    self.errors.push(OpyError::at(
425                        "macro-redeclaration",
426                        format!("enum member '{name}.{}' is already defined", member.text),
427                        member_span,
428                    ));
429                }
430                members.push((member.text, member_span));
431                if self.peek_kind() == TokenKind::Assign {
432                    self.advance();
433                    if self.parse_expr().is_err() {
434                        self.recover_line();
435                        continue;
436                    }
437                }
438            } else {
439                self.error_at_current("expected an enum member name".to_string());
440                self.recover_line();
441                continue;
442            }
443            if self.peek_kind() == TokenKind::Comma {
444                self.advance();
445            } else {
446                // A member must end the line (or be comma-separated).
447                if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
448                    self.error_at_current("expected ',' after the enum member".to_string());
449                    self.recover_line();
450                    continue;
451                }
452            }
453        }
454        declarations.push(Decl::Enum {
455            name,
456            members,
457            span: start.span,
458        });
459        true
460    }
461
462    fn parse_macro(&mut self, declarations: &mut Vec<Decl>) -> bool {
463        let start = self.advance();
464        let name_token = self.peek().clone();
465        let mut name = match self.expect_ident("a macro name") {
466            Ok(name) => name,
467            Err(()) => return false,
468        };
469        let qualified = if self.peek_kind() == TokenKind::Dot {
470            self.advance();
471            let member = match self.expect_ident("a macro member name") {
472                Ok(member) => member,
473                Err(()) => return false,
474            };
475            name.push('.');
476            name.push_str(&member);
477            true
478        } else {
479            false
480        };
481        if self.peek_kind() == TokenKind::Assign {
482            self.advance();
483            let value = match self.parse_expr() {
484                Ok(value) => value,
485                Err(()) => return false,
486            };
487            let span = Span::new(start.span.file, start.span.start, value.span().end);
488            if qualified {
489                declarations.push(Decl::Macro {
490                    name,
491                    args: vec!["self".to_string()],
492                    body: vec![Stmt::Expr { expr: value, span }],
493                    span,
494                });
495            } else {
496                declarations.push(Decl::Constant { name, value, span });
497            }
498            return true;
499        }
500        let mut args = match self.parse_param_list() {
501            Some(args) => args,
502            None => return false,
503        };
504        if qualified {
505            args.insert(0, "self".to_string());
506        }
507        if self
508            .expect(TokenKind::Colon, "':' after the macro signature")
509            .is_err()
510        {
511            return false;
512        }
513        let line_indent = start.span.start.col;
514        let body_indent = match self.block_indent(line_indent) {
515            Some(indent) => indent,
516            None => return false,
517        };
518        let body = self.parse_block(body_indent);
519        if !self.allow_macro_redeclaration
520            && declarations.iter().any(|declaration| {
521                matches!(declaration, Decl::Macro { name: existing, .. } if existing == &name)
522            })
523        {
524            self.errors.push(OpyError::at(
525                "macro-redeclaration",
526                format!("macro '{name}' is already defined"),
527                name_token.span,
528            ));
529        }
530        declarations.push(Decl::Macro {
531            name,
532            args,
533            body,
534            span: start.span,
535        });
536        true
537    }
538
539    fn parse_param_list(&mut self) -> Option<Vec<String>> {
540        if self.expect(TokenKind::LParen, "'('").is_err() {
541            return None;
542        }
543        let mut params = Vec::new();
544        self.skip_newlines();
545        if self.peek_kind() == TokenKind::RParen {
546            self.advance();
547            return Some(params);
548        }
549        loop {
550            match self.expect_ident("a parameter name") {
551                Ok(name) => params.push(name),
552                Err(()) => return None,
553            }
554            self.skip_newlines();
555            if self.peek_kind() == TokenKind::Comma {
556                self.advance();
557                self.skip_newlines();
558            } else {
559                break;
560            }
561        }
562        if self.expect(TokenKind::RParen, "')'").is_err() {
563            return None;
564        }
565        Some(params)
566    }
567
568    // ---- rules and definitions ----
569
570    fn parse_rule(&mut self, rules: &mut Vec<RuleEntry>, rule_prefix: Option<String>) -> bool {
571        let start = self.advance();
572        let name = match self.peek_kind() {
573            TokenKind::String => self.advance().text,
574            _ => {
575                self.error_at_current("expected a rule name string after `rule`".to_string());
576                return false;
577            }
578        };
579        let name_token_span = self.tokens[self.pos.saturating_sub(1)].span;
580        // The exact rule-name occurrence is the string content between the
581        // quotes (the `"name"` token itself spans the quotes).
582        let name_span = Span::new(
583            name_token_span.file,
584            Position::new(name_token_span.start.line, name_token_span.start.col + 1),
585            Position::new(
586                name_token_span.end.line,
587                name_token_span
588                    .end
589                    .col
590                    .saturating_sub(1)
591                    .max(name_token_span.start.col + 1),
592            ),
593        );
594        if self
595            .expect(TokenKind::Colon, "':' after the rule name")
596            .is_err()
597        {
598            return false;
599        }
600        let line_indent = start.span.start.col;
601        if self.block_indent(line_indent).is_none() {
602            return false;
603        }
604        // OverPy accepts a small amount of indentation drift between rule
605        // directives and actions. The rule itself is still top-level, so any
606        // indentation greater than its column belongs to this rule.
607        let body_indent = line_indent + 1;
608        let mut event = None;
609        let mut conditions = Vec::new();
610        let mut annotations = Vec::new();
611        let mut disabled = false;
612        let mut delimiter = false;
613        let mut new_page = None;
614        let mut actions = Vec::new();
615        loop {
616            self.skip_newlines();
617            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
618                break;
619            }
620            if self.peek_kind() == TokenKind::At {
621                if !self.parse_directive(
622                    &mut event,
623                    &mut conditions,
624                    &mut annotations,
625                    &mut disabled,
626                    &mut delimiter,
627                    &mut new_page,
628                    false,
629                ) {
630                    self.recover_line();
631                }
632                continue;
633            }
634            match self.parse_statement() {
635                Ok(stmt) => actions.push(stmt),
636                Err(()) => self.recover_line(),
637            }
638        }
639        rules.push(RuleEntry::Rule(Rule {
640            name,
641            span: Span::new(start.span.file, start.span.start, name_token_span.end),
642            name_span,
643            disabled,
644            delimiter,
645            new_page,
646            annotations,
647            rule_prefix,
648            event: event.unwrap_or_else(|| Event {
649                name: "global".to_string(),
650                args: Vec::new(),
651                span: start.span,
652            }),
653            conditions,
654            actions,
655        }));
656        true
657    }
658
659    #[allow(clippy::too_many_arguments)]
660    fn parse_directive(
661        &mut self,
662        event: &mut Option<Event>,
663        conditions: &mut Vec<Expr>,
664        annotations: &mut Vec<Annotation>,
665        disabled: &mut bool,
666        delimiter: &mut bool,
667        new_page: &mut Option<String>,
668        subroutine: bool,
669    ) -> bool {
670        let at = self.advance();
671        let name = match self.expect_ident("a directive name after '@'") {
672            Ok(name) => name,
673            Err(()) => return false,
674        };
675        if matches!(
676            name.as_str(),
677            "Event" | "Team" | "Slot" | "Hero" | "Name" | "Disabled" | "Delimiter" | "NewPage"
678        ) && annotations.iter().any(|annotation| annotation.name == name)
679        {
680            self.error_at_current(format!("annotation '@{name}' was already declared"));
681            return false;
682        }
683        match name.as_str() {
684            "Event" => {
685                let event_name = match self.expect_ident("an event name after @Event") {
686                    Ok(name) => name,
687                    Err(()) => return false,
688                };
689                let event_annotation_arg = AnnotationArg {
690                    text: event_name.clone(),
691                    span: self.tokens[self.pos.saturating_sub(1)].span,
692                };
693                let mut args = Vec::new();
694                if self.peek_kind() == TokenKind::LParen
695                    && self.parse_event_args(&mut args).is_err()
696                {
697                    return false;
698                }
699                let end = args
700                    .last()
701                    .map_or(event_annotation_arg.span.end, |arg| arg.span().end);
702                *event = Some(Event {
703                    name: event_name,
704                    args,
705                    span: Span::new(at.span.file, at.span.start, end),
706                });
707                annotations.push(Annotation {
708                    name,
709                    args: vec![event_annotation_arg],
710                    span: Span::new(at.span.file, at.span.start, end),
711                });
712                true
713            }
714            "Condition" => {
715                let start = self.pos;
716                match self.parse_expr() {
717                    Ok(expr) => {
718                        let end = expr.span().end;
719                        conditions.push(expr);
720                        annotations.push(Annotation {
721                            name,
722                            args: vec![self.raw_annotation_arg(start, self.pos)],
723                            span: Span::new(at.span.file, at.span.start, end),
724                        });
725                        true
726                    }
727                    Err(()) => false,
728                }
729            }
730            "Team" | "Slot" | "Hero" => {
731                let args = self.consume_annotation_args();
732                if args.len() != 1 {
733                    self.error_at_current(format!("@{name} expects exactly one argument"));
734                    return false;
735                }
736                if subroutine {
737                    self.error_at_current(format!("@{name} is not valid on a subroutine"));
738                    return false;
739                }
740                if (name == "Slot"
741                    && annotations
742                        .iter()
743                        .any(|annotation| annotation.name == "Hero"))
744                    || (name == "Hero"
745                        && annotations
746                            .iter()
747                            .any(|annotation| annotation.name == "Slot"))
748                {
749                    self.error_at_current("@Slot and @Hero cannot be used together".to_string());
750                    return false;
751                }
752                let end = args.last().map_or(at.span.end, |arg| arg.span.end);
753                annotations.push(Annotation {
754                    name,
755                    args,
756                    span: Span::new(at.span.file, at.span.start, end),
757                });
758                true
759            }
760            "Name" => {
761                let args = self.consume_annotation_args();
762                if args.len() != 1 || !self.annotation_arg_is_string(&args[0]) {
763                    self.error_at_current(
764                        "@Name expects exactly one plain string literal".to_string(),
765                    );
766                    return false;
767                }
768                let end = args.last().map_or(at.span.end, |arg| arg.span.end);
769                annotations.push(Annotation {
770                    name,
771                    args,
772                    span: Span::new(at.span.file, at.span.start, end),
773                });
774                true
775            }
776            "SuppressWarnings" => {
777                let args = self.consume_annotation_args();
778                if args.is_empty() || args.iter().any(|arg| !is_identifier(&arg.text)) {
779                    self.error_at_current(
780                        "@SuppressWarnings expects one or more warning identifiers".to_string(),
781                    );
782                    return false;
783                }
784                let end = args.last().map_or(at.span.end, |arg| arg.span.end);
785                annotations.push(Annotation {
786                    name,
787                    args,
788                    span: Span::new(at.span.file, at.span.start, end),
789                });
790                true
791            }
792            "Disabled" => {
793                if !self.expect_annotation_end("@Disabled") {
794                    return false;
795                }
796                *disabled = true;
797                annotations.push(Annotation {
798                    name,
799                    args: Vec::new(),
800                    span: at.span,
801                });
802                true
803            }
804            "Delimiter" => {
805                if !self.expect_annotation_end("@Delimiter") {
806                    return false;
807                }
808                *delimiter = true;
809                annotations.push(Annotation {
810                    name,
811                    args: Vec::new(),
812                    span: at.span,
813                });
814                true
815            }
816            "NewPage" => {
817                let args = self.consume_annotation_args();
818                if args.len() > 1
819                    || args
820                        .first()
821                        .is_some_and(|arg| !self.annotation_arg_is_string(arg))
822                {
823                    self.error_at_current(
824                        "@NewPage expects at most one plain string literal".to_string(),
825                    );
826                    return false;
827                }
828                let end = args.last().map_or(at.span.end, |arg| arg.span.end);
829                *new_page = args.first().map(|arg| unquote_annotation_arg(&arg.text));
830                annotations.push(Annotation {
831                    name,
832                    args,
833                    span: Span::new(at.span.file, at.span.start, end),
834                });
835                true
836            }
837            other => {
838                self.error_at_current(format!("unsupported directive '@{other}'"));
839                false
840            }
841        }
842    }
843
844    fn consume_annotation_args(&mut self) -> Vec<AnnotationArg> {
845        let start = self.pos;
846        while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
847            self.advance();
848        }
849        if self.pos == start {
850            return Vec::new();
851        }
852        let tokens = &self.tokens[start..self.pos];
853        if tokens.len() == 3
854            && tokens[1].kind == TokenKind::Dot
855            && tokens[0].kind == TokenKind::Ident
856        {
857            return vec![AnnotationArg {
858                text: tokens.iter().map(|token| token.text.as_str()).collect(),
859                span: Span::new(
860                    tokens[0].span.file,
861                    tokens[0].span.start,
862                    tokens[2].span.end,
863                ),
864            }];
865        }
866        tokens
867            .iter()
868            .map(|token| AnnotationArg {
869                text: if token.kind == TokenKind::String {
870                    format!("\"{}\"", token.text)
871                } else {
872                    token.text.clone()
873                },
874                span: token.span,
875            })
876            .collect()
877    }
878
879    fn raw_annotation_arg(&self, start: usize, end: usize) -> AnnotationArg {
880        let tokens = &self.tokens[start..end];
881        let first = tokens
882            .first()
883            .map(|token| token.span)
884            .unwrap_or(self.peek().span);
885        let last = tokens.last().map(|token| token.span).unwrap_or(first);
886        AnnotationArg {
887            text: tokens.iter().map(|token| token.text.as_str()).collect(),
888            span: Span::new(first.file, first.start, last.end),
889        }
890    }
891
892    fn annotation_arg_is_string(&self, arg: &AnnotationArg) -> bool {
893        arg.text.starts_with('"') && arg.text.ends_with('"')
894    }
895
896    fn expect_annotation_end(&mut self, name: &str) -> bool {
897        if self.peek_kind() == TokenKind::Newline || self.peek_kind() == TokenKind::Eof {
898            true
899        } else {
900            self.error_at_current(format!("{name} takes no arguments"));
901            false
902        }
903    }
904
905    fn parse_def(&mut self, rules: &mut Vec<RuleEntry>, rule_prefix: Option<String>) -> bool {
906        let start = self.advance();
907        // The name token follows the `def` keyword. `span` covers the
908        // definition (`def name`), and `name_span` is the exact identifier
909        // occurrence (rename targets, not the keyword).
910        let name_token = self.peek().clone();
911        let name = match self.expect_ident("a subroutine name after `def`") {
912            Ok(name) => name,
913            Err(()) => return false,
914        };
915        let name_span = if name_token.kind == TokenKind::Ident {
916            name_token.span
917        } else {
918            start.span
919        };
920        let params = match self.parse_param_list() {
921            Some(params) => params,
922            None => return false,
923        };
924        if !params.is_empty() {
925            self.error_at_current(
926                "subroutine parameters are outside the declared support matrix".to_string(),
927            );
928            return false;
929        }
930        if self
931            .expect(TokenKind::Colon, "':' after the subroutine signature")
932            .is_err()
933        {
934            return false;
935        }
936        let line_indent = start.span.start.col;
937        let body_indent = match self.block_indent(line_indent) {
938            Some(indent) => indent,
939            None => return false,
940        };
941        let mut annotations = Vec::new();
942        let mut event = None;
943        let mut conditions = Vec::new();
944        let mut disabled = false;
945        let mut delimiter = false;
946        let mut new_page = None;
947        loop {
948            self.skip_newlines();
949            if self.peek_kind() != TokenKind::At {
950                break;
951            }
952            if !self.parse_directive(
953                &mut event,
954                &mut conditions,
955                &mut annotations,
956                &mut disabled,
957                &mut delimiter,
958                &mut new_page,
959                true,
960            ) {
961                self.recover_line();
962                return false;
963            }
964        }
965        if event.is_some() || !conditions.is_empty() {
966            self.error_at_current("subroutines cannot have events or conditions".to_string());
967            return false;
968        }
969        let _ = (disabled, delimiter, new_page);
970        let presentation_name = annotations
971            .iter()
972            .find(|annotation| annotation.name == "Name")
973            .and_then(|annotation| annotation.args.first())
974            .map(|arg| unquote_annotation_arg(&arg.text));
975        let body = self.parse_block(body_indent);
976        let span = if name_token.kind == TokenKind::Ident {
977            Span::new(start.span.file, start.span.start, name_token.span.end)
978        } else {
979            start.span
980        };
981        rules.push(RuleEntry::SubroutineDef {
982            name,
983            presentation_name,
984            span,
985            name_span,
986            body,
987            annotations,
988            rule_prefix,
989        });
990        true
991    }
992
993    /// The indentation of the next non-empty line, which must exceed
994    /// `line_indent` (an indented block follows the colon).
995    fn block_indent(&mut self, line_indent: u32) -> Option<u32> {
996        self.skip_newlines();
997        if self.peek_kind() == TokenKind::Eof {
998            self.error_at_current("expected an indented block".to_string());
999            return None;
1000        }
1001        let indent = self.peek().span.start.col;
1002        if indent <= line_indent {
1003            self.error_at_current("expected an indented block after ':'".to_string());
1004            return None;
1005        }
1006        Some(indent)
1007    }
1008
1009    // ---- statements ----
1010
1011    fn parse_block(&mut self, block_indent: u32) -> Vec<Stmt> {
1012        let mut stmts = Vec::new();
1013        loop {
1014            self.skip_newlines();
1015            if self.peek_kind() == TokenKind::Eof {
1016                break;
1017            }
1018            if self.peek().span.start.col < block_indent {
1019                break;
1020            }
1021            if self.peek().span.start.col > block_indent {
1022                // A deeper indent without an introducer: recover by line.
1023                self.error_at_current("unexpected indentation".to_string());
1024                self.recover_line();
1025                continue;
1026            }
1027            match self.parse_statement() {
1028                Ok(stmt) => stmts.push(stmt),
1029                Err(()) => self.recover_line(),
1030            }
1031        }
1032        stmts
1033    }
1034
1035    fn parse_statement(&mut self) -> Result<Stmt, ()> {
1036        let token = self.peek();
1037        if token.kind == TokenKind::Ident {
1038            match token.text.as_str() {
1039                "if" => return self.parse_if(),
1040                "for" => return self.parse_for(),
1041                "while" => return self.parse_while(),
1042                "do" => return self.parse_do_while(),
1043                "switch" => return self.parse_switch(),
1044                "del" => return self.parse_delete(),
1045                "continue" => {
1046                    let token = self.advance();
1047                    self.expect_statement_end("the continue statement")?;
1048                    return Ok(Stmt::Continue { span: token.span });
1049                }
1050                "goto" => return self.parse_goto(),
1051                "break" => {
1052                    let token = self.advance();
1053                    return Ok(Stmt::Break { span: token.span });
1054                }
1055                "return" => {
1056                    let token = self.advance();
1057                    self.expect_statement_end("the return statement")?;
1058                    return Ok(Stmt::Return { span: token.span });
1059                }
1060                "pass" => {
1061                    let start = self.advance();
1062                    return Ok(Stmt::Pass { span: start.span });
1063                }
1064                _ => {}
1065            }
1066            if self.peek_at(1).kind == TokenKind::Colon {
1067                return self.parse_label();
1068            }
1069        }
1070        self.parse_expr_statement()
1071    }
1072
1073    fn parse_delete(&mut self) -> Result<Stmt, ()> {
1074        let start = self.advance();
1075        let target = self.parse_postfix()?;
1076        if !matches!(target, Expr::Index { .. }) {
1077            self.errors.push(OpyError::at(
1078                "parse-error",
1079                "the del statement requires an array index target".to_string(),
1080                target.span(),
1081            ));
1082            return Err(());
1083        }
1084        self.expect_statement_end("the del statement")?;
1085        Ok(Stmt::Delete {
1086            span: Span::new(start.span.file, start.span.start, target.span().end),
1087            target,
1088        })
1089    }
1090
1091    fn parse_goto(&mut self) -> Result<Stmt, ()> {
1092        let start = self.advance();
1093        if self.is_ident("loc") {
1094            self.advance();
1095            self.expect(TokenKind::Plus, "'+' after 'goto loc'")?;
1096            let offset = self.parse_expr()?;
1097            self.expect_statement_end("the goto target")?;
1098            return Ok(Stmt::Goto {
1099                label: None,
1100                span: Span::new(start.span.file, start.span.start, offset.span().end),
1101                offset: Some(offset),
1102                rule_start: false,
1103            });
1104        }
1105
1106        let label = self.expect_ident("a label or 'loc+...' after 'goto'")?;
1107        self.expect_statement_end("the goto target")?;
1108        let rule_start = label == "RULE_START";
1109        Ok(Stmt::Goto {
1110            label: (!rule_start).then_some(label),
1111            offset: None,
1112            rule_start,
1113            span: Span::new(
1114                start.span.file,
1115                start.span.start,
1116                self.tokens[self.pos - 1].span.end,
1117            ),
1118        })
1119    }
1120
1121    fn parse_label(&mut self) -> Result<Stmt, ()> {
1122        let name = self.advance();
1123        let colon = self.expect(TokenKind::Colon, "':' after a label")?;
1124        self.expect_statement_end("the label")?;
1125        Ok(Stmt::Label {
1126            name: name.text,
1127            span: Span::new(name.span.file, name.span.start, colon.span.end),
1128        })
1129    }
1130
1131    fn parse_expr_statement(&mut self) -> Result<Stmt, ()> {
1132        if self.peek_kind() == TokenKind::LParen {
1133            let save_pos = self.pos;
1134            let save_errors = self.errors.len();
1135            let start = self.advance().span;
1136            if let Ok(target) = self.parse_postfix()
1137                && self.peek_kind() == TokenKind::Assign
1138            {
1139                self.advance();
1140                let value = self.parse_expr()?;
1141                let end = self.expect(TokenKind::RParen, "')'")?.span.end;
1142                return Ok(Stmt::Assign {
1143                    target,
1144                    value,
1145                    span: Span::new(start.file, start.start, end),
1146                });
1147            }
1148            self.pos = save_pos;
1149            self.errors.truncate(save_errors);
1150        }
1151        let start = self.peek().span;
1152        let expr = self.parse_expr()?;
1153        match self.peek_kind() {
1154            TokenKind::Assign => {
1155                self.advance();
1156                let value = self.parse_expr()?;
1157                let end = value.span().end;
1158                Ok(Stmt::Assign {
1159                    target: expr,
1160                    value,
1161                    span: Span::new(start.file, start.start, end),
1162                })
1163            }
1164            TokenKind::PlusAssign
1165            | TokenKind::MinusAssign
1166            | TokenKind::StarAssign
1167            | TokenKind::SlashAssign
1168            | TokenKind::PercentAssign
1169            | TokenKind::DoubleStarAssign => {
1170                let op = match self.peek_kind() {
1171                    TokenKind::PlusAssign => "+",
1172                    TokenKind::MinusAssign => "-",
1173                    TokenKind::StarAssign => "*",
1174                    TokenKind::SlashAssign => "/",
1175                    TokenKind::PercentAssign => "%",
1176                    TokenKind::DoubleStarAssign => "**",
1177                    _ => unreachable!(),
1178                }
1179                .to_string();
1180                self.advance();
1181                let rhs = self.parse_expr()?;
1182                let end = rhs.span().end;
1183                let value = Expr::Binary {
1184                    op,
1185                    left: Box::new(expr.clone()),
1186                    right: Box::new(rhs),
1187                    span: Span::new(start.file, start.start, end),
1188                };
1189                Ok(Stmt::Assign {
1190                    target: expr,
1191                    value,
1192                    span: Span::new(start.file, start.start, end),
1193                })
1194            }
1195            TokenKind::Ident
1196                if matches!(self.peek().text.as_str(), "min" | "max")
1197                    && self.peek_at(1).kind == TokenKind::Assign =>
1198            {
1199                let op = self.advance().text;
1200                self.advance();
1201                let rhs = self.parse_expr()?;
1202                let end = rhs.span().end;
1203                let value = Expr::Binary {
1204                    op,
1205                    left: Box::new(expr.clone()),
1206                    right: Box::new(rhs),
1207                    span: Span::new(start.file, start.start, end),
1208                };
1209                Ok(Stmt::Assign {
1210                    target: expr,
1211                    value,
1212                    span: Span::new(start.file, start.start, end),
1213                })
1214            }
1215            TokenKind::Increment | TokenKind::Decrement => {
1216                let operator = self.advance();
1217                if !matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) {
1218                    self.error_at_current(
1219                        "postfix increment/decrement must be a standalone assignment".to_string(),
1220                    );
1221                    return Err(());
1222                }
1223                let operation = if operator.kind == TokenKind::Increment {
1224                    "+"
1225                } else {
1226                    "-"
1227                };
1228                let span = Span::new(start.file, start.start, operator.span.end);
1229                let value = Expr::Binary {
1230                    op: operation.to_string(),
1231                    left: Box::new(expr.clone()),
1232                    right: Box::new(Expr::Number {
1233                        value: 1.0,
1234                        text: "1".to_string(),
1235                        span: operator.span,
1236                    }),
1237                    span,
1238                };
1239                Ok(Stmt::Assign {
1240                    target: expr,
1241                    value,
1242                    span,
1243                })
1244            }
1245            _ => {
1246                let end = expr.span().end;
1247                Ok(Stmt::Expr {
1248                    expr,
1249                    span: Span::new(start.file, start.start, end),
1250                })
1251            }
1252        }
1253    }
1254
1255    fn parse_if(&mut self) -> Result<Stmt, ()> {
1256        let start = self.advance();
1257        let line_indent = start.span.start.col;
1258        let condition = self.parse_expr()?;
1259        if self
1260            .expect(TokenKind::Colon, "':' after the if condition")
1261            .is_err()
1262        {
1263            return Err(());
1264        }
1265        let body = self.parse_colon_body(line_indent)?;
1266        let mut branches = vec![IfBranch { condition, body }];
1267        let mut r#else = None;
1268        loop {
1269            let save = self.pos;
1270            self.skip_newlines();
1271            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col > line_indent {
1272                self.pos = save;
1273                break;
1274            }
1275            if self.is_ident("elif") {
1276                let branch_start = self.advance();
1277                let condition = match self.parse_expr() {
1278                    Ok(expr) => expr,
1279                    Err(()) => return Err(()),
1280                };
1281                if self
1282                    .expect(TokenKind::Colon, "':' after the elif condition")
1283                    .is_err()
1284                {
1285                    return Err(());
1286                }
1287                let body = self.parse_colon_body(branch_start.span.start.col)?;
1288                branches.push(IfBranch { condition, body });
1289            } else if self.is_ident("else") {
1290                let branch_start = self.advance();
1291                if self.is_ident("if") {
1292                    self.advance();
1293                    let condition = self.parse_expr()?;
1294                    if self
1295                        .expect(TokenKind::Colon, "':' after the else-if condition")
1296                        .is_err()
1297                    {
1298                        return Err(());
1299                    }
1300                    let body = self.parse_colon_body(branch_start.span.start.col)?;
1301                    branches.push(IfBranch { condition, body });
1302                    continue;
1303                }
1304                if self.expect(TokenKind::Colon, "':' after `else`").is_err() {
1305                    return Err(());
1306                }
1307                let body = self.parse_colon_body(branch_start.span.start.col)?;
1308                r#else = Some(body);
1309                break;
1310            } else {
1311                self.pos = save;
1312                break;
1313            }
1314        }
1315        Ok(Stmt::If {
1316            branches,
1317            r#else,
1318            span: start.span,
1319        })
1320    }
1321
1322    fn parse_colon_body(&mut self, line_indent: u32) -> Result<Vec<Stmt>, ()> {
1323        if matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) {
1324            let save = self.pos;
1325            self.skip_newlines();
1326            if self.peek_kind() != TokenKind::Eof && self.peek().span.start.col == line_indent {
1327                let statement = self.parse_statement()?;
1328                self.expect_statement_end("the inline statement")?;
1329                return Ok(vec![statement]);
1330            }
1331            self.pos = save;
1332            let body_indent = self.block_indent(line_indent).ok_or(())?;
1333            Ok(self.parse_block(body_indent))
1334        } else {
1335            let statement = self.parse_statement()?;
1336            self.expect_statement_end("the inline statement")?;
1337            Ok(vec![statement])
1338        }
1339    }
1340
1341    fn parse_for(&mut self) -> Result<Stmt, ()> {
1342        let start = self.advance();
1343        let variable = self.parse_postfix()?;
1344        if !self.is_ident("in") {
1345            self.error_at_current("expected `in` in the for statement".to_string());
1346            return Err(());
1347        }
1348        self.advance();
1349        let iterable = self.parse_expr()?;
1350        if self
1351            .expect(TokenKind::Colon, "':' after the for header")
1352            .is_err()
1353        {
1354            return Err(());
1355        }
1356        let line_indent = start.span.start.col;
1357        let body_indent = self.block_indent(line_indent).ok_or(())?;
1358        let body = self.parse_block(body_indent);
1359        Ok(Stmt::For {
1360            variable,
1361            iterable,
1362            body,
1363            span: start.span,
1364        })
1365    }
1366
1367    fn parse_while(&mut self) -> Result<Stmt, ()> {
1368        let start = self.advance();
1369        let condition = self.parse_expr()?;
1370        if self
1371            .expect(TokenKind::Colon, "':' after the while condition")
1372            .is_err()
1373        {
1374            return Err(());
1375        }
1376        let line_indent = start.span.start.col;
1377        let body_indent = self.block_indent(line_indent).ok_or(())?;
1378        let body = self.parse_block(body_indent);
1379        Ok(Stmt::While {
1380            condition,
1381            body,
1382            span: start.span,
1383        })
1384    }
1385
1386    fn parse_do_while(&mut self) -> Result<Stmt, ()> {
1387        let start = self.advance();
1388        if self.expect(TokenKind::Colon, "':' after `do`").is_err() {
1389            return Err(());
1390        }
1391        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1392        let body = self.parse_block(body_indent);
1393        if !self.is_ident("while") {
1394            self.error_at_current("expected `while` after the do block".to_string());
1395            return Err(());
1396        }
1397        self.advance();
1398        let condition = self.parse_expr()?;
1399        if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
1400            self.error_at_current("expected the end of the do-while condition".to_string());
1401            return Err(());
1402        }
1403        Ok(Stmt::DoWhile {
1404            condition,
1405            body,
1406            span: start.span,
1407        })
1408    }
1409
1410    fn parse_switch(&mut self) -> Result<Stmt, ()> {
1411        let start = self.advance();
1412        let value = self.parse_expr()?;
1413        if self
1414            .expect(TokenKind::Colon, "':' after the switch value")
1415            .is_err()
1416        {
1417            return Err(());
1418        }
1419        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1420        let mut arms = Vec::new();
1421        loop {
1422            self.skip_newlines();
1423            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
1424                break;
1425            }
1426            if self.peek().span.start.col != body_indent {
1427                self.error_at_current("unexpected indentation in switch".to_string());
1428                self.recover_line();
1429                continue;
1430            }
1431            if self.is_ident("case") {
1432                let case_start = self.advance();
1433                let case_value = self.parse_expr()?;
1434                if self
1435                    .expect(TokenKind::Colon, "':' after the case value")
1436                    .is_err()
1437                {
1438                    return Err(());
1439                }
1440                let case_body_indent = self.block_indent(body_indent).ok_or(())?;
1441                let body = self.parse_block(case_body_indent);
1442                arms.push(SwitchArm::Case {
1443                    value: case_value,
1444                    body,
1445                    span: case_start.span,
1446                });
1447            } else if self.is_ident("default") {
1448                let default_start = self.advance();
1449                if self
1450                    .expect(TokenKind::Colon, "':' after `default`")
1451                    .is_err()
1452                {
1453                    return Err(());
1454                }
1455                let default_body_indent = self.block_indent(body_indent).ok_or(())?;
1456                arms.push(SwitchArm::Default {
1457                    body: self.parse_block(default_body_indent),
1458                    span: default_start.span,
1459                });
1460                if default_start.span.start.col != body_indent {
1461                    self.error_at_current("invalid default indentation".to_string());
1462                    return Err(());
1463                }
1464            } else {
1465                self.error_at_current("expected `case` or `default` in switch".to_string());
1466                self.recover_line();
1467            }
1468        }
1469        if arms.is_empty() {
1470            self.errors.push(OpyError::at(
1471                "parse-error",
1472                "switch must contain at least one case or default arm".to_string(),
1473                start.span,
1474            ));
1475            return Err(());
1476        }
1477        Ok(Stmt::Switch {
1478            value,
1479            arms,
1480            span: start.span,
1481        })
1482    }
1483
1484    // ---- expressions ----
1485
1486    fn parse_expr(&mut self) -> Result<Expr, ()> {
1487        self.parse_expr_inner(false)
1488    }
1489
1490    fn parse_expr_inner(&mut self, allow_multiline_conditional: bool) -> Result<Expr, ()> {
1491        self.skip_expression_newlines();
1492        let then_value = self.parse_or()?;
1493        let has_newline_before_conditional = self.tokens[..self.pos].iter().any(|token| {
1494            token.kind == TokenKind::Newline
1495                && token.span.file == then_value.span().file
1496                && token.span.start.line > then_value.span().end.line
1497        });
1498        if self.is_ident("if")
1499            && !self.inside_delimiter_group()
1500            && has_newline_before_conditional
1501            && self.peek().span.start.line != then_value.span().end.line
1502        {
1503            return Ok(then_value);
1504        }
1505        if allow_multiline_conditional && self.inside_delimiter_group() {
1506            self.skip_newlines();
1507        }
1508        if !self.is_ident("if") {
1509            return Ok(then_value);
1510        }
1511
1512        let conditional_start = self.pos;
1513        self.advance();
1514        self.skip_expression_newlines();
1515        let condition = self.parse_or()?;
1516        self.skip_expression_newlines();
1517        if !self.is_ident("else") {
1518            if self.peek_kind() == TokenKind::Colon {
1519                self.pos = conditional_start;
1520                return Ok(then_value);
1521            }
1522            self.error_at_current("expected `else` in conditional expression".to_string());
1523            return Err(());
1524        }
1525        self.advance();
1526        // Conditional expressions are right-associative, so a chained form
1527        // such as `a if c else b if d else e` groups at the else branch.
1528        let else_value = self.parse_expr_inner(true)?;
1529        let span = Span::new(
1530            then_value.span().file,
1531            then_value.span().start,
1532            else_value.span().end,
1533        );
1534        Ok(Expr::Conditional {
1535            then_value: Box::new(then_value),
1536            condition: Box::new(condition),
1537            else_value: Box::new(else_value),
1538            span,
1539        })
1540    }
1541
1542    fn parse_or(&mut self) -> Result<Expr, ()> {
1543        self.skip_expression_newlines();
1544        let mut left = self.parse_and()?;
1545        loop {
1546            self.skip_expression_newlines();
1547            if !self.is_ident("or") {
1548                break;
1549            }
1550            self.advance();
1551            self.skip_expression_newlines();
1552            let right = self.parse_and()?;
1553            let span = Span::new(left.span().file, left.span().start, right.span().end);
1554            left = Expr::Binary {
1555                op: "or".to_string(),
1556                left: Box::new(left),
1557                right: Box::new(right),
1558                span,
1559            };
1560        }
1561        Ok(left)
1562    }
1563
1564    fn parse_and(&mut self) -> Result<Expr, ()> {
1565        self.skip_expression_newlines();
1566        let mut left = self.parse_not()?;
1567        loop {
1568            self.skip_expression_newlines();
1569            if !self.is_ident("and") {
1570                break;
1571            }
1572            self.advance();
1573            self.skip_expression_newlines();
1574            let right = self.parse_not()?;
1575            let span = Span::new(left.span().file, left.span().start, right.span().end);
1576            left = Expr::Binary {
1577                op: "and".to_string(),
1578                left: Box::new(left),
1579                right: Box::new(right),
1580                span,
1581            };
1582        }
1583        Ok(left)
1584    }
1585
1586    fn parse_not(&mut self) -> Result<Expr, ()> {
1587        self.skip_expression_newlines();
1588        if self.is_ident("not") {
1589            let start = self.advance();
1590            self.skip_expression_newlines();
1591            let operand = self.parse_not()?;
1592            let end = operand.span().end;
1593            return Ok(Expr::Unary {
1594                op: "not".to_string(),
1595                operand: Box::new(operand),
1596                span: Span::new(start.span.file, start.span.start, end),
1597            });
1598        }
1599        self.parse_comparison()
1600    }
1601
1602    fn parse_comparison(&mut self) -> Result<Expr, ()> {
1603        self.skip_expression_newlines();
1604        let mut left = self.parse_additive()?;
1605        loop {
1606            self.skip_expression_newlines();
1607            let op = match self.peek_kind() {
1608                TokenKind::Eq => "==",
1609                TokenKind::Ne => "!=",
1610                TokenKind::Lt => "<",
1611                TokenKind::Le => "<=",
1612                TokenKind::Gt => ">",
1613                TokenKind::Ge => ">=",
1614                _ if self.is_ident("in") => "in",
1615                _ if self.is_ident("not") && self.peek_at(1).text == "in" => "not in",
1616                _ => break,
1617            };
1618            self.advance();
1619            if op == "not in" {
1620                self.advance();
1621            }
1622            self.skip_expression_newlines();
1623            let right = self.parse_additive()?;
1624            let span = Span::new(left.span().file, left.span().start, right.span().end);
1625            left = Expr::Binary {
1626                op: op.to_string(),
1627                left: Box::new(left),
1628                right: Box::new(right),
1629                span,
1630            };
1631        }
1632        Ok(left)
1633    }
1634
1635    fn parse_additive(&mut self) -> Result<Expr, ()> {
1636        self.skip_expression_newlines();
1637        let mut left = self.parse_multiplicative()?;
1638        loop {
1639            self.skip_expression_newlines();
1640            if self.peek_kind() == TokenKind::Decrement
1641                && !matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof)
1642            {
1643                let operator = self.advance();
1644                let operand = self.parse_unary()?;
1645                let unary = Expr::Unary {
1646                    op: "-".to_string(),
1647                    span: Span::new(operator.span.file, operator.span.start, operand.span().end),
1648                    operand: Box::new(operand),
1649                };
1650                let right = self.parse_multiplicative_tail(unary)?;
1651                let span = Span::new(left.span().file, left.span().start, right.span().end);
1652                left = Expr::Binary {
1653                    op: "-".to_string(),
1654                    left: Box::new(left),
1655                    right: Box::new(right),
1656                    span,
1657                };
1658                continue;
1659            }
1660            let op = match self.peek_kind() {
1661                TokenKind::Plus => "+",
1662                TokenKind::Minus => "-",
1663                _ => break,
1664            };
1665            self.advance();
1666            self.skip_expression_newlines();
1667            let right = self.parse_multiplicative()?;
1668            let span = Span::new(left.span().file, left.span().start, right.span().end);
1669            left = Expr::Binary {
1670                op: op.to_string(),
1671                left: Box::new(left),
1672                right: Box::new(right),
1673                span,
1674            };
1675        }
1676        Ok(left)
1677    }
1678
1679    fn parse_multiplicative(&mut self) -> Result<Expr, ()> {
1680        self.skip_expression_newlines();
1681        let left = self.parse_unary()?;
1682        self.parse_multiplicative_tail(left)
1683    }
1684
1685    fn parse_multiplicative_tail(&mut self, mut left: Expr) -> Result<Expr, ()> {
1686        loop {
1687            self.skip_expression_newlines();
1688            let op = match self.peek_kind() {
1689                TokenKind::Star => "*",
1690                TokenKind::Slash => "/",
1691                TokenKind::Percent => "%",
1692                _ => break,
1693            };
1694            self.advance();
1695            self.skip_expression_newlines();
1696            let right = self.parse_unary()?;
1697            let span = Span::new(left.span().file, left.span().start, right.span().end);
1698            left = Expr::Binary {
1699                op: op.to_string(),
1700                left: Box::new(left),
1701                right: Box::new(right),
1702                span,
1703            };
1704        }
1705        Ok(left)
1706    }
1707
1708    fn parse_unary(&mut self) -> Result<Expr, ()> {
1709        self.skip_expression_newlines();
1710        if matches!(self.peek_kind(), TokenKind::Minus | TokenKind::Decrement) {
1711            let start = self.advance();
1712            self.skip_expression_newlines();
1713            let operand = self.parse_unary()?;
1714            let end = operand.span().end;
1715            let unary = Expr::Unary {
1716                op: "-".to_string(),
1717                operand: Box::new(operand),
1718                span: Span::new(start.span.file, start.span.start, end),
1719            };
1720            if start.kind == TokenKind::Decrement {
1721                return Ok(Expr::Unary {
1722                    op: "-".to_string(),
1723                    operand: Box::new(unary),
1724                    span: Span::new(start.span.file, start.span.start, end),
1725                });
1726            }
1727            return Ok(unary);
1728        }
1729        self.parse_power()
1730    }
1731
1732    fn parse_power(&mut self) -> Result<Expr, ()> {
1733        let base = self.parse_postfix()?;
1734        self.skip_expression_newlines();
1735        if self.peek_kind() == TokenKind::DoubleStar {
1736            self.advance();
1737            // Right-associative.
1738            self.skip_expression_newlines();
1739            let exponent = self.parse_unary()?;
1740            let span = Span::new(base.span().file, base.span().start, exponent.span().end);
1741            return Ok(Expr::Binary {
1742                op: "**".to_string(),
1743                left: Box::new(base),
1744                right: Box::new(exponent),
1745                span,
1746            });
1747        }
1748        Ok(base)
1749    }
1750
1751    fn parse_postfix(&mut self) -> Result<Expr, ()> {
1752        let mut base = self.parse_primary()?;
1753        loop {
1754            self.skip_expression_newlines();
1755            match self.peek_kind() {
1756                TokenKind::LParen => {
1757                    let mut args = Vec::new();
1758                    self.parse_call_args(&mut args)?;
1759                    let end = self.tokens[self.pos.saturating_sub(1)].span.end;
1760                    base = match base {
1761                        Expr::Name { name, span } => Expr::Call {
1762                            name,
1763                            args,
1764                            span: Span::new(span.file, span.start, end),
1765                        },
1766                        Expr::Member {
1767                            receiver,
1768                            member,
1769                            span,
1770                            ..
1771                        } => Expr::ReceiverCall {
1772                            receiver,
1773                            name: member,
1774                            args,
1775                            span: Span::new(span.file, span.start, end),
1776                        },
1777                        _other => {
1778                            self.errors.push(OpyError::at(
1779                                "parse-error",
1780                                "cannot call this expression".to_string(),
1781                                self.peek().span,
1782                            ));
1783                            return Err(());
1784                        }
1785                    };
1786                }
1787                TokenKind::LBracket => {
1788                    self.advance();
1789                    let index = self.parse_expr()?;
1790                    if self.peek_kind() == TokenKind::Colon {
1791                        self.advance();
1792                        let maximum = self.parse_expr()?;
1793                        let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
1794                        let Expr::Name { name, span } = &base else {
1795                            self.error_at_current(
1796                                "a range type must start with a type name".to_string(),
1797                            );
1798                            return Err(());
1799                        };
1800                        base = Expr::Type {
1801                            name: name.clone(),
1802                            args: vec![index, maximum],
1803                            span: Span::new(span.file, span.start, end),
1804                        };
1805                        continue;
1806                    }
1807                    let end = match self.expect(TokenKind::RBracket, "']'") {
1808                        Ok(token) => token.span.end,
1809                        Err(()) => return Err(()),
1810                    };
1811                    let span = Span::new(base.span().file, base.span().start, end);
1812                    base = Expr::Index {
1813                        array: Box::new(base),
1814                        index: Box::new(index),
1815                        span,
1816                    };
1817                }
1818                TokenKind::Dot => {
1819                    self.advance();
1820                    let member_token = self.peek().clone();
1821                    let member = match self.peek_kind() {
1822                        TokenKind::Ident | TokenKind::Number => self.advance().text,
1823                        _ => {
1824                            self.error_at_current("expected a member name after '.'".to_string());
1825                            return Err(());
1826                        }
1827                    };
1828                    let member_span = member_token.span;
1829                    let end = member_span.end;
1830                    let span = Span::new(base.span().file, base.span().start, end);
1831                    base = Expr::Member {
1832                        receiver: Box::new(base),
1833                        member,
1834                        member_span,
1835                        span,
1836                    };
1837                }
1838                _ => break,
1839            }
1840        }
1841        Ok(base)
1842    }
1843
1844    /// `@Event name(args)`: positional expressions only (keyword arguments
1845    /// are a call-argument form, not an event form).
1846    fn parse_event_args(&mut self, args: &mut Vec<Expr>) -> Result<(), ()> {
1847        self.expect(TokenKind::LParen, "'('")?;
1848        self.skip_newlines();
1849        if self.peek_kind() == TokenKind::RParen {
1850            self.advance();
1851            return Ok(());
1852        }
1853        loop {
1854            let expr = self.parse_expr()?;
1855            if self.peek_kind() == TokenKind::Assign {
1856                self.error_at_current("keyword arguments are not valid in @Event".to_string());
1857                return Err(());
1858            }
1859            args.push(expr);
1860            self.skip_newlines();
1861            if self.peek_kind() == TokenKind::Comma {
1862                self.advance();
1863                self.skip_newlines();
1864                if self.peek_kind() == TokenKind::RParen {
1865                    break;
1866                }
1867            } else {
1868                break;
1869            }
1870        }
1871        self.expect(TokenKind::RParen, "')'")?;
1872        Ok(())
1873    }
1874
1875    fn parse_call_args(&mut self, args: &mut Vec<CallArg>) -> Result<(), ()> {
1876        self.expect(TokenKind::LParen, "'('")?;
1877        self.skip_newlines();
1878        if self.peek_kind() == TokenKind::RParen {
1879            self.advance();
1880            return Ok(());
1881        }
1882        loop {
1883            match self.parse_expr() {
1884                Ok(expr) => {
1885                    // A keyword argument is `name = expr` (issue #110): a
1886                    // bare identifier immediately followed by `=`. Anything
1887                    // else (`expr = ...`) is not a call argument form and is
1888                    // rejected like the pinned reference rejects it.
1889                    if self.peek_kind() == TokenKind::Assign {
1890                        let Expr::Name { name, span } = expr else {
1891                            self.error_at_current(
1892                                "expected a keyword name before '=' in this call".to_string(),
1893                            );
1894                            return Err(());
1895                        };
1896                        self.advance();
1897                        let value = match self.parse_expr() {
1898                            Ok(value) => value,
1899                            Err(()) => return Err(()),
1900                        };
1901                        args.push(CallArg {
1902                            keyword: Some((name, span)),
1903                            value,
1904                        });
1905                    } else {
1906                        args.push(CallArg {
1907                            keyword: None,
1908                            value: expr,
1909                        });
1910                    }
1911                }
1912                Err(()) => return Err(()),
1913            }
1914            self.skip_newlines();
1915            if self.peek_kind() == TokenKind::Comma {
1916                self.advance();
1917                self.skip_newlines();
1918                if self.peek_kind() == TokenKind::RParen {
1919                    break;
1920                }
1921            } else {
1922                break;
1923            }
1924        }
1925        self.expect(TokenKind::RParen, "')'")?;
1926        Ok(())
1927    }
1928
1929    fn parse_primary(&mut self) -> Result<Expr, ()> {
1930        let token = self.peek();
1931        match token.kind {
1932            TokenKind::Number => {
1933                let token = self.advance();
1934                let value = if let Some(hex) = token
1935                    .text
1936                    .strip_prefix("0x")
1937                    .or_else(|| token.text.strip_prefix("0X"))
1938                {
1939                    u64::from_str_radix(hex, 16).map_or(f64::NAN, |value| value as f64)
1940                } else {
1941                    token.text.parse().unwrap_or(f64::NAN)
1942                };
1943                Ok(Expr::Number {
1944                    value,
1945                    text: token.text.clone(),
1946                    span: token.span,
1947                })
1948            }
1949            TokenKind::String => self.parse_string_literal(),
1950            TokenKind::Ident => {
1951                let token = self.advance();
1952                if token.text == "lambda" {
1953                    return self.parse_lambda(token.span);
1954                }
1955                if is_string_modifier(&token.text) && self.peek_kind() == TokenKind::String {
1956                    let string = self.advance();
1957                    let (format_text, interpolations) = if token.text == "f" {
1958                        let raw = string.raw.as_deref().unwrap_or(&string.text);
1959                        let (format_text, interpolations) =
1960                            self.parse_f_string(raw, string.span)?;
1961                        (Some(format_text), interpolations)
1962                    } else {
1963                        (None, Vec::new())
1964                    };
1965                    return Ok(Expr::StringModifier {
1966                        modifier: token.text.chars().next().unwrap_or_default(),
1967                        value: string.text,
1968                        format_text,
1969                        interpolations,
1970                        span: Span::new(token.span.file, token.span.start, string.span.end),
1971                    });
1972                }
1973                match token.text.as_str() {
1974                    "true" => Ok(Expr::Bool {
1975                        value: true,
1976                        span: token.span,
1977                    }),
1978                    "false" => Ok(Expr::Bool {
1979                        value: false,
1980                        span: token.span,
1981                    }),
1982                    "None" | "null" => Ok(Expr::Null { span: token.span }),
1983                    _ => Ok(Expr::Name {
1984                        name: token.text.clone(),
1985                        span: token.span,
1986                    }),
1987                }
1988            }
1989            TokenKind::LParen => {
1990                self.advance();
1991                self.skip_expression_newlines();
1992                let expr = self.parse_expr()?;
1993                self.skip_expression_newlines();
1994                self.expect(TokenKind::RParen, "')'")?;
1995                Ok(expr)
1996            }
1997            TokenKind::LBracket => {
1998                let open = self.advance();
1999                let mut elements = Vec::new();
2000                self.skip_newlines();
2001                if self.peek_kind() == TokenKind::RBracket {
2002                    let end = self.advance().span.end;
2003                    return Ok(Expr::Array {
2004                        elements,
2005                        span: Span::new(open.span.file, open.span.start, end),
2006                    });
2007                }
2008                let first = self.parse_expr()?;
2009                if self.is_ident("for") {
2010                    self.advance();
2011                    let variable_token =
2012                        self.expect(TokenKind::Ident, "a comprehension variable")?;
2013                    self.skip_newlines();
2014                    let index = if self.peek_kind() == TokenKind::Comma {
2015                        self.advance();
2016                        self.skip_newlines();
2017                        let index = self.expect(TokenKind::Ident, "a comprehension index")?;
2018                        Some((index.text, index.span))
2019                    } else {
2020                        None
2021                    };
2022                    self.skip_newlines();
2023                    if !self.is_ident("in") {
2024                        self.error_at_current("expected `in` in list comprehension".to_string());
2025                        return Err(());
2026                    }
2027                    self.advance();
2028                    self.skip_newlines();
2029                    let iterable = self.parse_or()?;
2030                    self.skip_newlines();
2031                    let condition = if self.is_ident("if") {
2032                        self.advance();
2033                        self.skip_newlines();
2034                        Some(Box::new(self.parse_or()?))
2035                    } else {
2036                        None
2037                    };
2038                    self.skip_newlines();
2039                    let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
2040                    return Ok(Expr::Comprehension {
2041                        element: Box::new(first),
2042                        variable: variable_token.text,
2043                        variable_span: variable_token.span,
2044                        index,
2045                        iterable: Box::new(iterable),
2046                        condition,
2047                        span: Span::new(open.span.file, open.span.start, end),
2048                    });
2049                }
2050                elements.push(first);
2051                loop {
2052                    self.skip_newlines();
2053                    if self.peek_kind() == TokenKind::Comma {
2054                        self.advance();
2055                        self.skip_newlines();
2056                        if self.peek_kind() == TokenKind::RBracket {
2057                            break;
2058                        }
2059                        elements.push(self.parse_expr()?);
2060                    } else {
2061                        break;
2062                    }
2063                }
2064                let end = match self.expect(TokenKind::RBracket, "']'") {
2065                    Ok(token) => token.span.end,
2066                    Err(()) => return Err(()),
2067                };
2068                Ok(Expr::Array {
2069                    elements,
2070                    span: Span::new(open.span.file, open.span.start, end),
2071                })
2072            }
2073            TokenKind::LBrace => self.parse_dict(),
2074            _ => {
2075                self.error_at_current(format!("expected an expression but found '{}'", token.text));
2076                Err(())
2077            }
2078        }
2079    }
2080
2081    /// Parse adjacent string literals as one source-language string value.
2082    ///
2083    /// Newlines are only ignored while looking for another literal inside a
2084    /// delimiter group. Outside a group, a newline remains a statement
2085    /// boundary, matching the bounded implicit-concatenation surface used by
2086    /// the OverPy examples.
2087    fn parse_string_literal(&mut self) -> Result<Expr, ()> {
2088        let first = self.advance();
2089        let mut value = first.text.clone();
2090        let mut end = first.span.end;
2091        loop {
2092            let saved = self.pos;
2093            if self.inside_delimiter_group() {
2094                self.skip_newlines();
2095            }
2096            if self.peek_kind() != TokenKind::String || self.peek().span.file != first.span.file {
2097                self.pos = saved;
2098                break;
2099            }
2100            let next = self.advance();
2101            value.push_str(&next.text);
2102            end = next.span.end;
2103        }
2104        Ok(Expr::String {
2105            value,
2106            span: Span::new(first.span.file, first.span.start, end),
2107        })
2108    }
2109
2110    /// Return whether the current parser position is inside `()`, `[]`, or
2111    /// `{}`. The token stream retains newlines, so this keeps multiline
2112    /// implicit concatenation scoped to syntactic grouping without adding
2113    /// parser state to every delimiter path.
2114    fn inside_delimiter_group(&self) -> bool {
2115        let mut depth = 0usize;
2116        for token in &self.tokens[..self.pos] {
2117            match token.kind {
2118                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => depth += 1,
2119                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
2120                    depth = depth.saturating_sub(1)
2121                }
2122                _ => {}
2123            }
2124        }
2125        depth != 0
2126    }
2127
2128    fn parse_dict(&mut self) -> Result<Expr, ()> {
2129        let open = self.advance();
2130        let mut entries = Vec::new();
2131        self.skip_newlines();
2132        if self.peek_kind() == TokenKind::RBrace {
2133            let end = self.advance().span.end;
2134            return Ok(Expr::Dict {
2135                entries,
2136                span: Span::new(open.span.file, open.span.start, end),
2137            });
2138        }
2139        loop {
2140            let key = self.parse_expr()?;
2141            self.expect(TokenKind::Colon, "':' in a dictionary entry")?;
2142            let value = self.parse_expr()?;
2143            let span = Span::new(key.span().file, key.span().start, value.span().end);
2144            entries.push(DictEntry { key, value, span });
2145            self.skip_newlines();
2146            if self.peek_kind() == TokenKind::Comma {
2147                self.advance();
2148                self.skip_newlines();
2149                if self.peek_kind() == TokenKind::RBrace {
2150                    break;
2151                }
2152            } else {
2153                break;
2154            }
2155        }
2156        let end = self.expect(TokenKind::RBrace, "'}'")?.span.end;
2157        Ok(Expr::Dict {
2158            entries,
2159            span: Span::new(open.span.file, open.span.start, end),
2160        })
2161    }
2162
2163    fn parse_lambda(&mut self, start: Span) -> Result<Expr, ()> {
2164        let mut params = Vec::new();
2165        loop {
2166            let param = self.expect(TokenKind::Ident, "a lambda parameter")?;
2167            params.push((param.text, param.span));
2168            if self.peek_kind() == TokenKind::Comma {
2169                self.advance();
2170            } else {
2171                break;
2172            }
2173        }
2174        self.expect(TokenKind::Colon, "':' after lambda parameters")?;
2175        let body = self.parse_expr()?;
2176        Ok(Expr::Lambda {
2177            params,
2178            body: Box::new(body.clone()),
2179            span: Span::new(start.file, start.start, body.span().end),
2180        })
2181    }
2182
2183    /// Parse the expression regions of a pinned-OverPy f-string. Double
2184    /// braces are literal braces; a single brace introduces one expression.
2185    /// The resulting expression tokens are shifted back into the source
2186    /// string so HIR and tooling retain source provenance.
2187    fn parse_f_string(&mut self, raw: &str, string_span: Span) -> Result<(String, Vec<Expr>), ()> {
2188        let chars: Vec<char> = raw.chars().collect();
2189        let mut text = String::new();
2190        let mut interpolations = Vec::new();
2191        let mut index = 0;
2192        while index < chars.len() {
2193            match chars[index] {
2194                '{' if chars.get(index + 1) == Some(&'{') => {
2195                    text.push_str("{{");
2196                    index += 2;
2197                }
2198                '}' if chars.get(index + 1) == Some(&'}') => {
2199                    text.push_str("}}");
2200                    index += 2;
2201                }
2202                '{' => {
2203                    let end = self.find_f_string_end(&chars, index + 1);
2204                    let Some(end) = end else {
2205                        self.errors.push(OpyError::at(
2206                            "parse-error",
2207                            "unterminated f-string interpolation".to_string(),
2208                            string_span,
2209                        ));
2210                        return Err(());
2211                    };
2212                    let expression: String = chars[index + 1..end].iter().collect();
2213                    if expression.trim().is_empty() {
2214                        self.errors.push(OpyError::at(
2215                            "parse-error",
2216                            "f-string interpolation cannot be empty".to_string(),
2217                            Span::new(
2218                                string_span.file,
2219                                Position::new(
2220                                    string_span.start.line,
2221                                    string_span.start.col + index as u32 + 1,
2222                                ),
2223                                Position::new(
2224                                    string_span.start.line,
2225                                    string_span.start.col + end as u32 + 1,
2226                                ),
2227                            ),
2228                        ));
2229                        return Err(());
2230                    }
2231                    let origin = Position::new(
2232                        string_span.start.line,
2233                        string_span.start.col + index as u32 + 1,
2234                    );
2235                    let parsed = parse_expression_fragment(&expression, string_span.file, origin)
2236                        .map_err(|error| {
2237                            self.errors.push(error);
2238                        });
2239                    let Ok(parsed) = parsed else {
2240                        return Err(());
2241                    };
2242                    text.push_str(&format!("{{{}}}", interpolations.len()));
2243                    interpolations.push(parsed);
2244                    index = end + 1;
2245                }
2246                '}' => {
2247                    self.errors.push(OpyError::at(
2248                        "parse-error",
2249                        "single '}' is not valid in an f-string".to_string(),
2250                        string_span,
2251                    ));
2252                    return Err(());
2253                }
2254                '\\' if index + 1 < chars.len() => {
2255                    text.push(decode_string_escape(chars[index + 1]));
2256                    index += 2;
2257                }
2258                character => {
2259                    text.push(character);
2260                    index += 1;
2261                }
2262            }
2263        }
2264        Ok((text, interpolations))
2265    }
2266
2267    fn find_f_string_end(&self, chars: &[char], start: usize) -> Option<usize> {
2268        let mut nested_braces = 0;
2269        let mut quote = None;
2270        let mut escaped = false;
2271        for (index, character) in chars.iter().enumerate().skip(start) {
2272            if escaped {
2273                escaped = false;
2274                continue;
2275            }
2276            if *character == '\\' && quote.is_some() {
2277                escaped = true;
2278                continue;
2279            }
2280            if let Some(active_quote) = quote {
2281                if *character == active_quote {
2282                    quote = None;
2283                }
2284                continue;
2285            }
2286            match character {
2287                '"' | '\'' => quote = Some(*character),
2288                '{' => nested_braces += 1,
2289                '}' if nested_braces == 0 => return Some(index),
2290                '}' => nested_braces -= 1,
2291                _ => {}
2292            }
2293        }
2294        None
2295    }
2296}
2297
2298/// Parse one f-string expression fragment and shift its local token spans
2299/// into the original source file.
2300fn parse_expression_fragment(text: &str, file: u32, origin: Position) -> Result<Expr, OpyError> {
2301    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
2302        file_id: file,
2303        text,
2304    })?;
2305    for token in &mut tokens {
2306        token.span = shift_span(token.span, origin);
2307    }
2308    let mut parser = Parser {
2309        tokens: &tokens,
2310        pos: 0,
2311        allow_macro_redeclaration: false,
2312        errors: Vec::new(),
2313    };
2314    let expression = parser.parse_expr().map_err(|()| {
2315        parser.errors.first().cloned().unwrap_or_else(|| {
2316            OpyError::at(
2317                "parse-error",
2318                "invalid f-string expression",
2319                Span::new(file, origin, origin),
2320            )
2321        })
2322    })?;
2323    if parser.peek_kind() != TokenKind::Eof {
2324        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
2325    }
2326    parser.errors.into_iter().next().map_or(Ok(expression), Err)
2327}
2328
2329fn shift_span(span: Span, origin: Position) -> Span {
2330    fn shift(position: Position, origin: Position) -> Position {
2331        Position::new(
2332            origin.line + position.line.saturating_sub(1),
2333            if position.line == 1 {
2334                origin.col + position.col.saturating_sub(1)
2335            } else {
2336                position.col
2337            },
2338        )
2339    }
2340    Span::new(
2341        span.file,
2342        shift(span.start, origin),
2343        shift(span.end, origin),
2344    )
2345}
2346
2347fn decode_string_escape(character: char) -> char {
2348    match character {
2349        'n' => '\n',
2350        't' => '\t',
2351        'r' => '\r',
2352        '\\' => '\\',
2353        '"' => '"',
2354        '\'' => '\'',
2355        other => other,
2356    }
2357}
2358
2359fn is_string_modifier(text: &str) -> bool {
2360    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
2361}
2362
2363#[cfg(test)]
2364mod tests {
2365    use super::*;
2366    use crate::lexer::{LexInput, lex};
2367
2368    fn parse_ok(text: &str) -> Program {
2369        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2370        let output = parse(&tokens);
2371        assert!(
2372            output.errors.is_empty(),
2373            "unexpected errors: {:?}",
2374            output.errors
2375        );
2376        output.program.unwrap()
2377    }
2378
2379    fn parse_err(text: &str) -> Vec<OpyError> {
2380        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2381        parse(&tokens).errors
2382    }
2383
2384    #[test]
2385    fn parses_basic_rule() {
2386        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
2387        assert_eq!(program.rules.len(), 1);
2388        let RuleEntry::Rule(rule) = &program.rules[0] else {
2389            panic!("expected rule");
2390        };
2391        assert_eq!(rule.name, "setup");
2392        assert_eq!(rule.event.name, "global");
2393        assert_eq!(rule.actions.len(), 1);
2394    }
2395
2396    #[test]
2397    fn parses_power_augmented_assignment() {
2398        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
2399        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
2400        let program =
2401            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
2402        let RuleEntry::Rule(rule) = &program.rules[0] else {
2403            panic!("expected rule");
2404        };
2405        let Stmt::Assign {
2406            value,
2407            target: assigned_target,
2408            ..
2409        } = &rule.actions[1]
2410        else {
2411            panic!("expected an assignment");
2412        };
2413        let Expr::Binary {
2414            op, left, right, ..
2415        } = value
2416        else {
2417            panic!("expected a binary modification, got {value:?}");
2418        };
2419        assert_eq!(op, "**");
2420        assert!(matches!(&**left, Expr::Name { .. }));
2421        assert!(matches!(
2422            assigned_target,
2423            Expr::Name { name, .. } if name == "a"
2424        ));
2425        assert!(matches!(right.as_ref(), Expr::Number { .. }));
2426    }
2427
2428    #[test]
2429    fn parses_postfix_increment_and_decrement_as_modifications() {
2430        let program =
2431            parse_ok("globalvar value\nrule \"r\":\n    @Event global\n    value++\n    value--\n");
2432        let RuleEntry::Rule(rule) = &program.rules[0] else {
2433            panic!("expected a rule");
2434        };
2435        for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
2436            let Stmt::Assign { target, value, .. } = statement else {
2437                panic!("expected a postfix assignment");
2438            };
2439            let Expr::Binary {
2440                op, left, right, ..
2441            } = value
2442            else {
2443                panic!("expected a synthetic modification value");
2444            };
2445            assert_eq!(op, expected_op);
2446            let Expr::Name {
2447                name: left_name, ..
2448            } = left.as_ref()
2449            else {
2450                panic!("expected the target to be the modification's left operand");
2451            };
2452            let Expr::Name {
2453                name: target_name, ..
2454            } = target
2455            else {
2456                panic!("expected a name target");
2457            };
2458            assert_eq!(left_name, target_name);
2459            assert!(
2460                matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
2461            );
2462        }
2463    }
2464
2465    #[test]
2466    fn rejects_prefix_increment_and_embedded_postfix_forms() {
2467        for source in [
2468            "globalvar value\nrule \"r\":\n    @Event global\n    ++value\n",
2469            "globalvar value\nrule \"r\":\n    @Event global\n    value++++\n",
2470        ] {
2471            let errors = parse_err(source);
2472            assert!(!errors.is_empty());
2473            assert!(errors.iter().all(|error| error.code == "parse-error"));
2474            assert!(errors.iter().all(|error| error.span.is_some()));
2475        }
2476    }
2477
2478    #[test]
2479    fn preserves_consecutive_unary_minus_expressions() {
2480        let source = concat!(
2481            "globalvar value = 0\n",
2482            "globalvar B = 1\n",
2483            "rule \"r\":\n",
2484            "    @Event global\n",
2485            "    value = --1\n",
2486            "    value = --B\n",
2487            "    value = B--1\n",
2488        );
2489        parse_ok(source);
2490    }
2491
2492    #[test]
2493    fn parses_control_flow() {
2494        let program = parse_ok(
2495            "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",
2496        );
2497        let RuleEntry::Rule(rule) = &program.rules[0] else {
2498            panic!();
2499        };
2500        assert!(matches!(rule.actions[0], Stmt::For { .. }));
2501        let Stmt::For { body, .. } = &rule.actions[0] else {
2502            panic!();
2503        };
2504        let Stmt::If {
2505            branches, r#else, ..
2506        } = &body[0]
2507        else {
2508            panic!();
2509        };
2510        assert_eq!(branches.len(), 2);
2511        assert!(r#else.is_some());
2512        let Stmt::While { body, .. } = &rule.actions[1] else {
2513            panic!();
2514        };
2515        assert_eq!(body.len(), 2);
2516    }
2517
2518    #[test]
2519    fn parses_issue_141_statement_surface() {
2520        let program = parse_ok(concat!(
2521            "globalvar value\n",
2522            "rule \"r\":\n",
2523            "    @Event global\n",
2524            "    del value[1]\n",
2525            "    value min= 2\n",
2526            "    value max= 3\n",
2527            "    while value < 4:\n",
2528            "        continue\n",
2529            "    goto RULE_START\n",
2530            "    goto target\n",
2531            "    goto loc + value\n",
2532            "    target:\n",
2533        ));
2534        let RuleEntry::Rule(rule) = &program.rules[0] else {
2535            panic!("expected rule");
2536        };
2537        assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
2538        for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
2539            let Stmt::Assign { value, .. } = statement else {
2540                panic!("expected augmented assignment");
2541            };
2542            assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
2543        }
2544        let Stmt::While { body, .. } = &rule.actions[3] else {
2545            panic!("expected while");
2546        };
2547        assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
2548        assert!(matches!(
2549            &rule.actions[4],
2550            Stmt::Goto {
2551                label: None,
2552                offset: None,
2553                rule_start: true,
2554                ..
2555            }
2556        ));
2557        assert!(matches!(
2558            &rule.actions[5],
2559            Stmt::Goto {
2560                label: Some(label),
2561                offset: None,
2562                rule_start: false,
2563                ..
2564            } if label == "target"
2565        ));
2566        assert!(matches!(
2567            &rule.actions[6],
2568            Stmt::Goto {
2569                label: None,
2570                offset: Some(_),
2571                rule_start: false,
2572                ..
2573            }
2574        ));
2575        assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
2576    }
2577
2578    #[test]
2579    fn rejects_invalid_issue_141_statement_forms() {
2580        for source in [
2581            "rule \"r\":\n    @Event global\n    del value\n",
2582            "rule \"r\":\n    @Event global\n    goto\n",
2583            "rule \"r\":\n    @Event global\n    goto loc\n",
2584            "rule \"r\":\n    @Event global\n    goto target extra\n",
2585            "rule \"r\":\n    @Event global\n    continue now\n",
2586            "rule \"r\":\n    @Event global\n    A = 1; A = 2\n",
2587        ] {
2588            let errors = parse_err(source);
2589            assert!(!errors.is_empty(), "invalid form parsed: {source}");
2590            assert!(errors.iter().all(|error| error.code == "parse-error"));
2591            assert!(errors.iter().all(|error| error.span.is_some()));
2592        }
2593    }
2594
2595    #[test]
2596    fn parses_issue_28_constructs() {
2597        let program = parse_ok(
2598            "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",
2599        );
2600        let RuleEntry::Rule(rule) = &program.rules[0] else {
2601            panic!("expected rule");
2602        };
2603        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
2604        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
2605    }
2606
2607    #[test]
2608    fn rejects_incomplete_do_while_and_dictionary_entries() {
2609        let errors = parse_err(
2610            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
2611        );
2612        assert!(!errors.is_empty());
2613        assert!(errors.iter().all(|error| error.code == "parse-error"));
2614    }
2615
2616    #[test]
2617    fn parses_explicit_enum_member_values() {
2618        let program = parse_ok("enum EventType:\n    BUFF = 0\n    DEBUFF\n    MECH = 2\n");
2619        let Decl::Enum { members, .. } = &program.declarations[0] else {
2620            panic!("expected enum");
2621        };
2622        assert_eq!(
2623            members
2624                .iter()
2625                .map(|(name, _)| name.as_str())
2626                .collect::<Vec<_>>(),
2627            ["BUFF", "DEBUFF", "MECH"]
2628        );
2629    }
2630
2631    #[test]
2632    fn parses_multi_line_array() {
2633        let program = parse_ok(
2634            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
2635        );
2636        let RuleEntry::Rule(rule) = &program.rules[0] else {
2637            panic!();
2638        };
2639        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2640            panic!();
2641        };
2642        let Expr::Array { elements, .. } = value else {
2643            panic!("expected array, got {value:?}");
2644        };
2645        assert_eq!(elements.len(), 2);
2646    }
2647
2648    #[test]
2649    fn missing_colon_is_a_structured_error() {
2650        let errors = parse_err("rule \"x\"\n    @Event global\n");
2651        assert!(!errors.is_empty());
2652        assert_eq!(errors[0].code, "parse-error");
2653        assert!(errors[0].span.is_some());
2654    }
2655
2656    #[test]
2657    fn def_and_macro_parse() {
2658        let program = parse_ok(
2659            "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
2660        );
2661        assert_eq!(program.declarations.len(), 3);
2662        assert!(matches!(program.declarations[1], Decl::Constant { .. }));
2663        assert!(matches!(program.declarations[2], Decl::Macro { .. }));
2664        let Decl::Macro { args, body, .. } = &program.declarations[2] else {
2665            panic!();
2666        };
2667        assert_eq!(args, &vec!["value".to_string()]);
2668        assert_eq!(body.len(), 1);
2669    }
2670
2671    #[test]
2672    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
2673        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
2674        let errors = parse_err(text);
2675        assert_eq!(
2676            errors
2677                .iter()
2678                .filter(|error| error.code == "macro-redeclaration")
2679                .count(),
2680            2
2681        );
2682
2683        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2684        let output = parse_with_options(&tokens, true);
2685        assert!(
2686            output.errors.is_empty(),
2687            "unexpected errors: {:?}",
2688            output.errors
2689        );
2690        assert!(output.program.is_some());
2691    }
2692
2693    #[test]
2694    fn multiple_errors_are_reported() {
2695        let errors =
2696            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
2697        assert!(!errors.is_empty());
2698    }
2699
2700    #[test]
2701    fn precedence_parses_python_like() {
2702        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
2703        let RuleEntry::Rule(rule) = &program.rules[0] else {
2704            panic!();
2705        };
2706        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2707            panic!();
2708        };
2709        let Expr::Binary {
2710            op, left, right, ..
2711        } = value
2712        else {
2713            panic!();
2714        };
2715        assert_eq!(op, "+");
2716        let Expr::Binary { op: inner, .. } = right.as_ref() else {
2717            panic!();
2718        };
2719        assert_eq!(inner, "*");
2720        assert!(matches!(left.as_ref(), Expr::Number { .. }));
2721    }
2722
2723    #[test]
2724    fn parses_right_associative_conditional_expressions() {
2725        let program = parse_ok(
2726            "rule \"r\":\n    @Event global\n    debug(1 if true else 2 if false else 3)\n",
2727        );
2728        let RuleEntry::Rule(rule) = &program.rules[0] else {
2729            panic!("expected a rule");
2730        };
2731        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2732            panic!("expected an expression statement");
2733        };
2734        let Expr::Call { args, .. } = expr else {
2735            panic!("expected a call");
2736        };
2737        let Expr::Conditional {
2738            then_value,
2739            condition,
2740            else_value,
2741            span,
2742        } = &args[0].value
2743        else {
2744            panic!("expected a conditional expression");
2745        };
2746        assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
2747        assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
2748        assert!(matches!(
2749            else_value.as_ref(),
2750            Expr::Conditional { then_value, condition, else_value, .. }
2751                if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
2752                    && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2753                    && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2754        ));
2755        assert_eq!(span.start.line, 3);
2756        assert_eq!(span.start.col, 11);
2757    }
2758
2759    #[test]
2760    fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
2761        let program = parse_ok(
2762            "rule \"r\":\n    @Event global\n    debug((1 if true else 2) if false else 3)\n",
2763        );
2764        let RuleEntry::Rule(rule) = &program.rules[0] else {
2765            panic!("expected a rule");
2766        };
2767        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2768            panic!("expected an expression statement");
2769        };
2770        let Expr::Call { args, .. } = expr else {
2771            panic!("expected a call");
2772        };
2773        assert!(matches!(
2774            &args[0].value,
2775            Expr::Conditional {
2776                then_value,
2777                condition,
2778                else_value,
2779                ..
2780            } if matches!(then_value.as_ref(), Expr::Conditional { .. })
2781                && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2782                && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2783        ));
2784
2785        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 if true)\n");
2786        assert_eq!(errors[0].code, "parse-error");
2787        assert!(errors[0].message.contains("expected `else`"));
2788    }
2789
2790    #[test]
2791    fn parses_receiver_calls() {
2792        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
2793        // member access followed by call arguments (#104).
2794        let program =
2795            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
2796        let RuleEntry::Rule(rule) = &program.rules[0] else {
2797            panic!("expected rule");
2798        };
2799        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2800            panic!("expected expression statement, got {:?}", rule.actions[0]);
2801        };
2802        let Expr::ReceiverCall {
2803            receiver,
2804            name,
2805            args,
2806            ..
2807        } = &expr
2808        else {
2809            panic!("expected receiver call, got {expr:?}");
2810        };
2811        assert_eq!(name, "setMoveSpeed");
2812        assert!(
2813            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
2814            "receiver must be the eventPlayer name"
2815        );
2816        assert_eq!(args.len(), 1);
2817        assert!(args[0].keyword.is_none(), "positional argument");
2818        assert!(matches!(&args[0].value, Expr::Number { .. }));
2819    }
2820
2821    #[test]
2822    fn parses_keyword_arguments_with_name_spans() {
2823        // `name = expr` call arguments are keyword arguments carrying the
2824        // name token's exact span (issue #110); comparisons stay positional.
2825        let program =
2826            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
2827        let RuleEntry::Rule(rule) = &program.rules[0] else {
2828            panic!("expected rule");
2829        };
2830        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2831            panic!("expected expression statement");
2832        };
2833        let Expr::Call { args, .. } = expr else {
2834            panic!("expected a call, got {expr:?}");
2835        };
2836        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
2837        assert_eq!(keyword, "time");
2838        assert_eq!(span.start.line, 3);
2839        assert!(matches!(&args[0].value, Expr::Number { .. }));
2840
2841        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
2842            panic!("expected expression statement");
2843        };
2844        let Expr::Call { args, .. } = expr else {
2845            panic!("expected a call, got {expr:?}");
2846        };
2847        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
2848        assert!(matches!(&args[0].value, Expr::Binary { .. }));
2849    }
2850
2851    #[test]
2852    fn adjacent_string_literals_concatenate_and_preserve_span() {
2853        let program = parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\" \"two\")\n");
2854        let RuleEntry::Rule(rule) = &program.rules[0] else {
2855            panic!("expected rule");
2856        };
2857        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2858            panic!("expected expression statement");
2859        };
2860        let Expr::Call { args, .. } = expr else {
2861            panic!("expected call");
2862        };
2863        let Expr::String { value, span } = &args[0].value else {
2864            panic!("expected concatenated string");
2865        };
2866        assert_eq!(value, "onetwo");
2867        assert_eq!(span.start.line, 3);
2868        assert_eq!(span.start.col, 11);
2869        assert_eq!(span.end.col, 22);
2870    }
2871
2872    #[test]
2873    fn multiline_adjacent_string_literals_concatenate_inside_group() {
2874        let program =
2875            parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\"\n        \"two\")\n");
2876        let RuleEntry::Rule(rule) = &program.rules[0] else {
2877            panic!("expected rule");
2878        };
2879        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2880            panic!("expected expression statement");
2881        };
2882        let Expr::Call { args, .. } = expr else {
2883            panic!("expected call");
2884        };
2885        assert!(matches!(
2886            &args[0].value,
2887            Expr::String { value, .. } if value == "onetwo"
2888        ));
2889    }
2890
2891    #[test]
2892    fn newline_outside_group_keeps_adjacent_literals_as_statements() {
2893        let program = parse_ok("rule \"r\":\n    @Event global\n    \"one\"\n    \"two\"\n");
2894        let RuleEntry::Rule(rule) = &program.rules[0] else {
2895            panic!("expected rule");
2896        };
2897        assert_eq!(rule.actions.len(), 2);
2898    }
2899
2900    #[test]
2901    fn non_name_keyword_lhs_is_a_parse_error() {
2902        // `f(1 = 2)` is not a call argument form; rejected explicitly.
2903        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
2904        assert!(!errors.is_empty());
2905        assert_eq!(errors[0].code, "parse-error");
2906    }
2907
2908    #[test]
2909    fn parses_member_call_on_call_result() {
2910        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
2911        // followed by call arguments on a call result stays a receiver call.
2912        let program = parse_ok(
2913            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
2914        );
2915        let RuleEntry::Rule(rule) = &program.rules[0] else {
2916            panic!("expected rule");
2917        };
2918        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2919            panic!("expected expression statement");
2920        };
2921        let Expr::ReceiverCall {
2922            receiver,
2923            name,
2924            args,
2925            ..
2926        } = &expr
2927        else {
2928            panic!("expected receiver call, got {expr:?}");
2929        };
2930        assert_eq!(name, "setStatusEffect");
2931        assert!(
2932            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
2933            "receiver must be the preceding call"
2934        );
2935        assert_eq!(args.len(), 2);
2936    }
2937
2938    #[test]
2939    fn member_without_call_is_not_a_call() {
2940        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
2941        // access; only a following `(` turns it into a receiver call.
2942        let program =
2943            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
2944        let RuleEntry::Rule(rule) = &program.rules[0] else {
2945            panic!("expected rule");
2946        };
2947        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2948            panic!("expected assignment");
2949        };
2950        assert!(matches!(
2951            &value,
2952            Expr::Member { member, .. } if member == "moveSpeed"
2953        ));
2954    }
2955
2956    #[test]
2957    fn parses_advanced_rule_annotations_with_source_arguments() {
2958        let program = parse_ok(
2959            "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",
2960        );
2961        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
2962            panic!("expected subroutine");
2963        };
2964        assert_eq!(annotations.len(), 2);
2965        let RuleEntry::Rule(rule) = &program.rules[1] else {
2966            panic!("expected rule");
2967        };
2968        assert!(rule.disabled);
2969        assert!(rule.delimiter);
2970        assert_eq!(rule.new_page.as_deref(), Some("Page"));
2971        assert_eq!(rule.annotations.len(), 7);
2972        assert_eq!(rule.annotations[1].args[0].text, "1");
2973        assert_eq!(rule.annotations[2].args[0].text, "dmon");
2974    }
2975}