Skip to main content

qql_core/
lexer.rs

1use crate::error::{QqlError, Span};
2use crate::token::{Token, TokenKind, lookup_keyword};
3
4/// QQL lexer yielding tokens with byte spans; halts after the first lexical error.
5#[derive(Debug, Clone)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize))]
7pub struct Lexer<'a> {
8    input: &'a str,
9    pos: usize,
10    /// Set after a lex error has been yielded. `next_token` may fail without
11    /// advancing `pos`, so the iterator must terminate instead of re-yielding
12    /// the same error forever.
13    halted: bool,
14}
15
16impl<'a> Lexer<'a> {
17    /// Creates a lexer over the given source input.
18    pub fn new(input: &'a str) -> Self {
19        Lexer {
20            input,
21            pos: 0,
22            halted: false,
23        }
24    }
25
26    /// Lexes and returns the next token, `Eof` at end of input, or a lexical error.
27    ///
28    /// After a lexical error the lexer stays halted: further calls return `Eof`
29    /// instead of re-yielding the same error.
30    pub fn next_token(&mut self) -> Result<Token<'a>, QqlError> {
31        if self.halted {
32            return Ok(Token::eof(self.pos));
33        }
34        let result = self.lex_one();
35        if result.is_err() {
36            self.halted = true;
37        }
38        result
39    }
40
41    fn lex_one(&mut self) -> Result<Token<'a>, QqlError> {
42        self.skip_whitespace();
43
44        if self.pos >= self.input.len() {
45            return Ok(Token::new(TokenKind::Eof, "", Span::point(self.pos)));
46        }
47
48        let bytes = self.input.as_bytes();
49        let ch = bytes[self.pos];
50
51        match ch {
52            b'{' => self.single_char(TokenKind::Lbrace),
53            b'}' => self.single_char(TokenKind::Rbrace),
54            b'[' => self.single_char(TokenKind::Lbracket),
55            b']' => self.single_char(TokenKind::Rbracket),
56            b'(' => self.single_char(TokenKind::Lparen),
57            b')' => self.single_char(TokenKind::Rparen),
58            b'*' => self.single_char(TokenKind::Star),
59            b':' => self.single_char(TokenKind::Colon),
60            b',' => self.single_char(TokenKind::Comma),
61            b'=' => self.single_char(TokenKind::Equals),
62            b'!' => self.read_not_equals(),
63            b'>' => self.read_gt_or_gte(),
64            b'<' => self.read_lt_or_lte(),
65            b'+' => self.single_char(TokenKind::Plus),
66            b'/' => self.single_char(TokenKind::Slash),
67            b';' => self.single_char(TokenKind::Semicolon),
68            b'?' => self.single_char(TokenKind::Question),
69            b'-' => self.read_minus_or_number(),
70            b'`' => self.read_backtick_string(),
71            b'"' | b'\'' => self.read_string(ch),
72            _ => {
73                // Raw strings use a lowercase `r` prefix only; the grammar
74                // (`raw_string`) has no uppercase form, so `R'…'` must lex as
75                // the identifier `R` followed by a string.
76                if ch == b'r'
77                    && self.pos + 1 < self.input.len()
78                    && (bytes[self.pos + 1] == b'\'' || bytes[self.pos + 1] == b'"')
79                {
80                    self.read_raw_string(bytes[self.pos + 1])
81                } else if self.input[self.pos..].starts_with('≥') {
82                    let pos = self.pos;
83                    self.pos += '≥'.len_utf8();
84                    Ok(Token::new(
85                        TokenKind::Gte,
86                        &self.input[pos..self.pos],
87                        Span::new(pos, self.pos),
88                    ))
89                } else if self.input[self.pos..].starts_with('≤') {
90                    let pos = self.pos;
91                    self.pos += '≤'.len_utf8();
92                    Ok(Token::new(
93                        TokenKind::Lte,
94                        &self.input[pos..self.pos],
95                        Span::new(pos, self.pos),
96                    ))
97                } else if self.input[self.pos..].starts_with('≠') {
98                    let pos = self.pos;
99                    self.pos += '≠'.len_utf8();
100                    Ok(Token::new(
101                        TokenKind::NotEquals,
102                        &self.input[pos..self.pos],
103                        Span::new(pos, self.pos),
104                    ))
105                } else if is_digit(ch) {
106                    self.read_number()
107                } else if is_ident_start(ch) {
108                    self.read_identifier()
109                } else {
110                    let c = self.input[self.pos..].chars().next().unwrap_or('?');
111                    let len = c.len_utf8();
112                    let err_msg = alloc::format!("Unexpected character '{}'", c);
113                    Err(QqlError::lex(
114                        "QQL-LEX-CHAR",
115                        err_msg,
116                        Span::new(self.pos, self.pos + len),
117                    ))
118                }
119            }
120        }
121    }
122
123    fn single_char(&mut self, kind: TokenKind) -> Result<Token<'a>, QqlError> {
124        let pos = self.pos;
125        self.pos += 1;
126        Ok(Token::new(
127            kind,
128            &self.input[pos..pos + 1],
129            Span::new(pos, pos + 1),
130        ))
131    }
132
133    fn read_not_equals(&mut self) -> Result<Token<'a>, QqlError> {
134        let bytes = self.input.as_bytes();
135        if self.pos + 1 < self.input.len() && bytes[self.pos + 1] == b'=' {
136            let pos = self.pos;
137            self.pos += 2;
138            Ok(Token::new(
139                TokenKind::NotEquals,
140                &self.input[pos..pos + 2],
141                Span::new(pos, pos + 2),
142            ))
143        } else {
144            Err(QqlError::lex(
145                "QQL-LEX-CHAR",
146                "Unexpected character '!'",
147                Span::new(self.pos, self.pos + 1),
148            ))
149        }
150    }
151
152    fn read_gt_or_gte(&mut self) -> Result<Token<'a>, QqlError> {
153        let bytes = self.input.as_bytes();
154        if self.pos + 1 < self.input.len() && bytes[self.pos + 1] == b'=' {
155            let pos = self.pos;
156            self.pos += 2;
157            Ok(Token::new(
158                TokenKind::Gte,
159                &self.input[pos..pos + 2],
160                Span::new(pos, pos + 2),
161            ))
162        } else {
163            self.single_char(TokenKind::Gt)
164        }
165    }
166
167    fn read_lt_or_lte(&mut self) -> Result<Token<'a>, QqlError> {
168        let bytes = self.input.as_bytes();
169        if self.pos + 1 < self.input.len() && bytes[self.pos + 1] == b'=' {
170            let pos = self.pos;
171            self.pos += 2;
172            Ok(Token::new(
173                TokenKind::Lte,
174                &self.input[pos..pos + 2],
175                Span::new(pos, pos + 2),
176            ))
177        } else {
178            self.single_char(TokenKind::Lt)
179        }
180    }
181
182    fn read_minus_or_number(&mut self) -> Result<Token<'a>, QqlError> {
183        let bytes = self.input.as_bytes();
184        if self.pos + 1 < self.input.len() && is_digit(bytes[self.pos + 1]) {
185            self.read_number()
186        } else if self.pos + 1 < self.input.len() && bytes[self.pos + 1] == b'.' {
187            // `-.5`: grammar `float`/`integer` require digits after the sign,
188            // so a `-` followed by `.` is never a number. Error at lex time
189            // instead of emitting `-` and a confusing bare-dot error.
190            Err(QqlError::lex(
191                "QQL-LEX-NUMBER",
192                "malformed numeric literal: '-' must be followed by a digit",
193                Span::new(self.pos, self.pos + 2),
194            ))
195        } else {
196            self.single_char(TokenKind::Minus)
197        }
198    }
199
200    fn read_string(&mut self, quote: u8) -> Result<Token<'a>, QqlError> {
201        let start = self.pos;
202        // A run of three quotes only starts a triple-quoted string when a
203        // matching closing delimiter exists later in the input. Otherwise the
204        // run is single-quoted content: `''''` (four quotes) is the
205        // SQL-escaped one-apostrophe string `'` + `''` + `'` per
206        // `single_quoted_string` in grammar.pest, and must fall back below.
207        let triple = if quote == b'\'' { "'''" } else { "\"\"\"" };
208        if self.input[start..].starts_with(triple)
209            && self.input[start + triple.len()..].find(triple).is_some()
210        {
211            return self.read_triple_quoted_string(quote);
212        }
213
214        self.pos += 1;
215        let content_start = self.pos;
216
217        while self.pos < self.input.len() {
218            let bytes = self.input.as_bytes();
219            if bytes[self.pos] == b'\\' {
220                if self.pos + 1 >= self.input.len() {
221                    return Err(QqlError::lex(
222                        "QQL-LEX-STRING",
223                        "unterminated string literal",
224                        Span::new(start, self.input.len()),
225                    ));
226                }
227                self.pos += 2;
228                continue;
229            }
230            if bytes[self.pos] == quote {
231                // SQL-style double single quotes ('') inside single-quoted strings
232                if quote == b'\'' && self.pos + 1 < self.input.len() && bytes[self.pos + 1] == b'\''
233                {
234                    self.pos += 2;
235                    continue;
236                }
237                let text = &self.input[content_start..self.pos];
238                self.pos += 1;
239                return Ok(Token::new(
240                    TokenKind::String,
241                    text,
242                    Span::new(start, self.pos),
243                ));
244            }
245            self.pos += 1;
246        }
247
248        Err(QqlError::lex(
249            "QQL-LEX-STRING",
250            "unterminated string literal",
251            Span::new(start, self.input.len()),
252        ))
253    }
254
255    fn read_triple_quoted_string(&mut self, quote: u8) -> Result<Token<'a>, QqlError> {
256        let start = self.pos;
257        self.pos += 3;
258        let content_start = self.pos;
259        let delimiter = if quote == b'\'' { "'''" } else { "\"\"\"" };
260
261        if let Some(rel_pos) = self.input[self.pos..].find(delimiter) {
262            let content_end = self.pos + rel_pos;
263            let text = &self.input[content_start..content_end];
264            self.pos = content_end + 3;
265            return Ok(Token::new(
266                TokenKind::String,
267                text,
268                Span::new(start, self.pos),
269            ));
270        }
271
272        Err(QqlError::lex(
273            "QQL-LEX-STRING",
274            "unterminated triple-quoted string literal",
275            Span::new(start, self.input.len()),
276        ))
277    }
278
279    fn read_raw_string(&mut self, quote: u8) -> Result<Token<'a>, QqlError> {
280        let start = self.pos;
281        self.pos += 2;
282        let content_start = self.pos;
283
284        while self.pos < self.input.len() {
285            if self.input.as_bytes()[self.pos] == quote {
286                let text = &self.input[content_start..self.pos];
287                self.pos += 1;
288                return Ok(Token::new(
289                    TokenKind::String,
290                    text,
291                    Span::new(start, self.pos),
292                ));
293            }
294            self.pos += 1;
295        }
296
297        Err(QqlError::lex(
298            "QQL-LEX-STRING",
299            "unterminated raw string literal",
300            Span::new(start, self.input.len()),
301        ))
302    }
303
304    fn read_backtick_string(&mut self) -> Result<Token<'a>, QqlError> {
305        let start = self.pos;
306        self.pos += 1;
307        let content_start = self.pos;
308
309        while self.pos < self.input.len() {
310            if self.input.as_bytes()[self.pos] == b'`' {
311                let text = &self.input[content_start..self.pos];
312                self.pos += 1;
313                return Ok(Token::new(
314                    TokenKind::String,
315                    text,
316                    Span::new(start, self.pos),
317                ));
318            }
319            self.pos += 1;
320        }
321
322        Err(QqlError::lex(
323            "QQL-LEX-STRING",
324            "unterminated backtick string literal",
325            Span::new(start, self.input.len()),
326        ))
327    }
328
329    fn read_number(&mut self) -> Result<Token<'a>, QqlError> {
330        let start = self.pos;
331        if self.input.as_bytes()[self.pos] == b'-' {
332            self.pos += 1;
333        }
334
335        while self.pos < self.input.len() && is_digit(self.input.as_bytes()[self.pos]) {
336            self.pos += 1;
337        }
338
339        let mut is_float = false;
340        if self.pos < self.input.len()
341            && self.input.as_bytes()[self.pos] == b'.'
342            && self.pos + 1 < self.input.len()
343            && is_digit(self.input.as_bytes()[self.pos + 1])
344        {
345            is_float = true;
346            self.pos += 1;
347            while self.pos < self.input.len() && is_digit(self.input.as_bytes()[self.pos]) {
348                self.pos += 1;
349            }
350        }
351
352        // Handle scientific notation exponent (e/E, e-5, e+5). An exponent
353        // must have at least one digit (after an optional sign): `1e`, `5e-`,
354        // `1e+` are malformed (grammar `exponent` = (^"e" | ^"E") ~ ("+" |
355        // "-")? ~ ASCII_DIGIT+), so error at lex time with a structured code
356        // instead of emitting a token that fails downstream `f64` parsing.
357        if self.pos < self.input.len()
358            && (self.input.as_bytes()[self.pos] == b'e' || self.input.as_bytes()[self.pos] == b'E')
359        {
360            let mut cursor = self.pos + 1;
361            if cursor < self.input.len()
362                && (self.input.as_bytes()[cursor] == b'+' || self.input.as_bytes()[cursor] == b'-')
363            {
364                cursor += 1;
365            }
366            if cursor >= self.input.len() || !is_digit(self.input.as_bytes()[cursor]) {
367                return Err(QqlError::lex(
368                    "QQL-LEX-NUMBER",
369                    "malformed numeric literal: exponent requires at least one digit",
370                    Span::new(start, cursor),
371                ));
372            }
373            is_float = true;
374            self.pos = cursor;
375            while self.pos < self.input.len() && is_digit(self.input.as_bytes()[self.pos]) {
376                self.pos += 1;
377            }
378        }
379
380        // A trailing `.` (e.g. `1.` or `1e5.3`) is never part of a valid
381        // number (grammar `float` requires digits after the decimal point).
382        if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'.' {
383            return Err(QqlError::lex(
384                "QQL-LEX-NUMBER",
385                "malformed numeric literal: unexpected '.'",
386                Span::new(start, self.pos + 1),
387            ));
388        }
389
390        if is_float {
391            Ok(Token::new(
392                TokenKind::Float,
393                &self.input[start..self.pos],
394                Span::new(start, self.pos),
395            ))
396        } else {
397            Ok(Token::new(
398                TokenKind::Integer,
399                &self.input[start..self.pos],
400                Span::new(start, self.pos),
401            ))
402        }
403    }
404
405    fn read_identifier(&mut self) -> Result<Token<'a>, QqlError> {
406        let start = self.pos;
407        let bytes = self.input.as_bytes();
408
409        while self.pos < self.input.len() && is_ident_continue(bytes[self.pos]) {
410            self.pos += 1;
411        }
412
413        loop {
414            if self.pos >= self.input.len() {
415                break;
416            }
417            if self.input[self.pos..].starts_with('.') {
418                let rest = &self.input[self.pos + 1..];
419                let first_byte = rest.as_bytes().first().copied().unwrap_or(0);
420                // `identifier_segment` starts with a letter or `_` only
421                // (grammar.pest); `$` cannot begin a dotted segment.
422                if first_byte.is_ascii_alphabetic() || first_byte == b'_' {
423                    self.pos += 1;
424                    while self.pos < self.input.len()
425                        && is_ident_continue(self.input.as_bytes()[self.pos])
426                    {
427                        self.pos += 1;
428                    }
429                } else {
430                    break;
431                }
432            } else if self.input[self.pos..].starts_with("[].") {
433                let rest = &self.input[self.pos + 3..];
434                let first_byte = rest.as_bytes().first().copied().unwrap_or(0);
435                if first_byte.is_ascii_alphabetic() || first_byte == b'_' {
436                    self.pos += 3;
437                    while self.pos < self.input.len()
438                        && is_ident_continue(self.input.as_bytes()[self.pos])
439                    {
440                        self.pos += 1;
441                    }
442                } else {
443                    break;
444                }
445            } else {
446                break;
447            }
448        }
449
450        let word = &self.input[start..self.pos];
451
452        if !word.contains('.')
453            && let Some(kind) = lookup_keyword(word)
454        {
455            return Ok(Token::new(kind, word, Span::new(start, self.pos)));
456        }
457
458        Ok(Token::new(
459            TokenKind::Identifier,
460            word,
461            Span::new(start, self.pos),
462        ))
463    }
464
465    fn skip_whitespace(&mut self) {
466        let bytes = self.input.as_bytes();
467        loop {
468            while self.pos < self.input.len() && is_whitespace(bytes[self.pos]) {
469                self.pos += 1;
470            }
471            // Skip `--` line comments
472            if self.pos + 1 < self.input.len()
473                && bytes[self.pos] == b'-'
474                && bytes[self.pos + 1] == b'-'
475            {
476                self.pos += 2;
477                while self.pos < self.input.len() && bytes[self.pos] != b'\n' {
478                    self.pos += 1;
479                }
480                continue;
481            }
482            break;
483        }
484    }
485}
486
487impl<'a> Iterator for Lexer<'a> {
488    type Item = Result<Token<'a>, QqlError>;
489
490    fn next(&mut self) -> Option<Self::Item> {
491        if self.halted || self.pos >= self.input.len() {
492            return None;
493        }
494        let result = self.next_token();
495        match &result {
496            Ok(t) if t.kind == TokenKind::Eof => None,
497            // Surface the first lex error exactly once, then terminate: the
498            // failing production may not advance `pos`, so re-polling would
499            // loop forever (and `flatten()`-style consumers would hang).
500            Err(_) => {
501                self.halted = true;
502                Some(result)
503            }
504            _ => Some(result),
505        }
506    }
507}
508
509fn is_whitespace(ch: u8) -> bool {
510    ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r'
511}
512
513fn is_digit(ch: u8) -> bool {
514    ch.is_ascii_digit()
515}
516
517fn is_ident_start(ch: u8) -> bool {
518    ch == b'$' || ch == b'_' || ch.is_ascii_alphabetic()
519}
520
521fn is_ident_continue(ch: u8) -> bool {
522    is_ident_start(ch) || is_digit(ch)
523}