Skip to main content

sql_dialect_fmt_lexer/
lexer.rs

1//! The single-pass tokenizer.
2
3use crate::token::{LexError, Lexed, Token};
4use crate::{BodyDelimiter, LexOptions};
5use sql_dialect_fmt_syntax::{Dialect, SyntaxKind};
6
7/// Tokenize Snowflake SQL into a lossless token stream.
8///
9/// The concatenation of `tokens[i].text` always equals `input`.
10pub fn tokenize(input: &str) -> Lexed<'_> {
11    tokenize_with_options(input, LexOptions::default())
12}
13
14pub fn tokenize_with_options<'a>(input: &'a str, options: LexOptions<'_>) -> Lexed<'a> {
15    Lexer::new(input, options).run()
16}
17
18/// Tokenize `input` for `dialect`, using that dialect's default lexing rules.
19///
20/// The dialect drives quoting and special-token behavior (`$$`/`$n` dollar handling, `@stage`
21/// references); see [`Dialect`].
22pub fn tokenize_for_dialect(input: &str, dialect: Dialect) -> Lexed<'_> {
23    tokenize_with_options(input, LexOptions::default().with_dialect(dialect))
24}
25
26struct Lexer<'a, 'cfg> {
27    input: &'a str,
28    bytes: &'a [u8],
29    options: LexOptions<'cfg>,
30    pos: usize,
31    tokens: Vec<Token<'a>>,
32    errors: Vec<LexError>,
33}
34
35impl<'a, 'cfg> Lexer<'a, 'cfg> {
36    fn new(input: &'a str, options: LexOptions<'cfg>) -> Self {
37        Lexer {
38            input,
39            bytes: input.as_bytes(),
40            options,
41            pos: 0,
42            tokens: Vec::new(),
43            errors: Vec::new(),
44        }
45    }
46
47    #[inline]
48    fn at_end(&self) -> bool {
49        self.pos >= self.bytes.len()
50    }
51
52    /// Current byte, or `0` at end of input (`0` never appears in valid SQL we care about).
53    #[inline]
54    fn peek(&self) -> u8 {
55        if self.pos < self.bytes.len() {
56            self.bytes[self.pos]
57        } else {
58            0
59        }
60    }
61
62    #[inline]
63    fn peek_at(&self, n: usize) -> u8 {
64        let i = self.pos + n;
65        if i < self.bytes.len() {
66            self.bytes[i]
67        } else {
68            0
69        }
70    }
71
72    /// Advance one byte and return it. Callers must ensure `!at_end()`.
73    #[inline]
74    fn bump(&mut self) -> u8 {
75        let c = self.bytes[self.pos];
76        self.pos += 1;
77        c
78    }
79
80    #[inline]
81    fn eat_while(&mut self, mut pred: impl FnMut(u8) -> bool) {
82        while !self.at_end() && pred(self.peek()) {
83            self.pos += 1;
84        }
85    }
86
87    #[inline]
88    fn starts_with(&self, text: &str) -> bool {
89        self.bytes[self.pos..].starts_with(text.as_bytes())
90    }
91
92    #[inline]
93    fn push(&mut self, kind: SyntaxKind, start: usize) {
94        // `self.input` is a `&'a str`; reborrowing through it yields a `&'a str` that does not
95        // borrow `self`, so this composes fine with the `&mut self` push.
96        let text = &self.input[start..self.pos];
97        self.tokens.push(Token { kind, text });
98    }
99
100    /// Record a lexical diagnostic spanning `offset..self.pos` (the offending token text consumed
101    /// so far). Called once the lexer has advanced past the start of the bad token, so the current
102    /// position marks the end of the span being reported.
103    #[inline]
104    fn error(&mut self, message: impl Into<String>, offset: usize) {
105        self.errors.push(LexError {
106            message: message.into(),
107            offset,
108            len: self.pos.saturating_sub(offset),
109        });
110    }
111
112    fn run(mut self) -> Lexed<'a> {
113        // Dialects that support dollar quoting (`$$ … $$`) treat such a run as one body token; the
114        // rest (none yet) lex the `$` characters as ordinary operators.
115        let dollar_quoting = self.options.dialect.supports_dollar_quoting();
116        while !self.at_end() {
117            let start = self.pos;
118            if dollar_quoting {
119                if let Some(delimiter) = self.body_delimiter_at() {
120                    self.delimited_body(start, delimiter);
121                    self.push(SyntaxKind::DOLLAR_STRING, start);
122                    continue;
123                }
124            }
125            match self.peek() {
126                b' ' | b'\t' => {
127                    self.eat_while(|c| c == b' ' || c == b'\t');
128                    self.push(SyntaxKind::WHITESPACE, start);
129                }
130                b'\n' => {
131                    self.pos += 1;
132                    self.push(SyntaxKind::NEWLINE, start);
133                }
134                b'\r' => {
135                    self.pos += 1;
136                    if self.peek() == b'\n' {
137                        self.pos += 1;
138                    }
139                    self.push(SyntaxKind::NEWLINE, start);
140                }
141                // Line comments: -- ...  and  // ...
142                b'-' if self.peek_at(1) == b'-' => {
143                    self.line_comment();
144                    self.push(SyntaxKind::COMMENT, start);
145                }
146                b'/' if self.peek_at(1) == b'/' => {
147                    self.line_comment();
148                    self.push(SyntaxKind::COMMENT, start);
149                }
150                // Block comment: /* ... */  (Snowflake block comments do not nest)
151                b'/' if self.peek_at(1) == b'*' => {
152                    self.block_comment(start);
153                    self.push(SyntaxKind::BLOCK_COMMENT, start);
154                }
155                b'\'' => {
156                    self.string_body(start);
157                    self.push(SyntaxKind::STRING, start);
158                }
159                b'"' => {
160                    self.quoted_ident_body(start);
161                    self.push(SyntaxKind::QUOTED_IDENT, start);
162                }
163                b'`' if self.options.dialect.supports_backtick_identifiers() => {
164                    self.backtick_ident_body(start);
165                    self.push(SyntaxKind::QUOTED_IDENT, start);
166                }
167                // $1 / $name variables (but not body delimiters, handled above). Gated on dollar
168                // quoting so non-Snowflake dialects lex a bare `$` as the DOLLAR operator instead.
169                b'$' if dollar_quoting
170                    && (is_ident_start(self.peek_at(1)) || self.peek_at(1).is_ascii_digit()) =>
171                {
172                    self.pos += 1; // $
173                    self.eat_while(is_ident_continue);
174                    self.push(SyntaxKind::VARIABLE, start);
175                }
176                c if c.is_ascii_digit() => self.number(start),
177                // Leading-dot float like `.5` (a bare `.` is the DOT operator, handled below).
178                b'.' if self.peek_at(1).is_ascii_digit() => self.number(start),
179                c if is_ident_start(c) => {
180                    self.eat_while(is_ident_continue);
181                    self.push(SyntaxKind::IDENT, start);
182                }
183                _ => self.operator(start),
184            }
185        }
186        Lexed {
187            tokens: self.tokens,
188            errors: self.errors,
189        }
190    }
191
192    /// Consume `--`/`//` and the rest of the physical line (not the newline itself).
193    fn line_comment(&mut self) {
194        self.pos += 2;
195        while !self.at_end() {
196            let c = self.peek();
197            if c == b'\n' || c == b'\r' {
198                break;
199            }
200            self.pos += 1;
201        }
202    }
203
204    /// Consume `/* ... */`. Records an error if unterminated. Non-nesting (Snowflake semantics).
205    fn block_comment(&mut self, start: usize) {
206        self.pos += 2; // /*
207        loop {
208            if self.at_end() {
209                self.error("unterminated block comment", start);
210                break;
211            }
212            if self.peek() == b'*' && self.peek_at(1) == b'/' {
213                self.pos += 2;
214                break;
215            }
216            self.pos += 1;
217        }
218    }
219
220    /// Consume a single-quoted string. Handles `''` (doubled quote) and `\` escapes
221    /// (Snowflake interprets backslash escape sequences in string literals by default).
222    fn string_body(&mut self, start: usize) {
223        self.pos += 1; // opening '
224        loop {
225            if self.at_end() {
226                self.error("unterminated string literal", start);
227                break;
228            }
229            match self.bump() {
230                b'\\' => {
231                    // Escape: consume the next byte if present (e.g. \' or \\).
232                    if !self.at_end() {
233                        self.pos += 1;
234                    }
235                }
236                b'\'' => {
237                    if self.peek() == b'\'' {
238                        self.pos += 1; // doubled quote → escaped quote, keep going
239                    } else {
240                        break; // closing quote
241                    }
242                }
243                _ => {}
244            }
245        }
246    }
247
248    /// Consume a `"quoted identifier"`. Handles `""` (doubled quote). No backslash escapes.
249    fn quoted_ident_body(&mut self, start: usize) {
250        self.pos += 1; // opening "
251        loop {
252            if self.at_end() {
253                self.error("unterminated quoted identifier", start);
254                break;
255            }
256            if self.bump() == b'"' {
257                if self.peek() == b'"' {
258                    self.pos += 1; // doubled quote → escaped, keep going
259                } else {
260                    break; // closing quote
261                }
262            }
263        }
264    }
265
266    /// Consume a Databricks/Spark `` `quoted identifier` ``. Handles doubled backticks.
267    fn backtick_ident_body(&mut self, start: usize) {
268        self.pos += 1; // opening `
269        loop {
270            if self.at_end() {
271                self.error("unterminated backtick identifier", start);
272                break;
273            }
274            if self.bump() == b'`' {
275                if self.peek() == b'`' {
276                    self.pos += 1; // doubled backtick, keep going
277                } else {
278                    break; // closing backtick
279                }
280            }
281        }
282    }
283
284    fn body_delimiter_at(&self) -> Option<BodyDelimiter> {
285        self.options
286            .body_delimiters
287            .iter()
288            .copied()
289            .filter(|delimiter| {
290                !delimiter.opener.is_empty()
291                    && !delimiter.closer.is_empty()
292                    && self.starts_with(delimiter.opener)
293            })
294            .max_by_key(|delimiter| delimiter.opener.len())
295    }
296
297    /// Consume a delimited embedded body. The current Snowflake default is `$$...$$`.
298    fn delimited_body(&mut self, start: usize, delimiter: BodyDelimiter) {
299        self.pos += delimiter.opener.len();
300        loop {
301            if self.at_end() {
302                self.error(format!("unterminated {}", delimiter.name), start);
303                break;
304            }
305            if self.starts_with(delimiter.closer) {
306                self.pos += delimiter.closer.len();
307                break;
308            }
309            self.pos += 1;
310        }
311    }
312
313    /// Lex a numeric literal. Entered on a digit or on `.` immediately followed by a digit.
314    fn number(&mut self, start: usize) {
315        let mut is_float = false;
316        if self.peek() == b'.' {
317            // Leading-dot float: `.5`
318            is_float = true;
319            self.pos += 1;
320            self.eat_while(|c| c.is_ascii_digit());
321        } else {
322            self.eat_while(|c| c.is_ascii_digit());
323            if self.peek() == b'.' {
324                // Fractional part (or a trailing dot, e.g. `100.`).
325                is_float = true;
326                self.pos += 1;
327                self.eat_while(|c| c.is_ascii_digit());
328            }
329        }
330        // Optional exponent: e / E [ + | - ] digits. Backtrack if no digits follow.
331        if self.peek() == b'e' || self.peek() == b'E' {
332            let save = self.pos;
333            self.pos += 1;
334            if self.peek() == b'+' || self.peek() == b'-' {
335                self.pos += 1;
336            }
337            if self.peek().is_ascii_digit() {
338                is_float = true;
339                self.eat_while(|c| c.is_ascii_digit());
340            } else {
341                self.pos = save; // not actually an exponent
342            }
343        }
344        self.push(
345            if is_float {
346                SyntaxKind::FLOAT_NUMBER
347            } else {
348                SyntaxKind::INT_NUMBER
349            },
350            start,
351        );
352    }
353
354    /// Lex punctuation / operators (everything not handled by the dispatch in `run`).
355    fn operator(&mut self, start: usize) {
356        let c = self.peek();
357        // A stray multi-byte (non-ASCII) char outside any literal: consume the whole char so
358        // we never slice the &str off a UTF-8 boundary, and report it. `c >= 0x80` guarantees a
359        // char starts here, but fall back to a single byte rather than panic if that ever changes.
360        if c >= 0x80 {
361            if let Some(ch) = self.input[self.pos..].chars().next() {
362                self.pos += ch.len_utf8();
363                self.error(format!("unexpected character {ch:?}"), start);
364            } else {
365                self.pos += 1;
366                self.error("unexpected byte", start);
367            }
368            self.push(SyntaxKind::ERROR, start);
369            return;
370        }
371
372        self.pos += 1; // consume `c`
373        let kind = match c {
374            b'(' => SyntaxKind::L_PAREN,
375            b')' => SyntaxKind::R_PAREN,
376            b'[' => SyntaxKind::L_BRACKET,
377            b']' => SyntaxKind::R_BRACKET,
378            b'{' => SyntaxKind::L_BRACE,
379            b'}' => SyntaxKind::R_BRACE,
380            b',' => SyntaxKind::COMMA,
381            b';' => SyntaxKind::SEMICOLON,
382            b'+' => SyntaxKind::PLUS,
383            b'*' => SyntaxKind::STAR,
384            b'/' => SyntaxKind::SLASH,
385            b'%' => SyntaxKind::PERCENT,
386            b'&' => SyntaxKind::AMP,
387            b'^' => SyntaxKind::CARET,
388            b'~' => SyntaxKind::TILDE,
389            // `@` introduces stage references (`@stage`, `@~`, `@%table`). Dialects without stage
390            // refs (none yet) treat it as an unexpected character rather than the AT operator.
391            b'@' if self.options.dialect.supports_stage_refs() => SyntaxKind::AT,
392            b'?' => SyntaxKind::QUESTION,
393            b'.' => SyntaxKind::DOT,
394            b'$' => SyntaxKind::DOLLAR,
395            b'=' => {
396                if self.peek() == b'>' {
397                    self.pos += 1;
398                    SyntaxKind::FAT_ARROW
399                } else {
400                    SyntaxKind::EQ
401                }
402            }
403            b'!' => {
404                if self.peek() == b'=' {
405                    self.pos += 1;
406                    SyntaxKind::NEQ
407                } else {
408                    self.error("unexpected '!'", start);
409                    SyntaxKind::BANG
410                }
411            }
412            b'<' => {
413                if self.peek() == b'=' {
414                    self.pos += 1;
415                    SyntaxKind::LTE
416                } else if self.peek() == b'>' {
417                    self.pos += 1;
418                    SyntaxKind::NEQ
419                } else {
420                    SyntaxKind::LT
421                }
422            }
423            b'>' => {
424                if self.peek() == b'=' {
425                    self.pos += 1;
426                    SyntaxKind::GTE
427                } else {
428                    SyntaxKind::GT
429                }
430            }
431            b':' => {
432                if self.peek() == b':' {
433                    self.pos += 1;
434                    SyntaxKind::COLON2
435                } else if self.peek() == b'=' {
436                    self.pos += 1;
437                    SyntaxKind::ASSIGN
438                } else {
439                    SyntaxKind::COLON
440                }
441            }
442            b'-' => {
443                if self.peek() == b'>' && self.peek_at(1) == b'>' {
444                    self.pos += 2;
445                    SyntaxKind::FLOW_PIPE
446                } else if self.peek() == b'>' {
447                    self.pos += 1;
448                    SyntaxKind::ARROW
449                } else {
450                    SyntaxKind::MINUS
451                }
452            }
453            b'|' => {
454                if self.peek() == b'>' {
455                    self.pos += 1;
456                    SyntaxKind::PIPE_GT
457                } else if self.peek() == b'|' {
458                    self.pos += 1;
459                    SyntaxKind::CONCAT
460                } else {
461                    SyntaxKind::PIPE
462                }
463            }
464            other => {
465                self.error(format!("unexpected character {:?}", other as char), start);
466                SyntaxKind::ERROR
467            }
468        };
469        self.push(kind, start);
470    }
471}
472
473#[inline]
474fn is_ident_start(c: u8) -> bool {
475    c.is_ascii_alphabetic() || c == b'_'
476}
477
478#[inline]
479fn is_ident_continue(c: u8) -> bool {
480    c.is_ascii_alphanumeric() || c == b'_' || c == b'$'
481}