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.
2300pub(crate) fn parse_expression_fragment(
2301    text: &str,
2302    file: u32,
2303    origin: Position,
2304) -> Result<Expr, OpyError> {
2305    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
2306        file_id: file,
2307        text,
2308    })?;
2309    for token in &mut tokens {
2310        token.span = shift_span(token.span, origin);
2311    }
2312    let mut parser = Parser {
2313        tokens: &tokens,
2314        pos: 0,
2315        allow_macro_redeclaration: false,
2316        errors: Vec::new(),
2317    };
2318    let expression = parser.parse_expr().map_err(|()| {
2319        parser.errors.first().cloned().unwrap_or_else(|| {
2320            OpyError::at(
2321                "parse-error",
2322                "invalid f-string expression",
2323                Span::new(file, origin, origin),
2324            )
2325        })
2326    })?;
2327    if parser.peek_kind() != TokenKind::Eof {
2328        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
2329    }
2330    parser.errors.into_iter().next().map_or(Ok(expression), Err)
2331}
2332
2333fn shift_span(span: Span, origin: Position) -> Span {
2334    fn shift(position: Position, origin: Position) -> Position {
2335        Position::new(
2336            origin.line + position.line.saturating_sub(1),
2337            if position.line == 1 {
2338                origin.col + position.col.saturating_sub(1)
2339            } else {
2340                position.col
2341            },
2342        )
2343    }
2344    Span::new(
2345        span.file,
2346        shift(span.start, origin),
2347        shift(span.end, origin),
2348    )
2349}
2350
2351fn decode_string_escape(character: char) -> char {
2352    match character {
2353        'n' => '\n',
2354        't' => '\t',
2355        'r' => '\r',
2356        '\\' => '\\',
2357        '"' => '"',
2358        '\'' => '\'',
2359        other => other,
2360    }
2361}
2362
2363fn is_string_modifier(text: &str) -> bool {
2364    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
2365}
2366
2367#[cfg(test)]
2368mod tests {
2369    use super::*;
2370    use crate::lexer::{LexInput, lex};
2371
2372    fn parse_ok(text: &str) -> Program {
2373        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2374        let output = parse(&tokens);
2375        assert!(
2376            output.errors.is_empty(),
2377            "unexpected errors: {:?}",
2378            output.errors
2379        );
2380        output.program.unwrap()
2381    }
2382
2383    fn parse_err(text: &str) -> Vec<OpyError> {
2384        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2385        parse(&tokens).errors
2386    }
2387
2388    #[test]
2389    fn parses_basic_rule() {
2390        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
2391        assert_eq!(program.rules.len(), 1);
2392        let RuleEntry::Rule(rule) = &program.rules[0] else {
2393            panic!("expected rule");
2394        };
2395        assert_eq!(rule.name, "setup");
2396        assert_eq!(rule.event.name, "global");
2397        assert_eq!(rule.actions.len(), 1);
2398    }
2399
2400    #[test]
2401    fn parses_power_augmented_assignment() {
2402        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
2403        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
2404        let program =
2405            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
2406        let RuleEntry::Rule(rule) = &program.rules[0] else {
2407            panic!("expected rule");
2408        };
2409        let Stmt::Assign {
2410            value,
2411            target: assigned_target,
2412            ..
2413        } = &rule.actions[1]
2414        else {
2415            panic!("expected an assignment");
2416        };
2417        let Expr::Binary {
2418            op, left, right, ..
2419        } = value
2420        else {
2421            panic!("expected a binary modification, got {value:?}");
2422        };
2423        assert_eq!(op, "**");
2424        assert!(matches!(&**left, Expr::Name { .. }));
2425        assert!(matches!(
2426            assigned_target,
2427            Expr::Name { name, .. } if name == "a"
2428        ));
2429        assert!(matches!(right.as_ref(), Expr::Number { .. }));
2430    }
2431
2432    #[test]
2433    fn parses_postfix_increment_and_decrement_as_modifications() {
2434        let program =
2435            parse_ok("globalvar value\nrule \"r\":\n    @Event global\n    value++\n    value--\n");
2436        let RuleEntry::Rule(rule) = &program.rules[0] else {
2437            panic!("expected a rule");
2438        };
2439        for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
2440            let Stmt::Assign { target, value, .. } = statement else {
2441                panic!("expected a postfix assignment");
2442            };
2443            let Expr::Binary {
2444                op, left, right, ..
2445            } = value
2446            else {
2447                panic!("expected a synthetic modification value");
2448            };
2449            assert_eq!(op, expected_op);
2450            let Expr::Name {
2451                name: left_name, ..
2452            } = left.as_ref()
2453            else {
2454                panic!("expected the target to be the modification's left operand");
2455            };
2456            let Expr::Name {
2457                name: target_name, ..
2458            } = target
2459            else {
2460                panic!("expected a name target");
2461            };
2462            assert_eq!(left_name, target_name);
2463            assert!(
2464                matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
2465            );
2466        }
2467    }
2468
2469    #[test]
2470    fn rejects_prefix_increment_and_embedded_postfix_forms() {
2471        for source in [
2472            "globalvar value\nrule \"r\":\n    @Event global\n    ++value\n",
2473            "globalvar value\nrule \"r\":\n    @Event global\n    value++++\n",
2474        ] {
2475            let errors = parse_err(source);
2476            assert!(!errors.is_empty());
2477            assert!(errors.iter().all(|error| error.code == "parse-error"));
2478            assert!(errors.iter().all(|error| error.span.is_some()));
2479        }
2480    }
2481
2482    #[test]
2483    fn preserves_consecutive_unary_minus_expressions() {
2484        let source = concat!(
2485            "globalvar value = 0\n",
2486            "globalvar B = 1\n",
2487            "rule \"r\":\n",
2488            "    @Event global\n",
2489            "    value = --1\n",
2490            "    value = --B\n",
2491            "    value = B--1\n",
2492        );
2493        parse_ok(source);
2494    }
2495
2496    #[test]
2497    fn parses_control_flow() {
2498        let program = parse_ok(
2499            "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",
2500        );
2501        let RuleEntry::Rule(rule) = &program.rules[0] else {
2502            panic!();
2503        };
2504        assert!(matches!(rule.actions[0], Stmt::For { .. }));
2505        let Stmt::For { body, .. } = &rule.actions[0] else {
2506            panic!();
2507        };
2508        let Stmt::If {
2509            branches, r#else, ..
2510        } = &body[0]
2511        else {
2512            panic!();
2513        };
2514        assert_eq!(branches.len(), 2);
2515        assert!(r#else.is_some());
2516        let Stmt::While { body, .. } = &rule.actions[1] else {
2517            panic!();
2518        };
2519        assert_eq!(body.len(), 2);
2520    }
2521
2522    #[test]
2523    fn parses_issue_141_statement_surface() {
2524        let program = parse_ok(concat!(
2525            "globalvar value\n",
2526            "rule \"r\":\n",
2527            "    @Event global\n",
2528            "    del value[1]\n",
2529            "    value min= 2\n",
2530            "    value max= 3\n",
2531            "    while value < 4:\n",
2532            "        continue\n",
2533            "    goto RULE_START\n",
2534            "    goto target\n",
2535            "    goto loc + value\n",
2536            "    target:\n",
2537        ));
2538        let RuleEntry::Rule(rule) = &program.rules[0] else {
2539            panic!("expected rule");
2540        };
2541        assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
2542        for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
2543            let Stmt::Assign { value, .. } = statement else {
2544                panic!("expected augmented assignment");
2545            };
2546            assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
2547        }
2548        let Stmt::While { body, .. } = &rule.actions[3] else {
2549            panic!("expected while");
2550        };
2551        assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
2552        assert!(matches!(
2553            &rule.actions[4],
2554            Stmt::Goto {
2555                label: None,
2556                offset: None,
2557                rule_start: true,
2558                ..
2559            }
2560        ));
2561        assert!(matches!(
2562            &rule.actions[5],
2563            Stmt::Goto {
2564                label: Some(label),
2565                offset: None,
2566                rule_start: false,
2567                ..
2568            } if label == "target"
2569        ));
2570        assert!(matches!(
2571            &rule.actions[6],
2572            Stmt::Goto {
2573                label: None,
2574                offset: Some(_),
2575                rule_start: false,
2576                ..
2577            }
2578        ));
2579        assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
2580    }
2581
2582    #[test]
2583    fn rejects_invalid_issue_141_statement_forms() {
2584        for source in [
2585            "rule \"r\":\n    @Event global\n    del value\n",
2586            "rule \"r\":\n    @Event global\n    goto\n",
2587            "rule \"r\":\n    @Event global\n    goto loc\n",
2588            "rule \"r\":\n    @Event global\n    goto target extra\n",
2589            "rule \"r\":\n    @Event global\n    continue now\n",
2590            "rule \"r\":\n    @Event global\n    A = 1; A = 2\n",
2591        ] {
2592            let errors = parse_err(source);
2593            assert!(!errors.is_empty(), "invalid form parsed: {source}");
2594            assert!(errors.iter().all(|error| error.code == "parse-error"));
2595            assert!(errors.iter().all(|error| error.span.is_some()));
2596        }
2597    }
2598
2599    #[test]
2600    fn parses_issue_28_constructs() {
2601        let program = parse_ok(
2602            "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",
2603        );
2604        let RuleEntry::Rule(rule) = &program.rules[0] else {
2605            panic!("expected rule");
2606        };
2607        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
2608        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
2609    }
2610
2611    #[test]
2612    fn rejects_incomplete_do_while_and_dictionary_entries() {
2613        let errors = parse_err(
2614            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
2615        );
2616        assert!(!errors.is_empty());
2617        assert!(errors.iter().all(|error| error.code == "parse-error"));
2618    }
2619
2620    #[test]
2621    fn parses_explicit_enum_member_values() {
2622        let program = parse_ok("enum EventType:\n    BUFF = 0\n    DEBUFF\n    MECH = 2\n");
2623        let Decl::Enum { members, .. } = &program.declarations[0] else {
2624            panic!("expected enum");
2625        };
2626        assert_eq!(
2627            members
2628                .iter()
2629                .map(|(name, _)| name.as_str())
2630                .collect::<Vec<_>>(),
2631            ["BUFF", "DEBUFF", "MECH"]
2632        );
2633    }
2634
2635    #[test]
2636    fn parses_multi_line_array() {
2637        let program = parse_ok(
2638            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
2639        );
2640        let RuleEntry::Rule(rule) = &program.rules[0] else {
2641            panic!();
2642        };
2643        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2644            panic!();
2645        };
2646        let Expr::Array { elements, .. } = value else {
2647            panic!("expected array, got {value:?}");
2648        };
2649        assert_eq!(elements.len(), 2);
2650    }
2651
2652    #[test]
2653    fn missing_colon_is_a_structured_error() {
2654        let errors = parse_err("rule \"x\"\n    @Event global\n");
2655        assert!(!errors.is_empty());
2656        assert_eq!(errors[0].code, "parse-error");
2657        assert!(errors[0].span.is_some());
2658    }
2659
2660    #[test]
2661    fn def_and_macro_parse() {
2662        let program = parse_ok(
2663            "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
2664        );
2665        assert_eq!(program.declarations.len(), 3);
2666        assert!(matches!(program.declarations[1], Decl::Constant { .. }));
2667        assert!(matches!(program.declarations[2], Decl::Macro { .. }));
2668        let Decl::Macro { args, body, .. } = &program.declarations[2] else {
2669            panic!();
2670        };
2671        assert_eq!(args, &vec!["value".to_string()]);
2672        assert_eq!(body.len(), 1);
2673    }
2674
2675    #[test]
2676    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
2677        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
2678        let errors = parse_err(text);
2679        assert_eq!(
2680            errors
2681                .iter()
2682                .filter(|error| error.code == "macro-redeclaration")
2683                .count(),
2684            2
2685        );
2686
2687        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2688        let output = parse_with_options(&tokens, true);
2689        assert!(
2690            output.errors.is_empty(),
2691            "unexpected errors: {:?}",
2692            output.errors
2693        );
2694        assert!(output.program.is_some());
2695    }
2696
2697    #[test]
2698    fn multiple_errors_are_reported() {
2699        let errors =
2700            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
2701        assert!(!errors.is_empty());
2702    }
2703
2704    #[test]
2705    fn precedence_parses_python_like() {
2706        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
2707        let RuleEntry::Rule(rule) = &program.rules[0] else {
2708            panic!();
2709        };
2710        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2711            panic!();
2712        };
2713        let Expr::Binary {
2714            op, left, right, ..
2715        } = value
2716        else {
2717            panic!();
2718        };
2719        assert_eq!(op, "+");
2720        let Expr::Binary { op: inner, .. } = right.as_ref() else {
2721            panic!();
2722        };
2723        assert_eq!(inner, "*");
2724        assert!(matches!(left.as_ref(), Expr::Number { .. }));
2725    }
2726
2727    #[test]
2728    fn parses_right_associative_conditional_expressions() {
2729        let program = parse_ok(
2730            "rule \"r\":\n    @Event global\n    debug(1 if true else 2 if false else 3)\n",
2731        );
2732        let RuleEntry::Rule(rule) = &program.rules[0] else {
2733            panic!("expected a rule");
2734        };
2735        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2736            panic!("expected an expression statement");
2737        };
2738        let Expr::Call { args, .. } = expr else {
2739            panic!("expected a call");
2740        };
2741        let Expr::Conditional {
2742            then_value,
2743            condition,
2744            else_value,
2745            span,
2746        } = &args[0].value
2747        else {
2748            panic!("expected a conditional expression");
2749        };
2750        assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
2751        assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
2752        assert!(matches!(
2753            else_value.as_ref(),
2754            Expr::Conditional { then_value, condition, else_value, .. }
2755                if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
2756                    && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2757                    && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2758        ));
2759        assert_eq!(span.start.line, 3);
2760        assert_eq!(span.start.col, 11);
2761    }
2762
2763    #[test]
2764    fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
2765        let program = parse_ok(
2766            "rule \"r\":\n    @Event global\n    debug((1 if true else 2) if false else 3)\n",
2767        );
2768        let RuleEntry::Rule(rule) = &program.rules[0] else {
2769            panic!("expected a rule");
2770        };
2771        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2772            panic!("expected an expression statement");
2773        };
2774        let Expr::Call { args, .. } = expr else {
2775            panic!("expected a call");
2776        };
2777        assert!(matches!(
2778            &args[0].value,
2779            Expr::Conditional {
2780                then_value,
2781                condition,
2782                else_value,
2783                ..
2784            } if matches!(then_value.as_ref(), Expr::Conditional { .. })
2785                && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2786                && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2787        ));
2788
2789        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 if true)\n");
2790        assert_eq!(errors[0].code, "parse-error");
2791        assert!(errors[0].message.contains("expected `else`"));
2792    }
2793
2794    #[test]
2795    fn parses_receiver_calls() {
2796        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
2797        // member access followed by call arguments (#104).
2798        let program =
2799            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
2800        let RuleEntry::Rule(rule) = &program.rules[0] else {
2801            panic!("expected rule");
2802        };
2803        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2804            panic!("expected expression statement, got {:?}", rule.actions[0]);
2805        };
2806        let Expr::ReceiverCall {
2807            receiver,
2808            name,
2809            args,
2810            ..
2811        } = &expr
2812        else {
2813            panic!("expected receiver call, got {expr:?}");
2814        };
2815        assert_eq!(name, "setMoveSpeed");
2816        assert!(
2817            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
2818            "receiver must be the eventPlayer name"
2819        );
2820        assert_eq!(args.len(), 1);
2821        assert!(args[0].keyword.is_none(), "positional argument");
2822        assert!(matches!(&args[0].value, Expr::Number { .. }));
2823    }
2824
2825    #[test]
2826    fn parses_keyword_arguments_with_name_spans() {
2827        // `name = expr` call arguments are keyword arguments carrying the
2828        // name token's exact span (issue #110); comparisons stay positional.
2829        let program =
2830            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
2831        let RuleEntry::Rule(rule) = &program.rules[0] else {
2832            panic!("expected rule");
2833        };
2834        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2835            panic!("expected expression statement");
2836        };
2837        let Expr::Call { args, .. } = expr else {
2838            panic!("expected a call, got {expr:?}");
2839        };
2840        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
2841        assert_eq!(keyword, "time");
2842        assert_eq!(span.start.line, 3);
2843        assert!(matches!(&args[0].value, Expr::Number { .. }));
2844
2845        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
2846            panic!("expected expression statement");
2847        };
2848        let Expr::Call { args, .. } = expr else {
2849            panic!("expected a call, got {expr:?}");
2850        };
2851        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
2852        assert!(matches!(&args[0].value, Expr::Binary { .. }));
2853    }
2854
2855    #[test]
2856    fn adjacent_string_literals_concatenate_and_preserve_span() {
2857        let program = parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\" \"two\")\n");
2858        let RuleEntry::Rule(rule) = &program.rules[0] else {
2859            panic!("expected rule");
2860        };
2861        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2862            panic!("expected expression statement");
2863        };
2864        let Expr::Call { args, .. } = expr else {
2865            panic!("expected call");
2866        };
2867        let Expr::String { value, span } = &args[0].value else {
2868            panic!("expected concatenated string");
2869        };
2870        assert_eq!(value, "onetwo");
2871        assert_eq!(span.start.line, 3);
2872        assert_eq!(span.start.col, 11);
2873        assert_eq!(span.end.col, 22);
2874    }
2875
2876    #[test]
2877    fn multiline_adjacent_string_literals_concatenate_inside_group() {
2878        let program =
2879            parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\"\n        \"two\")\n");
2880        let RuleEntry::Rule(rule) = &program.rules[0] else {
2881            panic!("expected rule");
2882        };
2883        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2884            panic!("expected expression statement");
2885        };
2886        let Expr::Call { args, .. } = expr else {
2887            panic!("expected call");
2888        };
2889        assert!(matches!(
2890            &args[0].value,
2891            Expr::String { value, .. } if value == "onetwo"
2892        ));
2893    }
2894
2895    #[test]
2896    fn newline_outside_group_keeps_adjacent_literals_as_statements() {
2897        let program = parse_ok("rule \"r\":\n    @Event global\n    \"one\"\n    \"two\"\n");
2898        let RuleEntry::Rule(rule) = &program.rules[0] else {
2899            panic!("expected rule");
2900        };
2901        assert_eq!(rule.actions.len(), 2);
2902    }
2903
2904    #[test]
2905    fn non_name_keyword_lhs_is_a_parse_error() {
2906        // `f(1 = 2)` is not a call argument form; rejected explicitly.
2907        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
2908        assert!(!errors.is_empty());
2909        assert_eq!(errors[0].code, "parse-error");
2910    }
2911
2912    #[test]
2913    fn parses_member_call_on_call_result() {
2914        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
2915        // followed by call arguments on a call result stays a receiver call.
2916        let program = parse_ok(
2917            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
2918        );
2919        let RuleEntry::Rule(rule) = &program.rules[0] else {
2920            panic!("expected rule");
2921        };
2922        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2923            panic!("expected expression statement");
2924        };
2925        let Expr::ReceiverCall {
2926            receiver,
2927            name,
2928            args,
2929            ..
2930        } = &expr
2931        else {
2932            panic!("expected receiver call, got {expr:?}");
2933        };
2934        assert_eq!(name, "setStatusEffect");
2935        assert!(
2936            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
2937            "receiver must be the preceding call"
2938        );
2939        assert_eq!(args.len(), 2);
2940    }
2941
2942    #[test]
2943    fn member_without_call_is_not_a_call() {
2944        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
2945        // access; only a following `(` turns it into a receiver call.
2946        let program =
2947            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
2948        let RuleEntry::Rule(rule) = &program.rules[0] else {
2949            panic!("expected rule");
2950        };
2951        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2952            panic!("expected assignment");
2953        };
2954        assert!(matches!(
2955            &value,
2956            Expr::Member { member, .. } if member == "moveSpeed"
2957        ));
2958    }
2959
2960    #[test]
2961    fn parses_advanced_rule_annotations_with_source_arguments() {
2962        let program = parse_ok(
2963            "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",
2964        );
2965        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
2966            panic!("expected subroutine");
2967        };
2968        assert_eq!(annotations.len(), 2);
2969        let RuleEntry::Rule(rule) = &program.rules[1] else {
2970            panic!("expected rule");
2971        };
2972        assert!(rule.disabled);
2973        assert!(rule.delimiter);
2974        assert_eq!(rule.new_page.as_deref(), Some("Page"));
2975        assert_eq!(rule.annotations.len(), 7);
2976        assert_eq!(rule.annotations[1].args[0].text, "1");
2977        assert_eq!(rule.annotations[2].args[0].text, "dmon");
2978    }
2979}