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