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            TokenKind::Increment | TokenKind::Decrement => {
953                let operator = self.advance();
954                if !matches!(self.peek_kind(), TokenKind::Newline | TokenKind::Eof) {
955                    self.error_at_current(
956                        "postfix increment/decrement must be a standalone assignment".to_string(),
957                    );
958                    return Err(());
959                }
960                let operation = if operator.kind == TokenKind::Increment {
961                    "+"
962                } else {
963                    "-"
964                };
965                let span = Span::new(start.file, start.start, operator.span.end);
966                let value = Expr::Binary {
967                    op: operation.to_string(),
968                    left: Box::new(expr.clone()),
969                    right: Box::new(Expr::Number {
970                        value: 1.0,
971                        text: "1".to_string(),
972                        span: operator.span,
973                    }),
974                    span,
975                };
976                Ok(Stmt::Assign {
977                    target: expr,
978                    value,
979                    span,
980                })
981            }
982            _ => {
983                let end = self.peek().span.start;
984                Ok(Stmt::Expr {
985                    expr,
986                    span: Span::new(start.file, start.start, end),
987                })
988            }
989        }
990    }
991
992    fn parse_if(&mut self) -> Result<Stmt, ()> {
993        let start = self.advance();
994        let line_indent = start.span.start.col;
995        let condition = self.parse_expr()?;
996        if self
997            .expect(TokenKind::Colon, "':' after the if condition")
998            .is_err()
999        {
1000            return Err(());
1001        }
1002        let body_indent = self.block_indent(line_indent).ok_or(())?;
1003        let body = self.parse_block(body_indent);
1004        let mut branches = vec![IfBranch { condition, body }];
1005        let mut r#else = None;
1006        loop {
1007            let save = self.pos;
1008            self.skip_newlines();
1009            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col != line_indent {
1010                self.pos = save;
1011                break;
1012            }
1013            if self.is_ident("elif") {
1014                self.advance();
1015                let condition = match self.parse_expr() {
1016                    Ok(expr) => expr,
1017                    Err(()) => return Err(()),
1018                };
1019                if self
1020                    .expect(TokenKind::Colon, "':' after the elif condition")
1021                    .is_err()
1022                {
1023                    return Err(());
1024                }
1025                let body_indent = self.block_indent(line_indent).ok_or(())?;
1026                let body = self.parse_block(body_indent);
1027                branches.push(IfBranch { condition, body });
1028            } else if self.is_ident("else") {
1029                self.advance();
1030                if self.expect(TokenKind::Colon, "':' after `else`").is_err() {
1031                    return Err(());
1032                }
1033                let body_indent = self.block_indent(line_indent).ok_or(())?;
1034                let body = self.parse_block(body_indent);
1035                r#else = Some(body);
1036                break;
1037            } else {
1038                self.pos = save;
1039                break;
1040            }
1041        }
1042        Ok(Stmt::If {
1043            branches,
1044            r#else,
1045            span: start.span,
1046        })
1047    }
1048
1049    fn parse_for(&mut self) -> Result<Stmt, ()> {
1050        let start = self.advance();
1051        let variable = self.parse_postfix()?;
1052        if !self.is_ident("in") {
1053            self.error_at_current("expected `in` in the for statement".to_string());
1054            return Err(());
1055        }
1056        self.advance();
1057        let iterable = self.parse_expr()?;
1058        if self
1059            .expect(TokenKind::Colon, "':' after the for header")
1060            .is_err()
1061        {
1062            return Err(());
1063        }
1064        let line_indent = start.span.start.col;
1065        let body_indent = self.block_indent(line_indent).ok_or(())?;
1066        let body = self.parse_block(body_indent);
1067        Ok(Stmt::For {
1068            variable,
1069            iterable,
1070            body,
1071            span: start.span,
1072        })
1073    }
1074
1075    fn parse_while(&mut self) -> Result<Stmt, ()> {
1076        let start = self.advance();
1077        let condition = self.parse_expr()?;
1078        if self
1079            .expect(TokenKind::Colon, "':' after the while condition")
1080            .is_err()
1081        {
1082            return Err(());
1083        }
1084        let line_indent = start.span.start.col;
1085        let body_indent = self.block_indent(line_indent).ok_or(())?;
1086        let body = self.parse_block(body_indent);
1087        Ok(Stmt::While {
1088            condition,
1089            body,
1090            span: start.span,
1091        })
1092    }
1093
1094    fn parse_do_while(&mut self) -> Result<Stmt, ()> {
1095        let start = self.advance();
1096        if self.expect(TokenKind::Colon, "':' after `do`").is_err() {
1097            return Err(());
1098        }
1099        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1100        let body = self.parse_block(body_indent);
1101        if !self.is_ident("while") {
1102            self.error_at_current("expected `while` after the do block".to_string());
1103            return Err(());
1104        }
1105        self.advance();
1106        let condition = self.parse_expr()?;
1107        if self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
1108            self.error_at_current("expected the end of the do-while condition".to_string());
1109            return Err(());
1110        }
1111        Ok(Stmt::DoWhile {
1112            condition,
1113            body,
1114            span: start.span,
1115        })
1116    }
1117
1118    fn parse_switch(&mut self) -> Result<Stmt, ()> {
1119        let start = self.advance();
1120        let value = self.parse_expr()?;
1121        if self
1122            .expect(TokenKind::Colon, "':' after the switch value")
1123            .is_err()
1124        {
1125            return Err(());
1126        }
1127        let body_indent = self.block_indent(start.span.start.col).ok_or(())?;
1128        let mut arms = Vec::new();
1129        loop {
1130            self.skip_newlines();
1131            if self.peek_kind() == TokenKind::Eof || self.peek().span.start.col < body_indent {
1132                break;
1133            }
1134            if self.peek().span.start.col != body_indent {
1135                self.error_at_current("unexpected indentation in switch".to_string());
1136                self.recover_line();
1137                continue;
1138            }
1139            if self.is_ident("case") {
1140                let case_start = self.advance();
1141                let case_value = self.parse_expr()?;
1142                if self
1143                    .expect(TokenKind::Colon, "':' after the case value")
1144                    .is_err()
1145                {
1146                    return Err(());
1147                }
1148                let case_body_indent = self.block_indent(body_indent).ok_or(())?;
1149                let body = self.parse_block(case_body_indent);
1150                arms.push(SwitchArm::Case {
1151                    value: case_value,
1152                    body,
1153                    span: case_start.span,
1154                });
1155            } else if self.is_ident("default") {
1156                let default_start = self.advance();
1157                if self
1158                    .expect(TokenKind::Colon, "':' after `default`")
1159                    .is_err()
1160                {
1161                    return Err(());
1162                }
1163                let default_body_indent = self.block_indent(body_indent).ok_or(())?;
1164                arms.push(SwitchArm::Default {
1165                    body: self.parse_block(default_body_indent),
1166                    span: default_start.span,
1167                });
1168                if default_start.span.start.col != body_indent {
1169                    self.error_at_current("invalid default indentation".to_string());
1170                    return Err(());
1171                }
1172            } else {
1173                self.error_at_current("expected `case` or `default` in switch".to_string());
1174                self.recover_line();
1175            }
1176        }
1177        if arms.is_empty() {
1178            self.errors.push(OpyError::at(
1179                "parse-error",
1180                "switch must contain at least one case or default arm".to_string(),
1181                start.span,
1182            ));
1183            return Err(());
1184        }
1185        Ok(Stmt::Switch {
1186            value,
1187            arms,
1188            span: start.span,
1189        })
1190    }
1191
1192    // ---- expressions ----
1193
1194    fn parse_expr(&mut self) -> Result<Expr, ()> {
1195        let then_value = self.parse_or()?;
1196        if !self.is_ident("if") {
1197            return Ok(then_value);
1198        }
1199
1200        self.advance();
1201        let condition = self.parse_or()?;
1202        if !self.is_ident("else") {
1203            self.error_at_current("expected `else` in conditional expression".to_string());
1204            return Err(());
1205        }
1206        self.advance();
1207        // Conditional expressions are right-associative, so a chained form
1208        // such as `a if c else b if d else e` groups at the else branch.
1209        let else_value = self.parse_expr()?;
1210        let span = Span::new(
1211            then_value.span().file,
1212            then_value.span().start,
1213            else_value.span().end,
1214        );
1215        Ok(Expr::Conditional {
1216            then_value: Box::new(then_value),
1217            condition: Box::new(condition),
1218            else_value: Box::new(else_value),
1219            span,
1220        })
1221    }
1222
1223    fn parse_or(&mut self) -> Result<Expr, ()> {
1224        let mut left = self.parse_and()?;
1225        while self.is_ident("or") {
1226            self.advance();
1227            let right = self.parse_and()?;
1228            let span = Span::new(left.span().file, left.span().start, right.span().end);
1229            left = Expr::Binary {
1230                op: "or".to_string(),
1231                left: Box::new(left),
1232                right: Box::new(right),
1233                span,
1234            };
1235        }
1236        Ok(left)
1237    }
1238
1239    fn parse_and(&mut self) -> Result<Expr, ()> {
1240        let mut left = self.parse_not()?;
1241        while self.is_ident("and") {
1242            self.advance();
1243            let right = self.parse_not()?;
1244            let span = Span::new(left.span().file, left.span().start, right.span().end);
1245            left = Expr::Binary {
1246                op: "and".to_string(),
1247                left: Box::new(left),
1248                right: Box::new(right),
1249                span,
1250            };
1251        }
1252        Ok(left)
1253    }
1254
1255    fn parse_not(&mut self) -> Result<Expr, ()> {
1256        if self.is_ident("not") {
1257            let start = self.advance();
1258            let operand = self.parse_not()?;
1259            let end = operand.span().end;
1260            return Ok(Expr::Unary {
1261                op: "not".to_string(),
1262                operand: Box::new(operand),
1263                span: Span::new(start.span.file, start.span.start, end),
1264            });
1265        }
1266        self.parse_comparison()
1267    }
1268
1269    fn parse_comparison(&mut self) -> Result<Expr, ()> {
1270        let mut left = self.parse_additive()?;
1271        loop {
1272            let op = match self.peek_kind() {
1273                TokenKind::Eq => "==",
1274                TokenKind::Ne => "!=",
1275                TokenKind::Lt => "<",
1276                TokenKind::Le => "<=",
1277                TokenKind::Gt => ">",
1278                TokenKind::Ge => ">=",
1279                _ if self.is_ident("in") => "in",
1280                _ if self.is_ident("not") && self.peek_at(1).text == "in" => "not in",
1281                _ => break,
1282            };
1283            self.advance();
1284            if op == "not in" {
1285                self.advance();
1286            }
1287            let right = self.parse_additive()?;
1288            let span = Span::new(left.span().file, left.span().start, right.span().end);
1289            left = Expr::Binary {
1290                op: op.to_string(),
1291                left: Box::new(left),
1292                right: Box::new(right),
1293                span,
1294            };
1295        }
1296        Ok(left)
1297    }
1298
1299    fn parse_additive(&mut self) -> Result<Expr, ()> {
1300        let mut left = self.parse_multiplicative()?;
1301        loop {
1302            if self.peek_kind() == TokenKind::Decrement
1303                && !matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof)
1304            {
1305                let operator = self.advance();
1306                let operand = self.parse_unary()?;
1307                let unary = Expr::Unary {
1308                    op: "-".to_string(),
1309                    span: Span::new(operator.span.file, operator.span.start, operand.span().end),
1310                    operand: Box::new(operand),
1311                };
1312                let right = self.parse_multiplicative_tail(unary)?;
1313                let span = Span::new(left.span().file, left.span().start, right.span().end);
1314                left = Expr::Binary {
1315                    op: "-".to_string(),
1316                    left: Box::new(left),
1317                    right: Box::new(right),
1318                    span,
1319                };
1320                continue;
1321            }
1322            let op = match self.peek_kind() {
1323                TokenKind::Plus => "+",
1324                TokenKind::Minus => "-",
1325                _ => break,
1326            };
1327            self.advance();
1328            let right = self.parse_multiplicative()?;
1329            let span = Span::new(left.span().file, left.span().start, right.span().end);
1330            left = Expr::Binary {
1331                op: op.to_string(),
1332                left: Box::new(left),
1333                right: Box::new(right),
1334                span,
1335            };
1336        }
1337        Ok(left)
1338    }
1339
1340    fn parse_multiplicative(&mut self) -> Result<Expr, ()> {
1341        let left = self.parse_unary()?;
1342        self.parse_multiplicative_tail(left)
1343    }
1344
1345    fn parse_multiplicative_tail(&mut self, mut left: Expr) -> Result<Expr, ()> {
1346        loop {
1347            let op = match self.peek_kind() {
1348                TokenKind::Star => "*",
1349                TokenKind::Slash => "/",
1350                TokenKind::Percent => "%",
1351                _ => break,
1352            };
1353            self.advance();
1354            let right = self.parse_unary()?;
1355            let span = Span::new(left.span().file, left.span().start, right.span().end);
1356            left = Expr::Binary {
1357                op: op.to_string(),
1358                left: Box::new(left),
1359                right: Box::new(right),
1360                span,
1361            };
1362        }
1363        Ok(left)
1364    }
1365
1366    fn parse_unary(&mut self) -> Result<Expr, ()> {
1367        if matches!(self.peek_kind(), TokenKind::Minus | TokenKind::Decrement) {
1368            let start = self.advance();
1369            let operand = self.parse_unary()?;
1370            let end = operand.span().end;
1371            let unary = Expr::Unary {
1372                op: "-".to_string(),
1373                operand: Box::new(operand),
1374                span: Span::new(start.span.file, start.span.start, end),
1375            };
1376            if start.kind == TokenKind::Decrement {
1377                return Ok(Expr::Unary {
1378                    op: "-".to_string(),
1379                    operand: Box::new(unary),
1380                    span: Span::new(start.span.file, start.span.start, end),
1381                });
1382            }
1383            return Ok(unary);
1384        }
1385        self.parse_power()
1386    }
1387
1388    fn parse_power(&mut self) -> Result<Expr, ()> {
1389        let base = self.parse_postfix()?;
1390        if self.peek_kind() == TokenKind::DoubleStar {
1391            self.advance();
1392            // Right-associative.
1393            let exponent = self.parse_unary()?;
1394            let span = Span::new(base.span().file, base.span().start, exponent.span().end);
1395            return Ok(Expr::Binary {
1396                op: "**".to_string(),
1397                left: Box::new(base),
1398                right: Box::new(exponent),
1399                span,
1400            });
1401        }
1402        Ok(base)
1403    }
1404
1405    fn parse_postfix(&mut self) -> Result<Expr, ()> {
1406        let mut base = self.parse_primary()?;
1407        loop {
1408            match self.peek_kind() {
1409                TokenKind::LParen => {
1410                    let mut args = Vec::new();
1411                    self.parse_call_args(&mut args)?;
1412                    let end = self.tokens[self.pos.saturating_sub(1)].span.end;
1413                    base = match base {
1414                        Expr::Name { name, span } => Expr::Call {
1415                            name,
1416                            args,
1417                            span: Span::new(span.file, span.start, end),
1418                        },
1419                        Expr::Member {
1420                            receiver,
1421                            member,
1422                            span,
1423                            ..
1424                        } => Expr::ReceiverCall {
1425                            receiver,
1426                            name: member,
1427                            args,
1428                            span: Span::new(span.file, span.start, end),
1429                        },
1430                        _other => {
1431                            self.errors.push(OpyError::at(
1432                                "parse-error",
1433                                "cannot call this expression".to_string(),
1434                                self.peek().span,
1435                            ));
1436                            return Err(());
1437                        }
1438                    };
1439                }
1440                TokenKind::LBracket => {
1441                    self.advance();
1442                    let index = self.parse_expr()?;
1443                    if self.peek_kind() == TokenKind::Colon {
1444                        self.advance();
1445                        let maximum = self.parse_expr()?;
1446                        let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
1447                        let Expr::Name { name, span } = &base else {
1448                            self.error_at_current(
1449                                "a range type must start with a type name".to_string(),
1450                            );
1451                            return Err(());
1452                        };
1453                        base = Expr::Type {
1454                            name: name.clone(),
1455                            args: vec![index, maximum],
1456                            span: Span::new(span.file, span.start, end),
1457                        };
1458                        continue;
1459                    }
1460                    let end = match self.expect(TokenKind::RBracket, "']'") {
1461                        Ok(token) => token.span.end,
1462                        Err(()) => return Err(()),
1463                    };
1464                    let span = Span::new(base.span().file, base.span().start, end);
1465                    base = Expr::Index {
1466                        array: Box::new(base),
1467                        index: Box::new(index),
1468                        span,
1469                    };
1470                }
1471                TokenKind::Dot => {
1472                    self.advance();
1473                    let member_token = self.peek().clone();
1474                    let member = match self.peek_kind() {
1475                        TokenKind::Ident | TokenKind::Number => self.advance().text,
1476                        _ => {
1477                            self.error_at_current("expected a member name after '.'".to_string());
1478                            return Err(());
1479                        }
1480                    };
1481                    let member_span = member_token.span;
1482                    let end = member_span.end;
1483                    let span = Span::new(base.span().file, base.span().start, end);
1484                    base = Expr::Member {
1485                        receiver: Box::new(base),
1486                        member,
1487                        member_span,
1488                        span,
1489                    };
1490                }
1491                _ => break,
1492            }
1493        }
1494        Ok(base)
1495    }
1496
1497    /// `@Event name(args)`: positional expressions only (keyword arguments
1498    /// are a call-argument form, not an event form).
1499    fn parse_event_args(&mut self, args: &mut Vec<Expr>) -> Result<(), ()> {
1500        self.expect(TokenKind::LParen, "'('")?;
1501        self.skip_newlines();
1502        if self.peek_kind() == TokenKind::RParen {
1503            self.advance();
1504            return Ok(());
1505        }
1506        loop {
1507            let expr = self.parse_expr()?;
1508            if self.peek_kind() == TokenKind::Assign {
1509                self.error_at_current("keyword arguments are not valid in @Event".to_string());
1510                return Err(());
1511            }
1512            args.push(expr);
1513            self.skip_newlines();
1514            if self.peek_kind() == TokenKind::Comma {
1515                self.advance();
1516                self.skip_newlines();
1517                if self.peek_kind() == TokenKind::RParen {
1518                    break;
1519                }
1520            } else {
1521                break;
1522            }
1523        }
1524        self.expect(TokenKind::RParen, "')'")?;
1525        Ok(())
1526    }
1527
1528    fn parse_call_args(&mut self, args: &mut Vec<CallArg>) -> Result<(), ()> {
1529        self.expect(TokenKind::LParen, "'('")?;
1530        self.skip_newlines();
1531        if self.peek_kind() == TokenKind::RParen {
1532            self.advance();
1533            return Ok(());
1534        }
1535        loop {
1536            match self.parse_expr() {
1537                Ok(expr) => {
1538                    // A keyword argument is `name = expr` (issue #110): a
1539                    // bare identifier immediately followed by `=`. Anything
1540                    // else (`expr = ...`) is not a call argument form and is
1541                    // rejected like the pinned reference rejects it.
1542                    if self.peek_kind() == TokenKind::Assign {
1543                        let Expr::Name { name, span } = expr else {
1544                            self.error_at_current(
1545                                "expected a keyword name before '=' in this call".to_string(),
1546                            );
1547                            return Err(());
1548                        };
1549                        self.advance();
1550                        let value = match self.parse_expr() {
1551                            Ok(value) => value,
1552                            Err(()) => return Err(()),
1553                        };
1554                        args.push(CallArg {
1555                            keyword: Some((name, span)),
1556                            value,
1557                        });
1558                    } else {
1559                        args.push(CallArg {
1560                            keyword: None,
1561                            value: expr,
1562                        });
1563                    }
1564                }
1565                Err(()) => return Err(()),
1566            }
1567            self.skip_newlines();
1568            if self.peek_kind() == TokenKind::Comma {
1569                self.advance();
1570                self.skip_newlines();
1571                if self.peek_kind() == TokenKind::RParen {
1572                    break;
1573                }
1574            } else {
1575                break;
1576            }
1577        }
1578        self.expect(TokenKind::RParen, "')'")?;
1579        Ok(())
1580    }
1581
1582    fn parse_primary(&mut self) -> Result<Expr, ()> {
1583        let token = self.peek();
1584        match token.kind {
1585            TokenKind::Number => {
1586                let token = self.advance();
1587                let value = if let Some(hex) = token
1588                    .text
1589                    .strip_prefix("0x")
1590                    .or_else(|| token.text.strip_prefix("0X"))
1591                {
1592                    u64::from_str_radix(hex, 16).map_or(f64::NAN, |value| value as f64)
1593                } else {
1594                    token.text.parse().unwrap_or(f64::NAN)
1595                };
1596                Ok(Expr::Number {
1597                    value,
1598                    text: token.text.clone(),
1599                    span: token.span,
1600                })
1601            }
1602            TokenKind::String => self.parse_string_literal(),
1603            TokenKind::Ident => {
1604                let token = self.advance();
1605                if token.text == "lambda" {
1606                    return self.parse_lambda(token.span);
1607                }
1608                if is_string_modifier(&token.text) && self.peek_kind() == TokenKind::String {
1609                    let string = self.advance();
1610                    let (format_text, interpolations) = if token.text == "f" {
1611                        let raw = string.raw.as_deref().unwrap_or(&string.text);
1612                        let (format_text, interpolations) =
1613                            self.parse_f_string(raw, string.span)?;
1614                        (Some(format_text), interpolations)
1615                    } else {
1616                        (None, Vec::new())
1617                    };
1618                    return Ok(Expr::StringModifier {
1619                        modifier: token.text.chars().next().unwrap_or_default(),
1620                        value: string.text,
1621                        format_text,
1622                        interpolations,
1623                        span: Span::new(token.span.file, token.span.start, string.span.end),
1624                    });
1625                }
1626                match token.text.as_str() {
1627                    "true" => Ok(Expr::Bool {
1628                        value: true,
1629                        span: token.span,
1630                    }),
1631                    "false" => Ok(Expr::Bool {
1632                        value: false,
1633                        span: token.span,
1634                    }),
1635                    "None" | "null" => Ok(Expr::Null { span: token.span }),
1636                    _ => Ok(Expr::Name {
1637                        name: token.text.clone(),
1638                        span: token.span,
1639                    }),
1640                }
1641            }
1642            TokenKind::LParen => {
1643                self.advance();
1644                let expr = self.parse_expr()?;
1645                self.expect(TokenKind::RParen, "')'")?;
1646                Ok(expr)
1647            }
1648            TokenKind::LBracket => {
1649                let open = self.advance();
1650                let mut elements = Vec::new();
1651                self.skip_newlines();
1652                if self.peek_kind() == TokenKind::RBracket {
1653                    let end = self.advance().span.end;
1654                    return Ok(Expr::Array {
1655                        elements,
1656                        span: Span::new(open.span.file, open.span.start, end),
1657                    });
1658                }
1659                let first = self.parse_expr()?;
1660                if self.is_ident("for") {
1661                    self.advance();
1662                    let variable_token =
1663                        self.expect(TokenKind::Ident, "a comprehension variable")?;
1664                    let index = if self.peek_kind() == TokenKind::Comma {
1665                        self.advance();
1666                        let index = self.expect(TokenKind::Ident, "a comprehension index")?;
1667                        Some((index.text, index.span))
1668                    } else {
1669                        None
1670                    };
1671                    if !self.is_ident("in") {
1672                        self.error_at_current("expected `in` in list comprehension".to_string());
1673                        return Err(());
1674                    }
1675                    self.advance();
1676                    let iterable = self.parse_or()?;
1677                    let condition = if self.is_ident("if") {
1678                        self.advance();
1679                        Some(Box::new(self.parse_or()?))
1680                    } else {
1681                        None
1682                    };
1683                    let end = self.expect(TokenKind::RBracket, "']'")?.span.end;
1684                    return Ok(Expr::Comprehension {
1685                        element: Box::new(first),
1686                        variable: variable_token.text,
1687                        variable_span: variable_token.span,
1688                        index,
1689                        iterable: Box::new(iterable),
1690                        condition,
1691                        span: Span::new(open.span.file, open.span.start, end),
1692                    });
1693                }
1694                elements.push(first);
1695                loop {
1696                    self.skip_newlines();
1697                    if self.peek_kind() == TokenKind::Comma {
1698                        self.advance();
1699                        self.skip_newlines();
1700                        if self.peek_kind() == TokenKind::RBracket {
1701                            break;
1702                        }
1703                        elements.push(self.parse_expr()?);
1704                    } else {
1705                        break;
1706                    }
1707                }
1708                let end = match self.expect(TokenKind::RBracket, "']'") {
1709                    Ok(token) => token.span.end,
1710                    Err(()) => return Err(()),
1711                };
1712                Ok(Expr::Array {
1713                    elements,
1714                    span: Span::new(open.span.file, open.span.start, end),
1715                })
1716            }
1717            TokenKind::LBrace => self.parse_dict(),
1718            _ => {
1719                self.error_at_current(format!("expected an expression but found '{}'", token.text));
1720                Err(())
1721            }
1722        }
1723    }
1724
1725    /// Parse adjacent string literals as one source-language string value.
1726    ///
1727    /// Newlines are only ignored while looking for another literal inside a
1728    /// delimiter group. Outside a group, a newline remains a statement
1729    /// boundary, matching the bounded implicit-concatenation surface used by
1730    /// the OverPy examples.
1731    fn parse_string_literal(&mut self) -> Result<Expr, ()> {
1732        let first = self.advance();
1733        let mut value = first.text.clone();
1734        let mut end = first.span.end;
1735        loop {
1736            let saved = self.pos;
1737            if self.inside_delimiter_group() {
1738                self.skip_newlines();
1739            }
1740            if self.peek_kind() != TokenKind::String || self.peek().span.file != first.span.file {
1741                self.pos = saved;
1742                break;
1743            }
1744            let next = self.advance();
1745            value.push_str(&next.text);
1746            end = next.span.end;
1747        }
1748        Ok(Expr::String {
1749            value,
1750            span: Span::new(first.span.file, first.span.start, end),
1751        })
1752    }
1753
1754    /// Return whether the current parser position is inside `()`, `[]`, or
1755    /// `{}`. The token stream retains newlines, so this keeps multiline
1756    /// implicit concatenation scoped to syntactic grouping without adding
1757    /// parser state to every delimiter path.
1758    fn inside_delimiter_group(&self) -> bool {
1759        let mut depth = 0usize;
1760        for token in &self.tokens[..self.pos] {
1761            match token.kind {
1762                TokenKind::LParen | TokenKind::LBracket | TokenKind::LBrace => depth += 1,
1763                TokenKind::RParen | TokenKind::RBracket | TokenKind::RBrace => {
1764                    depth = depth.saturating_sub(1)
1765                }
1766                _ => {}
1767            }
1768        }
1769        depth != 0
1770    }
1771
1772    fn parse_dict(&mut self) -> Result<Expr, ()> {
1773        let open = self.advance();
1774        let mut entries = Vec::new();
1775        self.skip_newlines();
1776        if self.peek_kind() == TokenKind::RBrace {
1777            let end = self.advance().span.end;
1778            return Ok(Expr::Dict {
1779                entries,
1780                span: Span::new(open.span.file, open.span.start, end),
1781            });
1782        }
1783        loop {
1784            let key = self.parse_expr()?;
1785            self.expect(TokenKind::Colon, "':' in a dictionary entry")?;
1786            let value = self.parse_expr()?;
1787            let span = Span::new(key.span().file, key.span().start, value.span().end);
1788            entries.push(DictEntry { key, value, span });
1789            self.skip_newlines();
1790            if self.peek_kind() == TokenKind::Comma {
1791                self.advance();
1792                self.skip_newlines();
1793                if self.peek_kind() == TokenKind::RBrace {
1794                    break;
1795                }
1796            } else {
1797                break;
1798            }
1799        }
1800        let end = self.expect(TokenKind::RBrace, "'}'")?.span.end;
1801        Ok(Expr::Dict {
1802            entries,
1803            span: Span::new(open.span.file, open.span.start, end),
1804        })
1805    }
1806
1807    fn parse_lambda(&mut self, start: Span) -> Result<Expr, ()> {
1808        let mut params = Vec::new();
1809        loop {
1810            let param = self.expect(TokenKind::Ident, "a lambda parameter")?;
1811            params.push((param.text, param.span));
1812            if self.peek_kind() == TokenKind::Comma {
1813                self.advance();
1814            } else {
1815                break;
1816            }
1817        }
1818        self.expect(TokenKind::Colon, "':' after lambda parameters")?;
1819        let body = self.parse_expr()?;
1820        Ok(Expr::Lambda {
1821            params,
1822            body: Box::new(body.clone()),
1823            span: Span::new(start.file, start.start, body.span().end),
1824        })
1825    }
1826
1827    /// Parse the expression regions of a pinned-OverPy f-string. Double
1828    /// braces are literal braces; a single brace introduces one expression.
1829    /// The resulting expression tokens are shifted back into the source
1830    /// string so HIR and tooling retain source provenance.
1831    fn parse_f_string(&mut self, raw: &str, string_span: Span) -> Result<(String, Vec<Expr>), ()> {
1832        let chars: Vec<char> = raw.chars().collect();
1833        let mut text = String::new();
1834        let mut interpolations = Vec::new();
1835        let mut index = 0;
1836        while index < chars.len() {
1837            match chars[index] {
1838                '{' if chars.get(index + 1) == Some(&'{') => {
1839                    text.push_str("{{");
1840                    index += 2;
1841                }
1842                '}' if chars.get(index + 1) == Some(&'}') => {
1843                    text.push_str("}}");
1844                    index += 2;
1845                }
1846                '{' => {
1847                    let end = self.find_f_string_end(&chars, index + 1);
1848                    let Some(end) = end else {
1849                        self.errors.push(OpyError::at(
1850                            "parse-error",
1851                            "unterminated f-string interpolation".to_string(),
1852                            string_span,
1853                        ));
1854                        return Err(());
1855                    };
1856                    let expression: String = chars[index + 1..end].iter().collect();
1857                    if expression.trim().is_empty() {
1858                        self.errors.push(OpyError::at(
1859                            "parse-error",
1860                            "f-string interpolation cannot be empty".to_string(),
1861                            Span::new(
1862                                string_span.file,
1863                                Position::new(
1864                                    string_span.start.line,
1865                                    string_span.start.col + index as u32 + 1,
1866                                ),
1867                                Position::new(
1868                                    string_span.start.line,
1869                                    string_span.start.col + end as u32 + 1,
1870                                ),
1871                            ),
1872                        ));
1873                        return Err(());
1874                    }
1875                    let origin = Position::new(
1876                        string_span.start.line,
1877                        string_span.start.col + index as u32 + 1,
1878                    );
1879                    let parsed = parse_expression_fragment(&expression, string_span.file, origin)
1880                        .map_err(|error| {
1881                            self.errors.push(error);
1882                        });
1883                    let Ok(parsed) = parsed else {
1884                        return Err(());
1885                    };
1886                    text.push_str(&format!("{{{}}}", interpolations.len()));
1887                    interpolations.push(parsed);
1888                    index = end + 1;
1889                }
1890                '}' => {
1891                    self.errors.push(OpyError::at(
1892                        "parse-error",
1893                        "single '}' is not valid in an f-string".to_string(),
1894                        string_span,
1895                    ));
1896                    return Err(());
1897                }
1898                '\\' if index + 1 < chars.len() => {
1899                    text.push(decode_string_escape(chars[index + 1]));
1900                    index += 2;
1901                }
1902                character => {
1903                    text.push(character);
1904                    index += 1;
1905                }
1906            }
1907        }
1908        Ok((text, interpolations))
1909    }
1910
1911    fn find_f_string_end(&self, chars: &[char], start: usize) -> Option<usize> {
1912        let mut nested_braces = 0;
1913        let mut quote = None;
1914        let mut escaped = false;
1915        for (index, character) in chars.iter().enumerate().skip(start) {
1916            if escaped {
1917                escaped = false;
1918                continue;
1919            }
1920            if *character == '\\' && quote.is_some() {
1921                escaped = true;
1922                continue;
1923            }
1924            if let Some(active_quote) = quote {
1925                if *character == active_quote {
1926                    quote = None;
1927                }
1928                continue;
1929            }
1930            match character {
1931                '"' | '\'' => quote = Some(*character),
1932                '{' => nested_braces += 1,
1933                '}' if nested_braces == 0 => return Some(index),
1934                '}' => nested_braces -= 1,
1935                _ => {}
1936            }
1937        }
1938        None
1939    }
1940}
1941
1942/// Parse one f-string expression fragment and shift its local token spans
1943/// into the original source file.
1944fn parse_expression_fragment(text: &str, file: u32, origin: Position) -> Result<Expr, OpyError> {
1945    let mut tokens = crate::lexer::lex(crate::lexer::LexInput {
1946        file_id: file,
1947        text,
1948    })?;
1949    for token in &mut tokens {
1950        token.span = shift_span(token.span, origin);
1951    }
1952    let mut parser = Parser {
1953        tokens: &tokens,
1954        pos: 0,
1955        allow_macro_redeclaration: false,
1956        errors: Vec::new(),
1957    };
1958    let expression = parser.parse_expr().map_err(|()| {
1959        parser.errors.first().cloned().unwrap_or_else(|| {
1960            OpyError::at(
1961                "parse-error",
1962                "invalid f-string expression",
1963                Span::new(file, origin, origin),
1964            )
1965        })
1966    })?;
1967    if parser.peek_kind() != TokenKind::Eof {
1968        parser.error_at_current("unexpected tokens in f-string interpolation".to_string());
1969    }
1970    parser.errors.into_iter().next().map_or(Ok(expression), Err)
1971}
1972
1973fn shift_span(span: Span, origin: Position) -> Span {
1974    fn shift(position: Position, origin: Position) -> Position {
1975        Position::new(
1976            origin.line + position.line.saturating_sub(1),
1977            if position.line == 1 {
1978                origin.col + position.col.saturating_sub(1)
1979            } else {
1980                position.col
1981            },
1982        )
1983    }
1984    Span::new(
1985        span.file,
1986        shift(span.start, origin),
1987        shift(span.end, origin),
1988    )
1989}
1990
1991fn decode_string_escape(character: char) -> char {
1992    match character {
1993        'n' => '\n',
1994        't' => '\t',
1995        'r' => '\r',
1996        '\\' => '\\',
1997        '"' => '"',
1998        '\'' => '\'',
1999        other => other,
2000    }
2001}
2002
2003fn is_string_modifier(text: &str) -> bool {
2004    matches!(text, "f" | "w" | "l" | "b" | "c" | "t")
2005}
2006
2007#[cfg(test)]
2008mod tests {
2009    use super::*;
2010    use crate::lexer::{LexInput, lex};
2011
2012    fn parse_ok(text: &str) -> Program {
2013        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2014        let output = parse(&tokens);
2015        assert!(
2016            output.errors.is_empty(),
2017            "unexpected errors: {:?}",
2018            output.errors
2019        );
2020        output.program.unwrap()
2021    }
2022
2023    fn parse_err(text: &str) -> Vec<OpyError> {
2024        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2025        parse(&tokens).errors
2026    }
2027
2028    #[test]
2029    fn parses_basic_rule() {
2030        let program = parse_ok("rule \"setup\":\n    @Event global\n    disableInspector()\n");
2031        assert_eq!(program.rules.len(), 1);
2032        let RuleEntry::Rule(rule) = &program.rules[0] else {
2033            panic!("expected rule");
2034        };
2035        assert_eq!(rule.name, "setup");
2036        assert_eq!(rule.event.name, "global");
2037        assert_eq!(rule.actions.len(), 1);
2038    }
2039
2040    #[test]
2041    fn parses_power_augmented_assignment() {
2042        // The pinned OverPy 9.7.10 reference accepts `**=` as the power
2043        // augmented assignment (`a **= b` ⇔ `a = a ** b`).
2044        let program =
2045            parse_ok("globalvar a\nrule \"r\":\n    @Event global\n    a = 2\n    a **= 3\n");
2046        let RuleEntry::Rule(rule) = &program.rules[0] else {
2047            panic!("expected rule");
2048        };
2049        let Stmt::Assign {
2050            value,
2051            target: assigned_target,
2052            ..
2053        } = &rule.actions[1]
2054        else {
2055            panic!("expected an assignment");
2056        };
2057        let Expr::Binary {
2058            op, left, right, ..
2059        } = value
2060        else {
2061            panic!("expected a binary modification, got {value:?}");
2062        };
2063        assert_eq!(op, "**");
2064        assert!(matches!(&**left, Expr::Name { .. }));
2065        assert!(matches!(
2066            assigned_target,
2067            Expr::Name { name, .. } if name == "a"
2068        ));
2069        assert!(matches!(right.as_ref(), Expr::Number { .. }));
2070    }
2071
2072    #[test]
2073    fn parses_postfix_increment_and_decrement_as_modifications() {
2074        let program =
2075            parse_ok("globalvar value\nrule \"r\":\n    @Event global\n    value++\n    value--\n");
2076        let RuleEntry::Rule(rule) = &program.rules[0] else {
2077            panic!("expected a rule");
2078        };
2079        for (statement, expected_op) in [(&rule.actions[0], "+"), (&rule.actions[1], "-")] {
2080            let Stmt::Assign { target, value, .. } = statement else {
2081                panic!("expected a postfix assignment");
2082            };
2083            let Expr::Binary {
2084                op, left, right, ..
2085            } = value
2086            else {
2087                panic!("expected a synthetic modification value");
2088            };
2089            assert_eq!(op, expected_op);
2090            let Expr::Name {
2091                name: left_name, ..
2092            } = left.as_ref()
2093            else {
2094                panic!("expected the target to be the modification's left operand");
2095            };
2096            let Expr::Name {
2097                name: target_name, ..
2098            } = target
2099            else {
2100                panic!("expected a name target");
2101            };
2102            assert_eq!(left_name, target_name);
2103            assert!(
2104                matches!(right.as_ref(), Expr::Number { value, text, .. } if *value == 1.0 && text == "1")
2105            );
2106        }
2107    }
2108
2109    #[test]
2110    fn rejects_prefix_increment_and_embedded_postfix_forms() {
2111        for source in [
2112            "globalvar value\nrule \"r\":\n    @Event global\n    ++value\n",
2113            "globalvar value\nrule \"r\":\n    @Event global\n    value++++\n",
2114        ] {
2115            let errors = parse_err(source);
2116            assert!(!errors.is_empty());
2117            assert!(errors.iter().all(|error| error.code == "parse-error"));
2118            assert!(errors.iter().all(|error| error.span.is_some()));
2119        }
2120    }
2121
2122    #[test]
2123    fn preserves_consecutive_unary_minus_expressions() {
2124        let source = concat!(
2125            "globalvar value = 0\n",
2126            "globalvar B = 1\n",
2127            "rule \"r\":\n",
2128            "    @Event global\n",
2129            "    value = --1\n",
2130            "    value = --B\n",
2131            "    value = B--1\n",
2132        );
2133        parse_ok(source);
2134    }
2135
2136    #[test]
2137    fn parses_control_flow() {
2138        let program = parse_ok(
2139            "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",
2140        );
2141        let RuleEntry::Rule(rule) = &program.rules[0] else {
2142            panic!();
2143        };
2144        assert!(matches!(rule.actions[0], Stmt::For { .. }));
2145        let Stmt::For { body, .. } = &rule.actions[0] else {
2146            panic!();
2147        };
2148        let Stmt::If {
2149            branches, r#else, ..
2150        } = &body[0]
2151        else {
2152            panic!();
2153        };
2154        assert_eq!(branches.len(), 2);
2155        assert!(r#else.is_some());
2156        let Stmt::While { body, .. } = &rule.actions[1] else {
2157            panic!();
2158        };
2159        assert_eq!(body.len(), 2);
2160    }
2161
2162    #[test]
2163    fn parses_issue_28_constructs() {
2164        let program = parse_ok(
2165            "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",
2166        );
2167        let RuleEntry::Rule(rule) = &program.rules[0] else {
2168            panic!("expected rule");
2169        };
2170        assert!(matches!(rule.actions[0], Stmt::Switch { .. }));
2171        assert!(matches!(rule.actions[1], Stmt::Assign { .. }));
2172    }
2173
2174    #[test]
2175    fn rejects_incomplete_do_while_and_dictionary_entries() {
2176        let errors = parse_err(
2177            "rule \"r\":\n    @Event global\n    do:\n        pass\n    while\n    x = {\"x\"}\n",
2178        );
2179        assert!(!errors.is_empty());
2180        assert!(errors.iter().all(|error| error.code == "parse-error"));
2181    }
2182
2183    #[test]
2184    fn parses_multi_line_array() {
2185        let program = parse_ok(
2186            "globalvar p\nrule \"r\":\n    @Event global\n    p = [\n        vect(1, 0, 0),\n        vect(2, 0, 0),\n    ]\n",
2187        );
2188        let RuleEntry::Rule(rule) = &program.rules[0] else {
2189            panic!();
2190        };
2191        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2192            panic!();
2193        };
2194        let Expr::Array { elements, .. } = value else {
2195            panic!("expected array, got {value:?}");
2196        };
2197        assert_eq!(elements.len(), 2);
2198    }
2199
2200    #[test]
2201    fn missing_colon_is_a_structured_error() {
2202        let errors = parse_err("rule \"x\"\n    @Event global\n");
2203        assert!(!errors.is_empty());
2204        assert_eq!(errors[0].code, "parse-error");
2205        assert!(errors[0].span.is_some());
2206    }
2207
2208    #[test]
2209    fn def_and_macro_parse() {
2210        let program = parse_ok(
2211            "subroutine showStatus\n\ndef showStatus():\n    print(\"hi\")\n\nmacro double(value):\n    value + value\n",
2212        );
2213        assert_eq!(program.declarations.len(), 2);
2214        assert!(matches!(program.declarations[1], Decl::Macro { .. }));
2215        let Decl::Macro { args, body, .. } = &program.declarations[1] else {
2216            panic!();
2217        };
2218        assert_eq!(args, &vec!["value".to_string()]);
2219        assert_eq!(body.len(), 1);
2220    }
2221
2222    #[test]
2223    fn macro_and_enum_redeclarations_are_checked_at_ast_surfaces() {
2224        let text = "enum Kind:\n    First\n    First\nmacro helper():\n    pass\nmacro helper():\n    pass\n";
2225        let errors = parse_err(text);
2226        assert_eq!(
2227            errors
2228                .iter()
2229                .filter(|error| error.code == "macro-redeclaration")
2230                .count(),
2231            2
2232        );
2233
2234        let tokens = lex(LexInput { file_id: 0, text }).unwrap();
2235        let output = parse_with_options(&tokens, true);
2236        assert!(
2237            output.errors.is_empty(),
2238            "unexpected errors: {:?}",
2239            output.errors
2240        );
2241        assert!(output.program.is_some());
2242    }
2243
2244    #[test]
2245    fn multiple_errors_are_reported() {
2246        let errors =
2247            parse_err("rule \"a\"\n    bad statement here\nrule \"b\"\n    @Event global\n");
2248        assert!(!errors.is_empty());
2249    }
2250
2251    #[test]
2252    fn precedence_parses_python_like() {
2253        let program = parse_ok("globalvar x\nrule \"r\":\n    @Event global\n    x = 1 + 2 * 3\n");
2254        let RuleEntry::Rule(rule) = &program.rules[0] else {
2255            panic!();
2256        };
2257        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2258            panic!();
2259        };
2260        let Expr::Binary {
2261            op, left, right, ..
2262        } = value
2263        else {
2264            panic!();
2265        };
2266        assert_eq!(op, "+");
2267        let Expr::Binary { op: inner, .. } = right.as_ref() else {
2268            panic!();
2269        };
2270        assert_eq!(inner, "*");
2271        assert!(matches!(left.as_ref(), Expr::Number { .. }));
2272    }
2273
2274    #[test]
2275    fn parses_right_associative_conditional_expressions() {
2276        let program = parse_ok(
2277            "rule \"r\":\n    @Event global\n    debug(1 if true else 2 if false else 3)\n",
2278        );
2279        let RuleEntry::Rule(rule) = &program.rules[0] else {
2280            panic!("expected a rule");
2281        };
2282        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2283            panic!("expected an expression statement");
2284        };
2285        let Expr::Call { args, .. } = expr else {
2286            panic!("expected a call");
2287        };
2288        let Expr::Conditional {
2289            then_value,
2290            condition,
2291            else_value,
2292            span,
2293        } = &args[0].value
2294        else {
2295            panic!("expected a conditional expression");
2296        };
2297        assert!(matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 1.0));
2298        assert!(matches!(condition.as_ref(), Expr::Bool { value: true, .. }));
2299        assert!(matches!(
2300            else_value.as_ref(),
2301            Expr::Conditional { then_value, condition, else_value, .. }
2302                if matches!(then_value.as_ref(), Expr::Number { value, .. } if *value == 2.0)
2303                    && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2304                    && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2305        ));
2306        assert_eq!(span.start.line, 3);
2307        assert_eq!(span.start.col, 11);
2308    }
2309
2310    #[test]
2311    fn parses_parenthesized_nested_conditional_and_rejects_missing_else() {
2312        let program = parse_ok(
2313            "rule \"r\":\n    @Event global\n    debug((1 if true else 2) if false else 3)\n",
2314        );
2315        let RuleEntry::Rule(rule) = &program.rules[0] else {
2316            panic!("expected a rule");
2317        };
2318        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2319            panic!("expected an expression statement");
2320        };
2321        let Expr::Call { args, .. } = expr else {
2322            panic!("expected a call");
2323        };
2324        assert!(matches!(
2325            &args[0].value,
2326            Expr::Conditional {
2327                then_value,
2328                condition,
2329                else_value,
2330                ..
2331            } if matches!(then_value.as_ref(), Expr::Conditional { .. })
2332                && matches!(condition.as_ref(), Expr::Bool { value: false, .. })
2333                && matches!(else_value.as_ref(), Expr::Number { value, .. } if *value == 3.0)
2334        ));
2335
2336        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 if true)\n");
2337        assert_eq!(errors[0].code, "parse-error");
2338        assert!(errors[0].message.contains("expected `else`"));
2339    }
2340
2341    #[test]
2342    fn parses_receiver_calls() {
2343        // `eventPlayer.setMoveSpeed(100)` is a receiver call: postfix `.`
2344        // member access followed by call arguments (#104).
2345        let program =
2346            parse_ok("rule \"r\":\n    @Event eachPlayer\n    eventPlayer.setMoveSpeed(100)\n");
2347        let RuleEntry::Rule(rule) = &program.rules[0] else {
2348            panic!("expected rule");
2349        };
2350        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2351            panic!("expected expression statement, got {:?}", rule.actions[0]);
2352        };
2353        let Expr::ReceiverCall {
2354            receiver,
2355            name,
2356            args,
2357            ..
2358        } = &expr
2359        else {
2360            panic!("expected receiver call, got {expr:?}");
2361        };
2362        assert_eq!(name, "setMoveSpeed");
2363        assert!(
2364            matches!(receiver.as_ref(), Expr::Name { name, .. } if name == "eventPlayer"),
2365            "receiver must be the eventPlayer name"
2366        );
2367        assert_eq!(args.len(), 1);
2368        assert!(args[0].keyword.is_none(), "positional argument");
2369        assert!(matches!(&args[0].value, Expr::Number { .. }));
2370    }
2371
2372    #[test]
2373    fn parses_keyword_arguments_with_name_spans() {
2374        // `name = expr` call arguments are keyword arguments carrying the
2375        // name token's exact span (issue #110); comparisons stay positional.
2376        let program =
2377            parse_ok("rule \"r\":\n    @Event global\n    wait(time=1)\n    debug(g == 1)\n");
2378        let RuleEntry::Rule(rule) = &program.rules[0] else {
2379            panic!("expected rule");
2380        };
2381        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2382            panic!("expected expression statement");
2383        };
2384        let Expr::Call { args, .. } = expr else {
2385            panic!("expected a call, got {expr:?}");
2386        };
2387        let (keyword, span) = args[0].keyword.as_ref().expect("keyword argument");
2388        assert_eq!(keyword, "time");
2389        assert_eq!(span.start.line, 3);
2390        assert!(matches!(&args[0].value, Expr::Number { .. }));
2391
2392        let Stmt::Expr { expr, .. } = &rule.actions[1] else {
2393            panic!("expected expression statement");
2394        };
2395        let Expr::Call { args, .. } = expr else {
2396            panic!("expected a call, got {expr:?}");
2397        };
2398        assert!(args[0].keyword.is_none(), "comparisons are not keywords");
2399        assert!(matches!(&args[0].value, Expr::Binary { .. }));
2400    }
2401
2402    #[test]
2403    fn adjacent_string_literals_concatenate_and_preserve_span() {
2404        let program = parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\" \"two\")\n");
2405        let RuleEntry::Rule(rule) = &program.rules[0] else {
2406            panic!("expected rule");
2407        };
2408        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2409            panic!("expected expression statement");
2410        };
2411        let Expr::Call { args, .. } = expr else {
2412            panic!("expected call");
2413        };
2414        let Expr::String { value, span } = &args[0].value else {
2415            panic!("expected concatenated string");
2416        };
2417        assert_eq!(value, "onetwo");
2418        assert_eq!(span.start.line, 3);
2419        assert_eq!(span.start.col, 11);
2420        assert_eq!(span.end.col, 22);
2421    }
2422
2423    #[test]
2424    fn multiline_adjacent_string_literals_concatenate_inside_group() {
2425        let program =
2426            parse_ok("rule \"r\":\n    @Event global\n    debug(\"one\"\n        \"two\")\n");
2427        let RuleEntry::Rule(rule) = &program.rules[0] else {
2428            panic!("expected rule");
2429        };
2430        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2431            panic!("expected expression statement");
2432        };
2433        let Expr::Call { args, .. } = expr else {
2434            panic!("expected call");
2435        };
2436        assert!(matches!(
2437            &args[0].value,
2438            Expr::String { value, .. } if value == "onetwo"
2439        ));
2440    }
2441
2442    #[test]
2443    fn newline_outside_group_keeps_adjacent_literals_as_statements() {
2444        let program = parse_ok("rule \"r\":\n    @Event global\n    \"one\"\n    \"two\"\n");
2445        let RuleEntry::Rule(rule) = &program.rules[0] else {
2446            panic!("expected rule");
2447        };
2448        assert_eq!(rule.actions.len(), 2);
2449    }
2450
2451    #[test]
2452    fn non_name_keyword_lhs_is_a_parse_error() {
2453        // `f(1 = 2)` is not a call argument form; rejected explicitly.
2454        let errors = parse_err("rule \"r\":\n    @Event global\n    debug(1 = 2)\n");
2455        assert!(!errors.is_empty());
2456        assert_eq!(errors[0].code, "parse-error");
2457    }
2458
2459    #[test]
2460    fn parses_member_call_on_call_result() {
2461        // `getPlayersInRadius(...).setStatusEffect(...)`: member access
2462        // followed by call arguments on a call result stays a receiver call.
2463        let program = parse_ok(
2464            "rule \"r\":\n    @Event eachPlayer\n    getPlayersInRadius(eventPlayer, 10).setStatusEffect(eventPlayer, 30)\n",
2465        );
2466        let RuleEntry::Rule(rule) = &program.rules[0] else {
2467            panic!("expected rule");
2468        };
2469        let Stmt::Expr { expr, .. } = &rule.actions[0] else {
2470            panic!("expected expression statement");
2471        };
2472        let Expr::ReceiverCall {
2473            receiver,
2474            name,
2475            args,
2476            ..
2477        } = &expr
2478        else {
2479            panic!("expected receiver call, got {expr:?}");
2480        };
2481        assert_eq!(name, "setStatusEffect");
2482        assert!(
2483            matches!(receiver.as_ref(), Expr::Call { name, .. } if name == "getPlayersInRadius"),
2484            "receiver must be the preceding call"
2485        );
2486        assert_eq!(args.len(), 2);
2487    }
2488
2489    #[test]
2490    fn member_without_call_is_not_a_call() {
2491        // `eventPlayer.moveSpeed` alone (no parentheses) stays a member
2492        // access; only a following `(` turns it into a receiver call.
2493        let program =
2494            parse_ok("rule \"r\":\n    @Event eachPlayer\n    x = eventPlayer.moveSpeed\n");
2495        let RuleEntry::Rule(rule) = &program.rules[0] else {
2496            panic!("expected rule");
2497        };
2498        let Stmt::Assign { value, .. } = &rule.actions[0] else {
2499            panic!("expected assignment");
2500        };
2501        assert!(matches!(
2502            &value,
2503            Expr::Member { member, .. } if member == "moveSpeed"
2504        ));
2505    }
2506
2507    #[test]
2508    fn parses_advanced_rule_annotations_with_source_arguments() {
2509        let program = parse_ok(
2510            "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",
2511        );
2512        let RuleEntry::SubroutineDef { annotations, .. } = &program.rules[0] else {
2513            panic!("expected subroutine");
2514        };
2515        assert_eq!(annotations.len(), 2);
2516        let RuleEntry::Rule(rule) = &program.rules[1] else {
2517            panic!("expected rule");
2518        };
2519        assert!(rule.disabled);
2520        assert!(rule.delimiter);
2521        assert_eq!(rule.new_page.as_deref(), Some("Page"));
2522        assert_eq!(rule.annotations.len(), 7);
2523        assert_eq!(rule.annotations[1].args[0].text, "1");
2524        assert_eq!(rule.annotations[2].args[0].text, "dmon");
2525    }
2526}