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 language support surface"
927                    .to_string(),
928            );
929            return false;
930        }
931        if self
932            .expect(TokenKind::Colon, "':' after the subroutine signature")
933            .is_err()
934        {
935            return false;
936        }
937        let line_indent = start.span.start.col;
938        let body_indent = match self.block_indent(line_indent) {
939            Some(indent) => indent,
940            None => return false,
941        };
942        let mut annotations = Vec::new();
943        let mut event = None;
944        let mut conditions = Vec::new();
945        let mut disabled = false;
946        let mut delimiter = false;
947        let mut new_page = None;
948        loop {
949            self.skip_newlines();
950            if self.peek_kind() != TokenKind::At {
951                break;
952            }
953            if !self.parse_directive(
954                &mut event,
955                &mut conditions,
956                &mut annotations,
957                &mut disabled,
958                &mut delimiter,
959                &mut new_page,
960                true,
961            ) {
962                self.recover_line();
963                return false;
964            }
965        }
966        if event.is_some() || !conditions.is_empty() {
967            self.error_at_current("subroutines cannot have events or conditions".to_string());
968            return false;
969        }
970        let _ = (disabled, delimiter, new_page);
971        let presentation_name = annotations
972            .iter()
973            .find(|annotation| annotation.name == "Name")
974            .and_then(|annotation| annotation.args.first())
975            .map(|arg| unquote_annotation_arg(&arg.text));
976        let body = self.parse_block(body_indent);
977        let span = if name_token.kind == TokenKind::Ident {
978            Span::new(start.span.file, start.span.start, name_token.span.end)
979        } else {
980            start.span
981        };
982        rules.push(RuleEntry::SubroutineDef {
983            name,
984            presentation_name,
985            span,
986            name_span,
987            body,
988            annotations,
989            rule_prefix,
990        });
991        true
992    }
993
994    /// The indentation of the next non-empty line, which must exceed
995    /// `line_indent` (an indented block follows the colon).
996    fn block_indent(&mut self, line_indent: u32) -> Option<u32> {
997        self.skip_newlines();
998        if self.peek_kind() == TokenKind::Eof {
999            self.error_at_current("expected an indented block".to_string());
1000            return None;
1001        }
1002        let indent = self.peek().span.start.col;
1003        if indent <= line_indent {
1004            self.error_at_current("expected an indented block after ':'".to_string());
1005            return None;
1006        }
1007        Some(indent)
1008    }
1009
1010    // ---- statements ----
1011
1012    fn parse_block(&mut self, block_indent: u32) -> Vec<Stmt> {
1013        let mut stmts = Vec::new();
1014        loop {
1015            self.skip_newlines();
1016            if self.peek_kind() == TokenKind::Eof {
1017                break;
1018            }
1019            if self.peek().span.start.col < block_indent {
1020                break;
1021            }
1022            if self.peek().span.start.col > block_indent {
1023                // A deeper indent without an introducer: recover by line.
1024                self.error_at_current("unexpected indentation".to_string());
1025                self.recover_line();
1026                continue;
1027            }
1028            match self.parse_statement() {
1029                Ok(stmt) => stmts.push(stmt),
1030                Err(()) => self.recover_line(),
1031            }
1032        }
1033        stmts
1034    }
1035
1036    fn parse_statement(&mut self) -> Result<Stmt, ()> {
1037        let token = self.peek();
1038        if token.kind == TokenKind::Ident {
1039            match token.text.as_str() {
1040                "if" => return self.parse_if(),
1041                "for" => return self.parse_for(),
1042                "while" => return self.parse_while(),
1043                "do" => return self.parse_do_while(),
1044                "switch" => return self.parse_switch(),
1045                "del" => return self.parse_delete(),
1046                "continue" => {
1047                    let token = self.advance();
1048                    self.expect_statement_end("the continue statement")?;
1049                    return Ok(Stmt::Continue { span: token.span });
1050                }
1051                "goto" => return self.parse_goto(),
1052                "break" => {
1053                    let token = self.advance();
1054                    return Ok(Stmt::Break { span: token.span });
1055                }
1056                "return" => {
1057                    let token = self.advance();
1058                    self.expect_statement_end("the return statement")?;
1059                    return Ok(Stmt::Return { span: token.span });
1060                }
1061                "pass" => {
1062                    let start = self.advance();
1063                    return Ok(Stmt::Pass { span: start.span });
1064                }
1065                _ => {}
1066            }
1067            if self.peek_at(1).kind == TokenKind::Colon {
1068                return self.parse_label();
1069            }
1070        }
1071        self.parse_expr_statement()
1072    }
1073
1074    fn parse_delete(&mut self) -> Result<Stmt, ()> {
1075        let start = self.advance();
1076        let target = self.parse_postfix()?;
1077        if !matches!(target, Expr::Index { .. }) {
1078            self.errors.push(OpyError::at(
1079                "parse-error",
1080                "the del statement requires an array index target".to_string(),
1081                target.span(),
1082            ));
1083            return Err(());
1084        }
1085        self.expect_statement_end("the del statement")?;
1086        Ok(Stmt::Delete {
1087            span: Span::new(start.span.file, start.span.start, target.span().end),
1088            target,
1089        })
1090    }
1091
1092    fn parse_goto(&mut self) -> Result<Stmt, ()> {
1093        let start = self.advance();
1094        if self.is_ident("loc") {
1095            self.advance();
1096            self.expect(TokenKind::Plus, "'+' after 'goto loc'")?;
1097            let offset = self.parse_expr()?;
1098            self.expect_statement_end("the goto target")?;
1099            return Ok(Stmt::Goto {
1100                label: None,
1101                span: Span::new(start.span.file, start.span.start, offset.span().end),
1102                offset: Some(offset),
1103                rule_start: false,
1104            });
1105        }
1106
1107        let label = self.expect_ident("a label or 'loc+...' after 'goto'")?;
1108        self.expect_statement_end("the goto target")?;
1109        let rule_start = label == "RULE_START";
1110        Ok(Stmt::Goto {
1111            label: (!rule_start).then_some(label),
1112            offset: None,
1113            rule_start,
1114            span: Span::new(
1115                start.span.file,
1116                start.span.start,
1117                self.tokens[self.pos - 1].span.end,
1118            ),
1119        })
1120    }
1121
1122    fn parse_label(&mut self) -> Result<Stmt, ()> {
1123        let name = self.advance();
1124        let colon = self.expect(TokenKind::Colon, "':' after a label")?;
1125        self.expect_statement_end("the label")?;
1126        Ok(Stmt::Label {
1127            name: name.text,
1128            span: Span::new(name.span.file, name.span.start, colon.span.end),
1129        })
1130    }
1131
1132    fn parse_expr_statement(&mut self) -> Result<Stmt, ()> {
1133        if self.peek_kind() == TokenKind::LParen {
1134            let save_pos = self.pos;
1135            let save_errors = self.errors.len();
1136            let start = self.advance().span;
1137            if let Ok(target) = self.parse_postfix()
1138                && self.peek_kind() == TokenKind::Assign
1139            {
1140                self.advance();
1141                let value = self.parse_expr()?;
1142                let end = self.expect(TokenKind::RParen, "')'")?.span.end;
1143                return Ok(Stmt::Assign {
1144                    target,
1145                    value,
1146                    span: Span::new(start.file, start.start, end),
1147                });
1148            }
1149            self.pos = save_pos;
1150            self.errors.truncate(save_errors);
1151        }
1152        let start = self.peek().span;
1153        let expr = self.parse_expr()?;
1154        match self.peek_kind() {
1155            TokenKind::Assign => {
1156                self.advance();
1157                let value = self.parse_expr()?;
1158                let end = value.span().end;
1159                Ok(Stmt::Assign {
1160                    target: expr,
1161                    value,
1162                    span: Span::new(start.file, start.start, end),
1163                })
1164            }
1165            TokenKind::PlusAssign
1166            | TokenKind::MinusAssign
1167            | TokenKind::StarAssign
1168            | TokenKind::SlashAssign
1169            | TokenKind::PercentAssign
1170            | TokenKind::DoubleStarAssign => {
1171                let op = match self.peek_kind() {
1172                    TokenKind::PlusAssign => "+",
1173                    TokenKind::MinusAssign => "-",
1174                    TokenKind::StarAssign => "*",
1175                    TokenKind::SlashAssign => "/",
1176                    TokenKind::PercentAssign => "%",
1177                    TokenKind::DoubleStarAssign => "**",
1178                    _ => unreachable!(),
1179                }
1180                .to_string();
1181                self.advance();
1182                let rhs = self.parse_expr()?;
1183                let end = rhs.span().end;
1184                let value = Expr::Binary {
1185                    op,
1186                    left: Box::new(expr.clone()),
1187                    right: Box::new(rhs),
1188                    span: Span::new(start.file, start.start, end),
1189                };
1190                Ok(Stmt::Assign {
1191                    target: expr,
1192                    value,
1193                    span: Span::new(start.file, start.start, end),
1194                })
1195            }
1196            TokenKind::Ident
1197                if matches!(self.peek().text.as_str(), "min" | "max")
1198                    && self.peek_at(1).kind == TokenKind::Assign =>
1199            {
1200                let op = self.advance().text;
1201                self.advance();
1202                let rhs = self.parse_expr()?;
1203                let end = rhs.span().end;
1204                let value = Expr::Binary {
1205                    op,
1206                    left: Box::new(expr.clone()),
1207                    right: Box::new(rhs),
1208                    span: Span::new(start.file, start.start, end),
1209                };
1210                Ok(Stmt::Assign {
1211                    target: expr,
1212                    value,
1213                    span: Span::new(start.file, start.start, end),
1214                })
1215            }
1216            TokenKind::Increment | TokenKind::Decrement => {
1217                let operator = self.advance();
1218                if !matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) {
1219                    self.error_at_current(
1220                        "postfix increment/decrement must be a standalone assignment".to_string(),
1221                    );
1222                    return Err(());
1223                }
1224                let operation = if operator.kind == TokenKind::Increment {
1225                    "+"
1226                } else {
1227                    "-"
1228                };
1229                let span = Span::new(start.file, start.start, operator.span.end);
1230                let value = Expr::Binary {
1231                    op: operation.to_string(),
1232                    left: Box::new(expr.clone()),
1233                    right: Box::new(Expr::Number {
1234                        value: 1.0,
1235                        text: "1".to_string(),
1236                        span: operator.span,
1237                    }),
1238                    span,
1239                };
1240                Ok(Stmt::Assign {
1241                    target: expr,
1242                    value,
1243                    span,
1244                })
1245            }
1246            _ => {
1247                let end = expr.span().end;
1248                Ok(Stmt::Expr {
1249                    expr,
1250                    span: Span::new(start.file, start.start, end),
1251                })
1252            }
1253        }
1254    }
1255
1256    fn parse_if(&mut self) -> Result<Stmt, ()> {
1257        let start = self.advance();
1258        let line_indent = start.span.start.col;
1259        let condition = self.parse_expr()?;
1260        if self
1261            .expect(TokenKind::Colon, "':' after the if condition")
1262            .is_err()
1263        {
1264            return Err(());
1265        }
1266        let body = self.parse_colon_body(line_indent)?;
1267        let mut branches = vec![IfBranch { condition, body }];
1268        let mut r#else = None;
1269        loop {
1270            let save = self.pos;
1271            self.skip_newlines();
1272            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col > line_indent {
1273                self.pos = save;
1274                break;
1275            }
1276            if self.is_ident("elif") {
1277                let branch_start = self.advance();
1278                let condition = match self.parse_expr() {
1279                    Ok(expr) => expr,
1280                    Err(()) => return Err(()),
1281                };
1282                if self
1283                    .expect(TokenKind::Colon, "':' after the elif condition")
1284                    .is_err()
1285                {
1286                    return Err(());
1287                }
1288                let body = self.parse_colon_body(branch_start.span.start.col)?;
1289                branches.push(IfBranch { condition, body });
1290            } else if self.is_ident("else") {
1291                let branch_start = self.advance();
1292                if self.is_ident("if") {
1293                    self.advance();
1294                    let condition = self.parse_expr()?;
1295                    if self
1296                        .expect(TokenKind::Colon, "':' after the else-if condition")
1297                        .is_err()
1298                    {
1299                        return Err(());
1300                    }
1301                    let body = self.parse_colon_body(branch_start.span.start.col)?;
1302                    branches.push(IfBranch { condition, body });
1303                    continue;
1304                }
1305                if self.expect(TokenKind::Colon, "':' after `else`").is_err() {
1306                    return Err(());
1307                }
1308                let body = self.parse_colon_body(branch_start.span.start.col)?;
1309                r#else = Some(body);
1310                break;
1311            } else {
1312                self.pos = save;
1313                break;
1314            }
1315        }
1316        Ok(Stmt::If {
1317            branches,
1318            r#else,
1319            span: start.span,
1320        })
1321    }
1322
1323    fn parse_colon_body(&mut self, line_indent: u32) -> Result<Vec<Stmt>, ()> {
1324        if matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) {
1325            let save = self.pos;
1326            self.skip_newlines();
1327            if self.peek_kind() != TokenKind::Eof && self.peek().span.start.col == line_indent {
1328                let statement = self.parse_statement()?;
1329                self.expect_statement_end("the inline statement")?;
1330                return Ok(vec![statement]);
1331            }
1332            self.pos = save;
1333            let body_indent = self.block_indent(line_indent).ok_or(())?;
1334            Ok(self.parse_block(body_indent))
1335        } else {
1336            let statement = self.parse_statement()?;
1337            self.expect_statement_end("the inline statement")?;
1338            Ok(vec![statement])
1339        }
1340    }
1341
1342    fn parse_for(&mut self) -> Result<Stmt, ()> {
1343        let start = self.advance();
1344        let variable = self.parse_postfix()?;
1345        if !self.is_ident("in") {
1346            self.error_at_current("expected `in` in the for statement".to_string());
1347            return Err(());
1348        }
1349        self.advance();
1350        let iterable = self.parse_expr()?;
1351        if self
1352            .expect(TokenKind::Colon, "':' after the for header")
1353            .is_err()
1354        {
1355            return Err(());
1356        }
1357        let line_indent = start.span.start.col;
1358        let body_indent = self.block_indent(line_indent).ok_or(())?;
1359        let body = self.parse_block(body_indent);
1360        Ok(Stmt::For {
1361            variable,
1362            iterable,
1363            body,
1364            span: start.span,
1365        })
1366    }
1367
1368    fn parse_while(&mut self) -> Result<Stmt, ()> {
1369        let start = self.advance();
1370        let condition = self.parse_expr()?;
1371        if self
1372            .expect(TokenKind::Colon, "':' after the while condition")
1373            .is_err()
1374        {
1375            return Err(());
1376        }
1377        let line_indent = start.span.start.col;
1378        let body_indent = self.block_indent(line_indent).ok_or(())?;
1379        let body = self.parse_block(body_indent);
1380        Ok(Stmt::While {
1381            condition,
1382            body,
1383            span: start.span,
1384        })
1385    }
1386
1387    fn parse_do_while(&mut self) -> Result<Stmt, ()> {
1388        let start = self.advance();
1389        if self.expect(TokenKind::Colon, "':' after `do`").is_err() {
1390            return Err(());
1391        }
1392        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1393        let body = self.parse_block(body_indent);
1394        if !self.is_ident("while") {
1395            self.error_at_current("expected `while` after the do block".to_string());
1396            return Err(());
1397        }
1398        self.advance();
1399        let condition = self.parse_expr()?;
1400        if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
1401            self.error_at_current("expected the end of the do-while condition".to_string());
1402            return Err(());
1403        }
1404        Ok(Stmt::DoWhile {
1405            condition,
1406            body,
1407            span: start.span,
1408        })
1409    }
1410
1411    fn parse_switch(&mut self) -> Result<Stmt, ()> {
1412        let start = self.advance();
1413        let value = self.parse_expr()?;
1414        if self
1415            .expect(TokenKind::Colon, "':' after the switch value")
1416            .is_err()
1417        {
1418            return Err(());
1419        }
1420        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1421        let mut arms = Vec::new();
1422        loop {
1423            self.skip_newlines();
1424            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
1425                break;
1426            }
1427            if self.peek().span.start.col != body_indent {
1428                self.error_at_current("unexpected indentation in switch".to_string());
1429                self.recover_line();
1430                continue;
1431            }
1432            if self.is_ident("case") {
1433                let case_start = self.advance();
1434                let case_value = self.parse_expr()?;
1435                if self
1436                    .expect(TokenKind::Colon, "':' after the case value")
1437                    .is_err()
1438                {
1439                    return Err(());
1440                }
1441                let case_body_indent = self.block_indent(body_indent).ok_or(())?;
1442                let body = self.parse_block(case_body_indent);
1443                arms.push(SwitchArm::Case {
1444                    value: case_value,
1445                    body,
1446                    span: case_start.span,
1447                });
1448            } else if self.is_ident("default") {
1449                let default_start = self.advance();
1450                if self
1451                    .expect(TokenKind::Colon, "':' after `default`")
1452                    .is_err()
1453                {
1454                    return Err(());
1455                }
1456                let default_body_indent = self.block_indent(body_indent).ok_or(())?;
1457                arms.push(SwitchArm::Default {
1458                    body: self.parse_block(default_body_indent),
1459                    span: default_start.span,
1460                });
1461                if default_start.span.start.col != body_indent {
1462                    self.error_at_current("invalid default indentation".to_string());
1463                    return Err(());
1464                }
1465            } else {
1466                self.error_at_current("expected `case` or `default` in switch".to_string());
1467                self.recover_line();
1468            }
1469        }
1470        if arms.is_empty() {
1471            self.errors.push(OpyError::at(
1472                "parse-error",
1473                "switch must contain at least one case or default arm".to_string(),
1474                start.span,
1475            ));
1476            return Err(());
1477        }
1478        Ok(Stmt::Switch {
1479            value,
1480            arms,
1481            span: start.span,
1482        })
1483    }
1484
1485    // ---- expressions ----
1486
1487    fn parse_expr(&mut self) -> Result<Expr, ()> {
1488        self.parse_expr_inner(false)
1489    }
1490
1491    fn parse_expr_inner(&mut self, allow_multiline_conditional: bool) -> Result<Expr, ()> {
1492        self.skip_expression_newlines();
1493        let then_value = self.parse_or()?;
1494        let has_newline_before_conditional = self.tokens[..self.pos].iter().any(|token| {
1495            token.kind == TokenKind::Newline
1496                && token.span.file == then_value.span().file
1497                && token.span.start.line > then_value.span().end.line
1498        });
1499        if self.is_ident("if")
1500            && !self.inside_delimiter_group()
1501            && has_newline_before_conditional
1502            && self.peek().span.start.line != then_value.span().end.line
1503        {
1504            return Ok(then_value);
1505        }
1506        if allow_multiline_conditional && self.inside_delimiter_group() {
1507            self.skip_newlines();
1508        }
1509        if !self.is_ident("if") {
1510            return Ok(then_value);
1511        }
1512
1513        let conditional_start = self.pos;
1514        self.advance();
1515        self.skip_expression_newlines();
1516        let condition = self.parse_or()?;
1517        self.skip_expression_newlines();
1518        if !self.is_ident("else") {
1519            if self.peek_kind() == TokenKind::Colon {
1520                self.pos = conditional_start;
1521                return Ok(then_value);
1522            }
1523            self.error_at_current("expected `else` in conditional expression".to_string());
1524            return Err(());
1525        }
1526        self.advance();
1527        // Conditional expressions are right-associative, so a chained form
1528        // such as `a if c else b if d else e` groups at the else branch.
1529        let else_value = self.parse_expr_inner(true)?;
1530        let span = Span::new(
1531            then_value.span().file,
1532            then_value.span().start,
1533            else_value.span().end,
1534        );
1535        Ok(Expr::Conditional {
1536            then_value: Box::new(then_value),
1537            condition: Box::new(condition),
1538            else_value: Box::new(else_value),
1539            span,
1540        })
1541    }
1542
1543    fn parse_or(&mut self) -> Result<Expr, ()> {
1544        self.skip_expression_newlines();
1545        let mut left = self.parse_and()?;
1546        loop {
1547            self.skip_expression_newlines();
1548            if !self.is_ident("or") {
1549                break;
1550            }
1551            self.advance();
1552            self.skip_expression_newlines();
1553            let right = self.parse_and()?;
1554            let span = Span::new(left.span().file, left.span().start, right.span().end);
1555            left = Expr::Binary {
1556                op: "or".to_string(),
1557                left: Box::new(left),
1558                right: Box::new(right),
1559                span,
1560            };
1561        }
1562        Ok(left)
1563    }
1564
1565    fn parse_and(&mut self) -> Result<Expr, ()> {
1566        self.skip_expression_newlines();
1567        let mut left = self.parse_not()?;
1568        loop {
1569            self.skip_expression_newlines();
1570            if !self.is_ident("and") {
1571                break;
1572            }
1573            self.advance();
1574            self.skip_expression_newlines();
1575            let right = self.parse_not()?;
1576            let span = Span::new(left.span().file, left.span().start, right.span().end);
1577            left = Expr::Binary {
1578                op: "and".to_string(),
1579                left: Box::new(left),
1580                right: Box::new(right),
1581                span,
1582            };
1583        }
1584        Ok(left)
1585    }
1586
1587    fn parse_not(&mut self) -> Result<Expr, ()> {
1588        self.skip_expression_newlines();
1589        if self.is_ident("not") {
1590            let start = self.advance();
1591            self.skip_expression_newlines();
1592            let operand = self.parse_not()?;
1593            let end = operand.span().end;
1594            return Ok(Expr::Unary {
1595                op: "not".to_string(),
1596                operand: Box::new(operand),
1597                span: Span::new(start.span.file, start.span.start, end),
1598            });
1599        }
1600        self.parse_comparison()
1601    }
1602
1603    fn parse_comparison(&mut self) -> Result<Expr, ()> {
1604        self.skip_expression_newlines();
1605        let mut left = self.parse_additive()?;
1606        loop {
1607            self.skip_expression_newlines();
1608            let op = match self.peek_kind() {
1609                TokenKind::Eq => "==",
1610                TokenKind::Ne => "!=",
1611                TokenKind::Lt => "<",
1612                TokenKind::Le => "<=",
1613                TokenKind::Gt => ">",
1614                TokenKind::Ge => ">=",
1615                _ if self.is_ident("in") => "in",
1616                _ if self.is_ident("not") && self.peek_at(1).text == "in" => "not in",
1617                _ => break,
1618            };
1619            self.advance();
1620            if op == "not in" {
1621                self.advance();
1622            }
1623            self.skip_expression_newlines();
1624            let right = self.parse_additive()?;
1625            let span = Span::new(left.span().file, left.span().start, right.span().end);
1626            left = Expr::Binary {
1627                op: op.to_string(),
1628                left: Box::new(left),
1629                right: Box::new(right),
1630                span,
1631            };
1632        }
1633        Ok(left)
1634    }
1635
1636    fn parse_additive(&mut self) -> Result<Expr, ()> {
1637        self.skip_expression_newlines();
1638        let mut left = self.parse_multiplicative()?;
1639        loop {
1640            self.skip_expression_newlines();
1641            if self.peek_kind() == TokenKind::Decrement
1642                && !matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof)
1643            {
1644                let operator = self.advance();
1645                let operand = self.parse_unary()?;
1646                let unary = Expr::Unary {
1647                    op: "-".to_string(),
1648                    span: Span::new(operator.span.file, operator.span.start, operand.span().end),
1649                    operand: Box::new(operand),
1650                };
1651                let right = self.parse_multiplicative_tail(unary)?;
1652                let span = Span::new(left.span().file, left.span().start, right.span().end);
1653                left = Expr::Binary {
1654                    op: "-".to_string(),
1655                    left: Box::new(left),
1656                    right: Box::new(right),
1657                    span,
1658                };
1659                continue;
1660            }
1661            let op = match self.peek_kind() {
1662                TokenKind::Plus => "+",
1663                TokenKind::Minus => "-",
1664                _ => break,
1665            };
1666            self.advance();
1667            self.skip_expression_newlines();
1668            let right = self.parse_multiplicative()?;
1669            let span = Span::new(left.span().file, left.span().start, right.span().end);
1670            left = Expr::Binary {
1671                op: op.to_string(),
1672                left: Box::new(left),
1673                right: Box::new(right),
1674                span,
1675            };
1676        }
1677        Ok(left)
1678    }
1679
1680    fn parse_multiplicative(&mut self) -> Result<Expr, ()> {
1681        self.skip_expression_newlines();
1682        let left = self.parse_unary()?;
1683        self.parse_multiplicative_tail(left)
1684    }
1685
1686    fn parse_multiplicative_tail(&mut self, mut left: Expr) -> Result<Expr, ()> {
1687        loop {
1688            self.skip_expression_newlines();
1689            let op = match self.peek_kind() {
1690                TokenKind::Star => "*",
1691                TokenKind::Slash => "/",
1692                TokenKind::Percent => "%",
1693                _ => break,
1694            };
1695            self.advance();
1696            self.skip_expression_newlines();
1697            let right = self.parse_unary()?;
1698            let span = Span::new(left.span().file, left.span().start, right.span().end);
1699            left = Expr::Binary {
1700                op: op.to_string(),
1701                left: Box::new(left),
1702                right: Box::new(right),
1703                span,
1704            };
1705        }
1706        Ok(left)
1707    }
1708
1709    fn parse_unary(&mut self) -> Result<Expr, ()> {
1710        self.skip_expression_newlines();
1711        if matches!(self.peek_kind(), TokenKind::Minus | TokenKind::Decrement) {
1712            let start = self.advance();
1713            self.skip_expression_newlines();
1714            let operand = self.parse_unary()?;
1715            let end = operand.span().end;
1716            let unary = Expr::Unary {
1717                op: "-".to_string(),
1718                operand: Box::new(operand),
1719                span: Span::new(start.span.file, start.span.start, end),
1720            };
1721            if start.kind == TokenKind::Decrement {
1722                return Ok(Expr::Unary {
1723                    op: "-".to_string(),
1724                    operand: Box::new(unary),
1725                    span: Span::new(start.span.file, start.span.start, end),
1726                });
1727            }
1728            return Ok(unary);
1729        }
1730        self.parse_power()
1731    }
1732
1733    fn parse_power(&mut self) -> Result<Expr, ()> {
1734        let base = self.parse_postfix()?;
1735        self.skip_expression_newlines();
1736        if self.peek_kind() == TokenKind::DoubleStar {
1737            self.advance();
1738            // Right-associative.
1739            self.skip_expression_newlines();
1740            let exponent = self.parse_unary()?;
1741            let span = Span::new(base.span().file, base.span().start, exponent.span().end);
1742            return Ok(Expr::Binary {
1743                op: "**".to_string(),
1744                left: Box::new(base),
1745                right: Box::new(exponent),
1746                span,
1747            });
1748        }
1749        Ok(base)
1750    }
1751
1752    fn parse_postfix(&mut self) -> Result<Expr, ()> {
1753        let mut base = self.parse_primary()?;
1754        loop {
1755            self.skip_expression_newlines();
1756            match self.peek_kind() {
1757                TokenKind::LParen => {
1758                    let mut args = Vec::new();
1759                    self.parse_call_args(&mut args)?;
1760                    let end = self.tokens[self.pos.saturating_sub(1)].span.end;
1761                    base = match base {
1762                        Expr::Name { name, span } => Expr::Call {
1763                            name,
1764                            args,
1765                            span: Span::new(span.file, span.start, end),
1766                        },
1767                        Expr::Member {
1768                            receiver,
1769                            member,
1770                            span,
1771                            ..
1772                        } => Expr::ReceiverCall {
1773                            receiver,
1774                            name: member,
1775                            args,
1776                            span: Span::new(span.file, span.start, end),
1777                        },
1778                        _other => {
1779                            self.errors.push(OpyError::at(
1780                                "parse-error",
1781                                "cannot call this expression".to_string(),
1782                                self.peek().span,
1783                            ));
1784                            return Err(());
1785                        }
1786                    };
1787                }
1788                TokenKind::LBracket => {
1789                    self.advance();
1790                    let index = self.parse_expr()?;
1791                    if self.peek_kind() == TokenKind::Colon {
1792                        self.advance();
1793                        let maximum = self.parse_expr()?;
1794                        let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
1795                        let Expr::Name { name, span } = &base else {
1796                            self.error_at_current(
1797                                "a range type must start with a type name".to_string(),
1798                            );
1799                            return Err(());
1800                        };
1801                        base = Expr::Type {
1802                            name: name.clone(),
1803                            args: vec![index, maximum],
1804                            span: Span::new(span.file, span.start, end),
1805                        };
1806                        continue;
1807                    }
1808                    let end = match self.expect(TokenKind::RBracket, "']'") {
1809                        Ok(token) => token.span.end,
1810                        Err(()) => return Err(()),
1811                    };
1812                    let span = Span::new(base.span().file, base.span().start, end);
1813                    base = Expr::Index {
1814                        array: Box::new(base),
1815                        index: Box::new(index),
1816                        span,
1817                    };
1818                }
1819                TokenKind::Dot => {
1820                    self.advance();
1821                    let member_token = self.peek().clone();
1822                    let member = match self.peek_kind() {
1823                        TokenKind::Ident | TokenKind::Number => self.advance().text,
1824                        _ => {
1825                            self.error_at_current("expected a member name after '.'".to_string());
1826                            return Err(());
1827                        }
1828                    };
1829                    let member_span = member_token.span;
1830                    let end = member_span.end;
1831                    let span = Span::new(base.span().file, base.span().start, end);
1832                    base = Expr::Member {
1833                        receiver: Box::new(base),
1834                        member,
1835                        member_span,
1836                        span,
1837                    };
1838                }
1839                _ => break,
1840            }
1841        }
1842        Ok(base)
1843    }
1844
1845    /// `@Event name(args)`: positional expressions only (keyword arguments
1846    /// are a call-argument form, not an event form).
1847    fn parse_event_args(&mut self, args: &mut Vec<Expr>) -> Result<(), ()> {
1848        self.expect(TokenKind::LParen, "'('")?;
1849        self.skip_newlines();
1850        if self.peek_kind() == TokenKind::RParen {
1851            self.advance();
1852            return Ok(());
1853        }
1854        loop {
1855            let expr = self.parse_expr()?;
1856            if self.peek_kind() == TokenKind::Assign {
1857                self.error_at_current("keyword arguments are not valid in @Event".to_string());
1858                return Err(());
1859            }
1860            args.push(expr);
1861            self.skip_newlines();
1862            if self.peek_kind() == TokenKind::Comma {
1863                self.advance();
1864                self.skip_newlines();
1865                if self.peek_kind() == TokenKind::RParen {
1866                    break;
1867                }
1868            } else {
1869                break;
1870            }
1871        }
1872        self.expect(TokenKind::RParen, "')'")?;
1873        Ok(())
1874    }
1875
1876    fn parse_call_args(&mut self, args: &mut Vec<CallArg>) -> Result<(), ()> {
1877        self.expect(TokenKind::LParen, "'('")?;
1878        self.skip_newlines();
1879        if self.peek_kind() == TokenKind::RParen {
1880            self.advance();
1881            return Ok(());
1882        }
1883        loop {
1884            match self.parse_expr() {
1885                Ok(expr) => {
1886                    // A keyword argument is `name = expr` (issue #110): a
1887                    // bare identifier immediately followed by `=`. Anything
1888                    // else (`expr = ...`) is not a call argument form and is
1889                    // rejected like the pinned reference rejects it.
1890                    if self.peek_kind() == TokenKind::Assign {
1891                        let Expr::Name { name, span } = expr else {
1892                            self.error_at_current(
1893                                "expected a keyword name before '=' in this call".to_string(),
1894                            );
1895                            return Err(());
1896                        };
1897                        self.advance();
1898                        let value = match self.parse_expr() {
1899                            Ok(value) => value,
1900                            Err(()) => return Err(()),
1901                        };
1902                        args.push(CallArg {
1903                            keyword: Some((name, span)),
1904                            value,
1905                        });
1906                    } else {
1907                        args.push(CallArg {
1908                            keyword: None,
1909                            value: expr,
1910                        });
1911                    }
1912                }
1913                Err(()) => return Err(()),
1914            }
1915            self.skip_newlines();
1916            if self.peek_kind() == TokenKind::Comma {
1917                self.advance();
1918                self.skip_newlines();
1919                if self.peek_kind() == TokenKind::RParen {
1920                    break;
1921                }
1922            } else {
1923                break;
1924            }
1925        }
1926        self.expect(TokenKind::RParen, "')'")?;
1927        Ok(())
1928    }
1929
1930    fn parse_primary(&mut self) -> Result<Expr, ()> {
1931        let token = self.peek();
1932        match token.kind {
1933            TokenKind::Number => {
1934                let token = self.advance();
1935                let value = if let Some(hex) = token
1936                    .text
1937                    .strip_prefix("0x")
1938                    .or_else(|| token.text.strip_prefix("0X"))
1939                {
1940                    u64::from_str_radix(hex, 16).map_or(f64::NAN, |value| value as f64)
1941                } else {
1942                    token.text.parse().unwrap_or(f64::NAN)
1943                };
1944                Ok(Expr::Number {
1945                    value,
1946                    text: token.text.clone(),
1947                    span: token.span,
1948                })
1949            }
1950            TokenKind::String => self.parse_string_literal(),
1951            TokenKind::Ident => {
1952                let token = self.advance();
1953                if token.text == "lambda" {
1954                    return self.parse_lambda(token.span);
1955                }
1956                if is_string_modifier(&token.text) && self.peek_kind() == TokenKind::String {
1957                    let string = self.advance();
1958                    let (format_text, interpolations) = if token.text == "f" {
1959                        let raw = string.raw.as_deref().unwrap_or(&string.text);
1960                        let (format_text, interpolations) =
1961                            self.parse_f_string(raw, string.span)?;
1962                        (Some(format_text), interpolations)
1963                    } else {
1964                        (None, Vec::new())
1965                    };
1966                    return Ok(Expr::StringModifier {
1967                        modifier: token.text.chars().next().unwrap_or_default(),
1968                        value: string.text,
1969                        format_text,
1970                        interpolations,
1971                        span: Span::new(token.span.file, token.span.start, string.span.end),
1972                    });
1973                }
1974                match token.text.as_str() {
1975                    "true" => Ok(Expr::Bool {
1976                        value: true,
1977                        span: token.span,
1978                    }),
1979                    "false" => Ok(Expr::Bool {
1980                        value: false,
1981                        span: token.span,
1982                    }),
1983                    "None" | "null" => Ok(Expr::Null { span: token.span }),
1984                    _ => Ok(Expr::Name {
1985                        name: token.text.clone(),
1986                        span: token.span,
1987                    }),
1988                }
1989            }
1990            TokenKind::LParen => {
1991                self.advance();
1992                self.skip_expression_newlines();
1993                let expr = self.parse_expr()?;
1994                self.skip_expression_newlines();
1995                self.expect(TokenKind::RParen, "')'")?;
1996                Ok(expr)
1997            }
1998            TokenKind::LBracket => {
1999                let open = self.advance();
2000                let mut elements = Vec::new();
2001                self.skip_newlines();
2002                if self.peek_kind() == TokenKind::RBracket {
2003                    let end = self.advance().span.end;
2004                    return Ok(Expr::Array {
2005                        elements,
2006                        span: Span::new(open.span.file, open.span.start, end),
2007                    });
2008                }
2009                let first = self.parse_expr()?;
2010                if self.is_ident("for") {
2011                    self.advance();
2012                    let variable_token =
2013                        self.expect(TokenKind::Ident, "a comprehension variable")?;
2014                    self.skip_newlines();
2015                    let index = if self.peek_kind() == TokenKind::Comma {
2016                        self.advance();
2017                        self.skip_newlines();
2018                        let index = self.expect(TokenKind::Ident, "a comprehension index")?;
2019                        Some((index.text, index.span))
2020                    } else {
2021                        None
2022                    };
2023                    self.skip_newlines();
2024                    if !self.is_ident("in") {
2025                        self.error_at_current("expected `in` in list comprehension".to_string());
2026                        return Err(());
2027                    }
2028                    self.advance();
2029                    self.skip_newlines();
2030                    let iterable = self.parse_or()?;
2031                    self.skip_newlines();
2032                    let condition = if self.is_ident("if") {
2033                        self.advance();
2034                        self.skip_newlines();
2035                        Some(Box::new(self.parse_or()?))
2036                    } else {
2037                        None
2038                    };
2039                    self.skip_newlines();
2040                    let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
2041                    return Ok(Expr::Comprehension {
2042                        element: Box::new(first),
2043                        variable: variable_token.text,
2044                        variable_span: variable_token.span,
2045                        index,
2046                        iterable: Box::new(iterable),
2047                        condition,
2048                        span: Span::new(open.span.file, open.span.start, end),
2049                    });
2050                }
2051                elements.push(first);
2052                loop {
2053                    self.skip_newlines();
2054                    if self.peek_kind() == TokenKind::Comma {
2055                        self.advance();
2056                        self.skip_newlines();
2057                        if self.peek_kind() == TokenKind::RBracket {
2058                            break;
2059                        }
2060                        elements.push(self.parse_expr()?);
2061                    } else {
2062                        break;
2063                    }
2064                }
2065                let end = match self.expect(TokenKind::RBracket, "']'") {
2066                    Ok(token) => token.span.end,
2067                    Err(()) => return Err(()),
2068                };
2069                Ok(Expr::Array {
2070                    elements,
2071                    span: Span::new(open.span.file, open.span.start, end),
2072                })
2073            }
2074            TokenKind::LBrace => self.parse_dict(),
2075            _ => {
2076                self.error_at_current(format!("expected an expression but found '{}'", token.text));
2077                Err(())
2078            }
2079        }
2080    }
2081
2082    /// Parse adjacent string literals as one source-language string value.
2083    ///
2084    /// Newlines are only ignored while looking for another literal inside a
2085    /// delimiter group. Outside a group, a newline remains a statement
2086    /// boundary, matching the bounded implicit-concatenation surface used by
2087    /// the OverPy examples.
2088    fn parse_string_literal(&mut self) -> Result<Expr, ()> {
2089        let first = self.advance();
2090        let mut value = first.text.clone();
2091        let mut end = first.span.end;
2092        loop {
2093            let saved = self.pos;
2094            if self.inside_delimiter_group() {
2095                self.skip_newlines();
2096            }
2097            if self.peek_kind() != TokenKind::String || self.peek().span.file != first.span.file {
2098                self.pos = saved;
2099                break;
2100            }
2101            let next = self.advance();
2102            value.push_str(&next.text);
2103            end = next.span.end;
2104        }
2105        Ok(Expr::String {
2106            value,
2107            span: Span::new(first.span.file, first.span.start, end),
2108        })
2109    }
2110
2111    /// Return whether the current parser position is inside `()`, `[]`, or
2112    /// `{}`. The token stream retains newlines, so this keeps multiline
2113    /// implicit concatenation scoped to syntactic grouping without adding
2114    /// parser state to every delimiter path.
2115    fn inside_delimiter_group(&self) -> bool {
2116        let mut depth = 0usize;
2117        for token in &self.tokens[..self.pos] {
2118            match token.kind {
2119                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => depth += 1,
2120                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
2121                    depth = depth.saturating_sub(1)
2122                }
2123                _ => {}
2124            }
2125        }
2126        depth != 0
2127    }
2128
2129    fn parse_dict(&mut self) -> Result<Expr, ()> {
2130        let open = self.advance();
2131        let mut entries = Vec::new();
2132        self.skip_newlines();
2133        if self.peek_kind() == TokenKind::RBrace {
2134            let end = self.advance().span.end;
2135            return Ok(Expr::Dict {
2136                entries,
2137                span: Span::new(open.span.file, open.span.start, end),
2138            });
2139        }
2140        loop {
2141            let key = self.parse_expr()?;
2142            self.expect(TokenKind::Colon, "':' in a dictionary entry")?;
2143            let value = self.parse_expr()?;
2144            let span = Span::new(key.span().file, key.span().start, value.span().end);
2145            entries.push(DictEntry { key, value, span });
2146            self.skip_newlines();
2147            if self.peek_kind() == TokenKind::Comma {
2148                self.advance();
2149                self.skip_newlines();
2150                if self.peek_kind() == TokenKind::RBrace {
2151                    break;
2152                }
2153            } else {
2154                break;
2155            }
2156        }
2157        let end = self.expect(TokenKind::RBrace, "'}'")?.span.end;
2158        Ok(Expr::Dict {
2159            entries,
2160            span: Span::new(open.span.file, open.span.start, end),
2161        })
2162    }
2163
2164    fn parse_lambda(&mut self, start: Span) -> Result<Expr, ()> {
2165        let mut params = Vec::new();
2166        loop {
2167            let param = self.expect(TokenKind::Ident, "a lambda parameter")?;
2168            params.push((param.text, param.span));
2169            if self.peek_kind() == TokenKind::Comma {
2170                self.advance();
2171            } else {
2172                break;
2173            }
2174        }
2175        self.expect(TokenKind::Colon, "':' after lambda parameters")?;
2176        let body = self.parse_expr()?;
2177        Ok(Expr::Lambda {
2178            params,
2179            body: Box::new(body.clone()),
2180            span: Span::new(start.file, start.start, body.span().end),
2181        })
2182    }
2183
2184    /// Parse the expression regions of a pinned-OverPy f-string. Double
2185    /// braces are literal braces; a single brace introduces one expression.
2186    /// The resulting expression tokens are shifted back into the source
2187    /// string so HIR and tooling retain source provenance.
2188    fn parse_f_string(&mut self, raw: &str, string_span: Span) -> Result<(String, Vec<Expr>), ()> {
2189        let chars: Vec<char> = raw.chars().collect();
2190        let mut text = String::new();
2191        let mut interpolations = Vec::new();
2192        let mut index = 0;
2193        while index < chars.len() {
2194            match chars[index] {
2195                '{' if chars.get(index + 1) == Some(&'{') => {
2196                    text.push_str("{{");
2197                    index += 2;
2198                }
2199                '}' if chars.get(index + 1) == Some(&'}') => {
2200                    text.push_str("}}");
2201                    index += 2;
2202                }
2203                '{' => {
2204                    let end = self.find_f_string_end(&chars, index + 1);
2205                    let Some(end) = end else {
2206                        self.errors.push(OpyError::at(
2207                            "parse-error",
2208                            "unterminated f-string interpolation".to_string(),
2209                            string_span,
2210                        ));
2211                        return Err(());
2212                    };
2213                    let expression: String = chars[index + 1..end].iter().collect();
2214                    if expression.trim().is_empty() {
2215                        self.errors.push(OpyError::at(
2216                            "parse-error",
2217                            "f-string interpolation cannot be empty".to_string(),
2218                            Span::new(
2219                                string_span.file,
2220                                Position::new(
2221                                    string_span.start.line,
2222                                    string_span.start.col + index as u32 + 1,
2223                                ),
2224                                Position::new(
2225                                    string_span.start.line,
2226                                    string_span.start.col + end as u32 + 1,
2227                                ),
2228                            ),
2229                        ));
2230                        return Err(());
2231                    }
2232                    let origin = Position::new(
2233                        string_span.start.line,
2234                        string_span.start.col + index as u32 + 1,
2235                    );
2236                    let parsed = parse_expression_fragment(&expression, string_span.file, origin)
2237                        .map_err(|error| {
2238                            self.errors.push(error);
2239                        });
2240                    let Ok(parsed) = parsed else {
2241                        return Err(());
2242                    };
2243                    text.push_str(&format!("{{{}}}", interpolations.len()));
2244                    interpolations.push(parsed);
2245                    index = end + 1;
2246                }
2247                '}' => {
2248                    self.errors.push(OpyError::at(
2249                        "parse-error",
2250                        "single '}' is not valid in an f-string".to_string(),
2251                        string_span,
2252                    ));
2253                    return Err(());
2254                }
2255                '\\' if index + 1 < chars.len() => {
2256                    text.push(decode_string_escape(chars[index + 1]));
2257                    index += 2;
2258                }
2259                character => {
2260                    text.push(character);
2261                    index += 1;
2262                }
2263            }
2264        }
2265        Ok((text, interpolations))
2266    }
2267
2268    fn find_f_string_end(&self, chars: &[char], start: usize) -> Option<usize> {
2269        let mut nested_braces = 0;
2270        let mut quote = None;
2271        let mut escaped = false;
2272        for (index, character) in chars.iter().enumerate().skip(start) {
2273            if escaped {
2274                escaped = false;
2275                continue;
2276            }
2277            if *character == '\\' && quote.is_some() {
2278                escaped = true;
2279                continue;
2280            }
2281            if let Some(active_quote) = quote {
2282                if *character == active_quote {
2283                    quote = None;
2284                }
2285                continue;
2286            }
2287            match character {
2288                '"' | '\'' => quote = Some(*character),
2289                '{' => nested_braces += 1,
2290                '}' if nested_braces == 0 => return Some(index),
2291                '}' => nested_braces -= 1,
2292                _ => {}
2293            }
2294        }
2295        None
2296    }
2297}
2298
2299/// Parse one f-string expression fragment and shift its local token spans
2300/// into the original source file.
2301pub(crate) fn parse_expression_fragment(
2302    text: &str,
2303    file: u32,
2304    origin: Position,
2305) -> Result<Expr, OpyError> {
2306    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
2307        file_id: file,
2308        text,
2309    })?;
2310    for token in &mut tokens {
2311        token.span = shift_span(token.span, origin);
2312    }
2313    let mut parser = Parser {
2314        tokens: &tokens,
2315        pos: 0,
2316        allow_macro_redeclaration: false,
2317        errors: Vec::new(),
2318    };
2319    let expression = parser.parse_expr().map_err(|()| {
2320        parser.errors.first().cloned().unwrap_or_else(|| {
2321            OpyError::at(
2322                "parse-error",
2323                "invalid f-string expression",
2324                Span::new(file, origin, origin),
2325            )
2326        })
2327    })?;
2328    if parser.peek_kind() != TokenKind::Eof {
2329        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
2330    }
2331    parser.errors.into_iter().next().map_or(Ok(expression), Err)
2332}
2333
2334fn shift_span(span: Span, origin: Position) -> Span {
2335    fn shift(position: Position, origin: Position) -> Position {
2336        Position::new(
2337            origin.line + position.line.saturating_sub(1),
2338            if position.line == 1 {
2339                origin.col + position.col.saturating_sub(1)
2340            } else {
2341                position.col
2342            },
2343        )
2344    }
2345    Span::new(
2346        span.file,
2347        shift(span.start, origin),
2348        shift(span.end, origin),
2349    )
2350}
2351
2352fn decode_string_escape(character: char) -> char {
2353    match character {
2354        'n' => '\n',
2355        't' => '\t',
2356        'r' => '\r',
2357        '\\' => '\\',
2358        '"' => '"',
2359        '\'' => '\'',
2360        other => other,
2361    }
2362}
2363
2364fn is_string_modifier(text: &str) -> bool {
2365    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
2366}
2367
2368#[cfg(test)]
2369mod tests {
2370    use super::*;
2371    use crate::lexer::{LexInput, lex};
2372
2373    fn parse_ok(text: &str) -> Program {
2374        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2375        let output = parse(&tokens);
2376        assert!(
2377            output.errors.is_empty(),
2378            "unexpected errors: {:?}",
2379            output.errors
2380        );
2381        output.program.unwrap()
2382    }
2383
2384    fn parse_err(text: &str) -> Vec<OpyError> {
2385        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2386        parse(&tokens).errors
2387    }
2388
2389    #[test]
2390    fn parses_basic_rule() {
2391        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
2392        assert_eq!(program.rules.len(), 1);
2393        let RuleEntry::Rule(rule) = &program.rules[0] else {
2394            panic!("expected rule");
2395        };
2396        assert_eq!(rule.name, "setup");
2397        assert_eq!(rule.event.name, "global");
2398        assert_eq!(rule.actions.len(), 1);
2399    }
2400
2401    #[test]
2402    fn parses_power_augmented_assignment() {
2403        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
2404        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
2405        let program =
2406            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
2407        let RuleEntry::Rule(rule) = &program.rules[0] else {
2408            panic!("expected rule");
2409        };
2410        let Stmt::Assign {
2411            value,
2412            target: assigned_target,
2413            ..
2414        } = &rule.actions[1]
2415        else {
2416            panic!("expected an assignment");
2417        };
2418        let Expr::Binary {
2419            op, left, right, ..
2420        } = value
2421        else {
2422            panic!("expected a binary modification, got {value:?}");
2423        };
2424        assert_eq!(op, "**");
2425        assert!(matches!(&**left, Expr::Name { .. }));
2426        assert!(matches!(
2427            assigned_target,
2428            Expr::Name { name, .. } if name == "a"
2429        ));
2430        assert!(matches!(right.as_ref(), Expr::Number { .. }));
2431    }
2432
2433    #[test]
2434    fn parses_postfix_increment_and_decrement_as_modifications() {
2435        let program =
2436            parse_ok("globalvar value\nrule \"r\":\n    @Event global\n    value++\n    value--\n");
2437        let RuleEntry::Rule(rule) = &program.rules[0] else {
2438            panic!("expected a rule");
2439        };
2440        for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
2441            let Stmt::Assign { target, value, .. } = statement else {
2442                panic!("expected a postfix assignment");
2443            };
2444            let Expr::Binary {
2445                op, left, right, ..
2446            } = value
2447            else {
2448                panic!("expected a synthetic modification value");
2449            };
2450            assert_eq!(op, expected_op);
2451            let Expr::Name {
2452                name: left_name, ..
2453            } = left.as_ref()
2454            else {
2455                panic!("expected the target to be the modification's left operand");
2456            };
2457            let Expr::Name {
2458                name: target_name, ..
2459            } = target
2460            else {
2461                panic!("expected a name target");
2462            };
2463            assert_eq!(left_name, target_name);
2464            assert!(
2465                matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
2466            );
2467        }
2468    }
2469
2470    #[test]
2471    fn rejects_prefix_increment_and_embedded_postfix_forms() {
2472        for source in [
2473            "globalvar value\nrule \"r\":\n    @Event global\n    ++value\n",
2474            "globalvar value\nrule \"r\":\n    @Event global\n    value++++\n",
2475        ] {
2476            let errors = parse_err(source);
2477            assert!(!errors.is_empty());
2478            assert!(errors.iter().all(|error| error.code == "parse-error"));
2479            assert!(errors.iter().all(|error| error.span.is_some()));
2480        }
2481    }
2482
2483    #[test]
2484    fn preserves_consecutive_unary_minus_expressions() {
2485        let source = concat!(
2486            "globalvar value = 0\n",
2487            "globalvar B = 1\n",
2488            "rule \"r\":\n",
2489            "    @Event global\n",
2490            "    value = --1\n",
2491            "    value = --B\n",
2492            "    value = B--1\n",
2493        );
2494        parse_ok(source);
2495    }
2496
2497    #[test]
2498    fn parses_control_flow() {
2499        let program = parse_ok(
2500            "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",
2501        );
2502        let RuleEntry::Rule(rule) = &program.rules[0] else {
2503            panic!();
2504        };
2505        assert!(matches!(rule.actions[0], Stmt::For { .. }));
2506        let Stmt::For { body, .. } = &rule.actions[0] else {
2507            panic!();
2508        };
2509        let Stmt::If {
2510            branches, r#else, ..
2511        } = &body[0]
2512        else {
2513            panic!();
2514        };
2515        assert_eq!(branches.len(), 2);
2516        assert!(r#else.is_some());
2517        let Stmt::While { body, .. } = &rule.actions[1] else {
2518            panic!();
2519        };
2520        assert_eq!(body.len(), 2);
2521    }
2522
2523    #[test]
2524    fn parses_source_statement_surface() {
2525        let program = parse_ok(concat!(
2526            "globalvar value\n",
2527            "rule \"r\":\n",
2528            "    @Event global\n",
2529            "    del value[1]\n",
2530            "    value min= 2\n",
2531            "    value max= 3\n",
2532            "    while value < 4:\n",
2533            "        continue\n",
2534            "    goto RULE_START\n",
2535            "    goto target\n",
2536            "    goto loc + value\n",
2537            "    target:\n",
2538        ));
2539        let RuleEntry::Rule(rule) = &program.rules[0] else {
2540            panic!("expected rule");
2541        };
2542        assert!(matches!(rule.actions[0], Stmt::Delete { .. }));
2543        for (statement, expected) in [(&rule.actions[1], "min"), (&rule.actions[2], "max")] {
2544            let Stmt::Assign { value, .. } = statement else {
2545                panic!("expected augmented assignment");
2546            };
2547            assert!(matches!(value, Expr::Binary { op, .. } if op == expected));
2548        }
2549        let Stmt::While { body, .. } = &rule.actions[3] else {
2550            panic!("expected while");
2551        };
2552        assert!(matches!(body.as_slice(), [Stmt::Continue { .. }]));
2553        assert!(matches!(
2554            &rule.actions[4],
2555            Stmt::Goto {
2556                label: None,
2557                offset: None,
2558                rule_start: true,
2559                ..
2560            }
2561        ));
2562        assert!(matches!(
2563            &rule.actions[5],
2564            Stmt::Goto {
2565                label: Some(label),
2566                offset: None,
2567                rule_start: false,
2568                ..
2569            } if label == "target"
2570        ));
2571        assert!(matches!(
2572            &rule.actions[6],
2573            Stmt::Goto {
2574                label: None,
2575                offset: Some(_),
2576                rule_start: false,
2577                ..
2578            }
2579        ));
2580        assert!(matches!(&rule.actions[7], Stmt::Label { name, .. } if name == "target"));
2581    }
2582
2583    #[test]
2584    fn rejects_invalid_source_statement_forms() {
2585        for source in [
2586            "rule \"r\":\n    @Event global\n    del value\n",
2587            "rule \"r\":\n    @Event global\n    goto\n",
2588            "rule \"r\":\n    @Event global\n    goto loc\n",
2589            "rule \"r\":\n    @Event global\n    goto target extra\n",
2590            "rule \"r\":\n    @Event global\n    continue now\n",
2591            "rule \"r\":\n    @Event global\n    A = 1; A = 2\n",
2592        ] {
2593            let errors = parse_err(source);
2594            assert!(!errors.is_empty(), "invalid form parsed: {source}");
2595            assert!(errors.iter().all(|error| error.code == "parse-error"));
2596            assert!(errors.iter().all(|error| error.span.is_some()));
2597        }
2598    }
2599
2600    #[test]
2601    fn parses_syntax_constructs() {
2602        let program = parse_ok(
2603            "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",
2604        );
2605        let RuleEntry::Rule(rule) = &program.rules[0] else {
2606            panic!("expected rule");
2607        };
2608        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
2609        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
2610    }
2611
2612    #[test]
2613    fn rejects_incomplete_do_while_and_dictionary_entries() {
2614        let errors = parse_err(
2615            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
2616        );
2617        assert!(!errors.is_empty());
2618        assert!(errors.iter().all(|error| error.code == "parse-error"));
2619    }
2620
2621    #[test]
2622    fn parses_explicit_enum_member_values() {
2623        let program = parse_ok("enum EventType:\n    BUFF = 0\n    DEBUFF\n    MECH = 2\n");
2624        let Decl::Enum { members, .. } = &program.declarations[0] else {
2625            panic!("expected enum");
2626        };
2627        assert_eq!(
2628            members
2629                .iter()
2630                .map(|(name, _)| name.as_str())
2631                .collect::<Vec<_>>(),
2632            ["BUFF", "DEBUFF", "MECH"]
2633        );
2634    }
2635
2636    #[test]
2637    fn parses_multi_line_array() {
2638        let program = parse_ok(
2639            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
2640        );
2641        let RuleEntry::Rule(rule) = &program.rules[0] else {
2642            panic!();
2643        };
2644        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2645            panic!();
2646        };
2647        let Expr::Array { elements, .. } = value else {
2648            panic!("expected array, got {value:?}");
2649        };
2650        assert_eq!(elements.len(), 2);
2651    }
2652
2653    #[test]
2654    fn missing_colon_is_a_structured_error() {
2655        let errors = parse_err("rule \"x\"\n    @Event global\n");
2656        assert!(!errors.is_empty());
2657        assert_eq!(errors[0].code, "parse-error");
2658        assert!(errors[0].span.is_some());
2659    }
2660
2661    #[test]
2662    fn def_and_macro_parse() {
2663        let program = parse_ok(
2664            "subroutine showStatus\n\nmacro VERSION = \"1.4.3\"\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
2665        );
2666        assert_eq!(program.declarations.len(), 3);
2667        assert!(matches!(program.declarations[1], Decl::Constant { .. }));
2668        assert!(matches!(program.declarations[2], Decl::Macro { .. }));
2669        let Decl::Macro { args, body, .. } = &program.declarations[2] else {
2670            panic!();
2671        };
2672        assert_eq!(args, &vec!["value".to_string()]);
2673        assert_eq!(body.len(), 1);
2674    }
2675
2676    #[test]
2677    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
2678        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
2679        let errors = parse_err(text);
2680        assert_eq!(
2681            errors
2682                .iter()
2683                .filter(|error| error.code == "macro-redeclaration")
2684                .count(),
2685            2
2686        );
2687
2688        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2689        let output = parse_with_options(&tokens, true);
2690        assert!(
2691            output.errors.is_empty(),
2692            "unexpected errors: {:?}",
2693            output.errors
2694        );
2695        assert!(output.program.is_some());
2696    }
2697
2698    #[test]
2699    fn multiple_errors_are_reported() {
2700        let errors =
2701            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
2702        assert!(!errors.is_empty());
2703    }
2704
2705    #[test]
2706    fn precedence_parses_python_like() {
2707        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
2708        let RuleEntry::Rule(rule) = &program.rules[0] else {
2709            panic!();
2710        };
2711        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2712            panic!();
2713        };
2714        let Expr::Binary {
2715            op, left, right, ..
2716        } = value
2717        else {
2718            panic!();
2719        };
2720        assert_eq!(op, "+");
2721        let Expr::Binary { op: inner, .. } = right.as_ref() else {
2722            panic!();
2723        };
2724        assert_eq!(inner, "*");
2725        assert!(matches!(left.as_ref(), Expr::Number { .. }));
2726    }
2727
2728    #[test]
2729    fn parses_right_associative_conditional_expressions() {
2730        let program = parse_ok(
2731            "rule \"r\":\n    @Event global\n    debug(1 if true else 2 if false else 3)\n",
2732        );
2733        let RuleEntry::Rule(rule) = &program.rules[0] else {
2734            panic!("expected a rule");
2735        };
2736        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2737            panic!("expected an expression statement");
2738        };
2739        let Expr::Call { args, .. } = expr else {
2740            panic!("expected a call");
2741        };
2742        let Expr::Conditional {
2743            then_value,
2744            condition,
2745            else_value,
2746            span,
2747        } = &args[0].value
2748        else {
2749            panic!("expected a conditional expression");
2750        };
2751        assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
2752        assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
2753        assert!(matches!(
2754            else_value.as_ref(),
2755            Expr::Conditional { then_value, condition, else_value, .. }
2756                if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
2757                    && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2758                    && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2759        ));
2760        assert_eq!(span.start.line, 3);
2761        assert_eq!(span.start.col, 11);
2762    }
2763
2764    #[test]
2765    fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
2766        let program = parse_ok(
2767            "rule \"r\":\n    @Event global\n    debug((1 if true else 2) if false else 3)\n",
2768        );
2769        let RuleEntry::Rule(rule) = &program.rules[0] else {
2770            panic!("expected a rule");
2771        };
2772        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2773            panic!("expected an expression statement");
2774        };
2775        let Expr::Call { args, .. } = expr else {
2776            panic!("expected a call");
2777        };
2778        assert!(matches!(
2779            &args[0].value,
2780            Expr::Conditional {
2781                then_value,
2782                condition,
2783                else_value,
2784                ..
2785            } if matches!(then_value.as_ref(), Expr::Conditional { .. })
2786                && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2787                && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2788        ));
2789
2790        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 if true)\n");
2791        assert_eq!(errors[0].code, "parse-error");
2792        assert!(errors[0].message.contains("expected `else`"));
2793    }
2794
2795    #[test]
2796    fn parses_receiver_calls() {
2797        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
2798        // member access followed by call arguments (#104).
2799        let program =
2800            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
2801        let RuleEntry::Rule(rule) = &program.rules[0] else {
2802            panic!("expected rule");
2803        };
2804        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2805            panic!("expected expression statement, got {:?}", rule.actions[0]);
2806        };
2807        let Expr::ReceiverCall {
2808            receiver,
2809            name,
2810            args,
2811            ..
2812        } = &expr
2813        else {
2814            panic!("expected receiver call, got {expr:?}");
2815        };
2816        assert_eq!(name, "setMoveSpeed");
2817        assert!(
2818            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
2819            "receiver must be the eventPlayer name"
2820        );
2821        assert_eq!(args.len(), 1);
2822        assert!(args[0].keyword.is_none(), "positional argument");
2823        assert!(matches!(&args[0].value, Expr::Number { .. }));
2824    }
2825
2826    #[test]
2827    fn parses_keyword_arguments_with_name_spans() {
2828        // `name = expr` call arguments are keyword arguments carrying the
2829        // name token's exact span (issue #110); comparisons stay positional.
2830        let program =
2831            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
2832        let RuleEntry::Rule(rule) = &program.rules[0] else {
2833            panic!("expected rule");
2834        };
2835        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2836            panic!("expected expression statement");
2837        };
2838        let Expr::Call { args, .. } = expr else {
2839            panic!("expected a call, got {expr:?}");
2840        };
2841        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
2842        assert_eq!(keyword, "time");
2843        assert_eq!(span.start.line, 3);
2844        assert!(matches!(&args[0].value, Expr::Number { .. }));
2845
2846        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
2847            panic!("expected expression statement");
2848        };
2849        let Expr::Call { args, .. } = expr else {
2850            panic!("expected a call, got {expr:?}");
2851        };
2852        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
2853        assert!(matches!(&args[0].value, Expr::Binary { .. }));
2854    }
2855
2856    #[test]
2857    fn adjacent_string_literals_concatenate_and_preserve_span() {
2858        let program = parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\" \"two\")\n");
2859        let RuleEntry::Rule(rule) = &program.rules[0] else {
2860            panic!("expected rule");
2861        };
2862        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2863            panic!("expected expression statement");
2864        };
2865        let Expr::Call { args, .. } = expr else {
2866            panic!("expected call");
2867        };
2868        let Expr::String { value, span } = &args[0].value else {
2869            panic!("expected concatenated string");
2870        };
2871        assert_eq!(value, "onetwo");
2872        assert_eq!(span.start.line, 3);
2873        assert_eq!(span.start.col, 11);
2874        assert_eq!(span.end.col, 22);
2875    }
2876
2877    #[test]
2878    fn multiline_adjacent_string_literals_concatenate_inside_group() {
2879        let program =
2880            parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\"\n        \"two\")\n");
2881        let RuleEntry::Rule(rule) = &program.rules[0] else {
2882            panic!("expected rule");
2883        };
2884        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2885            panic!("expected expression statement");
2886        };
2887        let Expr::Call { args, .. } = expr else {
2888            panic!("expected call");
2889        };
2890        assert!(matches!(
2891            &args[0].value,
2892            Expr::String { value, .. } if value == "onetwo"
2893        ));
2894    }
2895
2896    #[test]
2897    fn newline_outside_group_keeps_adjacent_literals_as_statements() {
2898        let program = parse_ok("rule \"r\":\n    @Event global\n    \"one\"\n    \"two\"\n");
2899        let RuleEntry::Rule(rule) = &program.rules[0] else {
2900            panic!("expected rule");
2901        };
2902        assert_eq!(rule.actions.len(), 2);
2903    }
2904
2905    #[test]
2906    fn non_name_keyword_lhs_is_a_parse_error() {
2907        // `f(1 = 2)` is not a call argument form; rejected explicitly.
2908        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
2909        assert!(!errors.is_empty());
2910        assert_eq!(errors[0].code, "parse-error");
2911    }
2912
2913    #[test]
2914    fn parses_member_call_on_call_result() {
2915        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
2916        // followed by call arguments on a call result stays a receiver call.
2917        let program = parse_ok(
2918            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
2919        );
2920        let RuleEntry::Rule(rule) = &program.rules[0] else {
2921            panic!("expected rule");
2922        };
2923        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2924            panic!("expected expression statement");
2925        };
2926        let Expr::ReceiverCall {
2927            receiver,
2928            name,
2929            args,
2930            ..
2931        } = &expr
2932        else {
2933            panic!("expected receiver call, got {expr:?}");
2934        };
2935        assert_eq!(name, "setStatusEffect");
2936        assert!(
2937            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
2938            "receiver must be the preceding call"
2939        );
2940        assert_eq!(args.len(), 2);
2941    }
2942
2943    #[test]
2944    fn member_without_call_is_not_a_call() {
2945        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
2946        // access; only a following `(` turns it into a receiver call.
2947        let program =
2948            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
2949        let RuleEntry::Rule(rule) = &program.rules[0] else {
2950            panic!("expected rule");
2951        };
2952        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2953            panic!("expected assignment");
2954        };
2955        assert!(matches!(
2956            &value,
2957            Expr::Member { member, .. } if member == "moveSpeed"
2958        ));
2959    }
2960
2961    #[test]
2962    fn parses_advanced_rule_annotations_with_source_arguments() {
2963        let program = parse_ok(
2964            "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",
2965        );
2966        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
2967            panic!("expected subroutine");
2968        };
2969        assert_eq!(annotations.len(), 2);
2970        let RuleEntry::Rule(rule) = &program.rules[1] else {
2971            panic!("expected rule");
2972        };
2973        assert!(rule.disabled);
2974        assert!(rule.delimiter);
2975        assert_eq!(rule.new_page.as_deref(), Some("Page"));
2976        assert_eq!(rule.annotations.len(), 7);
2977        assert_eq!(rule.annotations[1].args[0].text, "1");
2978        assert_eq!(rule.annotations[2].args[0].text, "dmon");
2979    }
2980}