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