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 {
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
49fn is_symbol_start(ch: char) -> bool {
53 is_symbol_char(ch) && !ch.is_ascii_digit() && ch != '#'
54}
55
56pub struct Lexer {
59 source: Arc<String>,
60 file: Arc<String>,
61 pos: usize, line: u32, col: u32, }
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 pub fn source(&self) -> &Arc<String> {
80 &self.source
81 }
82
83 pub fn file(&self) -> &Arc<String> {
84 &self.file
85 }
86
87 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(); 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 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 fn skip_whitespace_and_comments(&mut self) {
147 loop {
148 match self.peek() {
149 Some('#') if self.pos == 0 => {
151 if self.peek_next() == Some('!') {
152 while let Some(ch) = self.advance() {
154 if ch == '\n' {
155 break;
156 }
157 }
158 } else {
159 break; }
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 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(); 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 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(); 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(); 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(); 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 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(); 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 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(); 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 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 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(); 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 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 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 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 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 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(); if self.peek() == Some(':') {
514 self.advance(); 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 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 if self.peek() == Some('@') {
553 let version_candidate = self.peek_version_hash();
554 if let Some(hash) = version_candidate {
555 self.advance(); 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 fn peek_version_hash(&self) -> Option<String> {
579 let at_byte = self.pos + 1; 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 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 fn lex_number(
601 &mut self,
602 start_pos: usize,
603 start_line: u32,
604 start_col: u32,
605 ) -> CljxResult<(Token, Span)> {
606 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 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 if int_part == "0" && matches!(self.peek(), Some('x') | Some('X')) {
633 self.advance(); 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 if value <= (i64::MAX as u128) + 1 {
652 Ok((Token::Int(0i64.wrapping_sub(value as i64)), span))
653 } else {
654 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 if matches!(self.peek(), Some('r') | Some('R')) {
666 let radix: u32 = int_part.parse().unwrap_or(0);
667 self.advance(); 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 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 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 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 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 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 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 if self.peek() == Some('/') && matches!(self.peek_next(), Some(c) if c.is_ascii_digit()) {
782 self.advance(); 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 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 Ok((
808 Token::BigInt(full),
809 self.span_from(start_pos, start_line, start_col),
810 ))
811 }
812 }
813 }
814
815 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 '+' | '-' => 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#[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 #[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 #[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 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 let tok = lex_one("10r18446744073709551616");
1020 match tok {
1021 Token::BigInt(_) => {}
1022 other => panic!("expected BigInt, got {other:?}"),
1023 }
1024 }
1025
1026 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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(); let (_tok, span) = l.next_token().unwrap(); assert_eq!(span.line, 2);
1263 assert_eq!(span.col, 1);
1264 }
1265
1266 #[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 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}