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