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