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