1#![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
14fn 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
54fn is_symbol_start(ch: char) -> bool {
59 is_symbol_char(ch) && !ch.is_ascii_digit() && ch != '#' && ch != ':'
60}
61
62pub struct Lexer {
65 source: Arc<String>,
66 file: Arc<String>,
67 pos: usize, line: u32, col: u32, }
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 pub fn source(&self) -> &Arc<String> {
86 &self.source
87 }
88
89 pub fn file(&self) -> &Arc<String> {
90 &self.file
91 }
92
93 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(); 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 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 fn skip_whitespace_and_comments(&mut self) {
153 loop {
154 match self.peek() {
155 Some('#') if self.pos == 0 => {
157 if self.peek_next() == Some('!') {
158 while let Some(ch) = self.advance() {
160 if ch == '\n' {
161 break;
162 }
163 }
164 } else {
165 break; }
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 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(); 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 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(); 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(); 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(); 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 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(); 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 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(); 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 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 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(); 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 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 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 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 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 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(); if self.peek() == Some(':') {
520 self.advance(); 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 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 if self.peek() == Some('@') {
559 let version_candidate = self.peek_version_hash();
560 if let Some(hash) = version_candidate {
561 self.advance(); 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 fn peek_version_hash(&self) -> Option<String> {
585 let at_byte = self.pos + 1; 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 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 fn lex_number(
607 &mut self,
608 start_pos: usize,
609 start_line: u32,
610 start_col: u32,
611 ) -> CljxResult<(Token, Span)> {
612 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 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 if int_part == "0" && matches!(self.peek(), Some('x') | Some('X')) {
639 self.advance(); 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 if value <= (i64::MAX as u128) + 1 {
658 Ok((Token::Int(0i64.wrapping_sub(value as i64)), span))
659 } else {
660 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 if matches!(self.peek(), Some('r') | Some('R')) {
672 let radix: u32 = int_part.parse().unwrap_or(0);
673 self.advance(); 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 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 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 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 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 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 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 if self.peek() == Some('/') && matches!(self.peek_next(), Some(c) if c.is_ascii_digit()) {
788 self.advance(); 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 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 Ok((
814 Token::BigInt(full),
815 self.span_from(start_pos, start_line, start_col),
816 ))
817 }
818 }
819 }
820
821 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 '+' | '-' => 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#[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 #[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 #[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 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 let tok = lex_one("10r18446744073709551616");
1026 match tok {
1027 Token::BigInt(_) => {}
1028 other => panic!("expected BigInt, got {other:?}"),
1029 }
1030 }
1031
1032 #[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 #[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 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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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(); let (_tok, span) = l.next_token().unwrap(); assert_eq!(span.line, 2);
1288 assert_eq!(span.col, 1);
1289 }
1290
1291 #[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 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}