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,
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 is_ident(&self, text: &str) -> bool {
106        self.peek_kind() == TokenKind::Ident && self.peek().text == text
107    }
108
109    fn expect_ident(&mut self, what: &str) -> Result<String, ()> {
110        if self.peek_kind() == TokenKind::Ident {
111            Ok(self.advance().text)
112        } else {
113            self.error_at_current(format!("expected {what}"));
114            Err(())
115        }
116    }
117
118    fn expect(&mut self, kind: TokenKind, what: &str) -> Result<Token, ()> {
119        if self.peek_kind() == kind {
120            Ok(self.advance())
121        } else {
122            self.error_at_current(format!("expected {what}"));
123            Err(())
124        }
125    }
126
127    fn error_at_current(&mut self, message: String) {
128        let span = self.peek().span;
129        self.errors.push(OpyError::at("parse-error", message, span));
130    }
131
132    // ---- program ----
133
134    fn parse_program(&mut self) -> Program {
135        let mut declarations = Vec::new();
136        let mut rules = Vec::new();
137        loop {
138            self.skip_newlines();
139            if self.peek_kind() == TokenKind::Eof {
140                break;
141            }
142            let rule_prefix = if self.peek_kind() == TokenKind::RulePrefixMarker {
143                Some(self.advance().text)
144            } else {
145                None
146            };
147            let ok = self.parse_top_level(&mut declarations, &mut rules, rule_prefix);
148            if !ok {
149                self.recover_line();
150            }
151        }
152        Program {
153            declarations,
154            rules,
155            settings: None,
156        }
157    }
158
159    fn parse_top_level(
160        &mut self,
161        declarations: &mut Vec<Decl>,
162        rules: &mut Vec<RuleEntry>,
163        rule_prefix: Option<String>,
164    ) -> bool {
165        let token = self.peek();
166        if token.kind == TokenKind::Ident {
167            match token.text.as_str() {
168                "rule" => return self.parse_rule(rules, rule_prefix),
169                "def" => return self.parse_def(rules, rule_prefix),
170                "globalvar" => return self.parse_variable(declarations, true),
171                "playervar" => return self.parse_variable(declarations, false),
172                "subroutine" => return self.parse_subroutine(declarations),
173                "enum" => return self.parse_enum(declarations),
174                "macro" => return self.parse_macro(declarations),
175                _ => {}
176            }
177        }
178        self.error_at_current(format!(
179            "expected a top-level declaration (rule/def/globalvar/playervar/subroutine/enum/macro) but found '{}'",
180            token.text
181        ));
182        false
183    }
184
185    /// Skip to the end of the current line (error recovery).
186    fn recover_line(&mut self) {
187        while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
188            self.advance();
189        }
190    }
191
192    // ---- declarations ----
193
194    fn parse_variable(&mut self, declarations: &mut Vec<Decl>, global: bool) -> bool {
195        let start = self.advance(); // `globalvar`/`playervar`
196        // The name token follows the keyword; its span is the exact declared
197        // identifier occurrence (rename targets, not the keyword/statement).
198        let name_token = self.peek().clone();
199        let name = match self.expect_ident("a variable name after the keyword") {
200            Ok(name) => name,
201            Err(()) => return false,
202        };
203        let name_span = if name_token.kind == TokenKind::Ident {
204            name_token.span
205        } else {
206            start.span
207        };
208        let mut index = None;
209        let mut initializer = None;
210        if self.peek_kind() == TokenKind::Assign {
211            self.advance();
212            match self.parse_expr() {
213                Ok(expr) => initializer = Some(expr),
214                Err(()) => return false,
215            }
216        } else if self.peek_kind() == TokenKind::Number {
217            // `globalvar cakePos 100`: an explicit Workshop variable index.
218            let token = self.advance();
219            index = token.text.parse::<u32>().ok();
220            if index.is_none() {
221                self.errors.push(OpyError::at(
222                    "parse-error",
223                    format!(
224                        "invalid variable index '{}' (expected an integer)",
225                        token.text
226                    ),
227                    token.span,
228                ));
229                return false;
230            }
231        } else if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
232            self.error_at_current(
233                "expected '=', an integer index, or end of line after the variable name"
234                    .to_string(),
235            );
236            return false;
237        }
238        let end = self.peek().span.start;
239        let span = Span::new(start.span.file, start.span.start, end);
240        let decl = if global {
241            Decl::GlobalVariable {
242                name,
243                index,
244                span,
245                name_span,
246                initializer,
247            }
248        } else {
249            Decl::PlayerVariable {
250                name,
251                index,
252                span,
253                name_span,
254                initializer,
255            }
256        };
257        declarations.push(decl);
258        true
259    }
260
261    fn parse_subroutine(&mut self, declarations: &mut Vec<Decl>) -> bool {
262        let start = self.advance();
263        // The name token follows the `subroutine` keyword; its span is the
264        // exact declared identifier occurrence.
265        let name_token = self.peek().clone();
266        let name = match self.expect_ident("a subroutine name") {
267            Ok(name) => name,
268            Err(()) => return false,
269        };
270        let name_span = if name_token.kind == TokenKind::Ident {
271            name_token.span
272        } else {
273            start.span
274        };
275        let end = self.peek().span.start;
276        declarations.push(Decl::Subroutine {
277            name,
278            span: Span::new(start.span.file, start.span.start, end),
279            name_span,
280        });
281        true
282    }
283
284    fn parse_enum(&mut self, declarations: &mut Vec<Decl>) -> bool {
285        let start = self.advance();
286        let name = match self.expect_ident("an enum name") {
287            Ok(name) => name,
288            Err(()) => return false,
289        };
290        if self
291            .expect(TokenKind::Colon, "':' after the enum name")
292            .is_err()
293        {
294            return false;
295        }
296        let line_indent = start.span.start.col;
297        let body_indent = match self.block_indent(line_indent) {
298            Some(indent) => indent,
299            None => return false,
300        };
301        let mut members = Vec::new();
302        loop {
303            self.skip_newlines();
304            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
305                break;
306            }
307            if self.peek_kind() == TokenKind::Ident {
308                let member = self.advance();
309                let member_span = member.span;
310                if !self.allow_macro_redeclaration
311                    && members.iter().any(|(name, _)| name == &member.text)
312                {
313                    self.errors.push(OpyError::at(
314                        "macro-redeclaration",
315                        format!("enum member '{name}.{}' is already defined", member.text),
316                        member_span,
317                    ));
318                }
319                members.push((member.text, member_span));
320            } else {
321                self.error_at_current("expected an enum member name".to_string());
322                self.recover_line();
323                continue;
324            }
325            if self.peek_kind() == TokenKind::Comma {
326                self.advance();
327            } else {
328                // A member must end the line (or be comma-separated).
329                if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
330                    self.error_at_current("expected ',' after the enum member".to_string());
331                    self.recover_line();
332                    continue;
333                }
334            }
335        }
336        declarations.push(Decl::Enum {
337            name,
338            members,
339            span: start.span,
340        });
341        true
342    }
343
344    fn parse_macro(&mut self, declarations: &mut Vec<Decl>) -> bool {
345        let start = self.advance();
346        let name_token = self.peek().clone();
347        let name = match self.expect_ident("a macro name") {
348            Ok(name) => name,
349            Err(()) => return false,
350        };
351        let args = match self.parse_param_list() {
352            Some(args) => args,
353            None => return false,
354        };
355        if self
356            .expect(TokenKind::Colon, "':' after the macro signature")
357            .is_err()
358        {
359            return false;
360        }
361        let line_indent = start.span.start.col;
362        let body_indent = match self.block_indent(line_indent) {
363            Some(indent) => indent,
364            None => return false,
365        };
366        let body = self.parse_block(body_indent);
367        if !self.allow_macro_redeclaration
368            && declarations.iter().any(|declaration| {
369                matches!(declaration, Decl::Macro { name: existing, .. } if existing == &name)
370            })
371        {
372            self.errors.push(OpyError::at(
373                "macro-redeclaration",
374                format!("macro '{name}' is already defined"),
375                name_token.span,
376            ));
377        }
378        declarations.push(Decl::Macro {
379            name,
380            args,
381            body,
382            span: start.span,
383        });
384        true
385    }
386
387    fn parse_param_list(&mut self) -> Option<Vec<String>> {
388        if self.expect(TokenKind::LParen, "'('").is_err() {
389            return None;
390        }
391        let mut params = Vec::new();
392        self.skip_newlines();
393        if self.peek_kind() == TokenKind::RParen {
394            self.advance();
395            return Some(params);
396        }
397        loop {
398            match self.expect_ident("a parameter name") {
399                Ok(name) => params.push(name),
400                Err(()) => return None,
401            }
402            self.skip_newlines();
403            if self.peek_kind() == TokenKind::Comma {
404                self.advance();
405                self.skip_newlines();
406            } else {
407                break;
408            }
409        }
410        if self.expect(TokenKind::RParen, "')'").is_err() {
411            return None;
412        }
413        Some(params)
414    }
415
416    // ---- rules and definitions ----
417
418    fn parse_rule(&mut self, rules: &mut Vec<RuleEntry>, rule_prefix: Option<String>) -> bool {
419        let start = self.advance();
420        let name = match self.peek_kind() {
421            TokenKind::String => self.advance().text,
422            _ => {
423                self.error_at_current("expected a rule name string after `rule`".to_string());
424                return false;
425            }
426        };
427        let name_token_span = self.tokens[self.pos.saturating_sub(1)].span;
428        // The exact rule-name occurrence is the string content between the
429        // quotes (the `"name"` token itself spans the quotes).
430        let name_span = Span::new(
431            name_token_span.file,
432            Position::new(name_token_span.start.line, name_token_span.start.col + 1),
433            Position::new(
434                name_token_span.end.line,
435                name_token_span
436                    .end
437                    .col
438                    .saturating_sub(1)
439                    .max(name_token_span.start.col + 1),
440            ),
441        );
442        if self
443            .expect(TokenKind::Colon, "':' after the rule name")
444            .is_err()
445        {
446            return false;
447        }
448        let line_indent = start.span.start.col;
449        let body_indent = match self.block_indent(line_indent) {
450            Some(indent) => indent,
451            None => return false,
452        };
453        let mut event = None;
454        let mut conditions = Vec::new();
455        let mut annotations = Vec::new();
456        let mut disabled = false;
457        let mut delimiter = false;
458        let mut new_page = None;
459        let mut actions = Vec::new();
460        loop {
461            self.skip_newlines();
462            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
463                break;
464            }
465            if self.peek_kind() == TokenKind::At {
466                if !self.parse_directive(
467                    &mut event,
468                    &mut conditions,
469                    &mut annotations,
470                    &mut disabled,
471                    &mut delimiter,
472                    &mut new_page,
473                    false,
474                ) {
475                    self.recover_line();
476                }
477                continue;
478            }
479            match self.parse_statement() {
480                Ok(stmt) => actions.push(stmt),
481                Err(()) => self.recover_line(),
482            }
483        }
484        rules.push(RuleEntry::Rule(Rule {
485            name,
486            span: Span::new(start.span.file, start.span.start, name_token_span.end),
487            name_span,
488            disabled,
489            delimiter,
490            new_page,
491            annotations,
492            rule_prefix,
493            event: event.unwrap_or_else(|| Event {
494                name: "global".to_string(),
495                args: Vec::new(),
496                span: start.span,
497            }),
498            conditions,
499            actions,
500        }));
501        true
502    }
503
504    #[allow(clippy::too_many_arguments)]
505    fn parse_directive(
506        &mut self,
507        event: &mut Option<Event>,
508        conditions: &mut Vec<Expr>,
509        annotations: &mut Vec<Annotation>,
510        disabled: &mut bool,
511        delimiter: &mut bool,
512        new_page: &mut Option<String>,
513        subroutine: bool,
514    ) -> bool {
515        let at = self.advance();
516        let name = match self.expect_ident("a directive name after '@'") {
517            Ok(name) => name,
518            Err(()) => return false,
519        };
520        if matches!(
521            name.as_str(),
522            "Event" | "Team" | "Slot" | "Hero" | "Name" | "Disabled" | "Delimiter" | "NewPage"
523        ) && annotations.iter().any(|annotation| annotation.name == name)
524        {
525            self.error_at_current(format!("annotation '@{name}' was already declared"));
526            return false;
527        }
528        match name.as_str() {
529            "Event" => {
530                let event_name = match self.expect_ident("an event name after @Event") {
531                    Ok(name) => name,
532                    Err(()) => return false,
533                };
534                let event_annotation_arg = AnnotationArg {
535                    text: event_name.clone(),
536                    span: self.tokens[self.pos.saturating_sub(1)].span,
537                };
538                let mut args = Vec::new();
539                if self.peek_kind() == TokenKind::LParen
540                    && self.parse_event_args(&mut args).is_err()
541                {
542                    return false;
543                }
544                let end = self.peek().span.start;
545                *event = Some(Event {
546                    name: event_name,
547                    args,
548                    span: Span::new(at.span.file, at.span.start, end),
549                });
550                annotations.push(Annotation {
551                    name,
552                    args: vec![event_annotation_arg],
553                    span: Span::new(at.span.file, at.span.start, end),
554                });
555                true
556            }
557            "Condition" => {
558                let start = self.pos;
559                match self.parse_expr() {
560                    Ok(expr) => {
561                        let end = self.peek().span.start;
562                        conditions.push(expr);
563                        annotations.push(Annotation {
564                            name,
565                            args: vec![self.raw_annotation_arg(start, self.pos)],
566                            span: Span::new(at.span.file, at.span.start, end),
567                        });
568                        true
569                    }
570                    Err(()) => false,
571                }
572            }
573            "Team" | "Slot" | "Hero" => {
574                let args = self.consume_annotation_args();
575                if args.len() != 1 {
576                    self.error_at_current(format!("@{name} expects exactly one argument"));
577                    return false;
578                }
579                if subroutine {
580                    self.error_at_current(format!("@{name} is not valid on a subroutine"));
581                    return false;
582                }
583                if (name == "Slot"
584                    && annotations
585                        .iter()
586                        .any(|annotation| annotation.name == "Hero"))
587                    || (name == "Hero"
588                        && annotations
589                            .iter()
590                            .any(|annotation| annotation.name == "Slot"))
591                {
592                    self.error_at_current("@Slot and @Hero cannot be used together".to_string());
593                    return false;
594                }
595                let end = self.peek().span.start;
596                annotations.push(Annotation {
597                    name,
598                    args,
599                    span: Span::new(at.span.file, at.span.start, end),
600                });
601                true
602            }
603            "Name" => {
604                let args = self.consume_annotation_args();
605                if args.len() != 1 || !self.annotation_arg_is_string(&args[0]) {
606                    self.error_at_current(
607                        "@Name expects exactly one plain string literal".to_string(),
608                    );
609                    return false;
610                }
611                if !subroutine {
612                    self.error_at_current(
613                        "@Name is only supported on subroutine definitions".to_string(),
614                    );
615                    return false;
616                }
617                let end = self.peek().span.start;
618                annotations.push(Annotation {
619                    name,
620                    args,
621                    span: Span::new(at.span.file, at.span.start, end),
622                });
623                true
624            }
625            "SuppressWarnings" => {
626                let args = self.consume_annotation_args();
627                if args.is_empty() || args.iter().any(|arg| !is_identifier(&arg.text)) {
628                    self.error_at_current(
629                        "@SuppressWarnings expects one or more warning identifiers".to_string(),
630                    );
631                    return false;
632                }
633                let end = self.peek().span.start;
634                annotations.push(Annotation {
635                    name,
636                    args,
637                    span: Span::new(at.span.file, at.span.start, end),
638                });
639                true
640            }
641            "Disabled" => {
642                if !self.expect_annotation_end("@Disabled") {
643                    return false;
644                }
645                *disabled = true;
646                annotations.push(Annotation {
647                    name,
648                    args: Vec::new(),
649                    span: at.span,
650                });
651                true
652            }
653            "Delimiter" => {
654                if !self.expect_annotation_end("@Delimiter") {
655                    return false;
656                }
657                *delimiter = true;
658                annotations.push(Annotation {
659                    name,
660                    args: Vec::new(),
661                    span: at.span,
662                });
663                true
664            }
665            "NewPage" => {
666                let args = self.consume_annotation_args();
667                if args.len() > 1
668                    || args
669                        .first()
670                        .is_some_and(|arg| !self.annotation_arg_is_string(arg))
671                {
672                    self.error_at_current(
673                        "@NewPage expects at most one plain string literal".to_string(),
674                    );
675                    return false;
676                }
677                let end = self.peek().span.start;
678                *new_page = args.first().map(|arg| unquote_annotation_arg(&arg.text));
679                annotations.push(Annotation {
680                    name,
681                    args,
682                    span: Span::new(at.span.file, at.span.start, end),
683                });
684                true
685            }
686            other => {
687                self.error_at_current(format!("unsupported directive '@{other}'"));
688                false
689            }
690        }
691    }
692
693    fn consume_annotation_args(&mut self) -> Vec<AnnotationArg> {
694        let start = self.pos;
695        while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
696            self.advance();
697        }
698        if self.pos == start {
699            return Vec::new();
700        }
701        let tokens = &self.tokens[start..self.pos];
702        if tokens.len() == 3
703            && tokens[1].kind == TokenKind::Dot
704            && tokens[0].kind == TokenKind::Ident
705        {
706            return vec![AnnotationArg {
707                text: tokens.iter().map(|token| token.text.as_str()).collect(),
708                span: Span::new(
709                    tokens[0].span.file,
710                    tokens[0].span.start,
711                    tokens[2].span.end,
712                ),
713            }];
714        }
715        tokens
716            .iter()
717            .map(|token| AnnotationArg {
718                text: if token.kind == TokenKind::String {
719                    format!("\"{}\"", token.text)
720                } else {
721                    token.text.clone()
722                },
723                span: token.span,
724            })
725            .collect()
726    }
727
728    fn raw_annotation_arg(&self, start: usize, end: usize) -> AnnotationArg {
729        let tokens = &self.tokens[start..end];
730        let first = tokens
731            .first()
732            .map(|token| token.span)
733            .unwrap_or(self.peek().span);
734        let last = tokens.last().map(|token| token.span).unwrap_or(first);
735        AnnotationArg {
736            text: tokens.iter().map(|token| token.text.as_str()).collect(),
737            span: Span::new(first.file, first.start, last.end),
738        }
739    }
740
741    fn annotation_arg_is_string(&self, arg: &AnnotationArg) -> bool {
742        arg.text.starts_with('"') && arg.text.ends_with('"')
743    }
744
745    fn expect_annotation_end(&mut self, name: &str) -> bool {
746        if self.peek_kind() == TokenKind::Newline || self.peek_kind() == TokenKind::Eof {
747            true
748        } else {
749            self.error_at_current(format!("{name} takes no arguments"));
750            false
751        }
752    }
753
754    fn parse_def(&mut self, rules: &mut Vec<RuleEntry>, rule_prefix: Option<String>) -> bool {
755        let start = self.advance();
756        // The name token follows the `def` keyword. `span` covers the
757        // definition (`def name`), and `name_span` is the exact identifier
758        // occurrence (rename targets, not the keyword).
759        let name_token = self.peek().clone();
760        let name = match self.expect_ident("a subroutine name after `def`") {
761            Ok(name) => name,
762            Err(()) => return false,
763        };
764        let name_span = if name_token.kind == TokenKind::Ident {
765            name_token.span
766        } else {
767            start.span
768        };
769        let params = match self.parse_param_list() {
770            Some(params) => params,
771            None => return false,
772        };
773        if !params.is_empty() {
774            self.error_at_current(
775                "subroutine parameters are outside the declared support matrix".to_string(),
776            );
777            return false;
778        }
779        if self
780            .expect(TokenKind::Colon, "':' after the subroutine signature")
781            .is_err()
782        {
783            return false;
784        }
785        let line_indent = start.span.start.col;
786        let body_indent = match self.block_indent(line_indent) {
787            Some(indent) => indent,
788            None => return false,
789        };
790        let mut annotations = Vec::new();
791        let mut event = None;
792        let mut conditions = Vec::new();
793        let mut disabled = false;
794        let mut delimiter = false;
795        let mut new_page = None;
796        loop {
797            self.skip_newlines();
798            if self.peek_kind() != TokenKind::At {
799                break;
800            }
801            if !self.parse_directive(
802                &mut event,
803                &mut conditions,
804                &mut annotations,
805                &mut disabled,
806                &mut delimiter,
807                &mut new_page,
808                true,
809            ) {
810                self.recover_line();
811                return false;
812            }
813        }
814        if event.is_some() || !conditions.is_empty() {
815            self.error_at_current("subroutines cannot have events or conditions".to_string());
816            return false;
817        }
818        let _ = (disabled, delimiter, new_page);
819        let presentation_name = annotations
820            .iter()
821            .find(|annotation| annotation.name == "Name")
822            .and_then(|annotation| annotation.args.first())
823            .map(|arg| unquote_annotation_arg(&arg.text));
824        let body = self.parse_block(body_indent);
825        let span = if name_token.kind == TokenKind::Ident {
826            Span::new(start.span.file, start.span.start, name_token.span.end)
827        } else {
828            start.span
829        };
830        rules.push(RuleEntry::SubroutineDef {
831            name,
832            presentation_name,
833            span,
834            name_span,
835            body,
836            annotations,
837            rule_prefix,
838        });
839        true
840    }
841
842    /// The indentation of the next non-empty line, which must exceed
843    /// `line_indent` (an indented block follows the colon).
844    fn block_indent(&mut self, line_indent: u32) -> Option<u32> {
845        self.skip_newlines();
846        if self.peek_kind() == TokenKind::Eof {
847            self.error_at_current("expected an indented block".to_string());
848            return None;
849        }
850        let indent = self.peek().span.start.col;
851        if indent <= line_indent {
852            self.error_at_current("expected an indented block after ':'".to_string());
853            return None;
854        }
855        Some(indent)
856    }
857
858    // ---- statements ----
859
860    fn parse_block(&mut self, block_indent: u32) -> Vec<Stmt> {
861        let mut stmts = Vec::new();
862        loop {
863            self.skip_newlines();
864            if self.peek_kind() == TokenKind::Eof {
865                break;
866            }
867            if self.peek().span.start.col < block_indent {
868                break;
869            }
870            if self.peek().span.start.col > block_indent {
871                // A deeper indent without an introducer: recover by line.
872                self.error_at_current("unexpected indentation".to_string());
873                self.recover_line();
874                continue;
875            }
876            match self.parse_statement() {
877                Ok(stmt) => stmts.push(stmt),
878                Err(()) => self.recover_line(),
879            }
880        }
881        stmts
882    }
883
884    fn parse_statement(&mut self) -> Result<Stmt, ()> {
885        let token = self.peek();
886        if token.kind == TokenKind::Ident {
887            match token.text.as_str() {
888                "if" => return self.parse_if(),
889                "for" => return self.parse_for(),
890                "while" => return self.parse_while(),
891                "do" => return self.parse_do_while(),
892                "switch" => return self.parse_switch(),
893                "break" => {
894                    let token = self.advance();
895                    return Ok(Stmt::Break { span: token.span });
896                }
897                "pass" => {
898                    let start = self.advance();
899                    return Ok(Stmt::Pass { span: start.span });
900                }
901                _ => {}
902            }
903        }
904        self.parse_expr_statement()
905    }
906
907    fn parse_expr_statement(&mut self) -> Result<Stmt, ()> {
908        let start = self.peek().span;
909        let expr = self.parse_expr()?;
910        match self.peek_kind() {
911            TokenKind::Assign => {
912                self.advance();
913                let value = self.parse_expr()?;
914                let end = self.peek().span.start;
915                Ok(Stmt::Assign {
916                    target: expr,
917                    value,
918                    span: Span::new(start.file, start.start, end),
919                })
920            }
921            TokenKind::PlusAssign
922            | TokenKind::MinusAssign
923            | TokenKind::StarAssign
924            | TokenKind::SlashAssign
925            | TokenKind::PercentAssign
926            | TokenKind::DoubleStarAssign => {
927                let op = match self.peek_kind() {
928                    TokenKind::PlusAssign => "+",
929                    TokenKind::MinusAssign => "-",
930                    TokenKind::StarAssign => "*",
931                    TokenKind::SlashAssign => "/",
932                    TokenKind::PercentAssign => "%",
933                    TokenKind::DoubleStarAssign => "**",
934                    _ => unreachable!(),
935                }
936                .to_string();
937                self.advance();
938                let rhs = self.parse_expr()?;
939                let end = self.peek().span.start;
940                let value = Expr::Binary {
941                    op,
942                    left: Box::new(expr.clone()),
943                    right: Box::new(rhs),
944                    span: Span::new(start.file, start.start, end),
945                };
946                Ok(Stmt::Assign {
947                    target: expr,
948                    value,
949                    span: Span::new(start.file, start.start, end),
950                })
951            }
952            _ => {
953                let end = self.peek().span.start;
954                Ok(Stmt::Expr {
955                    expr,
956                    span: Span::new(start.file, start.start, end),
957                })
958            }
959        }
960    }
961
962    fn parse_if(&mut self) -> Result<Stmt, ()> {
963        let start = self.advance();
964        let line_indent = start.span.start.col;
965        let condition = self.parse_expr()?;
966        if self
967            .expect(TokenKind::Colon, "':' after the if condition")
968            .is_err()
969        {
970            return Err(());
971        }
972        let body_indent = self.block_indent(line_indent).ok_or(())?;
973        let body = self.parse_block(body_indent);
974        let mut branches = vec![IfBranch { condition, body }];
975        let mut r#else = None;
976        loop {
977            let save = self.pos;
978            self.skip_newlines();
979            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col != line_indent {
980                self.pos = save;
981                break;
982            }
983            if self.is_ident("elif") {
984                self.advance();
985                let condition = match self.parse_expr() {
986                    Ok(expr) => expr,
987                    Err(()) => return Err(()),
988                };
989                if self
990                    .expect(TokenKind::Colon, "':' after the elif condition")
991                    .is_err()
992                {
993                    return Err(());
994                }
995                let body_indent = self.block_indent(line_indent).ok_or(())?;
996                let body = self.parse_block(body_indent);
997                branches.push(IfBranch { condition, body });
998            } else if self.is_ident("else") {
999                self.advance();
1000                if self.expect(TokenKind::Colon, "':' after `else`").is_err() {
1001                    return Err(());
1002                }
1003                let body_indent = self.block_indent(line_indent).ok_or(())?;
1004                let body = self.parse_block(body_indent);
1005                r#else = Some(body);
1006                break;
1007            } else {
1008                self.pos = save;
1009                break;
1010            }
1011        }
1012        Ok(Stmt::If {
1013            branches,
1014            r#else,
1015            span: start.span,
1016        })
1017    }
1018
1019    fn parse_for(&mut self) -> Result<Stmt, ()> {
1020        let start = self.advance();
1021        let variable = self.parse_primary()?;
1022        if !self.is_ident("in") {
1023            self.error_at_current("expected `in` in the for statement".to_string());
1024            return Err(());
1025        }
1026        self.advance();
1027        let iterable = self.parse_expr()?;
1028        if self
1029            .expect(TokenKind::Colon, "':' after the for header")
1030            .is_err()
1031        {
1032            return Err(());
1033        }
1034        let line_indent = start.span.start.col;
1035        let body_indent = self.block_indent(line_indent).ok_or(())?;
1036        let body = self.parse_block(body_indent);
1037        Ok(Stmt::For {
1038            variable,
1039            iterable,
1040            body,
1041            span: start.span,
1042        })
1043    }
1044
1045    fn parse_while(&mut self) -> Result<Stmt, ()> {
1046        let start = self.advance();
1047        let condition = self.parse_expr()?;
1048        if self
1049            .expect(TokenKind::Colon, "':' after the while condition")
1050            .is_err()
1051        {
1052            return Err(());
1053        }
1054        let line_indent = start.span.start.col;
1055        let body_indent = self.block_indent(line_indent).ok_or(())?;
1056        let body = self.parse_block(body_indent);
1057        Ok(Stmt::While {
1058            condition,
1059            body,
1060            span: start.span,
1061        })
1062    }
1063
1064    fn parse_do_while(&mut self) -> Result<Stmt, ()> {
1065        let start = self.advance();
1066        if self.expect(TokenKind::Colon, "':' after `do`").is_err() {
1067            return Err(());
1068        }
1069        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1070        let body = self.parse_block(body_indent);
1071        if !self.is_ident("while") {
1072            self.error_at_current("expected `while` after the do block".to_string());
1073            return Err(());
1074        }
1075        self.advance();
1076        let condition = self.parse_expr()?;
1077        if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
1078            self.error_at_current("expected the end of the do-while condition".to_string());
1079            return Err(());
1080        }
1081        Ok(Stmt::DoWhile {
1082            condition,
1083            body,
1084            span: start.span,
1085        })
1086    }
1087
1088    fn parse_switch(&mut self) -> Result<Stmt, ()> {
1089        let start = self.advance();
1090        let value = self.parse_expr()?;
1091        if self
1092            .expect(TokenKind::Colon, "':' after the switch value")
1093            .is_err()
1094        {
1095            return Err(());
1096        }
1097        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1098        let mut arms = Vec::new();
1099        loop {
1100            self.skip_newlines();
1101            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
1102                break;
1103            }
1104            if self.peek().span.start.col != body_indent {
1105                self.error_at_current("unexpected indentation in switch".to_string());
1106                self.recover_line();
1107                continue;
1108            }
1109            if self.is_ident("case") {
1110                let case_start = self.advance();
1111                let case_value = self.parse_expr()?;
1112                if self
1113                    .expect(TokenKind::Colon, "':' after the case value")
1114                    .is_err()
1115                {
1116                    return Err(());
1117                }
1118                let case_body_indent = self.block_indent(body_indent).ok_or(())?;
1119                let body = self.parse_block(case_body_indent);
1120                arms.push(SwitchArm::Case {
1121                    value: case_value,
1122                    body,
1123                    span: case_start.span,
1124                });
1125            } else if self.is_ident("default") {
1126                let default_start = self.advance();
1127                if self
1128                    .expect(TokenKind::Colon, "':' after `default`")
1129                    .is_err()
1130                {
1131                    return Err(());
1132                }
1133                let default_body_indent = self.block_indent(body_indent).ok_or(())?;
1134                arms.push(SwitchArm::Default {
1135                    body: self.parse_block(default_body_indent),
1136                    span: default_start.span,
1137                });
1138                if default_start.span.start.col != body_indent {
1139                    self.error_at_current("invalid default indentation".to_string());
1140                    return Err(());
1141                }
1142            } else {
1143                self.error_at_current("expected `case` or `default` in switch".to_string());
1144                self.recover_line();
1145            }
1146        }
1147        if arms.is_empty() {
1148            self.errors.push(OpyError::at(
1149                "parse-error",
1150                "switch must contain at least one case or default arm".to_string(),
1151                start.span,
1152            ));
1153            return Err(());
1154        }
1155        Ok(Stmt::Switch {
1156            value,
1157            arms,
1158            span: start.span,
1159        })
1160    }
1161
1162    // ---- expressions ----
1163
1164    fn parse_expr(&mut self) -> Result<Expr, ()> {
1165        self.parse_or()
1166    }
1167
1168    fn parse_or(&mut self) -> Result<Expr, ()> {
1169        let mut left = self.parse_and()?;
1170        while self.is_ident("or") {
1171            self.advance();
1172            let right = self.parse_and()?;
1173            let span = Span::new(left.span().file, left.span().start, right.span().end);
1174            left = Expr::Binary {
1175                op: "or".to_string(),
1176                left: Box::new(left),
1177                right: Box::new(right),
1178                span,
1179            };
1180        }
1181        Ok(left)
1182    }
1183
1184    fn parse_and(&mut self) -> Result<Expr, ()> {
1185        let mut left = self.parse_not()?;
1186        while self.is_ident("and") {
1187            self.advance();
1188            let right = self.parse_not()?;
1189            let span = Span::new(left.span().file, left.span().start, right.span().end);
1190            left = Expr::Binary {
1191                op: "and".to_string(),
1192                left: Box::new(left),
1193                right: Box::new(right),
1194                span,
1195            };
1196        }
1197        Ok(left)
1198    }
1199
1200    fn parse_not(&mut self) -> Result<Expr, ()> {
1201        if self.is_ident("not") {
1202            let start = self.advance();
1203            let operand = self.parse_not()?;
1204            let end = operand.span().end;
1205            return Ok(Expr::Unary {
1206                op: "not".to_string(),
1207                operand: Box::new(operand),
1208                span: Span::new(start.span.file, start.span.start, end),
1209            });
1210        }
1211        self.parse_comparison()
1212    }
1213
1214    fn parse_comparison(&mut self) -> Result<Expr, ()> {
1215        let mut left = self.parse_additive()?;
1216        loop {
1217            let op = match self.peek_kind() {
1218                TokenKind::Eq => "==",
1219                TokenKind::Ne => "!=",
1220                TokenKind::Lt => "<",
1221                TokenKind::Le => "<=",
1222                TokenKind::Gt => ">",
1223                TokenKind::Ge => ">=",
1224                _ if self.is_ident("in") => "in",
1225                _ if self.is_ident("not") && self.peek_at(1).text == "in" => "not in",
1226                _ => break,
1227            };
1228            self.advance();
1229            if op == "not in" {
1230                self.advance();
1231            }
1232            let right = self.parse_additive()?;
1233            let span = Span::new(left.span().file, left.span().start, right.span().end);
1234            left = Expr::Binary {
1235                op: op.to_string(),
1236                left: Box::new(left),
1237                right: Box::new(right),
1238                span,
1239            };
1240        }
1241        Ok(left)
1242    }
1243
1244    fn parse_additive(&mut self) -> Result<Expr, ()> {
1245        let mut left = self.parse_multiplicative()?;
1246        loop {
1247            let op = match self.peek_kind() {
1248                TokenKind::Plus => "+",
1249                TokenKind::Minus => "-",
1250                _ => break,
1251            };
1252            self.advance();
1253            let right = self.parse_multiplicative()?;
1254            let span = Span::new(left.span().file, left.span().start, right.span().end);
1255            left = Expr::Binary {
1256                op: op.to_string(),
1257                left: Box::new(left),
1258                right: Box::new(right),
1259                span,
1260            };
1261        }
1262        Ok(left)
1263    }
1264
1265    fn parse_multiplicative(&mut self) -> Result<Expr, ()> {
1266        let mut left = self.parse_unary()?;
1267        loop {
1268            let op = match self.peek_kind() {
1269                TokenKind::Star => "*",
1270                TokenKind::Slash => "/",
1271                TokenKind::Percent => "%",
1272                _ => break,
1273            };
1274            self.advance();
1275            let right = self.parse_unary()?;
1276            let span = Span::new(left.span().file, left.span().start, right.span().end);
1277            left = Expr::Binary {
1278                op: op.to_string(),
1279                left: Box::new(left),
1280                right: Box::new(right),
1281                span,
1282            };
1283        }
1284        Ok(left)
1285    }
1286
1287    fn parse_unary(&mut self) -> Result<Expr, ()> {
1288        if self.peek_kind() == TokenKind::Minus {
1289            let start = self.advance();
1290            let operand = self.parse_unary()?;
1291            let end = operand.span().end;
1292            return Ok(Expr::Unary {
1293                op: "-".to_string(),
1294                operand: Box::new(operand),
1295                span: Span::new(start.span.file, start.span.start, end),
1296            });
1297        }
1298        self.parse_power()
1299    }
1300
1301    fn parse_power(&mut self) -> Result<Expr, ()> {
1302        let base = self.parse_postfix()?;
1303        if self.peek_kind() == TokenKind::DoubleStar {
1304            self.advance();
1305            // Right-associative.
1306            let exponent = self.parse_unary()?;
1307            let span = Span::new(base.span().file, base.span().start, exponent.span().end);
1308            return Ok(Expr::Binary {
1309                op: "**".to_string(),
1310                left: Box::new(base),
1311                right: Box::new(exponent),
1312                span,
1313            });
1314        }
1315        Ok(base)
1316    }
1317
1318    fn parse_postfix(&mut self) -> Result<Expr, ()> {
1319        let mut base = self.parse_primary()?;
1320        loop {
1321            match self.peek_kind() {
1322                TokenKind::LParen => {
1323                    let mut args = Vec::new();
1324                    self.parse_call_args(&mut args)?;
1325                    let end = self.tokens[self.pos.saturating_sub(1)].span.end;
1326                    base = match base {
1327                        Expr::Name { name, span } => Expr::Call {
1328                            name,
1329                            args,
1330                            span: Span::new(span.file, span.start, end),
1331                        },
1332                        Expr::Member {
1333                            receiver,
1334                            member,
1335                            span,
1336                            ..
1337                        } => Expr::ReceiverCall {
1338                            receiver,
1339                            name: member,
1340                            args,
1341                            span: Span::new(span.file, span.start, end),
1342                        },
1343                        _other => {
1344                            self.errors.push(OpyError::at(
1345                                "parse-error",
1346                                "cannot call this expression".to_string(),
1347                                self.peek().span,
1348                            ));
1349                            return Err(());
1350                        }
1351                    };
1352                }
1353                TokenKind::LBracket => {
1354                    self.advance();
1355                    let index = self.parse_expr()?;
1356                    let end = match self.expect(TokenKind::RBracket, "']'") {
1357                        Ok(token) => token.span.end,
1358                        Err(()) => return Err(()),
1359                    };
1360                    let span = Span::new(base.span().file, base.span().start, end);
1361                    base = Expr::Index {
1362                        array: Box::new(base),
1363                        index: Box::new(index),
1364                        span,
1365                    };
1366                }
1367                TokenKind::Dot => {
1368                    self.advance();
1369                    let member_token = self.peek().clone();
1370                    let member = match self.expect_ident("a member name after '.'") {
1371                        Ok(member) => member,
1372                        Err(()) => return Err(()),
1373                    };
1374                    let member_span = member_token.span;
1375                    let end = member_span.end;
1376                    let span = Span::new(base.span().file, base.span().start, end);
1377                    base = Expr::Member {
1378                        receiver: Box::new(base),
1379                        member,
1380                        member_span,
1381                        span,
1382                    };
1383                }
1384                _ => break,
1385            }
1386        }
1387        Ok(base)
1388    }
1389
1390    /// `@Event name(args)`: positional expressions only (keyword arguments
1391    /// are a call-argument form, not an event form).
1392    fn parse_event_args(&mut self, args: &mut Vec<Expr>) -> Result<(), ()> {
1393        self.expect(TokenKind::LParen, "'('")?;
1394        self.skip_newlines();
1395        if self.peek_kind() == TokenKind::RParen {
1396            self.advance();
1397            return Ok(());
1398        }
1399        loop {
1400            let expr = self.parse_expr()?;
1401            if self.peek_kind() == TokenKind::Assign {
1402                self.error_at_current("keyword arguments are not valid in @Event".to_string());
1403                return Err(());
1404            }
1405            args.push(expr);
1406            self.skip_newlines();
1407            if self.peek_kind() == TokenKind::Comma {
1408                self.advance();
1409                self.skip_newlines();
1410                if self.peek_kind() == TokenKind::RParen {
1411                    break;
1412                }
1413            } else {
1414                break;
1415            }
1416        }
1417        self.expect(TokenKind::RParen, "')'")?;
1418        Ok(())
1419    }
1420
1421    fn parse_call_args(&mut self, args: &mut Vec<CallArg>) -> Result<(), ()> {
1422        self.expect(TokenKind::LParen, "'('")?;
1423        self.skip_newlines();
1424        if self.peek_kind() == TokenKind::RParen {
1425            self.advance();
1426            return Ok(());
1427        }
1428        loop {
1429            match self.parse_expr() {
1430                Ok(expr) => {
1431                    // A keyword argument is `name = expr` (issue #110): a
1432                    // bare identifier immediately followed by `=`. Anything
1433                    // else (`expr = ...`) is not a call argument form and is
1434                    // rejected like the pinned reference rejects it.
1435                    if self.peek_kind() == TokenKind::Assign {
1436                        let Expr::Name { name, span } = expr else {
1437                            self.error_at_current(
1438                                "expected a keyword name before '=' in this call".to_string(),
1439                            );
1440                            return Err(());
1441                        };
1442                        self.advance();
1443                        let value = match self.parse_expr() {
1444                            Ok(value) => value,
1445                            Err(()) => return Err(()),
1446                        };
1447                        args.push(CallArg {
1448                            keyword: Some((name, span)),
1449                            value,
1450                        });
1451                    } else {
1452                        args.push(CallArg {
1453                            keyword: None,
1454                            value: expr,
1455                        });
1456                    }
1457                }
1458                Err(()) => return Err(()),
1459            }
1460            self.skip_newlines();
1461            if self.peek_kind() == TokenKind::Comma {
1462                self.advance();
1463                self.skip_newlines();
1464                if self.peek_kind() == TokenKind::RParen {
1465                    break;
1466                }
1467            } else {
1468                break;
1469            }
1470        }
1471        self.expect(TokenKind::RParen, "')'")?;
1472        Ok(())
1473    }
1474
1475    fn parse_primary(&mut self) -> Result<Expr, ()> {
1476        let token = self.peek();
1477        match token.kind {
1478            TokenKind::Number => {
1479                let token = self.advance();
1480                let value = if let Some(hex) = token
1481                    .text
1482                    .strip_prefix("0x")
1483                    .or_else(|| token.text.strip_prefix("0X"))
1484                {
1485                    u64::from_str_radix(hex, 16).map_or(f64::NAN, |value| value as f64)
1486                } else {
1487                    token.text.parse().unwrap_or(f64::NAN)
1488                };
1489                Ok(Expr::Number {
1490                    value,
1491                    text: token.text.clone(),
1492                    span: token.span,
1493                })
1494            }
1495            TokenKind::String => {
1496                let token = self.advance();
1497                Ok(Expr::String {
1498                    value: token.text.clone(),
1499                    span: token.span,
1500                })
1501            }
1502            TokenKind::Ident => {
1503                let token = self.advance();
1504                if token.text == "lambda" {
1505                    return self.parse_lambda(token.span);
1506                }
1507                if is_string_modifier(&token.text) && self.peek_kind() == TokenKind::String {
1508                    let string = self.advance();
1509                    let (format_text, interpolations) = if token.text == "f" {
1510                        let raw = string.raw.as_deref().unwrap_or(&string.text);
1511                        let (format_text, interpolations) =
1512                            self.parse_f_string(raw, string.span)?;
1513                        (Some(format_text), interpolations)
1514                    } else {
1515                        (None, Vec::new())
1516                    };
1517                    return Ok(Expr::StringModifier {
1518                        modifier: token.text.chars().next().unwrap_or_default(),
1519                        value: string.text,
1520                        format_text,
1521                        interpolations,
1522                        span: Span::new(token.span.file, token.span.start, string.span.end),
1523                    });
1524                }
1525                match token.text.as_str() {
1526                    "true" => Ok(Expr::Bool {
1527                        value: true,
1528                        span: token.span,
1529                    }),
1530                    "false" => Ok(Expr::Bool {
1531                        value: false,
1532                        span: token.span,
1533                    }),
1534                    "None" | "null" => Ok(Expr::Null { span: token.span }),
1535                    _ => Ok(Expr::Name {
1536                        name: token.text.clone(),
1537                        span: token.span,
1538                    }),
1539                }
1540            }
1541            TokenKind::LParen => {
1542                self.advance();
1543                let expr = self.parse_expr()?;
1544                self.expect(TokenKind::RParen, "')'")?;
1545                Ok(expr)
1546            }
1547            TokenKind::LBracket => {
1548                let open = self.advance();
1549                let mut elements = Vec::new();
1550                self.skip_newlines();
1551                if self.peek_kind() == TokenKind::RBracket {
1552                    let end = self.advance().span.end;
1553                    return Ok(Expr::Array {
1554                        elements,
1555                        span: Span::new(open.span.file, open.span.start, end),
1556                    });
1557                }
1558                let first = self.parse_expr()?;
1559                if self.is_ident("for") {
1560                    self.advance();
1561                    let variable_token =
1562                        self.expect(TokenKind::Ident, "a comprehension variable")?;
1563                    let index = if self.peek_kind() == TokenKind::Comma {
1564                        self.advance();
1565                        let index = self.expect(TokenKind::Ident, "a comprehension index")?;
1566                        Some((index.text, index.span))
1567                    } else {
1568                        None
1569                    };
1570                    if !self.is_ident("in") {
1571                        self.error_at_current("expected `in` in list comprehension".to_string());
1572                        return Err(());
1573                    }
1574                    self.advance();
1575                    let iterable = self.parse_expr()?;
1576                    let condition = if self.is_ident("if") {
1577                        self.advance();
1578                        Some(Box::new(self.parse_expr()?))
1579                    } else {
1580                        None
1581                    };
1582                    let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
1583                    return Ok(Expr::Comprehension {
1584                        element: Box::new(first),
1585                        variable: variable_token.text,
1586                        variable_span: variable_token.span,
1587                        index,
1588                        iterable: Box::new(iterable),
1589                        condition,
1590                        span: Span::new(open.span.file, open.span.start, end),
1591                    });
1592                }
1593                elements.push(first);
1594                loop {
1595                    self.skip_newlines();
1596                    if self.peek_kind() == TokenKind::Comma {
1597                        self.advance();
1598                        self.skip_newlines();
1599                        if self.peek_kind() == TokenKind::RBracket {
1600                            break;
1601                        }
1602                        elements.push(self.parse_expr()?);
1603                    } else {
1604                        break;
1605                    }
1606                }
1607                let end = match self.expect(TokenKind::RBracket, "']'") {
1608                    Ok(token) => token.span.end,
1609                    Err(()) => return Err(()),
1610                };
1611                Ok(Expr::Array {
1612                    elements,
1613                    span: Span::new(open.span.file, open.span.start, end),
1614                })
1615            }
1616            TokenKind::LBrace => self.parse_dict(),
1617            _ => {
1618                self.error_at_current(format!("expected an expression but found '{}'", token.text));
1619                Err(())
1620            }
1621        }
1622    }
1623
1624    fn parse_dict(&mut self) -> Result<Expr, ()> {
1625        let open = self.advance();
1626        let mut entries = Vec::new();
1627        self.skip_newlines();
1628        if self.peek_kind() == TokenKind::RBrace {
1629            let end = self.advance().span.end;
1630            return Ok(Expr::Dict {
1631                entries,
1632                span: Span::new(open.span.file, open.span.start, end),
1633            });
1634        }
1635        loop {
1636            let key = self.parse_expr()?;
1637            self.expect(TokenKind::Colon, "':' in a dictionary entry")?;
1638            let value = self.parse_expr()?;
1639            let span = Span::new(key.span().file, key.span().start, value.span().end);
1640            entries.push(DictEntry { key, value, span });
1641            self.skip_newlines();
1642            if self.peek_kind() == TokenKind::Comma {
1643                self.advance();
1644                self.skip_newlines();
1645                if self.peek_kind() == TokenKind::RBrace {
1646                    break;
1647                }
1648            } else {
1649                break;
1650            }
1651        }
1652        let end = self.expect(TokenKind::RBrace, "'}'")?.span.end;
1653        Ok(Expr::Dict {
1654            entries,
1655            span: Span::new(open.span.file, open.span.start, end),
1656        })
1657    }
1658
1659    fn parse_lambda(&mut self, start: Span) -> Result<Expr, ()> {
1660        let mut params = Vec::new();
1661        loop {
1662            let param = self.expect(TokenKind::Ident, "a lambda parameter")?;
1663            params.push((param.text, param.span));
1664            if self.peek_kind() == TokenKind::Comma {
1665                self.advance();
1666            } else {
1667                break;
1668            }
1669        }
1670        self.expect(TokenKind::Colon, "':' after lambda parameters")?;
1671        let body = self.parse_expr()?;
1672        Ok(Expr::Lambda {
1673            params,
1674            body: Box::new(body.clone()),
1675            span: Span::new(start.file, start.start, body.span().end),
1676        })
1677    }
1678
1679    /// Parse the expression regions of a pinned-OverPy f-string. Double
1680    /// braces are literal braces; a single brace introduces one expression.
1681    /// The resulting expression tokens are shifted back into the source
1682    /// string so HIR and tooling retain source provenance.
1683    fn parse_f_string(&mut self, raw: &str, string_span: Span) -> Result<(String, Vec<Expr>), ()> {
1684        let chars: Vec<char> = raw.chars().collect();
1685        let mut text = String::new();
1686        let mut interpolations = Vec::new();
1687        let mut index = 0;
1688        while index < chars.len() {
1689            match chars[index] {
1690                '{' if chars.get(index + 1) == Some(&'{') => {
1691                    text.push_str("{{");
1692                    index += 2;
1693                }
1694                '}' if chars.get(index + 1) == Some(&'}') => {
1695                    text.push_str("}}");
1696                    index += 2;
1697                }
1698                '{' => {
1699                    let end = self.find_f_string_end(&chars, index + 1);
1700                    let Some(end) = end else {
1701                        self.errors.push(OpyError::at(
1702                            "parse-error",
1703                            "unterminated f-string interpolation".to_string(),
1704                            string_span,
1705                        ));
1706                        return Err(());
1707                    };
1708                    let expression: String = chars[index + 1..end].iter().collect();
1709                    if expression.trim().is_empty() {
1710                        self.errors.push(OpyError::at(
1711                            "parse-error",
1712                            "f-string interpolation cannot be empty".to_string(),
1713                            Span::new(
1714                                string_span.file,
1715                                Position::new(
1716                                    string_span.start.line,
1717                                    string_span.start.col + index as u32 + 1,
1718                                ),
1719                                Position::new(
1720                                    string_span.start.line,
1721                                    string_span.start.col + end as u32 + 1,
1722                                ),
1723                            ),
1724                        ));
1725                        return Err(());
1726                    }
1727                    let origin = Position::new(
1728                        string_span.start.line,
1729                        string_span.start.col + index as u32 + 1,
1730                    );
1731                    let parsed = parse_expression_fragment(&expression, string_span.file, origin)
1732                        .map_err(|error| {
1733                            self.errors.push(error);
1734                        });
1735                    let Ok(parsed) = parsed else {
1736                        return Err(());
1737                    };
1738                    text.push_str(&format!("{{{}}}", interpolations.len()));
1739                    interpolations.push(parsed);
1740                    index = end + 1;
1741                }
1742                '}' => {
1743                    self.errors.push(OpyError::at(
1744                        "parse-error",
1745                        "single '}' is not valid in an f-string".to_string(),
1746                        string_span,
1747                    ));
1748                    return Err(());
1749                }
1750                '\\' if index + 1 < chars.len() => {
1751                    text.push(decode_string_escape(chars[index + 1]));
1752                    index += 2;
1753                }
1754                character => {
1755                    text.push(character);
1756                    index += 1;
1757                }
1758            }
1759        }
1760        Ok((text, interpolations))
1761    }
1762
1763    fn find_f_string_end(&self, chars: &[char], start: usize) -> Option<usize> {
1764        let mut nested_braces = 0;
1765        let mut quote = None;
1766        let mut escaped = false;
1767        for (index, character) in chars.iter().enumerate().skip(start) {
1768            if escaped {
1769                escaped = false;
1770                continue;
1771            }
1772            if *character == '\\' && quote.is_some() {
1773                escaped = true;
1774                continue;
1775            }
1776            if let Some(active_quote) = quote {
1777                if *character == active_quote {
1778                    quote = None;
1779                }
1780                continue;
1781            }
1782            match character {
1783                '"' | '\'' => quote = Some(*character),
1784                '{' => nested_braces += 1,
1785                '}' if nested_braces == 0 => return Some(index),
1786                '}' => nested_braces -= 1,
1787                _ => {}
1788            }
1789        }
1790        None
1791    }
1792}
1793
1794/// Parse one f-string expression fragment and shift its local token spans
1795/// into the original source file.
1796fn parse_expression_fragment(text: &str, file: u32, origin: Position) -> Result<Expr, OpyError> {
1797    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
1798        file_id: file,
1799        text,
1800    })?;
1801    for token in &mut tokens {
1802        token.span = shift_span(token.span, origin);
1803    }
1804    let mut parser = Parser {
1805        tokens: &tokens,
1806        pos: 0,
1807        allow_macro_redeclaration: false,
1808        errors: Vec::new(),
1809    };
1810    let expression = parser.parse_expr().map_err(|()| {
1811        parser.errors.first().cloned().unwrap_or_else(|| {
1812            OpyError::at(
1813                "parse-error",
1814                "invalid f-string expression",
1815                Span::new(file, origin, origin),
1816            )
1817        })
1818    })?;
1819    if parser.peek_kind() != TokenKind::Eof {
1820        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
1821    }
1822    parser.errors.into_iter().next().map_or(Ok(expression), Err)
1823}
1824
1825fn shift_span(span: Span, origin: Position) -> Span {
1826    fn shift(position: Position, origin: Position) -> Position {
1827        Position::new(
1828            origin.line + position.line.saturating_sub(1),
1829            if position.line == 1 {
1830                origin.col + position.col.saturating_sub(1)
1831            } else {
1832                position.col
1833            },
1834        )
1835    }
1836    Span::new(
1837        span.file,
1838        shift(span.start, origin),
1839        shift(span.end, origin),
1840    )
1841}
1842
1843fn decode_string_escape(character: char) -> char {
1844    match character {
1845        'n' => '\n',
1846        't' => '\t',
1847        'r' => '\r',
1848        '\\' => '\\',
1849        '"' => '"',
1850        '\'' => '\'',
1851        other => other,
1852    }
1853}
1854
1855fn is_string_modifier(text: &str) -> bool {
1856    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
1857}
1858
1859#[cfg(test)]
1860mod tests {
1861    use super::*;
1862    use crate::lexer::{LexInput, lex};
1863
1864    fn parse_ok(text: &str) -> Program {
1865        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
1866        let output = parse(&tokens);
1867        assert!(
1868            output.errors.is_empty(),
1869            "unexpected errors: {:?}",
1870            output.errors
1871        );
1872        output.program.unwrap()
1873    }
1874
1875    fn parse_err(text: &str) -> Vec<OpyError> {
1876        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
1877        parse(&tokens).errors
1878    }
1879
1880    #[test]
1881    fn parses_basic_rule() {
1882        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
1883        assert_eq!(program.rules.len(), 1);
1884        let RuleEntry::Rule(rule) = &program.rules[0] else {
1885            panic!("expected rule");
1886        };
1887        assert_eq!(rule.name, "setup");
1888        assert_eq!(rule.event.name, "global");
1889        assert_eq!(rule.actions.len(), 1);
1890    }
1891
1892    #[test]
1893    fn parses_power_augmented_assignment() {
1894        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
1895        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
1896        let program =
1897            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
1898        let RuleEntry::Rule(rule) = &program.rules[0] else {
1899            panic!("expected rule");
1900        };
1901        let Stmt::Assign {
1902            value,
1903            target: assigned_target,
1904            ..
1905        } = &rule.actions[1]
1906        else {
1907            panic!("expected an assignment");
1908        };
1909        let Expr::Binary {
1910            op, left, right, ..
1911        } = value
1912        else {
1913            panic!("expected a binary modification, got {value:?}");
1914        };
1915        assert_eq!(op, "**");
1916        assert!(matches!(&**left, Expr::Name { .. }));
1917        assert!(matches!(
1918            assigned_target,
1919            Expr::Name { name, .. } if name == "a"
1920        ));
1921        assert!(matches!(right.as_ref(), Expr::Number { .. }));
1922    }
1923
1924    #[test]
1925    fn parses_control_flow() {
1926        let program = parse_ok(
1927            "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",
1928        );
1929        let RuleEntry::Rule(rule) = &program.rules[0] else {
1930            panic!();
1931        };
1932        assert!(matches!(rule.actions[0], Stmt::For { .. }));
1933        let Stmt::For { body, .. } = &rule.actions[0] else {
1934            panic!();
1935        };
1936        let Stmt::If {
1937            branches, r#else, ..
1938        } = &body[0]
1939        else {
1940            panic!();
1941        };
1942        assert_eq!(branches.len(), 2);
1943        assert!(r#else.is_some());
1944        let Stmt::While { body, .. } = &rule.actions[1] else {
1945            panic!();
1946        };
1947        assert_eq!(body.len(), 2);
1948    }
1949
1950    #[test]
1951    fn parses_issue_28_constructs() {
1952        let program = parse_ok(
1953            "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",
1954        );
1955        let RuleEntry::Rule(rule) = &program.rules[0] else {
1956            panic!("expected rule");
1957        };
1958        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
1959        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
1960    }
1961
1962    #[test]
1963    fn rejects_incomplete_do_while_and_dictionary_entries() {
1964        let errors = parse_err(
1965            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
1966        );
1967        assert!(!errors.is_empty());
1968        assert!(errors.iter().all(|error| error.code == "parse-error"));
1969    }
1970
1971    #[test]
1972    fn parses_multi_line_array() {
1973        let program = parse_ok(
1974            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
1975        );
1976        let RuleEntry::Rule(rule) = &program.rules[0] else {
1977            panic!();
1978        };
1979        let Stmt::Assign { value, .. } = &rule.actions[0] else {
1980            panic!();
1981        };
1982        let Expr::Array { elements, .. } = value else {
1983            panic!("expected array, got {value:?}");
1984        };
1985        assert_eq!(elements.len(), 2);
1986    }
1987
1988    #[test]
1989    fn missing_colon_is_a_structured_error() {
1990        let errors = parse_err("rule \"x\"\n    @Event global\n");
1991        assert!(!errors.is_empty());
1992        assert_eq!(errors[0].code, "parse-error");
1993        assert!(errors[0].span.is_some());
1994    }
1995
1996    #[test]
1997    fn def_and_macro_parse() {
1998        let program = parse_ok(
1999            "subroutine showStatus\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
2000        );
2001        assert_eq!(program.declarations.len(), 2);
2002        assert!(matches!(program.declarations[1], Decl::Macro { .. }));
2003        let Decl::Macro { args, body, .. } = &program.declarations[1] else {
2004            panic!();
2005        };
2006        assert_eq!(args, &vec!["value".to_string()]);
2007        assert_eq!(body.len(), 1);
2008    }
2009
2010    #[test]
2011    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
2012        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
2013        let errors = parse_err(text);
2014        assert_eq!(
2015            errors
2016                .iter()
2017                .filter(|error| error.code == "macro-redeclaration")
2018                .count(),
2019            2
2020        );
2021
2022        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2023        let output = parse_with_options(&tokens, true);
2024        assert!(
2025            output.errors.is_empty(),
2026            "unexpected errors: {:?}",
2027            output.errors
2028        );
2029        assert!(output.program.is_some());
2030    }
2031
2032    #[test]
2033    fn multiple_errors_are_reported() {
2034        let errors =
2035            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
2036        assert!(!errors.is_empty());
2037    }
2038
2039    #[test]
2040    fn precedence_parses_python_like() {
2041        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
2042        let RuleEntry::Rule(rule) = &program.rules[0] else {
2043            panic!();
2044        };
2045        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2046            panic!();
2047        };
2048        let Expr::Binary {
2049            op, left, right, ..
2050        } = value
2051        else {
2052            panic!();
2053        };
2054        assert_eq!(op, "+");
2055        let Expr::Binary { op: inner, .. } = right.as_ref() else {
2056            panic!();
2057        };
2058        assert_eq!(inner, "*");
2059        assert!(matches!(left.as_ref(), Expr::Number { .. }));
2060    }
2061
2062    #[test]
2063    fn parses_receiver_calls() {
2064        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
2065        // member access followed by call arguments (#104).
2066        let program =
2067            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
2068        let RuleEntry::Rule(rule) = &program.rules[0] else {
2069            panic!("expected rule");
2070        };
2071        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2072            panic!("expected expression statement, got {:?}", rule.actions[0]);
2073        };
2074        let Expr::ReceiverCall {
2075            receiver,
2076            name,
2077            args,
2078            ..
2079        } = &expr
2080        else {
2081            panic!("expected receiver call, got {expr:?}");
2082        };
2083        assert_eq!(name, "setMoveSpeed");
2084        assert!(
2085            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
2086            "receiver must be the eventPlayer name"
2087        );
2088        assert_eq!(args.len(), 1);
2089        assert!(args[0].keyword.is_none(), "positional argument");
2090        assert!(matches!(&args[0].value, Expr::Number { .. }));
2091    }
2092
2093    #[test]
2094    fn parses_keyword_arguments_with_name_spans() {
2095        // `name = expr` call arguments are keyword arguments carrying the
2096        // name token's exact span (issue #110); comparisons stay positional.
2097        let program =
2098            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
2099        let RuleEntry::Rule(rule) = &program.rules[0] else {
2100            panic!("expected rule");
2101        };
2102        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2103            panic!("expected expression statement");
2104        };
2105        let Expr::Call { args, .. } = expr else {
2106            panic!("expected a call, got {expr:?}");
2107        };
2108        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
2109        assert_eq!(keyword, "time");
2110        assert_eq!(span.start.line, 3);
2111        assert!(matches!(&args[0].value, Expr::Number { .. }));
2112
2113        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
2114            panic!("expected expression statement");
2115        };
2116        let Expr::Call { args, .. } = expr else {
2117            panic!("expected a call, got {expr:?}");
2118        };
2119        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
2120        assert!(matches!(&args[0].value, Expr::Binary { .. }));
2121    }
2122
2123    #[test]
2124    fn non_name_keyword_lhs_is_a_parse_error() {
2125        // `f(1 = 2)` is not a call argument form; rejected explicitly.
2126        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
2127        assert!(!errors.is_empty());
2128        assert_eq!(errors[0].code, "parse-error");
2129    }
2130
2131    #[test]
2132    fn parses_member_call_on_call_result() {
2133        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
2134        // followed by call arguments on a call result stays a receiver call.
2135        let program = parse_ok(
2136            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
2137        );
2138        let RuleEntry::Rule(rule) = &program.rules[0] else {
2139            panic!("expected rule");
2140        };
2141        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2142            panic!("expected expression statement");
2143        };
2144        let Expr::ReceiverCall {
2145            receiver,
2146            name,
2147            args,
2148            ..
2149        } = &expr
2150        else {
2151            panic!("expected receiver call, got {expr:?}");
2152        };
2153        assert_eq!(name, "setStatusEffect");
2154        assert!(
2155            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
2156            "receiver must be the preceding call"
2157        );
2158        assert_eq!(args.len(), 2);
2159    }
2160
2161    #[test]
2162    fn member_without_call_is_not_a_call() {
2163        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
2164        // access; only a following `(` turns it into a receiver call.
2165        let program =
2166            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
2167        let RuleEntry::Rule(rule) = &program.rules[0] else {
2168            panic!("expected rule");
2169        };
2170        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2171            panic!("expected assignment");
2172        };
2173        assert!(matches!(
2174            &value,
2175            Expr::Member { member, .. } if member == "moveSpeed"
2176        ));
2177    }
2178
2179    #[test]
2180    fn parses_advanced_rule_annotations_with_source_arguments() {
2181        let program = parse_ok(
2182            "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",
2183        );
2184        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
2185            panic!("expected subroutine");
2186        };
2187        assert_eq!(annotations.len(), 2);
2188        let RuleEntry::Rule(rule) = &program.rules[1] else {
2189            panic!("expected rule");
2190        };
2191        assert!(rule.disabled);
2192        assert!(rule.delimiter);
2193        assert_eq!(rule.new_page.as_deref(), Some("Page"));
2194        assert_eq!(rule.annotations.len(), 7);
2195        assert_eq!(rule.annotations[1].args[0].text, "1");
2196        assert_eq!(rule.annotations[2].args[0].text, "dmon");
2197    }
2198}