Skip to main content

cljrs_reader/
lexer.rs

1// CljxError embeds NamedSource<String> for miette diagnostics, which is
2// unavoidably large. Suppress the false-positive for every returning function.
3#![allow(clippy::result_large_err)]
4
5use std::sync::Arc;
6
7use miette::NamedSource;
8
9use cljrs_types::error::{CljxError, CljxResult};
10use cljrs_types::span::Span;
11
12use crate::token::Token;
13
14// ─── Character classification ─────────────────────────────────────────────────
15
16/// Returns `true` if `ch` is a valid constituent character for a symbol or
17/// keyword.  Defined *negatively*: everything that isn't a delimiter, whitespace,
18/// or special syntax character is a symbol constituent.
19///
20/// `#` is included here: it is a *non-terminating* macro character in the
21/// Clojure reader, meaning it doesn't end a symbol token that's already in
22/// progress (only a leading `#` triggers `#`-dispatch — see
23/// [`is_symbol_start`]). This is what makes auto-gensym symbols like `x#`
24/// tokenize as a single symbol rather than `x` followed by a stray `#`.
25///
26/// `:` is included here too — it is also non-terminating, so a keyword like
27/// `:xlink:href` reads as one token with the literal name `xlink:href`
28/// rather than splitting into two keywords at the embedded colon. Only a
29/// *leading* `:`/`::` is special (it triggers keyword dispatch — see
30/// [`is_symbol_start`]).
31fn is_symbol_char(ch: char) -> bool {
32    !matches!(
33        ch,
34        ' ' | '\t'
35            | '\n'
36            | '\r'
37            | ','
38            | '('
39            | ')'
40            | '['
41            | ']'
42            | '{'
43            | '}'
44            | '"'
45            | ';'
46            | '`'
47            | '~'
48            | '^'
49            | '@'
50            | '\\'
51    )
52}
53
54/// Returns `true` if `ch` can *start* a symbol (not a digit, not `#` since a
55/// leading `#` always triggers `#`-dispatch, not `:` since a leading `:`
56/// always triggers keyword dispatch, not `+`/`-` when the following char is
57/// a digit — but the caller handles the `+`/`-` case).
58fn is_symbol_start(ch: char) -> bool {
59    is_symbol_char(ch) && !ch.is_ascii_digit() && ch != '#' && ch != ':'
60}
61
62// ─── Lexer ───────────────────────────────────────────────────────────────────
63
64pub struct Lexer {
65    source: Arc<String>,
66    file: Arc<String>,
67    pos: usize, // byte offset, always on a char boundary
68    line: u32,  // 1-based
69    col: u32,   // 1-based byte offset from line start
70}
71
72impl Lexer {
73    pub fn new(source: String, file: String) -> Self {
74        Self {
75            source: Arc::new(source),
76            file: Arc::new(file),
77            pos: 0,
78            line: 1,
79            col: 1,
80        }
81    }
82
83    // ── Public getters ────────────────────────────────────────────────────
84
85    pub fn source(&self) -> &Arc<String> {
86        &self.source
87    }
88
89    pub fn file(&self) -> &Arc<String> {
90        &self.file
91    }
92
93    // ── Low-level helpers ─────────────────────────────────────────────────
94
95    fn peek(&self) -> Option<char> {
96        self.source[self.pos..].chars().next()
97    }
98
99    fn peek_next(&self) -> Option<char> {
100        let mut chars = self.source[self.pos..].chars();
101        chars.next(); // skip current
102        chars.next()
103    }
104
105    fn advance(&mut self) -> Option<char> {
106        let ch = self.peek()?;
107        self.pos += ch.len_utf8();
108        if ch == '\n' {
109            self.line += 1;
110            self.col = 1;
111        } else {
112            self.col += ch.len_utf8() as u32;
113        }
114        Some(ch)
115    }
116
117    fn span_from(&self, start_pos: usize, start_line: u32, start_col: u32) -> Span {
118        Span::new(
119            Arc::clone(&self.file),
120            start_pos,
121            self.pos,
122            start_line,
123            start_col,
124        )
125    }
126
127    fn make_error(&self, msg: impl Into<String>, span: Span) -> CljxError {
128        CljxError::ReadError {
129            message: msg.into(),
130            span: Some(miette::SourceSpan::from(span)),
131            src: NamedSource::new((*self.file).clone(), (*self.source).clone()),
132        }
133    }
134
135    /// Consume characters while `is_symbol_char` holds, returning the collected
136    /// string.
137    fn read_symbol_chars(&mut self) -> String {
138        let mut buf = String::new();
139        while let Some(ch) = self.peek() {
140            if is_symbol_char(ch) {
141                buf.push(ch);
142                self.advance();
143            } else {
144                break;
145            }
146        }
147        buf
148    }
149
150    // ── Whitespace / comment skipping ─────────────────────────────────────
151
152    fn skip_whitespace_and_comments(&mut self) {
153        loop {
154            match self.peek() {
155                // Shebang: only recognised at the very start of the file.
156                Some('#') if self.pos == 0 => {
157                    if self.peek_next() == Some('!') {
158                        // skip to end of line
159                        while let Some(ch) = self.advance() {
160                            if ch == '\n' {
161                                break;
162                            }
163                        }
164                    } else {
165                        break; // '#' is meaningful, stop skipping
166                    }
167                }
168                Some(' ') | Some('\t') | Some('\r') | Some('\n') | Some(',') => {
169                    self.advance();
170                }
171                Some(';') => {
172                    while let Some(ch) = self.advance() {
173                        if ch == '\n' {
174                            break;
175                        }
176                    }
177                }
178                _ => break,
179            }
180        }
181    }
182
183    // ── `~` (unquote / unquote-splicing) ─────────────────────────────────
184
185    fn lex_unquote(
186        &mut self,
187        start_pos: usize,
188        start_line: u32,
189        start_col: u32,
190    ) -> CljxResult<(Token, Span)> {
191        self.advance(); // consume '~'
192        if self.peek() == Some('@') {
193            self.advance();
194            Ok((
195                Token::UnquoteSplice,
196                self.span_from(start_pos, start_line, start_col),
197            ))
198        } else {
199            Ok((
200                Token::Unquote,
201                self.span_from(start_pos, start_line, start_col),
202            ))
203        }
204    }
205
206    // ── `#` dispatch ──────────────────────────────────────────────────────
207
208    fn lex_hash(
209        &mut self,
210        start_pos: usize,
211        start_line: u32,
212        start_col: u32,
213    ) -> CljxResult<(Token, Span)> {
214        self.advance(); // consume '#'
215        match self.peek() {
216            Some('(') => {
217                self.advance();
218                Ok((
219                    Token::HashFn,
220                    self.span_from(start_pos, start_line, start_col),
221                ))
222            }
223            Some('{') => {
224                self.advance();
225                Ok((
226                    Token::HashSet,
227                    self.span_from(start_pos, start_line, start_col),
228                ))
229            }
230            Some('\'') => {
231                self.advance();
232                Ok((
233                    Token::HashVar,
234                    self.span_from(start_pos, start_line, start_col),
235                ))
236            }
237            Some('_') => {
238                self.advance();
239                Ok((
240                    Token::HashDiscard,
241                    self.span_from(start_pos, start_line, start_col),
242                ))
243            }
244            Some('"') => self.lex_regex(start_pos, start_line, start_col),
245            Some('?') => {
246                self.advance(); // consume '?'
247                if self.peek() == Some('@') {
248                    self.advance();
249                    Ok((
250                        Token::ReaderCondSplice,
251                        self.span_from(start_pos, start_line, start_col),
252                    ))
253                } else {
254                    Ok((
255                        Token::ReaderCond,
256                        self.span_from(start_pos, start_line, start_col),
257                    ))
258                }
259            }
260            Some('#') => self.lex_symbolic(start_pos, start_line, start_col),
261            Some(c) if is_symbol_start(c) => {
262                let name = self.read_symbol_chars();
263                Ok((
264                    Token::TaggedLiteral(name),
265                    self.span_from(start_pos, start_line, start_col),
266                ))
267            }
268            other => {
269                let span = self.span_from(start_pos, start_line, start_col);
270                Err(self.make_error(format!("unknown # dispatch character: {:?}", other), span))
271            }
272        }
273    }
274
275    fn lex_regex(
276        &mut self,
277        start_pos: usize,
278        start_line: u32,
279        start_col: u32,
280    ) -> CljxResult<(Token, Span)> {
281        self.advance(); // consume opening '"'
282        let mut buf = String::new();
283        loop {
284            match self.advance() {
285                None => {
286                    let span = self.span_from(start_pos, start_line, start_col);
287                    return Err(self.make_error("unterminated regex literal", span));
288                }
289                Some('"') => break,
290                Some('\\') => {
291                    // Store escape verbatim (two chars) — no processing.
292                    buf.push('\\');
293                    match self.advance() {
294                        Some(c) => buf.push(c),
295                        None => {
296                            let span = self.span_from(start_pos, start_line, start_col);
297                            return Err(self.make_error("unterminated regex literal", span));
298                        }
299                    }
300                }
301                Some(c) => buf.push(c),
302            }
303        }
304        Ok((
305            Token::Regex(buf),
306            self.span_from(start_pos, start_line, start_col),
307        ))
308    }
309
310    fn lex_symbolic(
311        &mut self,
312        start_pos: usize,
313        start_line: u32,
314        start_col: u32,
315    ) -> CljxResult<(Token, Span)> {
316        self.advance(); // consume second '#'
317        let name = self.read_symbol_chars();
318        match name.as_str() {
319            "Inf" | "-Inf" | "NaN" => Ok((
320                Token::Symbolic(name),
321                self.span_from(start_pos, start_line, start_col),
322            )),
323            _ => {
324                let span = self.span_from(start_pos, start_line, start_col);
325                Err(self.make_error(format!("unknown symbolic value: ##{name}"), span))
326            }
327        }
328    }
329
330    // ── String literal ────────────────────────────────────────────────────
331
332    fn lex_string(
333        &mut self,
334        start_pos: usize,
335        start_line: u32,
336        start_col: u32,
337    ) -> CljxResult<(Token, Span)> {
338        self.advance(); // consume opening '"'
339        let mut buf = String::new();
340        loop {
341            match self.advance() {
342                None => {
343                    let span = self.span_from(start_pos, start_line, start_col);
344                    return Err(self.make_error("unterminated string literal", span));
345                }
346                Some('"') => break,
347                Some('\\') => match self.advance() {
348                    Some('n') => buf.push('\n'),
349                    Some('t') => buf.push('\t'),
350                    Some('r') => buf.push('\r'),
351                    Some('b') => buf.push('\x08'),
352                    Some('f') => buf.push('\x0C'),
353                    Some('\\') => buf.push('\\'),
354                    Some('"') => buf.push('"'),
355                    Some('u') => {
356                        let ch = self.read_unicode_escape(start_pos, start_line, start_col)?;
357                        buf.push(ch);
358                    }
359                    Some(c) => {
360                        let span = self.span_from(start_pos, start_line, start_col);
361                        return Err(self.make_error(format!("unknown string escape: \\{c}"), span));
362                    }
363                    None => {
364                        let span = self.span_from(start_pos, start_line, start_col);
365                        return Err(self.make_error("unterminated string literal", span));
366                    }
367                },
368                Some(c) => buf.push(c),
369            }
370        }
371        Ok((
372            Token::Str(buf),
373            self.span_from(start_pos, start_line, start_col),
374        ))
375    }
376
377    /// Read exactly 4 hex digits after `\u` and return the corresponding char.
378    fn read_unicode_escape(
379        &mut self,
380        start_pos: usize,
381        start_line: u32,
382        start_col: u32,
383    ) -> CljxResult<char> {
384        let mut hex = String::with_capacity(4);
385        for _ in 0..4 {
386            match self.advance() {
387                Some(c) if c.is_ascii_hexdigit() => hex.push(c),
388                Some(c) => {
389                    let span = self.span_from(start_pos, start_line, start_col);
390                    return Err(self.make_error(
391                        format!("invalid \\u escape: expected hex digit, got {c:?}"),
392                        span,
393                    ));
394                }
395                None => {
396                    let span = self.span_from(start_pos, start_line, start_col);
397                    return Err(self.make_error("unterminated \\u escape", span));
398                }
399            }
400        }
401        let code = u32::from_str_radix(&hex, 16).unwrap();
402        char::from_u32(code).ok_or_else(|| {
403            let span = self.span_from(start_pos, start_line, start_col);
404            self.make_error(format!("invalid unicode code point: \\u{hex}"), span)
405        })
406    }
407
408    // ── Character literal `\X` ────────────────────────────────────────────
409
410    fn lex_char_literal(
411        &mut self,
412        start_pos: usize,
413        start_line: u32,
414        start_col: u32,
415    ) -> CljxResult<(Token, Span)> {
416        self.advance(); // consume '\'
417
418        // Peek ahead at all symbol-constituent chars to figure out the name.
419        let rest_start = self.pos;
420        let rest: String = self.source[rest_start..]
421            .chars()
422            .take_while(|&c| c.is_alphanumeric() || c == '-')
423            .collect();
424
425        let ch = match rest.as_str() {
426            "newline" => {
427                self.pos += "newline".len();
428                self.col += "newline".len() as u32;
429                '\n'
430            }
431            "space" => {
432                self.pos += "space".len();
433                self.col += "space".len() as u32;
434                ' '
435            }
436            "tab" => {
437                self.pos += "tab".len();
438                self.col += "tab".len() as u32;
439                '\t'
440            }
441            "backspace" => {
442                self.pos += "backspace".len();
443                self.col += "backspace".len() as u32;
444                '\x08'
445            }
446            "formfeed" => {
447                self.pos += "formfeed".len();
448                self.col += "formfeed".len() as u32;
449                '\x0C'
450            }
451            "return" => {
452                self.pos += "return".len();
453                self.col += "return".len() as u32;
454                '\r'
455            }
456            _ if rest.starts_with('u') && rest.len() >= 5 => {
457                // Try \uXXXX
458                let hex_part = &rest[1..5];
459                if hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
460                    let code = u32::from_str_radix(hex_part, 16).unwrap();
461                    let c = char::from_u32(code).ok_or_else(|| {
462                        let span = self.span_from(start_pos, start_line, start_col);
463                        self.make_error(
464                            format!("invalid unicode code point in char literal: \\u{hex_part}"),
465                            span,
466                        )
467                    })?;
468                    // advance 5 bytes: 'u' + 4 hex digits
469                    self.pos += 5;
470                    self.col += 5;
471                    c
472                } else {
473                    let span = self.span_from(start_pos, start_line, start_col);
474                    return Err(self.make_error(format!("unknown character name: {rest}"), span));
475                }
476            }
477            _ if rest.len() == 1 => {
478                // Single ASCII or first char
479                let c = self.source[rest_start..].chars().next().unwrap();
480                self.pos += c.len_utf8();
481                self.col += c.len_utf8() as u32;
482                c
483            }
484            _ if rest.is_empty() => {
485                // Nothing after backslash — try a single non-alphanumeric char
486                match self.source[rest_start..].chars().next() {
487                    Some(c) => {
488                        self.pos += c.len_utf8();
489                        self.col += c.len_utf8() as u32;
490                        c
491                    }
492                    None => {
493                        let span = self.span_from(start_pos, start_line, start_col);
494                        return Err(self.make_error("unexpected end of file after \\", span));
495                    }
496                }
497            }
498            _ => {
499                let span = self.span_from(start_pos, start_line, start_col);
500                return Err(self.make_error(format!("unknown character name: {rest}"), span));
501            }
502        };
503
504        Ok((
505            Token::Char(ch),
506            self.span_from(start_pos, start_line, start_col),
507        ))
508    }
509
510    // ── Keyword ───────────────────────────────────────────────────────────
511
512    fn lex_keyword(
513        &mut self,
514        start_pos: usize,
515        start_line: u32,
516        start_col: u32,
517    ) -> CljxResult<(Token, Span)> {
518        self.advance(); // consume first ':'
519        if self.peek() == Some(':') {
520            self.advance(); // consume second ':'
521            let name = self.read_symbol_chars();
522            if name.is_empty() {
523                let span = self.span_from(start_pos, start_line, start_col);
524                return Err(self.make_error("empty auto-resolved keyword", span));
525            }
526            Ok((
527                Token::AutoKeyword(name),
528                self.span_from(start_pos, start_line, start_col),
529            ))
530        } else {
531            let name = self.read_symbol_chars();
532            if name.is_empty() {
533                let span = self.span_from(start_pos, start_line, start_col);
534                return Err(self.make_error("empty keyword", span));
535            }
536            Ok((
537                Token::Keyword(name),
538                self.span_from(start_pos, start_line, start_col),
539            ))
540        }
541    }
542
543    // ── Symbol (and nil/true/false) ────────────────────────────────────────
544
545    fn lex_symbol(
546        &mut self,
547        start_pos: usize,
548        start_line: u32,
549        start_col: u32,
550    ) -> CljxResult<(Token, Span)> {
551        let mut name = self.read_symbol_chars();
552
553        // Peek for a version suffix: `@<commit-hash>`.  We only consume the `@`
554        // when it is immediately followed by 7–40 hex characters so that a
555        // standalone `@expr` (deref reader macro) is never affected — deref
556        // always starts a *new* form where `@` is the first character, not a
557        // mid-symbol suffix.
558        if self.peek() == Some('@') {
559            let version_candidate = self.peek_version_hash();
560            if let Some(hash) = version_candidate {
561                self.advance(); // consume '@'
562                for _ in 0..hash.len() {
563                    self.advance();
564                }
565                name.push('@');
566                name.push_str(&hash);
567            }
568        }
569
570        let tok = match name.as_str() {
571            "nil" => Token::Nil,
572            "true" => Token::Bool(true),
573            "false" => Token::Bool(false),
574            _ => Token::Symbol(name),
575        };
576        Ok((tok, self.span_from(start_pos, start_line, start_col)))
577    }
578
579    /// Look ahead past the `@` that `peek()` just returned and collect
580    /// characters as long as they are ASCII hex digits, up to 40.  Returns
581    /// `Some(hash)` if the candidate is 7–40 hex chars followed by a
582    /// non-hex-digit (or EOF), `None` otherwise.  Does **not** advance the
583    /// cursor.
584    fn peek_version_hash(&self) -> Option<String> {
585        // Start one byte past the current `@`.
586        let at_byte = self.pos + 1; // '@' is single-byte ASCII
587        let rest = &self.source[at_byte..];
588        let hash: String = rest
589            .chars()
590            .take(40)
591            .take_while(|c| c.is_ascii_hexdigit())
592            .collect();
593        if hash.len() >= 7 {
594            // Make sure the character after the hash is a delimiter (or EOF).
595            let after = rest[hash.len()..].chars().next();
596            let is_delimited = after.is_none_or(|c| !c.is_ascii_hexdigit());
597            if is_delimited {
598                return Some(hash);
599            }
600        }
601        None
602    }
603
604    // ── Number ────────────────────────────────────────────────────────────
605
606    fn lex_number(
607        &mut self,
608        start_pos: usize,
609        start_line: u32,
610        start_col: u32,
611    ) -> CljxResult<(Token, Span)> {
612        // Optional sign
613        let negative = match self.peek() {
614            Some('-') => {
615                self.advance();
616                true
617            }
618            Some('+') => {
619                self.advance();
620                false
621            }
622            _ => false,
623        };
624        let sign_str = if negative { "-" } else { "" };
625
626        // Integer part (decimal digits)
627        let mut int_part = String::new();
628        while let Some(c) = self.peek() {
629            if c.is_ascii_digit() {
630                int_part.push(c);
631                self.advance();
632            } else {
633                break;
634            }
635        }
636
637        // Hex literal: 0x / 0X  (also -0x…)
638        if int_part == "0" && matches!(self.peek(), Some('x') | Some('X')) {
639            self.advance(); // consume 'x'/'X'
640            let mut hex = String::new();
641            while let Some(c) = self.peek() {
642                if c.is_ascii_hexdigit() {
643                    hex.push(c);
644                    self.advance();
645                } else {
646                    break;
647                }
648            }
649            if hex.is_empty() {
650                let span = self.span_from(start_pos, start_line, start_col);
651                return Err(self.make_error("expected hex digits after 0x", span));
652            }
653            let value = u128::from_str_radix(&hex, 16).unwrap_or(u128::MAX);
654            let span = self.span_from(start_pos, start_line, start_col);
655            return if negative {
656                // -0x8000000000000000 == i64::MIN is valid; anything larger overflows.
657                if value <= (i64::MAX as u128) + 1 {
658                    Ok((Token::Int(0i64.wrapping_sub(value as i64)), span))
659                } else {
660                    // Store as signed decimal string for BigInt.
661                    Ok((Token::BigInt(format!("-{value}")), span))
662                }
663            } else if value <= i64::MAX as u128 {
664                Ok((Token::Int(value as i64), span))
665            } else {
666                Ok((Token::BigInt(value.to_string()), span))
667            };
668        }
669
670        // Radix literal: NNrDIGITS
671        if matches!(self.peek(), Some('r') | Some('R')) {
672            let radix: u32 = int_part.parse().unwrap_or(0);
673            self.advance(); // consume 'r'/'R'
674            let mut digits = String::new();
675            while let Some(c) = self.peek() {
676                if c.is_ascii_alphanumeric() {
677                    digits.push(c);
678                    self.advance();
679                } else {
680                    break;
681                }
682            }
683            let mut value: u128 = 0;
684            for c in digits.chars() {
685                let d = c.to_digit(radix).ok_or_else(|| {
686                    let span = self.span_from(start_pos, start_line, start_col);
687                    self.make_error(format!("invalid digit {c:?} for radix {radix}"), span)
688                })?;
689                value = value.wrapping_mul(radix as u128).wrapping_add(d as u128);
690            }
691            if negative {
692                // Check if it fits as negative i64
693                if value <= (i64::MAX as u128) + 1 {
694                    let signed = -(value as i64);
695                    return Ok((
696                        Token::Int(signed),
697                        self.span_from(start_pos, start_line, start_col),
698                    ));
699                } else {
700                    // Store as decimal string with sign
701                    return Ok((
702                        Token::BigInt(format!("-{value}")),
703                        self.span_from(start_pos, start_line, start_col),
704                    ));
705                }
706            } else if value <= i64::MAX as u128 {
707                return Ok((
708                    Token::Int(value as i64),
709                    self.span_from(start_pos, start_line, start_col),
710                ));
711            } else {
712                return Ok((
713                    Token::BigInt(value.to_string()),
714                    self.span_from(start_pos, start_line, start_col),
715                ));
716            }
717        }
718
719        // BigInt suffix 'N'
720        if self.peek() == Some('N') {
721            self.advance();
722            return Ok((
723                Token::BigInt(format!("{sign_str}{int_part}")),
724                self.span_from(start_pos, start_line, start_col),
725            ));
726        }
727
728        // BigDecimal suffix 'M' on integer literal (e.g. 4M)
729        if self.peek() == Some('M') {
730            self.advance();
731            return Ok((
732                Token::BigDecimal(format!("{sign_str}{int_part}")),
733                self.span_from(start_pos, start_line, start_col),
734            ));
735        }
736
737        // Float: decimal point or exponent
738        if matches!(self.peek(), Some('.') | Some('e') | Some('E')) {
739            let mut raw = format!("{sign_str}{int_part}");
740            if self.peek() == Some('.') {
741                raw.push('.');
742                self.advance();
743                while let Some(c) = self.peek() {
744                    if c.is_ascii_digit() {
745                        raw.push(c);
746                        self.advance();
747                    } else {
748                        break;
749                    }
750                }
751            }
752            if matches!(self.peek(), Some('e') | Some('E')) {
753                raw.push('e');
754                self.advance();
755                if matches!(self.peek(), Some('+') | Some('-')) {
756                    raw.push(self.peek().unwrap());
757                    self.advance();
758                }
759                while let Some(c) = self.peek() {
760                    if c.is_ascii_digit() {
761                        raw.push(c);
762                        self.advance();
763                    } else {
764                        break;
765                    }
766                }
767            }
768            // BigDecimal suffix 'M'
769            if self.peek() == Some('M') {
770                self.advance();
771                return Ok((
772                    Token::BigDecimal(raw),
773                    self.span_from(start_pos, start_line, start_col),
774                ));
775            }
776            let val: f64 = raw.parse().map_err(|_| {
777                let span = self.span_from(start_pos, start_line, start_col);
778                self.make_error(format!("invalid float: {raw}"), span)
779            })?;
780            return Ok((
781                Token::Float(val),
782                self.span_from(start_pos, start_line, start_col),
783            ));
784        }
785
786        // Ratio: INT/DIGITS — only if next char after '/' is a digit
787        if self.peek() == Some('/') && matches!(self.peek_next(), Some(c) if c.is_ascii_digit()) {
788            self.advance(); // consume '/'
789            let mut denom = String::new();
790            while let Some(c) = self.peek() {
791                if c.is_ascii_digit() {
792                    denom.push(c);
793                    self.advance();
794                } else {
795                    break;
796                }
797            }
798            return Ok((
799                Token::Ratio(format!("{sign_str}{int_part}/{denom}")),
800                self.span_from(start_pos, start_line, start_col),
801            ));
802        }
803
804        // Plain integer
805        let full = format!("{sign_str}{int_part}");
806        match full.parse::<i64>() {
807            Ok(n) => Ok((
808                Token::Int(n),
809                self.span_from(start_pos, start_line, start_col),
810            )),
811            Err(_) => {
812                // Overflow: store decimal string
813                Ok((
814                    Token::BigInt(full),
815                    self.span_from(start_pos, start_line, start_col),
816                ))
817            }
818        }
819    }
820
821    // ── Top-level token dispatch ──────────────────────────────────────────
822
823    pub fn next_token(&mut self) -> CljxResult<(Token, Span)> {
824        self.skip_whitespace_and_comments();
825
826        let start_pos = self.pos;
827        let start_line = self.line;
828        let start_col = self.col;
829
830        let ch = match self.peek() {
831            None => {
832                return Ok((Token::Eof, self.span_from(start_pos, start_line, start_col)));
833            }
834            Some(c) => c,
835        };
836
837        match ch {
838            '(' => {
839                self.advance();
840                Ok((
841                    Token::LParen,
842                    self.span_from(start_pos, start_line, start_col),
843                ))
844            }
845            ')' => {
846                self.advance();
847                Ok((
848                    Token::RParen,
849                    self.span_from(start_pos, start_line, start_col),
850                ))
851            }
852            '[' => {
853                self.advance();
854                Ok((
855                    Token::LBracket,
856                    self.span_from(start_pos, start_line, start_col),
857                ))
858            }
859            ']' => {
860                self.advance();
861                Ok((
862                    Token::RBracket,
863                    self.span_from(start_pos, start_line, start_col),
864                ))
865            }
866            '{' => {
867                self.advance();
868                Ok((
869                    Token::LBrace,
870                    self.span_from(start_pos, start_line, start_col),
871                ))
872            }
873            '}' => {
874                self.advance();
875                Ok((
876                    Token::RBrace,
877                    self.span_from(start_pos, start_line, start_col),
878                ))
879            }
880            '\'' => {
881                self.advance();
882                Ok((
883                    Token::Quote,
884                    self.span_from(start_pos, start_line, start_col),
885                ))
886            }
887            '`' => {
888                self.advance();
889                Ok((
890                    Token::SyntaxQuote,
891                    self.span_from(start_pos, start_line, start_col),
892                ))
893            }
894            '@' => {
895                self.advance();
896                Ok((
897                    Token::Deref,
898                    self.span_from(start_pos, start_line, start_col),
899                ))
900            }
901            '^' => {
902                self.advance();
903                Ok((
904                    Token::Meta,
905                    self.span_from(start_pos, start_line, start_col),
906                ))
907            }
908            '~' => self.lex_unquote(start_pos, start_line, start_col),
909            '#' => self.lex_hash(start_pos, start_line, start_col),
910            '"' => self.lex_string(start_pos, start_line, start_col),
911            '\\' => self.lex_char_literal(start_pos, start_line, start_col),
912            ':' => self.lex_keyword(start_pos, start_line, start_col),
913            c if c.is_ascii_digit() => self.lex_number(start_pos, start_line, start_col),
914            '+' | '-' if matches!(self.peek_next(), Some(d) if d.is_ascii_digit()) => {
915                self.lex_number(start_pos, start_line, start_col)
916            }
917            c if is_symbol_start(c) => self.lex_symbol(start_pos, start_line, start_col),
918            // '+' and '-' alone (or before non-digit) are symbols
919            '+' | '-' => self.lex_symbol(start_pos, start_line, start_col),
920            c => {
921                self.advance();
922                let span = self.span_from(start_pos, start_line, start_col);
923                Err(self.make_error(format!("unexpected character: {c:?}"), span))
924            }
925        }
926    }
927}
928
929impl Iterator for Lexer {
930    type Item = CljxResult<(Token, Span)>;
931
932    fn next(&mut self) -> Option<Self::Item> {
933        match self.next_token() {
934            Ok((Token::Eof, _)) => None,
935            result => Some(result),
936        }
937    }
938}
939
940// ─── Tests ────────────────────────────────────────────────────────────────────
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945
946    fn lex_all(src: &str) -> Vec<Token> {
947        Lexer::new(src.to_string(), "<test>".to_string())
948            .map(|r: CljxResult<(Token, Span)>| r.expect("lex error").0)
949            .collect()
950    }
951
952    fn lex_one(src: &str) -> Token {
953        let mut l = Lexer::new(src.to_string(), "<test>".to_string());
954        l.next_token().expect("lex error").0
955    }
956
957    fn lex_err(src: &str) -> String {
958        let mut l = Lexer::new(src.to_string(), "<test>".to_string());
959        loop {
960            match l.next_token() {
961                Err(CljxError::ReadError { message, .. }) => return message,
962                Err(e) => panic!("unexpected error type: {e}"),
963                Ok((Token::Eof, _)) => panic!("expected an error but got Eof"),
964                Ok(_) => {}
965            }
966        }
967    }
968
969    // ── nil / bool ────────────────────────────────────────────────────────
970
971    #[test]
972    fn test_nil() {
973        assert_eq!(lex_one("nil"), Token::Nil);
974    }
975
976    #[test]
977    fn test_bool() {
978        assert_eq!(lex_one("true"), Token::Bool(true));
979        assert_eq!(lex_one("false"), Token::Bool(false));
980    }
981
982    // ── Integers ──────────────────────────────────────────────────────────
983
984    #[test]
985    fn test_int_plain() {
986        assert_eq!(lex_one("42"), Token::Int(42));
987        assert_eq!(lex_one("-42"), Token::Int(-42));
988        assert_eq!(lex_one("+42"), Token::Int(42));
989        assert_eq!(lex_one("0"), Token::Int(0));
990    }
991
992    #[test]
993    fn test_bigint_suffix() {
994        assert_eq!(lex_one("42N"), Token::BigInt("42".to_string()));
995        assert_eq!(lex_one("-42N"), Token::BigInt("-42".to_string()));
996    }
997
998    #[test]
999    fn test_hex_literal() {
1000        assert_eq!(lex_one("0xff"), Token::Int(255));
1001        assert_eq!(lex_one("0xFF"), Token::Int(255));
1002        assert_eq!(lex_one("0x0"), Token::Int(0));
1003        assert_eq!(lex_one("0x7FFFFFFFFFFFFFFF"), Token::Int(i64::MAX));
1004        assert_eq!(lex_one("-0x8000000000000000"), Token::Int(i64::MIN));
1005        assert_eq!(lex_one("-0xff"), Token::Int(-255));
1006        // Overflow → BigInt
1007        match lex_one("0xFFFFFFFFFFFFFFFF") {
1008            Token::BigInt(_) => {}
1009            other => panic!("expected BigInt for 0xFFFF…, got {other:?}"),
1010        }
1011    }
1012
1013    #[test]
1014    fn test_radix() {
1015        assert_eq!(lex_one("2r1010"), Token::Int(10));
1016        assert_eq!(lex_one("8r77"), Token::Int(63));
1017        assert_eq!(lex_one("16rFF"), Token::Int(255));
1018        assert_eq!(lex_one("16rff"), Token::Int(255));
1019        assert_eq!(lex_one("36rZ"), Token::Int(35));
1020    }
1021
1022    #[test]
1023    fn test_radix_overflow() {
1024        // 2^64 fits in u128 but not i64
1025        let tok = lex_one("10r18446744073709551616");
1026        match tok {
1027            Token::BigInt(_) => {}
1028            other => panic!("expected BigInt, got {other:?}"),
1029        }
1030    }
1031
1032    // ── Floats ────────────────────────────────────────────────────────────
1033
1034    #[test]
1035    #[allow(clippy::approx_constant)]
1036    fn test_floats() {
1037        assert_eq!(lex_one("3.14"), Token::Float(3.14));
1038        assert_eq!(lex_one("1e10"), Token::Float(1e10));
1039        assert_eq!(lex_one("1.5e-3"), Token::Float(1.5e-3));
1040        assert_eq!(lex_one("-0.5"), Token::Float(-0.5));
1041    }
1042
1043    #[test]
1044    fn test_bigdecimal() {
1045        assert_eq!(lex_one("3.14M"), Token::BigDecimal("3.14".to_string()));
1046        assert_eq!(lex_one("1e5M"), Token::BigDecimal("1e5".to_string()));
1047    }
1048
1049    // ── Ratio ────────────────────────────────────────────────────────────
1050
1051    #[test]
1052    fn test_ratio() {
1053        assert_eq!(lex_one("3/4"), Token::Ratio("3/4".to_string()));
1054        assert_eq!(lex_one("-1/2"), Token::Ratio("-1/2".to_string()));
1055    }
1056
1057    #[test]
1058    fn test_ratio_vs_symbol() {
1059        // "3/foo" should lex as Int(3) then Symbol("/foo") — not a ratio
1060        let toks = lex_all("3/foo");
1061        assert_eq!(toks[0], Token::Int(3));
1062        assert_eq!(toks[1], Token::Symbol("/foo".to_string()));
1063    }
1064
1065    // ── Char literals ────────────────────────────────────────────────────
1066
1067    #[test]
1068    fn test_char_simple() {
1069        assert_eq!(lex_one("\\a"), Token::Char('a'));
1070    }
1071
1072    #[test]
1073    fn test_char_named() {
1074        assert_eq!(lex_one("\\newline"), Token::Char('\n'));
1075        assert_eq!(lex_one("\\space"), Token::Char(' '));
1076        assert_eq!(lex_one("\\tab"), Token::Char('\t'));
1077        assert_eq!(lex_one("\\backspace"), Token::Char('\x08'));
1078        assert_eq!(lex_one("\\formfeed"), Token::Char('\x0C'));
1079        assert_eq!(lex_one("\\return"), Token::Char('\r'));
1080    }
1081
1082    #[test]
1083    fn test_char_unicode() {
1084        assert_eq!(lex_one("\\u0041"), Token::Char('A'));
1085        assert_eq!(lex_one("\\u00e9"), Token::Char('é'));
1086    }
1087
1088    // ── Strings ──────────────────────────────────────────────────────────
1089
1090    #[test]
1091    fn test_string_basic() {
1092        assert_eq!(lex_one("\"hello\""), Token::Str("hello".to_string()));
1093    }
1094
1095    #[test]
1096    fn test_string_escapes() {
1097        assert_eq!(
1098            lex_one(r#""\n\t\r\b\f\\\"" "#),
1099            Token::Str("\n\t\r\x08\x0C\\\"".to_string())
1100        );
1101    }
1102
1103    #[test]
1104    fn test_string_unicode_escape() {
1105        assert_eq!(lex_one("\"\\u0041\""), Token::Str("A".to_string()));
1106    }
1107
1108    // ── Symbols ──────────────────────────────────────────────────────────
1109
1110    #[test]
1111    fn test_symbols() {
1112        assert_eq!(lex_one("foo"), Token::Symbol("foo".to_string()));
1113        assert_eq!(lex_one("ns/name"), Token::Symbol("ns/name".to_string()));
1114        assert_eq!(lex_one("/"), Token::Symbol("/".to_string()));
1115        assert_eq!(lex_one(".."), Token::Symbol("..".to_string()));
1116        assert_eq!(lex_one(".method"), Token::Symbol(".method".to_string()));
1117        assert_eq!(lex_one("+"), Token::Symbol("+".to_string()));
1118        assert_eq!(lex_one("-"), Token::Symbol("-".to_string()));
1119        assert_eq!(lex_one("+foo"), Token::Symbol("+foo".to_string()));
1120    }
1121
1122    #[test]
1123    fn test_auto_gensym_symbol() {
1124        // `x#` is a single symbol token (trailing `#` is auto-gensym syntax,
1125        // not a `#`-dispatch macro — `#` is non-terminating mid-token).
1126        assert_eq!(lex_one("x#"), Token::Symbol("x#".to_string()));
1127        let toks = lex_all("`(let [x# 1] x#)");
1128        assert_eq!(
1129            toks,
1130            vec![
1131                Token::SyntaxQuote,
1132                Token::LParen,
1133                Token::Symbol("let".to_string()),
1134                Token::LBracket,
1135                Token::Symbol("x#".to_string()),
1136                Token::Int(1),
1137                Token::RBracket,
1138                Token::Symbol("x#".to_string()),
1139                Token::RParen,
1140            ]
1141        );
1142    }
1143
1144    // ── Keywords ─────────────────────────────────────────────────────────
1145
1146    #[test]
1147    fn test_keyword() {
1148        assert_eq!(lex_one(":foo"), Token::Keyword("foo".to_string()));
1149        assert_eq!(lex_one(":ns/name"), Token::Keyword("ns/name".to_string()));
1150    }
1151
1152    #[test]
1153    fn test_keyword_with_embedded_colon() {
1154        // An embedded `:` is a literal name character, not a sub-token
1155        // boundary — `:xlink:href` is one keyword, not two.
1156        assert_eq!(
1157            lex_one(":xlink:href"),
1158            Token::Keyword("xlink:href".to_string())
1159        );
1160        let toks = lex_all("[:xlink:href]");
1161        assert_eq!(
1162            toks,
1163            vec![
1164                Token::LBracket,
1165                Token::Keyword("xlink:href".to_string()),
1166                Token::RBracket,
1167            ]
1168        );
1169    }
1170
1171    #[test]
1172    fn test_auto_keyword() {
1173        assert_eq!(lex_one("::foo"), Token::AutoKeyword("foo".to_string()));
1174        assert_eq!(
1175            lex_one("::ns/alias"),
1176            Token::AutoKeyword("ns/alias".to_string())
1177        );
1178    }
1179
1180    // ── Delimiters ───────────────────────────────────────────────────────
1181
1182    #[test]
1183    fn test_delimiters() {
1184        assert_eq!(
1185            lex_all("([{}])"),
1186            vec![
1187                Token::LParen,
1188                Token::LBracket,
1189                Token::LBrace,
1190                Token::RBrace,
1191                Token::RBracket,
1192                Token::RParen,
1193            ]
1194        );
1195    }
1196
1197    // ── Reader macros ────────────────────────────────────────────────────
1198
1199    #[test]
1200    fn test_reader_macros() {
1201        assert_eq!(lex_one("'x"), Token::Quote);
1202        assert_eq!(lex_one("`x"), Token::SyntaxQuote);
1203        assert_eq!(lex_one("~x"), Token::Unquote);
1204        assert_eq!(lex_one("~@x"), Token::UnquoteSplice);
1205        assert_eq!(lex_one("@x"), Token::Deref);
1206        assert_eq!(lex_one("^x"), Token::Meta);
1207    }
1208
1209    // ── `#` dispatch ─────────────────────────────────────────────────────
1210
1211    #[test]
1212    fn test_hash_dispatch() {
1213        assert_eq!(lex_one("#("), Token::HashFn);
1214        assert_eq!(lex_one("#{"), Token::HashSet);
1215        assert_eq!(lex_one("#'"), Token::HashVar);
1216        assert_eq!(lex_one("#_"), Token::HashDiscard);
1217        assert_eq!(lex_one("#?"), Token::ReaderCond);
1218        assert_eq!(lex_one("#?@"), Token::ReaderCondSplice);
1219    }
1220
1221    #[test]
1222    fn test_regex() {
1223        assert_eq!(lex_one("#\"[a-z]+\""), Token::Regex("[a-z]+".to_string()));
1224    }
1225
1226    #[test]
1227    fn test_symbolic() {
1228        assert_eq!(lex_one("##Inf"), Token::Symbolic("Inf".to_string()));
1229        assert_eq!(lex_one("##-Inf"), Token::Symbolic("-Inf".to_string()));
1230        assert_eq!(lex_one("##NaN"), Token::Symbolic("NaN".to_string()));
1231    }
1232
1233    #[test]
1234    fn test_tagged_literal() {
1235        assert_eq!(lex_one("#mytag"), Token::TaggedLiteral("mytag".to_string()));
1236    }
1237
1238    // ── Multi-token ──────────────────────────────────────────────────────
1239
1240    #[test]
1241    fn test_multi_token() {
1242        let toks = lex_all("(+ 1 2)");
1243        assert_eq!(
1244            toks,
1245            vec![
1246                Token::LParen,
1247                Token::Symbol("+".to_string()),
1248                Token::Int(1),
1249                Token::Int(2),
1250                Token::RParen,
1251            ]
1252        );
1253    }
1254
1255    // ── Whitespace / comments ────────────────────────────────────────────
1256
1257    #[test]
1258    fn test_comma_skipped() {
1259        assert_eq!(lex_all("{,,,}"), vec![Token::LBrace, Token::RBrace]);
1260    }
1261
1262    #[test]
1263    fn test_comment_skipped() {
1264        assert_eq!(lex_all("; this is a comment\n42"), vec![Token::Int(42)]);
1265    }
1266
1267    #[test]
1268    fn test_shebang_skipped() {
1269        assert_eq!(lex_all("#!/usr/bin/env cljx\n42"), vec![Token::Int(42)]);
1270    }
1271
1272    // ── Span tracking ────────────────────────────────────────────────────
1273
1274    #[test]
1275    fn test_span_col() {
1276        let mut l = Lexer::new("  foo".to_string(), "<test>".to_string());
1277        let (_tok, span) = l.next_token().unwrap();
1278        assert_eq!(span.start, 2);
1279        assert_eq!(span.col, 3);
1280    }
1281
1282    #[test]
1283    fn test_span_newline() {
1284        let mut l = Lexer::new("a\nb".to_string(), "<test>".to_string());
1285        l.next_token().unwrap(); // consume 'a'
1286        let (_tok, span) = l.next_token().unwrap(); // 'b'
1287        assert_eq!(span.line, 2);
1288        assert_eq!(span.col, 1);
1289    }
1290
1291    // ── Errors ───────────────────────────────────────────────────────────
1292
1293    #[test]
1294    fn test_error_unterminated_string() {
1295        let msg = lex_err("\"unterminated");
1296        assert!(msg.contains("unterminated string"));
1297    }
1298
1299    #[test]
1300    fn test_error_bad_hash_dispatch() {
1301        // '#1' is invalid: '1' is not a symbol start and not a special dispatch char
1302        let msg = lex_err("#1");
1303        assert!(msg.contains("unknown # dispatch"));
1304    }
1305
1306    #[test]
1307    fn test_error_bad_unicode_escape_in_string() {
1308        let msg = lex_err("\"\\uGHIJ\"");
1309        assert!(msg.contains("invalid") || msg.contains("hex"));
1310    }
1311
1312    #[test]
1313    fn test_error_unknown_char_name() {
1314        let msg = lex_err("\\bogus");
1315        assert!(msg.contains("unknown character name"));
1316    }
1317
1318    #[test]
1319    fn test_error_unknown_symbolic() {
1320        let msg = lex_err("##Bogus");
1321        assert!(msg.contains("unknown symbolic value"));
1322    }
1323
1324    #[test]
1325    fn test_error_bad_string_escape() {
1326        let msg = lex_err("\"\\q\"");
1327        assert!(msg.contains("unknown string escape"));
1328    }
1329}