Skip to main content

gdscript_syntax/
parser.rs

1//! WS3 — the resilient recursive-descent parser.
2//!
3//! Architecture (matklad's "Resilient LL Parsing", adapted to build a [`cstree`]
4//! tree — see `plans/PHASE-1-IMPLEMENTATION-PLAYBOOK.md` §WS3):
5//!
6//! - The parser walks the **non-trivia** tokens (real tokens + the synthetic
7//!   `Newline`/`Indent`/`Dedent` markers) and emits a flat [`Event`] stream
8//!   (`Open`/`Close`/`Advance`). It never returns `Result`: parsing *always* yields a
9//!   tree plus a list of [`SyntaxError`]s.
10//! - A [`Marker`]/[`MarkClosed`] API lets a node be opened, closed with its final
11//!   kind, or wrapped retroactively (`open_before`) — e.g. promoting an expression to
12//!   a `BinExpr` once an operator is seen.
13//! - A **fuel** counter turns any accidental non-advancing loop into an immediate
14//!   panic the robustness harness catches, instead of a hang.
15//! - The `sink` replays the events over the *full* token stream (trivia included),
16//!   building the lossless green tree and re-attaching trivia.
17//!
18//! The grammar productions live in [`grammar`]; this module owns the machinery.
19
20use std::cell::Cell;
21use std::sync::Arc;
22
23use cstree::Syntax;
24use cstree::build::GreenNodeBuilder;
25use cstree::green::GreenNode;
26use cstree::interning::TokenInterner;
27use cstree::syntax::ResolvedNode;
28use text_size::{TextRange, TextSize};
29
30use crate::SyntaxKind;
31use crate::lexer::{RawToken, tokenize};
32use crate::prepass::run as run_prepass;
33
34mod grammar;
35
36/// The result of parsing a source file: a lossless green tree, the interner needed to
37/// read token text back, and the diagnostics gathered while parsing.
38#[derive(Debug, Clone)]
39pub struct Parse {
40    green: GreenNode,
41    interner: Arc<TokenInterner>,
42    errors: Vec<SyntaxError>,
43}
44
45impl Parse {
46    /// The resolved (interner-carrying) red tree root. Cheap to produce; supports
47    /// `Display`/`.text()` and the byte-exact round-trip.
48    #[must_use]
49    pub fn syntax_node(&self) -> ResolvedNode<SyntaxKind> {
50        ResolvedNode::new_root_with_resolver(self.green.clone(), Arc::clone(&self.interner))
51    }
52
53    /// The parse diagnostics (lexer/parser recovery + indentation issues).
54    #[must_use]
55    pub fn errors(&self) -> &[SyntaxError] {
56        &self.errors
57    }
58
59    /// The raw green tree (position-independent, shared).
60    #[must_use]
61    pub fn green(&self) -> &GreenNode {
62        &self.green
63    }
64
65    /// A stable, indented S-expression dump of the tree (kinds + byte ranges + token
66    /// text) — the golden-fixture review surface.
67    #[must_use]
68    pub fn debug_tree(&self) -> String {
69        cstree::syntax::SyntaxNode::<SyntaxKind>::new_root(self.green.clone())
70            .debug(&self.interner, true)
71    }
72}
73
74/// Equality compares the lossless green tree and the diagnostics; the **interner is excluded**
75/// because it is a derived token-text cache (two parses with equal green trees reference equal
76/// token text). This makes [`Parse`] a sound `salsa` tracked-fn return: an unchanged reparse
77/// *backdates* instead of invalidating dependents — the Phase-3 incrementality precondition
78/// (Playbook §4). `GreenNode` equality is structural, so this is `O(tree)` worst case but
79/// short-circuits on the first difference.
80impl PartialEq for Parse {
81    fn eq(&self, other: &Self) -> bool {
82        self.green == other.green && self.errors == other.errors
83    }
84}
85impl Eq for Parse {}
86
87/// A byte-ranged syntax diagnostic with an "expected X" style message.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct SyntaxError {
90    /// The byte range the error applies to.
91    pub range: TextRange,
92    /// A human-readable message.
93    pub message: String,
94}
95
96/// Parse GDScript source into a lossless [`Parse`]. Never fails.
97#[must_use]
98pub fn parse(text: &str) -> Parse {
99    let raw = tokenize(text);
100    let (tokens, indent_diags) = run_prepass(&raw, text);
101
102    let mut p = Parser::new(text, &tokens);
103    p.source_file();
104    let Parser {
105        events, mut errors, ..
106    } = p;
107
108    errors.extend(indent_diags.into_iter().map(|d| SyntaxError {
109        range: d.range,
110        message: d.message,
111    }));
112
113    let (green, interner) = build_tree(&events, &tokens, text);
114    Parse {
115        green,
116        interner,
117        errors,
118    }
119}
120
121/// A parser event. `Open`'s kind is `Tombstone` until the matching [`Parser::close`]
122/// overwrites it; an `Open` left as `Tombstone` is an abandoned marker the sink skips.
123#[derive(Debug, Clone, Copy)]
124enum Event {
125    Open { kind: SyntaxKind },
126    Close,
127    Advance,
128}
129
130/// A handle to an opened-but-not-yet-closed node (an index into the event list).
131struct Marker {
132    pos: usize,
133}
134
135/// A handle to a closed node, usable to wrap it retroactively via
136/// [`Parser::open_before`].
137#[derive(Clone, Copy)]
138struct MarkClosed {
139    pos: usize,
140}
141
142/// How many times [`Parser::nth`] may be called without an intervening
143/// [`Parser::advance`] before we declare the parser stuck. Generous; only a genuine
144/// non-advancing loop trips it.
145const FUEL: u32 = 256;
146
147struct Parser<'s> {
148    src: &'s str,
149    tokens: &'s [RawToken],
150    /// Indices into `tokens` of the non-trivia tokens the grammar walks.
151    nontrivia: Vec<usize>,
152    /// Cursor into `nontrivia`.
153    pos: usize,
154    fuel: Cell<u32>,
155    events: Vec<Event>,
156    errors: Vec<SyntaxError>,
157}
158
159impl<'s> Parser<'s> {
160    fn new(src: &'s str, tokens: &'s [RawToken]) -> Self {
161        let nontrivia = tokens
162            .iter()
163            .enumerate()
164            .filter(|(_, t)| !t.kind.is_trivia())
165            .map(|(i, _)| i)
166            .collect();
167        Self {
168            src,
169            tokens,
170            nontrivia,
171            pos: 0,
172            fuel: Cell::new(FUEL),
173            events: Vec::new(),
174            errors: Vec::new(),
175        }
176    }
177
178    /// The kind `n` non-trivia tokens ahead (`Eof` past the end). Burns a unit of fuel.
179    fn nth(&self, n: usize) -> SyntaxKind {
180        assert!(self.fuel.get() > 0, "parser stuck at position {}", self.pos);
181        self.fuel.set(self.fuel.get() - 1);
182        self.nontrivia
183            .get(self.pos + n)
184            .map_or(SyntaxKind::Eof, |&i| self.tokens[i].kind)
185    }
186
187    fn at(&self, kind: SyntaxKind) -> bool {
188        self.nth(0) == kind
189    }
190
191    /// Whether a statement-initial `match` begins a `match` STATEMENT (vs. `match` used as an
192    /// *identifier* expression — the soft-keyword extension). It is an identifier use when the next
193    /// token is a member access (`.`), an assignment operator, or a call/index (`(` / `[`) whose
194    /// bracket group is **not** followed by `:` — a parenthesised / array match *subject* is
195    /// `match (x):` / `match [a]:` (colon-terminated), whereas `match(x)` / `match[i]` is a call /
196    /// index on a variable named `match`. Reads the raw non-trivia buffer directly (no `nth`, so it
197    /// is fuel-free and safe over an arbitrarily long subject); the keyword reading is the safe
198    /// default for any ambiguity.
199    fn match_begins_statement(&self) -> bool {
200        use SyntaxKind as K;
201        let kind_at = |off: usize| {
202            self.nontrivia
203                .get(self.pos + off)
204                .map_or(K::Eof, |&i| self.tokens[i].kind)
205        };
206        match kind_at(1) {
207            // A member access (`.`) or an assignment operator is an unambiguous identifier use.
208            K::Dot
209            | K::Eq
210            | K::ColonEq
211            | K::PlusEq
212            | K::MinusEq
213            | K::StarEq
214            | K::SlashEq
215            | K::StarStarEq
216            | K::PercentEq
217            | K::AmpEq
218            | K::PipeEq
219            | K::CaretEq
220            | K::ShlEq
221            | K::ShrEq => false,
222            K::LParen | K::LBrack => {
223                // Scan the balanced bracket group; an identifier use iff no `:` follows its close.
224                let mut depth = 0usize;
225                let mut off = 1;
226                loop {
227                    match kind_at(off) {
228                        K::Eof => return true, // unterminated → treat as the keyword (safe)
229                        K::LParen | K::LBrack | K::LBrace => depth += 1,
230                        K::RParen | K::RBrack | K::RBrace => {
231                            depth -= 1;
232                            if depth == 0 {
233                                // A colon right after the close ⇒ a parenthesised/array match SUBJECT
234                                // (`match (x):`); anything else ⇒ a call/index on the identifier.
235                                return kind_at(off + 1) == K::Colon;
236                            }
237                        }
238                        _ => {}
239                    }
240                    off += 1;
241                }
242            }
243            _ => true,
244        }
245    }
246
247    fn at_any(&self, kinds: &[SyntaxKind]) -> bool {
248        kinds.contains(&self.nth(0))
249    }
250
251    fn eof(&self) -> bool {
252        self.pos >= self.nontrivia.len()
253    }
254
255    /// The byte range of the current token (an empty range at EOF), for diagnostics.
256    fn cur_range(&self) -> TextRange {
257        self.nontrivia.get(self.pos).map_or_else(
258            || TextRange::empty(TextSize::of(self.src)),
259            |&i| self.tokens[i].range,
260        )
261    }
262
263    /// The source text of the current token (`""` at EOF) — used for the few
264    /// contextual keywords GDScript lexes as identifiers (`get`/`set`).
265    fn cur_text(&self) -> &str {
266        self.nontrivia
267            .get(self.pos)
268            .map_or("", |&i| &self.src[self.tokens[i].range])
269    }
270
271    fn advance(&mut self) {
272        // A resilient parser treats `advance` at EOF as a no-op: recovery paths may
273        // reach it, and every list/loop re-checks `eof()`, so this can't spin. (Fuel is
274        // only reset on a real advance, so a stuck loop still trips the fuel guard.)
275        if self.eof() {
276            return;
277        }
278        self.fuel.set(FUEL);
279        self.events.push(Event::Advance);
280        self.pos += 1;
281    }
282
283    fn open(&mut self) -> Marker {
284        let m = Marker {
285            pos: self.events.len(),
286        };
287        self.events.push(Event::Open {
288            kind: SyntaxKind::Tombstone,
289        });
290        m
291    }
292
293    // `Marker` is intentionally consumed by value: moving it enforces "close a node
294    // exactly once" at the type level (a used Marker can't be reused or dropped).
295    #[allow(clippy::needless_pass_by_value)]
296    fn close(&mut self, m: Marker, kind: SyntaxKind) -> MarkClosed {
297        self.events[m.pos] = Event::Open { kind };
298        self.events.push(Event::Close);
299        MarkClosed { pos: m.pos }
300    }
301
302    /// Wrap an already-closed node in a new (outer) node — the retroactive-wrap used
303    /// by the Pratt parser to promote operands into `BinExpr`/`CallExpr`/etc.
304    fn open_before(&mut self, m: MarkClosed) -> Marker {
305        self.events.insert(
306            m.pos,
307            Event::Open {
308                kind: SyntaxKind::Tombstone,
309            },
310        );
311        Marker { pos: m.pos }
312    }
313
314    fn eat(&mut self, kind: SyntaxKind) -> bool {
315        if self.at(kind) {
316            self.advance();
317            true
318        } else {
319            false
320        }
321    }
322
323    /// Consume `kind` or record an `Expected "…".` diagnostic (without consuming) —
324    /// Godot's quoting convention via [`SyntaxKind::display_name`], never the Rust
325    /// Debug name.
326    fn expect(&mut self, kind: SyntaxKind) {
327        if self.eat(kind) {
328            return;
329        }
330        self.error(format!("Expected {}.", kind.display_name()));
331    }
332
333    /// [`Self::expect`] with Godot's contextual wording: `Expected ":" after "if"
334    /// condition.` — `context` carries its own quoting (probes p11/q27/s01-s04/s12).
335    fn expect_after(&mut self, kind: SyntaxKind, context: &str) {
336        if self.eat(kind) {
337            return;
338        }
339        self.error(format!("Expected {} after {context}.", kind.display_name()));
340    }
341
342    /// [`Self::expect`] for closing brackets, Godot-style: `Expected closing ")" after
343    /// call arguments.` (probes p16/p18/s07/s08).
344    fn expect_closing(&mut self, kind: SyntaxKind, context: &str) {
345        if self.eat(kind) {
346            return;
347        }
348        self.error(format!(
349            "Expected closing {} after {context}.",
350            kind.display_name()
351        ));
352    }
353
354    /// Record a diagnostic at the current token.
355    fn error(&mut self, message: String) {
356        self.errors.push(SyntaxError {
357            range: self.cur_range(),
358            message,
359        });
360    }
361
362    /// Wrap the current (unexpected) token in an `ErrorNode` and report it — the
363    /// skip-one-token recovery step. Makes progress so loops terminate. Returns the
364    /// closed node so it can be used as an operand placeholder in expression recovery.
365    fn advance_with_error(&mut self, message: &str) -> MarkClosed {
366        let m = self.open();
367        self.error(message.to_owned());
368        if !self.eof() {
369            self.advance();
370        }
371        self.close(m, SyntaxKind::ErrorNode)
372    }
373}
374
375/// Replay the parser events over the full token stream (trivia included) to build the
376/// lossless green tree. Trivia is flushed before each advanced token; trailing trivia
377/// is flushed inside the root just before it closes.
378fn build_tree(events: &[Event], tokens: &[RawToken], src: &str) -> (GreenNode, Arc<TokenInterner>) {
379    let mut builder: GreenNodeBuilder<'static, 'static, SyntaxKind> = GreenNodeBuilder::new();
380    let mut tok = 0usize;
381    let mut depth: u32 = 0;
382
383    for event in events {
384        match *event {
385            Event::Open { kind } => {
386                if kind == SyntaxKind::Tombstone {
387                    continue; // abandoned marker
388                }
389                depth += 1;
390                builder.start_node(kind);
391            }
392            Event::Close => {
393                depth -= 1;
394                if depth == 0 {
395                    // Root closing: flush any remaining tokens (trailing trivia) inside
396                    // it so nothing escapes the single root.
397                    while tok < tokens.len() {
398                        emit(&mut builder, tokens[tok], src);
399                        tok += 1;
400                    }
401                }
402                builder.finish_node();
403            }
404            Event::Advance => {
405                while tok < tokens.len() && tokens[tok].kind.is_trivia() {
406                    emit(&mut builder, tokens[tok], src);
407                    tok += 1;
408                }
409                if tok < tokens.len() {
410                    emit(&mut builder, tokens[tok], src);
411                    tok += 1;
412                }
413            }
414        }
415    }
416
417    let (green, cache) = builder.finish();
418    let interner = cache
419        .expect("a builder created with `new()` owns its cache")
420        .into_interner()
421        .expect("the cache owns its interner");
422    (green, Arc::new(interner))
423}
424
425/// Emit one token into the builder: fixed-lexeme kinds via `static_token`, everything
426/// else (identifiers, literals, trivia, the zero-width synthetic markers) via the
427/// interning `token`.
428fn emit(builder: &mut GreenNodeBuilder<'static, 'static, SyntaxKind>, t: RawToken, src: &str) {
429    if <SyntaxKind as Syntax>::static_text(t.kind).is_some() {
430        builder.static_token(t.kind);
431    } else {
432        builder.token(t.kind, &src[t.range]);
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    fn round_trips(src: &str) {
441        let parse = parse(src);
442        assert_eq!(
443            parse.syntax_node().to_string(),
444            src,
445            "round-trip mismatch for {src:?}",
446        );
447    }
448
449    #[test]
450    fn round_trips_a_function() {
451        round_trips("func f():\n\tpass\n");
452    }
453
454    #[test]
455    fn round_trips_inline_function() {
456        round_trips("func square(a): return a\n");
457    }
458
459    #[test]
460    fn round_trips_with_trivia() {
461        round_trips("## doc\nfunc _ready() -> void:\n\tpass\n\n# trailing comment\n");
462    }
463
464    #[test]
465    fn round_trips_multiple_functions() {
466        round_trips("func a():\n\tpass\nfunc b():\n\tpass\n");
467    }
468
469    #[test]
470    fn round_trips_empty_and_blank() {
471        round_trips("");
472        round_trips("\n\n");
473        round_trips("# only a comment\n");
474    }
475
476    #[test]
477    fn produces_expected_top_level_shape() {
478        let parse = parse("func f():\n\tpass\n");
479        let root = parse.syntax_node();
480        assert_eq!(root.kind(), SyntaxKind::SourceFile);
481        let func = root
482            .children()
483            .find(|n| n.kind() == SyntaxKind::FuncDecl)
484            .expect("a FuncDecl child");
485        assert!(func.children().any(|n| n.kind() == SyntaxKind::Block));
486    }
487
488    fn contains_node(node: &ResolvedNode<SyntaxKind>, kind: SyntaxKind) -> bool {
489        node.kind() == kind || node.children().any(|c| contains_node(c, kind))
490    }
491
492    #[test]
493    fn syntax_error_messages_match_godot() {
494        // Verbatim wordings probed on Godot 4.7.stable.official (the same binary and
495        // methodology as gdscript-hir's `godot_messages_tests`; the p../q../s.. ids name
496        // the probe scripts). Each case asserts the parser produces Godot's exact text.
497        let cases: &[(&str, &str)] = &[
498            // p11
499            (
500                "func t() -> void:\n\tif true\n\t\tpass\n",
501                "Expected \":\" after \"if\" condition.",
502            ),
503            // s01
504            (
505                "func t(b: bool) -> void:\n\tif b:\n\t\tpass\n\telif b\n\t\tpass\n",
506                "Expected \":\" after \"elif\" condition.",
507            ),
508            // s02
509            (
510                "func t(b: bool) -> void:\n\tif b:\n\t\tpass\n\telse\n\t\tpass\n",
511                "Expected \":\" after \"else\".",
512            ),
513            // s03
514            (
515                "func t() -> void:\n\twhile false\n\t\tpass\n",
516                "Expected \":\" after \"while\" condition.",
517            ),
518            // s04
519            (
520                "func t() -> void:\n\tfor i in 3\n\t\tpass\n",
521                "Expected \":\" after \"for\" condition.",
522            ),
523            // p13
524            (
525                "func t() -> void:\n\tfor i 10:\n\t\tpass\n",
526                "Expected \"in\" or \":\" after \"for\" variable name.",
527            ),
528            // q27
529            (
530                "func t(v: int) -> void:\n\tmatch v\n\t\t1:\n\t\t\tpass\n",
531                "Expected \":\" after \"match\" expression.",
532            ),
533            // s06
534            (
535                "func t(v: int) -> void:\n\tmatch v:\n\t\t1\n\t\t\tpass\n",
536                "Expected \":\" or \"when\" after \"match\" patterns.",
537            ),
538            // s12
539            (
540                "class Inner\n\tpass\n",
541                "Expected \":\" after class declaration.",
542            ),
543            // s07
544            (
545                "func t(a: int -> void:\n\tpass\n",
546                "Expected closing \")\" after function parameters.",
547            ),
548            // p16
549            (
550                "func t() -> void:\n\tvar toggle = func(broken: pass\n",
551                "Expected closing \")\" after lambda parameters.",
552            ),
553            // s08
554            (
555                "func t() -> void:\n\tprint(1, 2\n",
556                "Expected closing \")\" after call arguments.",
557            ),
558            // p18
559            (
560                "func t() -> void:\n\tvar x = (1 + 2\n\tprint(x)\n",
561                "Expected closing \")\" after grouping expression.",
562            ),
563            // s09
564            (
565                "@\nfunc t() -> void:\n\tpass\n",
566                "Expected annotation identifier after \"@\".",
567            ),
568            // s10
569            (
570                "signal\n\nfunc t() -> void:\n\tpass\n",
571                "Expected signal name after \"signal\".",
572            ),
573            // p25's shape — Godot aborts the func and reports the indented body as class
574            // junk; this parser recovers INTO the body instead, so the pinned message is
575            // the generic quoted form (never the Rust Debug name `Colon`).
576            ("func t() -> void\n\tpass\n", "Expected \":\"."),
577            // The class-body junk message names the offending token, Godot-style (p25).
578            (
579                "5\n\nfunc t() -> void:\n\tpass\n",
580                "Unexpected \"Literal\" in class body.",
581            ),
582        ];
583        for (src, expected) in cases {
584            let errors = parse(src).errors().to_vec();
585            assert!(
586                errors.iter().any(|e| e.message == *expected),
587                "source {src:?}\nexpected {expected:?}\ngot {errors:#?}"
588            );
589        }
590        // The generic fallback quotes the token's fixed text instead of leaking Debug.
591        let errors = parse("func t() -> void:\n\tvar x = [1, 2\n")
592            .errors()
593            .to_vec();
594        assert!(
595            errors.iter().any(|e| e.message == "Expected \"]\"."),
596            "got {errors:#?}"
597        );
598    }
599
600    #[test]
601    fn property_accessors_parse_cleanly() {
602        let indented = "var x: int:\n\tget:\n\t\treturn 1\n\tset(v):\n\t\tx = v\n";
603        round_trips(indented);
604        assert!(
605            parse(indented).errors().is_empty(),
606            "valid get/set accessors must not error: {:?}",
607            parse(indented).errors()
608        );
609        let inline = "var y: int : get = _get_y, set = _set_y\n";
610        round_trips(inline);
611        assert!(
612            parse(inline).errors().is_empty(),
613            "{:?}",
614            parse(inline).errors()
615        );
616    }
617
618    #[test]
619    fn a_non_get_set_accessor_keyword_is_a_parse_error() {
620        // Tightened: a property accessor keyword must be exactly `get` or `set` — `foo` is an error,
621        // not a silently-accepted setter.
622        let src = "var x: int:\n\tfoo:\n\t\treturn 1\n";
623        assert!(
624            !parse(src).errors().is_empty(),
625            "a non-get/set accessor must be a parse error"
626        );
627    }
628
629    fn has_match_stmt(src: &str) -> bool {
630        contains_node(&parse(src).syntax_node(), SyntaxKind::MatchStmt)
631    }
632
633    #[test]
634    fn a_real_match_statement_still_parses() {
635        let src = "func f():\n\tmatch x:\n\t\t1:\n\t\t\tpass\n";
636        round_trips(src);
637        assert!(has_match_stmt(src));
638    }
639
640    #[test]
641    fn a_parenthesised_match_subject_is_still_a_statement() {
642        let src = "func f():\n\tmatch (x):\n\t\t_:\n\t\t\tpass\n";
643        round_trips(src);
644        assert!(has_match_stmt(src), "`match (x):` is a match statement");
645    }
646
647    #[test]
648    fn match_used_as_an_identifier_is_an_expression_not_a_statement() {
649        // `match` as a soft-keyword identifier: member access, assignment, and a call/index whose
650        // bracket group is not colon-terminated. None is the match statement.
651        for src in [
652            "func f():\n\tmatch.foo()\n",
653            "func f():\n\tmatch = 5\n",
654            "func f():\n\tmatch += 1\n",
655            "func f():\n\tmatch(x)\n",
656            "func f():\n\tmatch[0] = 1\n",
657        ] {
658            round_trips(src);
659            assert!(
660                !has_match_stmt(src),
661                "should be an expression, not a match statement: {src:?}"
662            );
663        }
664    }
665
666    /// A node-only S-expression (no tokens, no trivia) — the structural shape, used to
667    /// assert operator precedence/associativity.
668    fn node_sexpr(node: &ResolvedNode<SyntaxKind>) -> String {
669        let mut s = format!("({:?}", node.kind());
670        for child in node.children() {
671            s.push(' ');
672            s.push_str(&node_sexpr(child));
673        }
674        s.push(')');
675        s
676    }
677
678    fn structure(src: &str) -> String {
679        node_sexpr(&parse(src).syntax_node())
680    }
681
682    #[test]
683    fn precedence_factor_binds_tighter_than_add() {
684        // 1 + 2 * 3  →  1 + (2 * 3)
685        assert_eq!(
686            structure("var x = 1 + 2 * 3\n"),
687            "(SourceFile (VarDecl (Name) (BinExpr (Literal) (BinExpr (Literal) (Literal)))))"
688        );
689        // 1 * 2 + 3  →  (1 * 2) + 3
690        assert_eq!(
691            structure("var x = 1 * 2 + 3\n"),
692            "(SourceFile (VarDecl (Name) (BinExpr (BinExpr (Literal) (Literal)) (Literal))))"
693        );
694    }
695
696    #[test]
697    fn power_is_left_associative() {
698        // GDScript: 2 ** 3 ** 4  →  (2 ** 3) ** 4  (unlike Python's right-assoc)
699        assert_eq!(
700            structure("var x = 2 ** 3 ** 4\n"),
701            "(SourceFile (VarDecl (Name) (BinExpr (BinExpr (Literal) (Literal)) (Literal))))"
702        );
703    }
704
705    #[test]
706    fn unary_minus_then_power() {
707        // -2 ** 2  →  -(2 ** 2)  (power binds tighter than the unary sign)
708        assert_eq!(
709            structure("var x = -2 ** 2\n"),
710            "(SourceFile (VarDecl (Name) (UnaryExpr (BinExpr (Literal) (Literal)))))"
711        );
712    }
713
714    #[test]
715    fn ternary_is_right_associative() {
716        assert_eq!(
717            structure("var x = a if c else b\n"),
718            "(SourceFile (VarDecl (Name) (TernaryExpr (NameRef) (NameRef) (NameRef))))"
719        );
720    }
721
722    #[test]
723    fn postfix_chain_call_field_index() {
724        // a.b().c[0]
725        assert_eq!(
726            structure("var x = a.b().c[0]\n"),
727            "(SourceFile (VarDecl (Name) (IndexExpr (FieldExpr (CallExpr (FieldExpr (NameRef) \
728             (NameRef)) (ArgList)) (NameRef)) (Literal))))"
729        );
730    }
731
732    #[test]
733    fn leading_utf8_bom_is_trivia_not_an_error() {
734        // A `.gd` saved with a UTF-8 BOM is valid GDScript (Godot strips it). The BOM must
735        // be lexed as trivia, round-trip byte-for-byte, and NOT produce a parse error at 1:1.
736        let src = "\u{feff}class_name Foo\nextends Node\n";
737        let parse = parse(src);
738        assert_eq!(
739            parse.syntax_node().to_string(),
740            src,
741            "BOM file must round-trip byte-for-byte"
742        );
743        assert!(
744            parse.errors().is_empty(),
745            "BOM-prefixed file should parse clean: {:?}",
746            parse.errors()
747        );
748        // The BOM does not shift the first declaration's indentation: `class_name` is at col 0.
749        assert!(
750            structure(src).starts_with("(SourceFile (ClassNameDecl"),
751            "{}",
752            structure(src)
753        );
754    }
755
756    #[test]
757    fn multiline_lambda_does_not_absorb_following_paren_line() {
758        // A block-body lambda assigned to a var, followed by a statement that begins with
759        // `(`. The dedent ends the lambda; the `(...)` line is its OWN statement — it must
760        // NOT be parsed as a postfix call on the lambda. (Regression: the parser used to
761        // absorb the `(` as `CallExpr(LambdaExpr, …)`.)
762        let src = "func f():\n\tvar cb := func():\n\t\treturn 1\n\t(self).process()\n";
763        let st = structure(src);
764        assert!(
765            st.contains("(VarDecl (Name) (LambdaExpr"),
766            "lambda should be the var initializer, standalone: {st}"
767        );
768        assert!(
769            !st.contains("CallExpr (LambdaExpr"),
770            "the following `(` line must not be absorbed as a call on the lambda: {st}"
771        );
772        // The `(self).process()` line is a separate ExprStmt with its own call chain.
773        assert!(
774            st.contains("(ExprStmt (CallExpr (FieldExpr (ParenExpr"),
775            "the `(self).process()` line should be its own statement: {st}"
776        );
777        round_trips(src);
778    }
779
780    #[test]
781    fn inline_lambda_still_chains_postfix() {
782        // An *inline* (single-line) lambda has no dedent, so a postfix `.call()` on the same
783        // logical line must still chain — the fix only suppresses postfix after a block body.
784        let src = "var x = (func(): return 1).call()\n";
785        let st = structure(src);
786        assert!(
787            st.contains("CallExpr (FieldExpr (ParenExpr (LambdaExpr"),
788            "inline lambda should still accept a postfix chain: {st}"
789        );
790        round_trips(src);
791    }
792
793    #[test]
794    fn statement_level_annotation_in_a_body_parses_clean() {
795        // `@warning_ignore("…")` (and friends) can decorate a STATEMENT inside a function body, not
796        // just a declaration. It must parse as a sibling Annotation, not fall into expr-stmt and
797        // error. (Found on the godot-demo-projects corpus.)
798        let src = "func f():\n\t@warning_ignore(\"integer_division\")\n\tvar x := 1 / 2\n";
799        let parse = parse(src);
800        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
801        round_trips(src);
802    }
803
804    #[test]
805    fn multiline_lambda_arg_with_dedented_closer_parses_clean() {
806        // A multi-line lambda passed as a call argument, with the closing `)` on its own line at a
807        // column BETWEEN the lambda header and its body (real Godot style — the tween demo). The `)`
808        // ends the body via the bracket close — no spurious INDENT, no syntax error.
809        let src = "func f():\n\tobj.call(func():\n\t\t\tbody()\n\t\t)\n";
810        let parse = parse(src);
811        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
812        round_trips(src);
813    }
814
815    #[test]
816    fn multiline_lambda_body_ending_at_a_comma_parses_clean() {
817        // A multi-line lambda whose single-statement body is followed by `, more_args` on the same
818        // line (`call(func(v): body, 0.0, 1.0)`). A bare `,` at the lambda's enclosing bracket depth
819        // is the call's argument separator, so it ends the body. (Found on the corpus.)
820        let src = "func f():\n\tobj.call(\n\t\tfunc(v):\n\t\t\tuse(v), 0.0, 1.0)\n";
821        let parse = parse(src);
822        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
823        round_trips(src);
824    }
825
826    /// A broad, realistic GDScript file exercising most of the grammar. The key
827    /// invariant is that it round-trips byte-for-byte and parses without panicking.
828    const CORPUS: &str = r#"@tool
829class_name Player extends CharacterBody2D
830## A documented player controller.
831
832const SPEED := 300.0
833@export var health: int = 100
834@export_range(0, 100) var armor := 0
835static var instances: Array[Player] = []
836
837enum State { IDLE, RUNNING, JUMPING = 10 }
838
839signal died(reason: String)
840
841var _vel: Vector2 = Vector2.ZERO:
842	get:
843		return _vel
844	set(value):
845		_vel = value
846
847class Inner extends RefCounted:
848	var x = 1
849	func helper() -> int:
850		return x * 2
851
852func _ready() -> void:
853	var node := $Sprite2D
854	var unique = %HealthBar
855	add_child(preload("res://thing.tscn").instantiate())
856	for i in range(0, 10):
857		if i % 2 == 0 and i > 0:
858			print(i, " even")
859		elif i == 5:
860			continue
861		else:
862			pass
863	while health > 0:
864		health -= 1
865	match State.IDLE:
866		State.IDLE, State.RUNNING:
867			pass
868		[var first, ..]:
869			print(first)
870		{"key": var v} when v > 0:
871			print(v)
872		_:
873			breakpoint
874	var cb := func(a: int, b := 2) -> int: return a + b
875	var ok = node is Node2D
876	var cast = node as Sprite2D
877	assert(health >= 0, "negative health")
878	died.emit("test")
879"#;
880
881    #[test]
882    fn corpus_round_trips_byte_for_byte() {
883        round_trips(CORPUS);
884    }
885
886    #[test]
887    fn corpus_parses_without_unexpected_errors() {
888        // The corpus is valid GDScript; it should parse with no syntax errors.
889        let parse = parse(CORPUS);
890        assert!(
891            parse.errors().is_empty(),
892            "unexpected parse errors:\n{:#?}",
893            parse.errors()
894        );
895    }
896
897    #[test]
898    fn inline_if_elif_else_clauses_attach() {
899        // Real-corpus regression (ReactiveUI-Godot reconciler.gd / router matcher.gd):
900        // an inline branch body (`if c: stmt`) followed by `elif`/`else` on the next
901        // line. The inline body ends at a logical newline that must not orphan the
902        // clause as a stray statement.
903        let src = "func f():\n\tif a: x = 1\n\telif b: x = 2\n\telse: x = 3\n";
904        let parse = parse(src);
905        assert_eq!(parse.syntax_node().to_string(), src, "lossless");
906        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
907        let root = parse.syntax_node();
908        let if_stmt = root
909            .descendants()
910            .find(|n| n.kind() == SyntaxKind::IfStmt)
911            .expect("an IfStmt node");
912        assert!(
913            if_stmt
914                .descendants()
915                .any(|n| n.kind() == SyntaxKind::ElifClause),
916            "elif clause attached to the if"
917        );
918        assert!(
919            if_stmt
920                .descendants()
921                .any(|n| n.kind() == SyntaxKind::ElseClause),
922            "else clause attached to the if"
923        );
924    }
925
926    #[test]
927    fn soft_keyword_names_parse() {
928        // Real-corpus regression (ReactiveUI-Godot router): Godot's `is_identifier()` /
929        // `is_node_name()` soft keywords used as identifiers — `match` as a function
930        // name and a member name, `when` as a parameter and an identifier expression.
931        let src = "static func match(when: bool) -> int:\n\tvar r = RUIRouteMatcher.match(when)\n\treturn when\n";
932        let parse = parse(src);
933        assert_eq!(parse.syntax_node().to_string(), src, "lossless");
934        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
935    }
936
937    #[test]
938    fn multiline_lambda_with_trailing_call_paren() {
939        // Real-corpus regression (ReactiveUI-Godot media.gd): a multiline lambda whose
940        // enclosing call paren closes on the body's last line (`call(func(): … last())`).
941        let src = "func f():\n\tt.connect(func():\n\t\tif ok:\n\t\t\tp.free())\n";
942        let parse = parse(src);
943        assert_eq!(parse.syntax_node().to_string(), src, "lossless");
944        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
945    }
946
947    #[test]
948    fn multiline_lambda_in_call_argument_parses() {
949        // The fixed lambda-in-brackets case: a multiline lambda body inside a call.
950        let src = "func f():\n\tcb(func(a, b):\n\t\treturn a + b\n\t)\n";
951        let parse = parse(src);
952        assert_eq!(parse.syntax_node().to_string(), src, "lossless");
953        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
954        let root = parse.syntax_node();
955        let lambda = root
956            .descendants()
957            .find(|n| n.kind() == SyntaxKind::LambdaExpr)
958            .expect("a LambdaExpr node");
959        let block = lambda
960            .children()
961            .find(|n| n.kind() == SyntaxKind::Block)
962            .expect("the lambda body Block");
963        assert!(
964            block
965                .descendants()
966                .any(|n| n.kind() == SyntaxKind::ReturnStmt),
967            "the lambda body contains the return statement"
968        );
969    }
970
971    #[test]
972    fn single_line_lambda_in_call_argument_parses() {
973        // The body stops at the call's `)` (parser inline-block fix).
974        let src = "var m = arr.map(func(x): x * 2)\n";
975        let parse = parse(src);
976        assert_eq!(parse.syntax_node().to_string(), src, "lossless");
977        assert!(parse.errors().is_empty(), "no errors: {:?}", parse.errors());
978        assert!(
979            parse
980                .syntax_node()
981                .descendants()
982                .any(|n| n.kind() == SyntaxKind::LambdaExpr),
983            "a LambdaExpr node"
984        );
985    }
986
987    #[test]
988    fn broken_code_recovers_and_round_trips() {
989        // A malformed parameter list: a tree is still produced, errors are reported,
990        // siblings still parse, and the source round-trips.
991        let src = "func ok():\n\tpass\nfunc bad(:\n\tpass\nfunc also_ok():\n\tpass\n";
992        let parse = parse(src);
993        assert_eq!(
994            parse.syntax_node().to_string(),
995            src,
996            "recovery must stay lossless"
997        );
998        assert!(!parse.errors().is_empty(), "expected a syntax error");
999        // The two well-formed functions are still recognized.
1000        let funcs = parse
1001            .syntax_node()
1002            .children()
1003            .filter(|n| n.kind() == SyntaxKind::FuncDecl)
1004            .count();
1005        assert!(
1006            funcs >= 2,
1007            "siblings should survive a broken declaration, got {funcs}"
1008        );
1009    }
1010
1011    #[test]
1012    fn over_indented_body_line_does_not_cascade_to_class_level() {
1013        // Regression (BUG A4): one body line indented a level too far used to close the enclosing
1014        // block early — its stray INDENT was swallowed as an error token, leaving the matching
1015        // DEDENT to terminate the function. Every following body statement then spilled to class
1016        // level and cascaded into a swarm of "expected a declaration" errors (~13 for this input).
1017        // The over-indented run must now be recovered *inside* the function body, emitting exactly
1018        // one diagnostic, so the trailing statements stay in the body.
1019        let src =
1020            "func render():\n\tvar a = 1\n\t\tvar bad = 2\n\tvar b = 2\n\tfor i in 3:\n\t\tpass\n";
1021        let parse = parse(src);
1022        assert_eq!(
1023            parse.syntax_node().to_string(),
1024            src,
1025            "recovery stays lossless"
1026        );
1027        assert_eq!(
1028            parse.errors().len(),
1029            1,
1030            "exactly one diagnostic, not a cascade: {:?}",
1031            parse.errors()
1032        );
1033        // No statement escaped to class level: the file has a single FuncDecl and nothing else.
1034        let root = parse.syntax_node();
1035        let top_funcs = root
1036            .children()
1037            .filter(|n| n.kind() == SyntaxKind::FuncDecl)
1038            .count();
1039        assert_eq!(top_funcs, 1, "the whole body stays in one function");
1040        assert!(
1041            !root.children().any(|n| n.kind() == SyntaxKind::VarDecl),
1042            "no body statement leaked to class level: {}",
1043            parse.debug_tree()
1044        );
1045        // The `for` loop that followed the over-indented line is a real body statement now.
1046        assert!(
1047            root.descendants().any(|n| n.kind() == SyntaxKind::ForStmt),
1048            "the trailing `for` is recovered as a body statement"
1049        );
1050    }
1051
1052    #[test]
1053    fn golden_small_class() {
1054        let parse = parse("class_name Foo\nvar x := 1\n");
1055        expect_test::expect_file!["../test_data/golden/small_class.cst"]
1056            .assert_eq(&parse.debug_tree());
1057    }
1058}