Skip to main content

gdck_syntax/
lexer.rs

1//! A hand-written lexer for GDScript.
2//!
3//! Two properties matter here and are enforced by tests:
4//!
5//! 1. **Lossless.** Concatenating the source text of every token in order
6//!    reproduces the input byte for byte. Whitespace and comments are tokens,
7//!    not skipped input.
8//! 2. **Block-aware.** GDScript delimits blocks by indentation, so the lexer
9//!    emits zero-width [`Indent`](SyntaxKind::Indent) and
10//!    [`Dedent`](SyntaxKind::Dedent) tokens the way a Python tokenizer does,
11//!    which lets the parser stay a plain recursive-descent affair.
12
13use std::cmp::Ordering;
14
15use crate::error::SyntaxError;
16use crate::kind::SyntaxKind;
17use crate::text::TextRange;
18
19/// Column width of a tab when measuring indentation.
20///
21/// Only ever used to *compare* indentation depth between lines. The raw
22/// indentation text is preserved in the whitespace token, so a linter can still
23/// see whether a file mixes tabs and spaces.
24const TAB_WIDTH: u32 = 4;
25
26/// A lexed token: a kind plus the span of source it covers.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct Token {
29    pub kind: SyntaxKind,
30    pub range: TextRange,
31}
32
33impl Token {
34    #[must_use]
35    pub fn text(self, source: &str) -> &str {
36        self.range.slice(source)
37    }
38}
39
40/// The result of lexing a source file.
41#[derive(Debug, Clone)]
42pub struct LexResult {
43    pub tokens: Vec<Token>,
44    pub errors: Vec<SyntaxError>,
45}
46
47/// A multi-line lambda body opened inside brackets.
48///
49/// Indentation is normally meaningless inside `()`, `[]` and `{}`, but a lambda
50/// written across several lines needs it back:
51///
52/// ```gdscript
53/// button.pressed.connect(
54///     func() -> void:
55///         do_something()
56///         do_more()
57/// )
58/// ```
59///
60/// Godot's own tokenizer handles this the same way, by tracking where such a
61/// body starts and re-enabling indentation for exactly its extent.
62#[derive(Debug, Clone, Copy)]
63struct LambdaContext {
64    /// Bracket depth the lambda's `:` was seen at. Indentation is significant
65    /// again only at exactly this depth — nested brackets suppress it as usual.
66    bracket_depth: u32,
67    /// Indentation of the body's first line, once one has been seen.
68    ///
69    /// The body then runs until a line indented less than that, the same rule
70    /// Python uses for a suite. Deriving it from the body rather than from the
71    /// opening line matters because a lambda can open mid-line — after a comma
72    /// separating it from a previous argument — with its body at that same
73    /// column.
74    body_column: Option<u32>,
75    /// Height of the indent stack when the lambda opened, so closing it emits
76    /// exactly the dedents its body pushed.
77    indent_len: usize,
78    /// Whether the body started on the same line as the `:`.
79    ///
80    /// `func(): return 1` is a complete lambda, so it ends where its line does.
81    /// Without this the next line's indentation would be taken for the start
82    /// of the body, and an argument list broken across lines —
83    ///
84    /// ```gdscript
85    /// connect(
86    ///     func(): return 1
87    /// )
88    /// ```
89    ///
90    /// — would swallow its own closing bracket.
91    inline: bool,
92}
93
94/// Tokenize `source`.
95///
96/// Always succeeds. Malformed input produces [`SyntaxKind::Unknown`] tokens and
97/// entries in [`LexResult::errors`], never a hard failure.
98#[must_use]
99pub fn tokenize(source: &str) -> LexResult {
100    Lexer::new(source).run()
101}
102
103struct Lexer<'a> {
104    source: &'a str,
105    bytes: &'a [u8],
106    pos: u32,
107    tokens: Vec<Token>,
108    errors: Vec<SyntaxError>,
109    /// Indentation columns of the currently open blocks. Always starts with 0.
110    indents: Vec<u32>,
111    /// Nesting depth of `()`, `[]` and `{}`. Inside brackets, newlines are
112    /// trivia and indentation is not significant.
113    bracket_depth: u32,
114    /// Multi-line lambda bodies currently open inside brackets, innermost last.
115    lambda_stack: Vec<LambdaContext>,
116    /// Bracket depth at which a `func` was seen, arming lambda detection. The
117    /// next `:` at that same depth opens a lambda body.
118    pending_lambda: Option<u32>,
119    /// Set after a newline, cleared once the line is under way.
120    at_line_start: bool,
121}
122
123impl<'a> Lexer<'a> {
124    fn new(source: &'a str) -> Self {
125        Self {
126            source,
127            bytes: source.as_bytes(),
128            pos: 0,
129            // A typical GDScript file lands near one token per 4 bytes.
130            tokens: Vec::with_capacity(source.len() / 4),
131            errors: Vec::new(),
132            indents: vec![0],
133            bracket_depth: 0,
134            lambda_stack: Vec::new(),
135            pending_lambda: None,
136            at_line_start: true,
137        }
138    }
139
140    fn run(mut self) -> LexResult {
141        // A byte-order mark is not part of the program. Left in the stream it
142        // becomes the first character of the first identifier — `extends`
143        // stops being a keyword and the whole file fails to parse — so it is
144        // taken as trivia here. It is kept as a token rather than skipped, or
145        // the tree would no longer reproduce its input byte for byte.
146        //
147        // That it should not be there at all is the `byte-order-mark` rule's
148        // to say. The guide asks for UTF-8 without one, which makes it a
149        // matter of style rather than of syntax.
150        if self.source.starts_with('\u{feff}') {
151            let start = self.pos;
152            self.pos += '\u{feff}'.len_utf8() as u32;
153            self.push(SyntaxKind::Whitespace, TextRange::new(start, self.pos));
154        }
155
156        while !self.at_eof() {
157            if self.at_line_start && self.indent_significant() {
158                self.lex_line_start();
159            } else {
160                self.lex_token();
161            }
162        }
163
164        // Close any blocks still open at end of file.
165        let end = self.pos;
166        while !self.lambda_stack.is_empty() {
167            self.close_top_lambda(end);
168        }
169        while self.indents.len() > 1 {
170            self.indents.pop();
171            self.push(SyntaxKind::Dedent, TextRange::empty(end));
172        }
173        self.push(SyntaxKind::Eof, TextRange::empty(end));
174
175        LexResult {
176            tokens: self.tokens,
177            errors: self.errors,
178        }
179    }
180
181    // -- Indentation --------------------------------------------------------
182
183    /// Handle the start of a physical line: measure indentation and emit the
184    /// indent/dedent markers implied by it.
185    ///
186    /// Blank lines and comment-only lines carry no indentation information, so
187    /// they are passed through as trivia without touching the indent stack.
188    fn lex_line_start(&mut self) {
189        let start = self.pos;
190        let mut column = 0;
191        let mut cursor = self.pos;
192        while let Some(byte) = self.byte_at(cursor) {
193            match byte {
194                b' ' => column += 1,
195                // A tab advances to the next tab stop.
196                b'\t' => column = (column / TAB_WIDTH + 1) * TAB_WIDTH,
197                _ => break,
198            }
199            cursor += 1;
200        }
201
202        let indent_range = TextRange::new(start, cursor);
203        // End of file, a line break, a comment, or a line continuation on an
204        // otherwise empty line: none of these say anything about indentation.
205        let blank_line = matches!(
206            self.byte_at(cursor),
207            None | Some(b'\n' | b'\r' | b'#' | b'\\')
208        );
209
210        if blank_line {
211            // No indent bookkeeping; just emit the whitespace and carry on
212            // lexing the comment or newline that follows.
213            if !indent_range.is_empty() {
214                self.pos = cursor;
215                self.push(SyntaxKind::Whitespace, indent_range);
216            }
217            self.at_line_start = false;
218            // Re-arm at_line_start when the newline is consumed by lex_token.
219            self.lex_token();
220            return;
221        }
222
223        // The first line of a lambda body fixes that body's indentation; a
224        // later line indented less than it ends the lambda, however many
225        // brackets deep it sits.
226        while let Some(context) = self.lambda_stack.last() {
227            match context.body_column {
228                // A body written on the `:` line is already complete.
229                None if context.inline => self.close_top_lambda(start),
230                None => {
231                    self.lambda_stack
232                        .last_mut()
233                        .expect("checked immediately above")
234                        .body_column = Some(column);
235                    break;
236                }
237                Some(body_column) if column < body_column => self.close_top_lambda(start),
238                Some(_) => break,
239            }
240        }
241
242        // Closing a lambda can hand control back to a bracket, where
243        // indentation means nothing again.
244        if !self.indent_significant() {
245            if !indent_range.is_empty() {
246                self.pos = cursor;
247                self.push(SyntaxKind::Whitespace, indent_range);
248            }
249            self.at_line_start = false;
250            return;
251        }
252
253        // Dedents close blocks, so they belong before this line's indentation
254        // whitespace; an indent opens a block containing it, so it goes after.
255        let current = *self.indents.last().expect("indent stack is never empty");
256        match column.cmp(&current) {
257            Ordering::Greater => {
258                self.pos = cursor;
259                self.emit_indent_whitespace(indent_range);
260                self.indents.push(column);
261                self.push(SyntaxKind::Indent, TextRange::empty(cursor));
262            }
263            Ordering::Less => {
264                while *self.indents.last().expect("indent stack is never empty") > column {
265                    self.indents.pop();
266                    self.push(SyntaxKind::Dedent, TextRange::empty(start));
267                }
268                if *self.indents.last().expect("indent stack is never empty") != column {
269                    self.errors.push(SyntaxError::new(
270                        indent_range,
271                        "unindent does not match any outer indentation level",
272                    ));
273                    // Accept the column so one bad line does not cascade.
274                    self.indents.push(column);
275                }
276                self.pos = cursor;
277                self.emit_indent_whitespace(indent_range);
278            }
279            Ordering::Equal => {
280                self.pos = cursor;
281                self.emit_indent_whitespace(indent_range);
282            }
283        }
284
285        self.at_line_start = false;
286    }
287
288    /// Emit a line's leading whitespace, if it has any.
289    fn emit_indent_whitespace(&mut self, indent_range: TextRange) {
290        if !indent_range.is_empty() {
291            self.push(SyntaxKind::Whitespace, indent_range);
292        }
293    }
294
295    // -- Token dispatch -----------------------------------------------------
296
297    fn lex_token(&mut self) {
298        let start = self.pos;
299        let Some(byte) = self.byte_at(self.pos) else {
300            return;
301        };
302
303        let kind = match byte {
304            b' ' | b'\t' => {
305                self.bump_while(|b| b == b' ' || b == b'\t');
306                SyntaxKind::Whitespace
307            }
308            b'\n' => {
309                self.pos += 1;
310                self.finish_line();
311                SyntaxKind::Newline
312            }
313            b'\r' => {
314                self.pos += 1;
315                if self.byte_at(self.pos) == Some(b'\n') {
316                    self.pos += 1;
317                }
318                self.finish_line();
319                SyntaxKind::Newline
320            }
321            b'\\' => self.lex_backslash(),
322            b'#' => self.lex_comment(),
323            b'0'..=b'9' => self.lex_number(),
324            b'.' if matches!(self.byte_at(self.pos + 1), Some(b'0'..=b'9')) => self.lex_number(),
325            b'"' | b'\'' => self.lex_string(),
326            b'$' => self.lex_node_path(SyntaxKind::GetNode),
327            b'&' | b'^' if self.starts_annotated_string() => self.lex_annotated_string(byte),
328            b'%' if self.starts_unique_node() => self.lex_node_path(SyntaxKind::UniqueNode),
329            b if is_ident_start(b) => self.lex_ident_or_keyword(),
330            _ => self.lex_operator(),
331        };
332
333        // lex_operator already emitted its own token when it recovered.
334        if self.pos == start && kind == SyntaxKind::Unknown {
335            self.pos += 1;
336        }
337
338        // Arm and fire lambda detection. A `func` inside brackets is always the
339        // start of a lambda, and the next `:` at that same depth opens its body.
340        if kind == SyntaxKind::FuncKw && self.bracket_depth > 0 {
341            self.pending_lambda = Some(self.bracket_depth);
342        } else if kind == SyntaxKind::Colon
343            && self.bracket_depth > 0
344            && self.pending_lambda == Some(self.bracket_depth)
345        {
346            self.pending_lambda = None;
347            self.lambda_stack.push(LambdaContext {
348                bracket_depth: self.bracket_depth,
349                body_column: None,
350                indent_len: self.indents.len(),
351                inline: false,
352            });
353        } else if kind != SyntaxKind::Newline
354            && let Some(context) = self.lambda_stack.last_mut()
355            && context.body_column.is_none()
356        {
357            // A token after the `:` and before any newline means the
358            // body is on this line, so the lambda ends with it.
359            context.inline = true;
360        }
361
362        if kind != SyntaxKind::Newline {
363            self.at_line_start = false;
364        }
365
366        self.push(kind, TextRange::new(start, self.pos));
367    }
368
369    /// Whether indentation carries meaning at the current position.
370    ///
371    /// Outside brackets it always does. Inside them it does only within the
372    /// body of a multi-line lambda, and only at that lambda's own depth.
373    fn indent_significant(&self) -> bool {
374        match self.lambda_stack.last() {
375            Some(context) => self.bracket_depth == context.bracket_depth,
376            None => self.bracket_depth == 0,
377        }
378    }
379
380    /// Close the innermost lambda body, emitting the dedents it owes.
381    fn close_top_lambda(&mut self, offset: u32) {
382        let Some(context) = self.lambda_stack.pop() else {
383            return;
384        };
385        while self.indents.len() > context.indent_len {
386            self.indents.pop();
387            self.push(SyntaxKind::Dedent, TextRange::empty(offset));
388        }
389    }
390
391    /// Called after consuming a newline.
392    fn finish_line(&mut self) {
393        self.at_line_start = true;
394    }
395
396    fn lex_backslash(&mut self) -> SyntaxKind {
397        let start = self.pos;
398        let mut cursor = self.pos + 1;
399        // Tolerate trailing whitespace between the backslash and the newline;
400        // it is a common and otherwise invisible mistake.
401        while matches!(self.byte_at(cursor), Some(b' ' | b'\t')) {
402            cursor += 1;
403        }
404        match self.byte_at(cursor) {
405            Some(b'\n') => {
406                self.pos = cursor + 1;
407                SyntaxKind::LineContinuation
408            }
409            Some(b'\r') => {
410                cursor += 1;
411                if self.byte_at(cursor) == Some(b'\n') {
412                    cursor += 1;
413                }
414                self.pos = cursor;
415                SyntaxKind::LineContinuation
416            }
417            _ => {
418                self.pos = start + 1;
419                self.errors.push(SyntaxError::new(
420                    TextRange::new(start, self.pos),
421                    "stray `\\` outside a line continuation",
422                ));
423                SyntaxKind::Unknown
424            }
425        }
426    }
427
428    fn lex_comment(&mut self) -> SyntaxKind {
429        let doc = self.byte_at(self.pos + 1) == Some(b'#');
430        self.bump_while(|b| b != b'\n' && b != b'\r');
431        if doc {
432            SyntaxKind::DocComment
433        } else {
434            SyntaxKind::Comment
435        }
436    }
437
438    // -- Literals -----------------------------------------------------------
439
440    fn lex_number(&mut self) -> SyntaxKind {
441        let mut is_float = false;
442
443        if self.byte_at(self.pos) == Some(b'0')
444            && matches!(self.byte_at(self.pos + 1), Some(b'x' | b'X' | b'b' | b'B'))
445        {
446            let radix_marker = self.byte_at(self.pos + 1).expect("checked above");
447            self.pos += 2;
448            if radix_marker == b'x' || radix_marker == b'X' {
449                self.bump_while(|b| b.is_ascii_hexdigit() || b == b'_');
450            } else {
451                self.bump_while(|b| matches!(b, b'0' | b'1' | b'_'));
452            }
453            return SyntaxKind::Int;
454        }
455
456        self.bump_while(|b| b.is_ascii_digit() || b == b'_');
457
458        // A `.` is only part of the number when a digit follows, so `1..2` and
459        // `1.foo()` still lex as a range and a method call.
460        if self.byte_at(self.pos) == Some(b'.')
461            && matches!(self.byte_at(self.pos + 1), Some(b'0'..=b'9'))
462        {
463            is_float = true;
464            self.pos += 1;
465            self.bump_while(|b| b.is_ascii_digit() || b == b'_');
466        } else if self.byte_at(self.pos) == Some(b'.')
467            && !matches!(self.byte_at(self.pos + 1), Some(b'.'))
468            && !matches!(self.byte_at(self.pos + 1), Some(b) if is_ident_start(b))
469        {
470            // Trailing-dot form: `1.`
471            is_float = true;
472            self.pos += 1;
473        }
474
475        if matches!(self.byte_at(self.pos), Some(b'e' | b'E')) {
476            let mut cursor = self.pos + 1;
477            if matches!(self.byte_at(cursor), Some(b'+' | b'-')) {
478                cursor += 1;
479            }
480            if matches!(self.byte_at(cursor), Some(b'0'..=b'9')) {
481                is_float = true;
482                self.pos = cursor;
483                self.bump_while(|b| b.is_ascii_digit() || b == b'_');
484            }
485        }
486
487        if is_float {
488            SyntaxKind::Float
489        } else {
490            SyntaxKind::Int
491        }
492    }
493
494    /// Whether an `&` or `^` at the cursor introduces a `StringName` or
495    /// `NodePath` literal rather than a bitwise operator.
496    fn starts_annotated_string(&self) -> bool {
497        matches!(self.byte_at(self.pos + 1), Some(b'"' | b'\'')) && !self.prev_can_end_expr()
498    }
499
500    fn lex_annotated_string(&mut self, sigil: u8) -> SyntaxKind {
501        self.pos += 1;
502        self.lex_string();
503        if sigil == b'&' {
504            SyntaxKind::StringName
505        } else {
506            SyntaxKind::NodePath
507        }
508    }
509
510    /// Whether a `%` at the cursor introduces a unique-node path rather than
511    /// the modulo operator.
512    fn starts_unique_node(&self) -> bool {
513        if self.prev_can_end_expr() {
514            return false;
515        }
516        matches!(self.byte_at(self.pos + 1), Some(b'"' | b'\''))
517            || matches!(self.byte_at(self.pos + 1), Some(b) if is_ident_start(b))
518    }
519
520    /// Lex `$Node/Path`, `$"quoted/path"` or `%UniqueName`.
521    ///
522    /// Node paths contain `/` and `..`, neither of which can be lexed as an
523    /// operator here, so the whole path becomes one token.
524    fn lex_node_path(&mut self, kind: SyntaxKind) -> SyntaxKind {
525        let sigil_start = self.pos;
526        self.pos += 1;
527
528        if matches!(self.byte_at(self.pos), Some(b'"' | b'\'')) {
529            self.lex_string();
530            return kind;
531        }
532
533        let mut matched_any = false;
534
535        // An absolute path such as `$/root` starts with the separator.
536        if self.byte_at(self.pos) == Some(b'/') {
537            self.pos += 1;
538            matched_any = true;
539        }
540
541        loop {
542            // A segment is `..`, an optional `%` prefix, or a name.
543            if self.byte_at(self.pos) == Some(b'.') && self.byte_at(self.pos + 1) == Some(b'.') {
544                self.pos += 2;
545                matched_any = true;
546            } else {
547                if self.byte_at(self.pos) == Some(b'%') {
548                    self.pos += 1;
549                    matched_any = true;
550                }
551                if matches!(self.byte_at(self.pos), Some(b) if is_ident_start(b)) {
552                    self.bump_while(is_ident_continue);
553                    matched_any = true;
554                } else {
555                    break;
556                }
557            }
558
559            if self.byte_at(self.pos) == Some(b'/') {
560                self.pos += 1;
561            } else {
562                break;
563            }
564        }
565
566        if !matched_any {
567            self.errors.push(SyntaxError::new(
568                TextRange::new(sigil_start, self.pos),
569                "expected a node path after the sigil",
570            ));
571        }
572        kind
573    }
574
575    fn lex_string(&mut self) -> SyntaxKind {
576        let start = self.pos;
577        let quote = self.byte_at(self.pos).expect("caller checked for a quote");
578
579        // Triple-quoted strings span lines and end only on a matching triple.
580        let triple =
581            self.byte_at(self.pos + 1) == Some(quote) && self.byte_at(self.pos + 2) == Some(quote);
582        let delim_len = if triple { 3 } else { 1 };
583        self.pos += delim_len;
584
585        loop {
586            let Some(byte) = self.byte_at(self.pos) else {
587                self.errors.push(SyntaxError::new(
588                    TextRange::new(start, self.pos),
589                    "unterminated string literal",
590                ));
591                break;
592            };
593
594            // A backslash escapes the next byte even in raw strings: it stops
595            // the quote from terminating, it just stays in the value.
596            if byte == b'\\' {
597                self.pos += 1;
598                if self.pos < self.len() {
599                    self.pos += 1;
600                }
601                continue;
602            }
603
604            if !triple && matches!(byte, b'\n' | b'\r') {
605                self.errors.push(SyntaxError::new(
606                    TextRange::new(start, self.pos),
607                    "unterminated string literal",
608                ));
609                break;
610            }
611
612            if byte == quote {
613                if triple {
614                    if self.byte_at(self.pos + 1) == Some(quote)
615                        && self.byte_at(self.pos + 2) == Some(quote)
616                    {
617                        self.pos += 3;
618                        break;
619                    }
620                    self.pos += 1;
621                    continue;
622                }
623                self.pos += 1;
624                break;
625            }
626
627            self.pos += 1;
628        }
629
630        SyntaxKind::Str
631    }
632
633    fn lex_ident_or_keyword(&mut self) -> SyntaxKind {
634        let start = self.pos;
635        self.bump_while(is_ident_continue);
636        let text = TextRange::new(start, self.pos).slice(self.source);
637
638        // `r"..."` is a raw string, not the identifier `r`.
639        if text == "r" && matches!(self.byte_at(self.pos), Some(b'"' | b'\'')) {
640            self.lex_string();
641            return SyntaxKind::Str;
642        }
643
644        SyntaxKind::from_keyword(text).unwrap_or(SyntaxKind::Ident)
645    }
646
647    // -- Operators ----------------------------------------------------------
648
649    #[allow(clippy::too_many_lines)]
650    fn lex_operator(&mut self) -> SyntaxKind {
651        let byte = self.byte_at(self.pos).expect("caller checked for a byte");
652        let next = self.byte_at(self.pos + 1);
653        let after = self.byte_at(self.pos + 2);
654
655        // A lambda body inside brackets also ends at the comma separating it
656        // from the next element, or at the bracket that encloses it. The
657        // dedents must land before this token, so close before consuming it.
658        match byte {
659            b')' | b']' | b'}' => {
660                while self
661                    .lambda_stack
662                    .last()
663                    .is_some_and(|context| context.bracket_depth >= self.bracket_depth)
664                {
665                    self.close_top_lambda(self.pos);
666                }
667            }
668            b',' => {
669                while self
670                    .lambda_stack
671                    .last()
672                    .is_some_and(|context| context.bracket_depth == self.bracket_depth)
673                {
674                    self.close_top_lambda(self.pos);
675                }
676            }
677            _ => {}
678        }
679
680        self.pos += 1;
681
682        macro_rules! two {
683            ($kind:expr) => {{
684                self.pos += 1;
685                $kind
686            }};
687        }
688        macro_rules! three {
689            ($kind:expr) => {{
690                self.pos += 2;
691                $kind
692            }};
693        }
694
695        match byte {
696            b'+' if next == Some(b'=') => two!(SyntaxKind::PlusEq),
697            b'+' => SyntaxKind::Plus,
698            b'-' if next == Some(b'=') => two!(SyntaxKind::MinusEq),
699            b'-' if next == Some(b'>') => two!(SyntaxKind::Arrow),
700            b'-' => SyntaxKind::Minus,
701            b'*' if next == Some(b'*') && after == Some(b'=') => three!(SyntaxKind::StarStarEq),
702            b'*' if next == Some(b'*') => two!(SyntaxKind::StarStar),
703            b'*' if next == Some(b'=') => two!(SyntaxKind::StarEq),
704            b'*' => SyntaxKind::Star,
705            b'/' if next == Some(b'=') => two!(SyntaxKind::SlashEq),
706            b'/' => SyntaxKind::Slash,
707            b'%' if next == Some(b'=') => two!(SyntaxKind::PercentEq),
708            b'%' => SyntaxKind::Percent,
709            b'=' if next == Some(b'=') => two!(SyntaxKind::EqEq),
710            b'=' => SyntaxKind::Eq,
711            b'!' if next == Some(b'=') => two!(SyntaxKind::BangEq),
712            b'!' => SyntaxKind::Bang,
713            b'<' if next == Some(b'<') && after == Some(b'=') => three!(SyntaxKind::ShlEq),
714            b'<' if next == Some(b'<') => two!(SyntaxKind::Shl),
715            b'<' if next == Some(b'=') => two!(SyntaxKind::LtEq),
716            b'<' => SyntaxKind::Lt,
717            b'>' if next == Some(b'>') && after == Some(b'=') => three!(SyntaxKind::ShrEq),
718            b'>' if next == Some(b'>') => two!(SyntaxKind::Shr),
719            b'>' if next == Some(b'=') => two!(SyntaxKind::GtEq),
720            b'>' => SyntaxKind::Gt,
721            b'&' if next == Some(b'&') => two!(SyntaxKind::AmpAmp),
722            b'&' if next == Some(b'=') => two!(SyntaxKind::AmpEq),
723            b'&' => SyntaxKind::Amp,
724            b'|' if next == Some(b'|') => two!(SyntaxKind::PipePipe),
725            b'|' if next == Some(b'=') => two!(SyntaxKind::PipeEq),
726            b'|' => SyntaxKind::Pipe,
727            b'^' if next == Some(b'=') => two!(SyntaxKind::CaretEq),
728            b'^' => SyntaxKind::Caret,
729            b'~' => SyntaxKind::Tilde,
730            b':' if next == Some(b'=') => two!(SyntaxKind::ColonEq),
731            b':' => SyntaxKind::Colon,
732            b';' => SyntaxKind::Semicolon,
733            b',' => SyntaxKind::Comma,
734            b'.' if next == Some(b'.') && after == Some(b'.') => three!(SyntaxKind::Ellipsis),
735            b'.' if next == Some(b'.') => two!(SyntaxKind::DotDot),
736            b'.' => SyntaxKind::Dot,
737            b'@' => SyntaxKind::At,
738            b'$' => SyntaxKind::Dollar,
739            b'(' => {
740                self.bracket_depth += 1;
741                SyntaxKind::LParen
742            }
743            b')' => {
744                self.bracket_depth = self.bracket_depth.saturating_sub(1);
745                SyntaxKind::RParen
746            }
747            b'[' => {
748                self.bracket_depth += 1;
749                SyntaxKind::LBracket
750            }
751            b']' => {
752                self.bracket_depth = self.bracket_depth.saturating_sub(1);
753                SyntaxKind::RBracket
754            }
755            b'{' => {
756                self.bracket_depth += 1;
757                SyntaxKind::LBrace
758            }
759            b'}' => {
760                self.bracket_depth = self.bracket_depth.saturating_sub(1);
761                SyntaxKind::RBrace
762            }
763            _ => {
764                // Consume the whole UTF-8 sequence so spans stay on char
765                // boundaries.
766                while self.pos < self.len() && !self.source.is_char_boundary(self.pos as usize) {
767                    self.pos += 1;
768                }
769                self.errors.push(SyntaxError::new(
770                    TextRange::new(self.pos - 1, self.pos),
771                    "unexpected character",
772                ));
773                SyntaxKind::Unknown
774            }
775        }
776    }
777
778    // -- Helpers ------------------------------------------------------------
779
780    /// Whether the previous meaningful token could end an expression.
781    ///
782    /// This is the classic disambiguation trick: `%` after a value is modulo,
783    /// but `%` in value position starts a unique-node path. Same idea as
784    /// telling regex from division in a JavaScript lexer.
785    ///
786    /// A newline resets the answer, because the previous line's last token says
787    /// nothing about a token starting a fresh statement. Without that, a line
788    /// beginning `^"path"` would lex as bitwise-xor whenever the line above it
789    /// happened to end in a value.
790    fn prev_can_end_expr(&self) -> bool {
791        for token in self.tokens.iter().rev() {
792            // A line continuation is a distinct kind, so it correctly does not
793            // reset this.
794            if token.kind == SyntaxKind::Newline {
795                return false;
796            }
797            if token.kind.is_trivia() {
798                continue;
799            }
800            return matches!(
801                token.kind,
802                SyntaxKind::Ident
803                    | SyntaxKind::Int
804                    | SyntaxKind::Float
805                    | SyntaxKind::Str
806                    | SyntaxKind::StringName
807                    | SyntaxKind::NodePath
808                    | SyntaxKind::GetNode
809                    | SyntaxKind::UniqueNode
810                    | SyntaxKind::RParen
811                    | SyntaxKind::RBracket
812                    | SyntaxKind::RBrace
813                    | SyntaxKind::SelfKw
814                    | SyntaxKind::SuperKw
815                    | SyntaxKind::TrueKw
816                    | SyntaxKind::FalseKw
817                    | SyntaxKind::NullKw
818            );
819        }
820        false
821    }
822
823    fn push(&mut self, kind: SyntaxKind, range: TextRange) {
824        self.tokens.push(Token { kind, range });
825    }
826
827    fn bump_while(&mut self, predicate: impl Fn(u8) -> bool) {
828        while let Some(byte) = self.byte_at(self.pos) {
829            if !predicate(byte) {
830                break;
831            }
832            self.pos += 1;
833        }
834    }
835
836    fn byte_at(&self, pos: u32) -> Option<u8> {
837        self.bytes.get(pos as usize).copied()
838    }
839
840    fn len(&self) -> u32 {
841        self.bytes.len() as u32
842    }
843
844    fn at_eof(&self) -> bool {
845        self.pos >= self.len()
846    }
847}
848
849fn is_ident_start(byte: u8) -> bool {
850    byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
851}
852
853fn is_ident_continue(byte: u8) -> bool {
854    byte.is_ascii_alphanumeric() || byte == b'_' || byte >= 0x80
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    /// The invariant the whole formatter depends on.
862    fn assert_lossless(source: &str) {
863        let lexed = tokenize(source);
864        let rebuilt: String = lexed
865            .tokens
866            .iter()
867            .map(|token| token.text(source))
868            .collect();
869        assert_eq!(rebuilt, source, "token spans must cover the source exactly");
870    }
871
872    fn kinds(source: &str) -> Vec<SyntaxKind> {
873        tokenize(source)
874            .tokens
875            .into_iter()
876            .map(|token| token.kind)
877            .filter(|kind| !kind.is_trivia() && *kind != SyntaxKind::Eof)
878            .collect()
879    }
880
881    #[test]
882    fn round_trips_a_realistic_script() {
883        assert_lossless(
884            "@tool\nclass_name Player\nextends CharacterBody2D\n\n## Docs.\nsignal died\n\nconst MAX := 100\n\n\nfunc _ready() -> void:\n\tvar x := [1, 2, {\"a\": 1}]  # trailing\n\tif x and true:\n\t\tprint($Sprite2D/Label)\n",
885        );
886    }
887
888    #[test]
889    fn round_trips_edge_cases() {
890        for source in [
891            "",
892            "\n",
893            "\n\n\n",
894            "pass",
895            "\tpass\n",
896            "a\r\nb\r\n",
897            "var s = \"unterminated\n",
898            "if a \\\n\tand b:\n\tpass\n",
899            "x = 1 § 2\n",
900            "func f():\n\t\t\tpass\n\treturn\n",
901        ] {
902            assert_lossless(source);
903        }
904    }
905
906    #[test]
907    fn emits_indent_and_dedent_around_blocks() {
908        let kinds = kinds("func f():\n\tpass\nvar x = 1\n");
909        assert_eq!(
910            kinds,
911            vec![
912                SyntaxKind::FuncKw,
913                SyntaxKind::Ident,
914                SyntaxKind::LParen,
915                SyntaxKind::RParen,
916                SyntaxKind::Colon,
917                SyntaxKind::Indent,
918                SyntaxKind::PassKw,
919                SyntaxKind::Dedent,
920                SyntaxKind::VarKw,
921                SyntaxKind::Ident,
922                SyntaxKind::Eq,
923                SyntaxKind::Int,
924            ]
925        );
926    }
927
928    #[test]
929    fn blank_and_comment_lines_do_not_shift_indentation() {
930        // The comment sits at column 0 but must not close the function block.
931        let kinds = kinds("func f():\n\tvar a = 1\n\n# note\n\tvar b = 2\n");
932        assert_eq!(
933            kinds.iter().filter(|k| **k == SyntaxKind::Indent).count(),
934            1
935        );
936        assert_eq!(
937            kinds.iter().filter(|k| **k == SyntaxKind::Dedent).count(),
938            1
939        );
940    }
941
942    #[test]
943    fn newlines_inside_brackets_are_not_line_breaks() {
944        // No indent/dedent should be produced by the wrapped array.
945        let kinds = kinds("var a = [\n\t1,\n\t2,\n]\n");
946        assert!(!kinds.contains(&SyntaxKind::Indent));
947        assert!(!kinds.contains(&SyntaxKind::Dedent));
948    }
949
950    #[test]
951    fn distinguishes_modulo_from_unique_node() {
952        assert_eq!(
953            kinds("var a = b % c\n"),
954            vec![
955                SyntaxKind::VarKw,
956                SyntaxKind::Ident,
957                SyntaxKind::Eq,
958                SyntaxKind::Ident,
959                SyntaxKind::Percent,
960                SyntaxKind::Ident,
961            ]
962        );
963        assert_eq!(
964            kinds("var a = %HealthBar\n"),
965            vec![
966                SyntaxKind::VarKw,
967                SyntaxKind::Ident,
968                SyntaxKind::Eq,
969                SyntaxKind::UniqueNode,
970            ]
971        );
972        assert_eq!(
973            kinds("print(%Bar, a % 2)\n"),
974            vec![
975                SyntaxKind::Ident,
976                SyntaxKind::LParen,
977                SyntaxKind::UniqueNode,
978                SyntaxKind::Comma,
979                SyntaxKind::Ident,
980                SyntaxKind::Percent,
981                SyntaxKind::Int,
982                SyntaxKind::RParen,
983            ]
984        );
985    }
986
987    #[test]
988    fn lexes_node_paths_as_single_tokens() {
989        assert_eq!(kinds("$Sprite2D\n"), vec![SyntaxKind::GetNode]);
990        assert_eq!(kinds("$../Sibling/%Unique\n"), vec![SyntaxKind::GetNode]);
991        assert_eq!(kinds("$\"quoted/path\"\n"), vec![SyntaxKind::GetNode]);
992        // Attribute access after a path is not part of the path.
993        assert_eq!(
994            kinds("$Sprite2D.position\n"),
995            vec![SyntaxKind::GetNode, SyntaxKind::Dot, SyntaxKind::Ident]
996        );
997    }
998
999    #[test]
1000    fn lexes_string_name_and_node_path_literals() {
1001        assert_eq!(kinds("emit(&\"died\")\n")[2], SyntaxKind::StringName);
1002        assert_eq!(kinds("var p = ^\"a/b\"\n")[3], SyntaxKind::NodePath);
1003        // With a value to its left, `&` is still bitwise-and.
1004        assert_eq!(kinds("var x = a & b\n")[4], SyntaxKind::Amp);
1005    }
1006
1007    #[test]
1008    fn lexes_number_forms() {
1009        assert_eq!(kinds("1_000_000"), vec![SyntaxKind::Int]);
1010        assert_eq!(kinds("0xfb8c0b"), vec![SyntaxKind::Int]);
1011        assert_eq!(kinds("0b1010_1010"), vec![SyntaxKind::Int]);
1012        assert_eq!(kinds("0.234"), vec![SyntaxKind::Float]);
1013        assert_eq!(kinds("1e-5"), vec![SyntaxKind::Float]);
1014        assert_eq!(kinds("1.5e10"), vec![SyntaxKind::Float]);
1015        // A range, not a float followed by a number.
1016        assert_eq!(
1017            kinds("1..2"),
1018            vec![SyntaxKind::Int, SyntaxKind::DotDot, SyntaxKind::Int]
1019        );
1020        // Method call on an integer literal.
1021        assert_eq!(
1022            kinds("1.max(2)"),
1023            vec![
1024                SyntaxKind::Int,
1025                SyntaxKind::Dot,
1026                SyntaxKind::Ident,
1027                SyntaxKind::LParen,
1028                SyntaxKind::Int,
1029                SyntaxKind::RParen
1030            ]
1031        );
1032    }
1033
1034    #[test]
1035    fn lexes_string_forms() {
1036        assert_eq!(kinds("\"double\""), vec![SyntaxKind::Str]);
1037        assert_eq!(kinds("'single'"), vec![SyntaxKind::Str]);
1038        assert_eq!(kinds("\"\"\"triple\nspanning\"\"\""), vec![SyntaxKind::Str]);
1039        assert_eq!(kinds("r\"raw\\n\""), vec![SyntaxKind::Str]);
1040        assert_eq!(kinds("\"esc\\\"aped\""), vec![SyntaxKind::Str]);
1041    }
1042
1043    #[test]
1044    fn separates_doc_comments_from_plain_comments() {
1045        let lexed = tokenize("## doc\n# plain\n");
1046        let comments: Vec<_> = lexed
1047            .tokens
1048            .iter()
1049            .filter(|t| t.kind.is_comment())
1050            .map(|t| t.kind)
1051            .collect();
1052        assert_eq!(comments, vec![SyntaxKind::DocComment, SyntaxKind::Comment]);
1053    }
1054
1055    #[test]
1056    fn line_continuation_joins_lines() {
1057        let kinds = kinds("var a = 1 + \\\n\t2\n");
1058        assert!(!kinds.contains(&SyntaxKind::Indent));
1059        assert_eq!(kinds.last(), Some(&SyntaxKind::Int));
1060    }
1061
1062    #[test]
1063    fn reports_inconsistent_dedent() {
1064        let lexed = tokenize("func f():\n\t\tpass\n\treturn\n");
1065        assert!(
1066            lexed
1067                .errors
1068                .iter()
1069                .any(|e| e.message().contains("unindent")),
1070            "expected an unindent diagnostic, got {:?}",
1071            lexed.errors
1072        );
1073    }
1074
1075    #[test]
1076    fn reports_unterminated_string() {
1077        let lexed = tokenize("var s = \"oops\n");
1078        assert!(
1079            lexed
1080                .errors
1081                .iter()
1082                .any(|e| e.message().contains("unterminated"))
1083        );
1084    }
1085
1086    #[test]
1087    fn closes_open_blocks_at_eof() {
1088        let lexed = tokenize("func f():\n\tif a:\n\t\tpass");
1089        let dedents = lexed
1090            .tokens
1091            .iter()
1092            .filter(|t| t.kind == SyntaxKind::Dedent)
1093            .count();
1094        assert_eq!(dedents, 2);
1095        assert_eq!(lexed.tokens.last().map(|t| t.kind), Some(SyntaxKind::Eof));
1096    }
1097}