Skip to main content

swan_core/
lexer.rs

1//! The Swan lexer: turns source text into [`Token`]s.
2
3
4use std::iter::Peekable;
5use std::str::Chars;
6use crate::token::Token;
7
8
9/// A zero-copy lexer for Swan source text.
10///
11/// `Lexer` converts a source `&str` into a sequence of [`Token`]s.  Each
12/// token that carries a string payload (`Identifier`, `Text`, `Comment`,
13/// `Undefined`) borrows a slice directly from the original source, so no heap
14/// allocations are needed during tokenisation.
15///
16/// # Usage
17///
18/// Call [`next_token`](Lexer::next_token) in a loop to pull tokens one at a
19/// time, or use the [`Iterator`] implementation to drive the lexer with
20/// standard iterator adapters.  The iterator stops (returns `None`) at
21/// [`Token::EndOfExpression`]; `next_token` returns that sentinel instead.
22///
23/// ```
24/// use swan_core::lexer::Lexer;
25/// use swan_core::token::Token;
26///
27/// let tokens: Vec<_> = Lexer::new("x = 42").collect();
28/// assert_eq!(tokens[0], (0, Token::Identifier("x")));
29/// assert_eq!(tokens[1], (2, Token::AssignmentOp));
30/// assert_eq!(tokens[2], (4, Token::Number(42.0)));
31/// ```
32///
33/// # Position tracking
34///
35/// Every call to `next_token` returns a `(usize, Token)` pair where the
36/// `usize` is the **byte offset** of the token's first character inside the
37/// source string.  Positions are byte-based (not char-based) and are safe to
38/// use as indices into the original `&str`.
39
40pub struct Lexer<'a> {
41    /// The full source string — never modified; sliced into by token variants.
42    source: &'a str,
43    /// Character iterator with one-character lookahead.
44    chars: Peekable<Chars<'a>>,
45    /// Byte offset of the *next* character to be consumed by `next_char`.
46    /// Starts at 0.  Incremented by `ch.len_utf8()` with every consumed char,
47    /// keeping it valid as a UTF-8 byte index into `source`.
48    position: usize,
49}
50
51impl<'a> Lexer<'a> {
52
53    /// Creates a new `Lexer` positioned at the start of `source`.
54    pub fn new(source: &'a str) -> Self {
55        Self {
56            source,
57            chars: source.chars().peekable(),
58            position: 0,
59        }
60    }
61
62    // -------------------------------------------------------------------------
63    // Low-level character helpers
64    // -------------------------------------------------------------------------
65
66    /// Consumes and returns the next character, advancing `position` by the
67    /// character's UTF-8 byte length.  Returns `None` at end-of-input.
68    fn next_char(&mut self) -> Option<char> {
69        let ch = self.chars.next()?;
70        self.position += ch.len_utf8();
71        Some(ch)
72    }
73
74    /// Returns the next character without consuming it.
75    fn peek_char(&mut self) -> Option<char> {
76        self.chars.peek().copied()
77    }
78
79    /// Returns the second upcoming character without consuming any characters.
80    fn peek_second_char(&self) -> Option<char> {
81        let mut it = self.chars.clone();
82        it.next(); // Skip the first peeked character
83        it.next()   // Return the second character
84    }
85
86    /// Returns `true` if the next (unconsumed) character equals `ch`.
87    fn next_char_is(&mut self, ch: char) -> bool {
88        self.peek_char() == Some(ch)
89    }
90
91    /// Skips all leading whitespace characters (space, tab, newline, …).
92    fn skip_whitespaces(&mut self) {
93        while let Some(ch) = self.peek_char() {
94            if ch.is_whitespace() {
95                self.next_char();
96            } else {
97                break;
98            }
99        }
100    }
101
102    // -------------------------------------------------------------------------
103    // Token-specific scanners
104    // -------------------------------------------------------------------------
105
106    /// Scans the rest of an identifier, given that its first character has
107    /// already been consumed and `start_pos` is its byte offset.
108    ///
109    /// Identifier continuation characters are letters, digits, and `_`.
110    /// The first character (already consumed by the caller) is either a letter
111    /// or `_`, so this method only extends the span rightward.
112    ///
113    /// Returns a [`Token::Identifier`] borrowing the scanned slice from
114    /// `source`.
115    fn next_identifier(&mut self, start_pos: usize) -> Token<'a> {
116        while let Some(ch) = self.peek_char() {
117            if ch == '_' || ch.is_alphanumeric() {
118                self.next_char();
119            } else {
120                break;
121            }
122        }
123        let literal = &self.source[start_pos..self.position];
124        Token::Identifier(literal)
125    }
126
127    /// Consumes a run of ASCII decimal digits without returning anything.
128    /// Used as a building block inside [`next_numeric_literal`](Self::next_numeric_literal).
129    fn consume_digits(&mut self) {
130        while let Some(ch) = self.peek_char() {
131            if ch.is_ascii_digit() {
132                self.next_char();
133            } else {
134                break;
135            }
136        }
137    }
138
139    /// Scans a numeric literal, given that its first digit has already been
140    /// consumed and `start_pos` is its byte offset.
141    ///
142    /// Accepts the following forms (matching the JS reference grammar):
143    ///
144    /// ```text
145    /// integer:    123
146    /// decimal:    12.3
147    /// scientific: 12.3e2  12.3E+2  12.3e-2
148    /// ```
149    ///
150    /// Returns [`Token::Number`] on success, or [`Token::Undefined`] when the
151    /// literal is malformed: an exponent marker without digits (`1e+`), or a
152    /// number immediately followed by identifier characters (`123abc`).
153    ///
154    /// A dot that is not followed by a digit is not part of the number, so
155    /// `12.` lexes as `Number(12)` followed by `SubcontextingOp`.
156    fn next_numeric_literal(&mut self, start_pos: usize) -> Token<'a> {
157
158        // Consume the integer part (the first digit was already consumed by the
159        // caller, but consume_digits will handle any remaining leading digits).
160        self.consume_digits();
161
162        // Optional fractional part: `.` followed by one or more digits.
163        if self.peek_char() == Some('.') && matches!(self.peek_second_char(), Some('0'..='9')) {
164            self.next_char(); // consume '.'
165            self.consume_digits();
166        }
167
168        // Optional exponent part: `e` or `E`, optional sign, one or more digits.
169        if matches!(self.peek_char(), Some('e') | Some('E')) {
170            self.next_char(); // consume 'e'/'E'
171            if matches!(self.peek_char(), Some('+') | Some('-')) {
172                self.next_char(); // consume optional sign
173            }
174            if matches!(self.peek_char(), Some('0'..='9')) {
175                self.consume_digits();
176            } else {
177                // Exponent marker not followed by digits → malformed literal.
178                return Token::Undefined(&self.source[start_pos..self.position]);
179            }
180        }
181
182        // Trailing word-boundary check: numbers cannot be directly adjacent to identifier chars
183        if matches!(self.peek_char(), Some('_') | Some('a'..='z' | 'A'..='Z')) {
184            while let Some(ch) = self.peek_char() {
185                if ch == '_' || ch.is_alphanumeric() {
186                    self.next_char();
187                } else {
188                    break;
189                }
190            }
191            return Token::Undefined(&self.source[start_pos..self.position]);
192        }
193
194        // The accumulated substring is a well-formed f64 string; Rust's
195        // standard parser handles all three forms above.
196        let number_str = &self.source[start_pos..self.position];
197        let value: f64 = number_str
198            .parse()
199            .expect("BUG: lexer produced a number string that f64::parse() rejected");
200        Token::Number(value)
201    }
202
203    /// Scans a double-quoted string literal, given that the opening `"` has
204    /// already been consumed and `start_pos` is the byte offset of the first
205    /// content character (i.e. `self.position` right after consuming `"`).
206    ///
207    /// The returned [`Token::Text`] slice contains only the content between
208    /// the quotes; the delimiters are not included.
209    ///
210    /// If the input ends before a closing `"` is found, the slice extends to
211    /// the end of the source rather than returning an error.
212    fn next_string_literal(&mut self, start_pos: usize) -> Token<'a> {
213        let end_pos: usize = loop {
214            let ch = self.peek_char();
215            if ch == Some('"') {
216                // Found the closing quote.  Record the end position *before*
217                // consuming it so the quote is excluded from the slice.
218                let end_pos = self.position;
219                self.next_char(); // consume closing '"'
220                break end_pos;
221            } else if ch.is_none() {
222                // End of input without a closing quote — emit the content so
223                // far rather than erroring (lenient, matching the JS behaviour).
224                break self.position;
225            } else {
226                self.next_char();
227            }
228        };
229        Token::Text(&self.source[start_pos..end_pos])
230    }
231
232    /// Scans a `#`-prefixed line comment, given that the `#` has already been
233    /// consumed and `start_pos` is the byte offset of the first content
234    /// character (i.e. `self.position` right after consuming `#`).
235    ///
236    /// The comment extends to (but does not include) the next `\n`, or to the
237    /// end of the source if there is no newline.  The `\n` itself is consumed
238    /// and discarded.
239    ///
240    /// The returned [`Token::Comment`] slice contains only the text after `#`
241    /// and before the terminating newline.
242    fn next_comment(&mut self, start_pos: usize) -> Token<'a> {
243        let end_pos: usize = loop {
244            let ch = self.peek_char();
245            if ch == Some('\n') {
246                // Newline terminates the comment; exclude it from the slice.
247                let end_pos = self.position;
248                self.next_char(); // consume '\n'
249                break end_pos;
250            } else if ch.is_none() {
251                // End of input — comment runs to the end of the source.
252                break self.position;
253            } else {
254                self.next_char();
255            }
256        };
257        Token::Comment(&self.source[start_pos..end_pos])
258    }
259
260    // -------------------------------------------------------------------------
261    // Main entry point
262    // -------------------------------------------------------------------------
263
264    /// Returns the next token and its byte position in the source.
265    ///
266    /// Leading whitespace is skipped before each token.  When the source is
267    /// exhausted this method returns `(pos, Token::EndOfExpression)` and
268    /// continues to do so on every subsequent call — the position will be
269    /// pinned at the end of the source.
270    ///
271    /// Two-character operators (`==`, `!=`, `<=`, `>=`, `->`, `<<`, `>>`,
272    /// `=>`, `~>`) are recognised greedily: the first character is consumed by the
273    /// main dispatch, then if the following character completes the operator it
274    /// is consumed too before the token is returned.
275    pub fn next_token(&mut self) -> (usize, Token<'a>) {
276        self.skip_whitespaces();
277
278        // Snapshot the byte offset of the token we are about to produce.
279        let ch_pos = self.position;
280
281        // Consume the first character; return EndOfExpression at end-of-input.
282        let ch = match self.next_char() {
283            Some(ch) => ch,
284            None => return (ch_pos, Token::EndOfExpression),
285        };
286
287        // --- Identifiers -------------------------------------------------------
288        // Start character: letter or `_`.  The rest of the identifier is
289        // consumed by next_identifier.
290        if ch == '_' || ch.is_alphabetic() {
291            (ch_pos, self.next_identifier(ch_pos))
292        }
293
294        // --- Numeric literals --------------------------------------------------
295        // Start character: ASCII digit.
296        else if ch.is_ascii_digit() {
297            (ch_pos, self.next_numeric_literal(ch_pos))
298        }
299
300        // --- String literals ---------------------------------------------------
301        // Opening `"`.  self.position now points past the quote.
302        else if ch == '"' {
303            (ch_pos, self.next_string_literal(self.position))
304        }
305
306        // --- Line comments -----------------------------------------------------
307        // Opening `#`.  self.position now points past the `#`.
308        else if ch == '#' {
309            (ch_pos, self.next_comment(self.position))
310        }
311
312        // --- Two-character operators -------------------------------------------
313        // Each branch peeks at the next character before committing.
314
315        // `==`
316        else if ch == '=' && self.next_char_is('=') {
317            self.next_char(); (ch_pos, Token::EqualToOp)
318        }
319        // `!=`
320        else if ch == '!' && self.next_char_is('=') {
321            self.next_char(); (ch_pos, Token::NotEqualToOp)
322        }
323        // `<=`
324        else if ch == '<' && self.next_char_is('=') {
325            self.next_char(); (ch_pos, Token::LessThanOrEqualToOp)
326        }
327        // `>=`
328        else if ch == '>' && self.next_char_is('=') {
329            self.next_char(); (ch_pos, Token::GreaterThanOrEqualToOp)
330        }
331        // `->`
332        else if ch == '-' && self.next_char_is('>') {
333            self.next_char(); (ch_pos, Token::FunctionDefinitionOp)
334        }
335        // `<<`
336        else if ch == '<' && self.next_char_is('<') {
337            self.next_char(); (ch_pos, Token::CompositionOp)
338        }
339        // `>>`
340        else if ch == '>' && self.next_char_is('>') {
341            self.next_char(); (ch_pos, Token::ReverseCompositionOp)
342        }
343        // `=>`
344        else if ch == '=' && self.next_char_is('>') {
345            self.next_char(); (ch_pos, Token::MappingOp)
346        }
347        else if ch == '~' && self.next_char_is('>') {
348            self.next_char();
349            (ch_pos, Token::ThenOp)
350        }
351
352        // --- Single-character tokens -------------------------------------------
353        else {
354            match ch {
355                '(' => (ch_pos, Token::LeftRoundBracket),
356                ')' => (ch_pos, Token::RightRoundBracket),
357
358                '[' => (ch_pos, Token::LeftSquareBracket),
359                ']' => (ch_pos, Token::RightSquareBracket),
360
361                '{' => (ch_pos, Token::LeftCurlyBracket),
362                '}' => (ch_pos, Token::RightCurlyBracket),
363
364                '.' => (ch_pos, Token::SubcontextingOp),
365
366                '^' => (ch_pos, Token::PowerOp),
367                '*' => (ch_pos, Token::ProductOp),
368                '/' => (ch_pos, Token::DivisionOp),
369                '%' => (ch_pos, Token::ModuloOp),
370                '+' => (ch_pos, Token::PlusOp),
371                '-' => (ch_pos, Token::MinusOp),
372
373                '<' => (ch_pos, Token::LessThanOp),
374                '>' => (ch_pos, Token::GreaterThanOp),
375
376                '&' => (ch_pos, Token::AndOp),
377                '|' => (ch_pos, Token::OrOp),
378
379                '?' => (ch_pos, Token::ConditionalOp),
380                ';' => (ch_pos, Token::AlternativeOp),
381
382                '=' => (ch_pos, Token::AssignmentOp),
383                ':' => (ch_pos, Token::LabellingOp),
384
385                ',' => (ch_pos, Token::PairingOp),
386
387                '!' => (ch_pos, Token::NotOp),
388                '$' => (ch_pos, Token::LiftingOp),
389
390                // Anything else is returned as an Undefined token containing
391                // exactly the one unrecognised character.
392                _ => (ch_pos, Token::Undefined(&self.source[ch_pos..self.position])),
393            }
394        }
395    }
396}
397
398
399// -----------------------------------------------------------------------------
400// Iterator implementation
401// -----------------------------------------------------------------------------
402
403/// Drives the lexer as a standard Rust iterator.
404///
405/// Each call to `next` delegates to [`next_token`](Lexer::next_token) and maps
406/// `Token::EndOfExpression` to `None`, stopping iteration.  All other tokens
407/// (including [`Token::Comment`]) are yielded.
408///
409/// > **Note**: Comment tokens are *not* silently discarded here.  The parser
410/// > is responsible for skipping them during expression parsing.
411impl<'a> Iterator for Lexer<'a> {
412    type Item = (usize, Token<'a>);
413
414    fn next(&mut self) -> Option<Self::Item> {
415        let (pos, token) = self.next_token();
416        if token == Token::EndOfExpression {
417            None
418        } else {
419            Some((pos, token))
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_lexer_next_token() {
430
431        macro_rules! assert_token {
432            ($input:expr, $token:expr, $pos:expr) => {
433                assert_eq!(
434                    Lexer::new($input).next_token(),
435                    ($pos, $token))
436            }
437        }
438
439        // Identifier
440        assert_token!("Abc1", Token::Identifier("Abc1"), 0);
441        assert_token!("abc1", Token::Identifier("abc1"), 0);
442        assert_token!("_ab1", Token::Identifier("_ab1"), 0);
443
444        // Numeric Literal
445        assert_token!("123", Token::Number(123.0), 0);
446        assert_token!("12.3", Token::Number(12.3), 0);
447        assert_token!("12.3e2", Token::Number(1230.0), 0);
448        assert_token!("12.3E+2", Token::Number(1230.0), 0);
449        assert_token!("12.3e-2", Token::Number(0.123), 0);
450        assert_token!("12.", Token::Number(12.0), 0);
451        assert_token!("12.3e", Token::Undefined("12.3e"), 0);
452        assert_token!("12.3e+", Token::Undefined("12.3e+"), 0);
453        assert_token!("12.3E-", Token::Undefined("12.3E-"), 0);
454        assert_token!("12.3abc", Token::Undefined("12.3abc"), 0);
455
456        // Text Literal
457        assert_token!("\"abc\"", Token::Text("abc"), 0);
458        assert_token!("\"abc", Token::Text("abc"), 0);
459
460        // Brackets
461        assert_token!("(", Token::LeftRoundBracket, 0);
462        assert_token!(")", Token::RightRoundBracket, 0);
463        assert_token!("[", Token::LeftSquareBracket, 0);
464        assert_token!("]", Token::RightSquareBracket, 0);
465        assert_token!("{", Token::LeftCurlyBracket, 0);
466        assert_token!("}", Token::RightCurlyBracket, 0);
467
468        // Binary operators
469        assert_token!(".", Token::SubcontextingOp, 0);
470
471        assert_token!("^", Token::PowerOp, 0);
472        assert_token!("*", Token::ProductOp, 0);
473        assert_token!("/", Token::DivisionOp, 0);
474        assert_token!("%", Token::ModuloOp, 0);
475        assert_token!("+", Token::PlusOp, 0);
476        assert_token!("-", Token::MinusOp, 0);
477
478        assert_token!("==", Token::EqualToOp, 0);
479        assert_token!("!=", Token::NotEqualToOp, 0);
480        assert_token!("<", Token::LessThanOp, 0);
481        assert_token!("<=", Token::LessThanOrEqualToOp, 0);
482        assert_token!(">", Token::GreaterThanOp, 0);
483        assert_token!(">=", Token::GreaterThanOrEqualToOp, 0);
484        
485        assert_token!("&", Token::AndOp, 0);
486        assert_token!("|", Token::OrOp, 0);
487
488        assert_token!("?", Token::ConditionalOp, 0);
489        assert_token!(";", Token::AlternativeOp, 0);
490
491        assert_token!("->", Token::FunctionDefinitionOp, 0);
492
493        assert_token!("<<", Token::CompositionOp, 0);
494        assert_token!(">>", Token::ReverseCompositionOp, 0);
495
496        assert_token!("=>", Token::MappingOp, 0);
497
498        assert_token!("=", Token::AssignmentOp, 0);
499        assert_token!(":", Token::LabellingOp, 0);
500
501        assert_token!(",", Token::PairingOp, 0);
502
503        // Special tokens
504        assert_token!("@", Token::Undefined("@"), 0);
505
506        assert_token!("# this is a comment \n\n", Token::Comment(" this is a comment "), 0);
507        assert_token!("# this is a comment ", Token::Comment(" this is a comment "), 0);
508
509        assert_token!("", Token::EndOfExpression, 0);
510
511        // Spaces skipping
512        assert_token!("  *", Token::ProductOp, 2);
513    }
514
515    #[test]
516    fn test_lexer_iterator() {
517        let input = "msg = \"hello\"";
518        let lexer = Lexer::new(input);
519        
520        let tokens: Vec<(usize, Token)> = lexer.collect();
521        assert_eq!(
522            tokens,
523            vec![
524                (0, Token::Identifier("msg")),
525                (4, Token::AssignmentOp),
526                (6, Token::Text("hello")),
527            ]
528        );
529    }
530}
531
532