1use std::fmt;
23
24pub use tatara_lisp::Span;
39
40#[derive(Clone, Debug, PartialEq)]
41pub enum TokenKind {
42 Int(i64),
44 Float(f64),
45 Str(String),
46 InterpolatedStr {
54 parts: Vec<String>,
58 exprs: Vec<String>,
59 },
60 Sym(String),
62 True,
63 False,
64 Nil,
65
66 Ident(String),
69
70 LParen,
72 RParen,
73 LBracket,
74 RBracket,
75 LBrace,
76 RBrace,
77 Comma,
78 Dot,
79 Colon,
82 PathSep,
86 Label(String),
90 Rocket,
92 Pipe,
94
95 Op(String),
97
98 Doc {
102 module: bool,
103 text: String,
104 },
105
106 Comment(String),
108 Newline,
109
110 Eof,
111}
112
113impl fmt::Display for TokenKind {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::Int(v) => write!(f, "the number `{v}`"),
125 Self::Float(v) => write!(f, "the number `{v}`"),
126 Self::Str(_) | Self::InterpolatedStr { .. } => f.write_str("a string"),
127 Self::Sym(s) => write!(f, "the symbol `:{s}`"),
128 Self::True => f.write_str("`true`"),
129 Self::False => f.write_str("`false`"),
130 Self::Nil => f.write_str("`nil`"),
131 Self::Ident(n) => write!(f, "`{n}`"),
132 Self::LParen => f.write_str("`(`"),
133 Self::RParen => f.write_str("`)`"),
134 Self::LBracket => f.write_str("`[`"),
135 Self::RBracket => f.write_str("`]`"),
136 Self::LBrace => f.write_str("`{`"),
137 Self::RBrace => f.write_str("`}`"),
138 Self::Comma => f.write_str("`,`"),
139 Self::Dot => f.write_str("`.`"),
140 Self::Colon => f.write_str("`:`"),
141 Self::PathSep => f.write_str("`::`"),
142 Self::Label(n) => write!(f, "the label `{n}:`"),
143 Self::Rocket => f.write_str("`=>`"),
144 Self::Pipe => f.write_str("`|>`"),
145 Self::Op(o) => write!(f, "`{o}`"),
146 Self::Doc { module: false, .. } => f.write_str("`@doc`"),
147 Self::Doc { module: true, .. } => f.write_str("`@moduledoc`"),
148 Self::Comment(_) => f.write_str("a comment"),
149 Self::Newline => f.write_str("the end of the line"),
150 Self::Eof => f.write_str("the end of the file"),
151 }
152 }
153}
154
155#[derive(Clone, Debug, PartialEq)]
156pub struct Token {
157 pub kind: TokenKind,
158 pub span: Span,
159}
160
161impl Token {
162 pub fn is_trivia(&self) -> bool {
164 matches!(self.kind, TokenKind::Comment(_) | TokenKind::Newline)
165 }
166}
167
168#[derive(Clone, Debug, PartialEq)]
169pub struct LexError {
170 pub message: String,
171 pub span: Span,
172}
173
174impl fmt::Display for LexError {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(
177 f,
178 "{} at {}..{}",
179 self.message, self.span.start, self.span.end
180 )
181 }
182}
183
184impl std::error::Error for LexError {}
185
186const OP_CHARS: &str = "+-*/=<>!%&|";
188
189pub fn lex(src: &str) -> Result<Vec<Token>, LexError> {
191 Lexer::new(src).run()
192}
193
194struct Lexer<'a> {
195 src: &'a str,
196 bytes: &'a [u8],
197 pos: usize,
198 out: Vec<Token>,
199}
200
201impl<'a> Lexer<'a> {
202 fn new(src: &'a str) -> Self {
203 Self {
204 src,
205 bytes: src.as_bytes(),
206 pos: if src.starts_with('\u{feff}') {
207 '\u{feff}'.len_utf8()
208 } else {
209 0
210 },
211 out: Vec::new(),
212 }
213 }
214
215 fn decode_char(&self) -> Option<(char, usize)> {
224 let rest = self.src.get(self.pos..)?;
225 let ch = rest.chars().next()?;
226 Some((ch, ch.len_utf8()))
227 }
228
229 fn peek(&self) -> Option<u8> {
230 self.bytes.get(self.pos).copied()
231 }
232
233 fn peek_at(&self, n: usize) -> Option<u8> {
234 self.bytes.get(self.pos + n).copied()
235 }
236
237 fn push(&mut self, kind: TokenKind, start: usize) {
238 self.out.push(Token {
239 kind,
240 span: Span::new(start, self.pos),
241 });
242 }
243
244 fn err(&self, message: impl Into<String>, start: usize) -> LexError {
245 LexError {
246 message: message.into(),
247 span: Span::new(start, self.pos.max(start + 1)),
248 }
249 }
250
251 fn run(mut self) -> Result<Vec<Token>, LexError> {
252 while let Some(c) = self.peek() {
253 let start = self.pos;
254 match c {
255 b'\n' => {
256 self.pos += 1;
257 self.push(TokenKind::Newline, start);
258 }
259 b' ' | b'\t' | b'\r' => {
263 self.pos += 1;
264 }
265 b'#' => {
266 while let Some(c) = self.peek() {
267 if c == b'\n' {
268 break;
269 }
270 self.pos += 1;
271 }
272 let text = self.src[start..self.pos].to_string();
273 self.push(TokenKind::Comment(text), start);
274 }
275 b'"' => self.lex_string(start)?,
276 b'@' => self.lex_doc(start)?,
277 b'0'..=b'9' => self.lex_number(start)?,
278 b':' => self.lex_colon(start),
279 b'(' => self.one(TokenKind::LParen, start),
280 b')' => self.one(TokenKind::RParen, start),
281 b'[' => self.one(TokenKind::LBracket, start),
282 b']' => self.one(TokenKind::RBracket, start),
283 b'{' => self.one(TokenKind::LBrace, start),
284 b'}' => self.one(TokenKind::RBrace, start),
285 b',' => self.one(TokenKind::Comma, start),
286 b'.' => self.one(TokenKind::Dot, start),
287 c if c >= 0x80 => {
292 let Some((ch, width)) = self.decode_char() else {
293 self.pos += 1;
294 return Err(self.err("invalid UTF-8 in source", start));
295 };
296 match crate::kigou::classify(ch) {
297 crate::kigou::Class::Operator(op) => {
298 self.pos += width;
299 self.push(TokenKind::Op(op.to_owned()), start);
300 }
301 crate::kigou::Class::Word => self.lex_ident(start),
302 crate::kigou::Class::Reject => {
303 self.pos += width;
304 return Err(self.err(
305 format!(
306 "the character {ch:?} (U+{:04X}) is not legal in blue source \
307 outside a string — invisible and control characters are \
308 rejected because source that looks correct and is not costs \
309 far more than a clear error here",
310 ch as u32
311 ),
312 start,
313 ));
314 }
315 }
316 }
317 c if is_ident_start(c) => self.lex_ident(start),
318 c if OP_CHARS.as_bytes().contains(&c) => self.lex_op(start),
319 b';' => {
323 self.pos += 1;
324 return Err(self.err(
325 "unexpected `;`: blue ends a statement at the end of the line, \
326 so put the next statement on a new line",
327 start,
328 ));
329 }
330 _ => {
331 self.pos += 1;
332 return Err(self.err(format!("unexpected character `{}`", c as char), start));
333 }
334 }
335 }
336 let end = self.pos;
337 self.out.push(Token {
338 kind: TokenKind::Eof,
339 span: Span::new(end, end),
340 });
341 Ok(self.out)
342 }
343
344 fn one(&mut self, kind: TokenKind, start: usize) {
345 self.pos += 1;
346 self.push(kind, start);
347 }
348
349 fn lex_string(&mut self, start: usize) -> Result<(), LexError> {
350 self.pos += 1; let mut buf = String::new();
352 let mut parts: Vec<String> = Vec::new();
355 let mut exprs: Vec<String> = Vec::new();
356 loop {
357 if self.peek() == Some(b'#') && self.src.as_bytes().get(self.pos + 1) == Some(&b'{') {
360 self.pos += 2;
361 let expr_start = self.pos;
362 let Some(after) = skip_interpolation(self.bytes, self.pos) else {
363 return Err(self.err("unterminated `#{` interpolation", start));
364 };
365 self.pos = after - 1;
366 exprs.push(self.src[expr_start..self.pos].to_string());
367 self.pos += 1; parts.push(std::mem::take(&mut buf));
369 continue;
370 }
371 match self.peek() {
372 None => return Err(self.err("unterminated string literal", start)),
373 Some(b'"') => {
374 self.pos += 1;
375 break;
376 }
377 Some(b'\\') => {
378 self.pos += 1;
379 let esc = self
380 .peek()
381 .ok_or_else(|| self.err("unterminated escape", start))?;
382 let ch = match esc {
383 b'n' => '\n',
384 b't' => '\t',
385 b'r' => '\r',
386 b'\\' => '\\',
387 b'"' => '"',
388 b'0' => '\0',
389 b'u' => {
396 self.pos += 1; if self.peek() != Some(b'{') {
398 return Err(self.err("expected `{` after \\u", start));
399 }
400 self.pos += 1; let hex_start = self.pos;
402 while self.peek().is_some_and(|c| c != b'}') {
403 self.pos += 1;
404 }
405 if self.peek() != Some(b'}') {
406 return Err(self.err("unterminated \\u{...} escape", start));
407 }
408 let hex = &self.src[hex_start..self.pos];
409 let code = u32::from_str_radix(hex, 16).map_err(|_| {
410 self.err(format!("`{hex}` is not hexadecimal"), start)
411 })?;
412 let ch = char::from_u32(code).ok_or_else(|| {
417 self.err(format!("`{hex}` is not a Unicode scalar value"), start)
418 })?;
419 buf.push(ch);
420 self.pos += 1; continue;
422 }
423 other => {
424 return Err(
425 self.err(format!("unknown escape \\{}", other as char), start)
426 )
427 }
428 };
429 buf.push(ch);
430 self.pos += 1;
431 }
432 Some(_) => {
433 let ch = self.src[self.pos..]
434 .chars()
435 .next()
436 .expect("peek said there is a byte");
437 buf.push(ch);
438 self.pos += ch.len_utf8();
439 }
440 }
441 }
442 if exprs.is_empty() {
443 self.push(TokenKind::Str(buf), start);
444 } else {
445 parts.push(buf);
446 self.push(TokenKind::InterpolatedStr { parts, exprs }, start);
447 }
448 Ok(())
449 }
450
451 fn lex_doc(&mut self, start: usize) -> Result<(), LexError> {
452 self.pos += 1;
453 let word_start = self.pos;
454 while self.peek().is_some_and(is_ident_continue) {
455 self.pos += 1;
456 }
457 let module = match &self.src[word_start..self.pos] {
458 "doc" => false,
459 "moduledoc" => true,
460 other => {
461 return Err(self.err(
462 format!("unexpected `@{other}`: blue's attributes are `@doc` and `@moduledoc`"),
463 start,
464 ))
465 }
466 };
467 let word = if module { "@moduledoc" } else { "@doc" };
468 while matches!(self.peek(), Some(b' ' | b'\t')) {
469 self.pos += 1;
470 }
471 let opens = self.src[self.pos..].starts_with("\"\"\"");
472 let rest_of_line = self.src[self.pos..]
473 .split('\n')
474 .next()
475 .unwrap_or("")
476 .get(3..)
477 .unwrap_or("x");
478 if !opens || !rest_of_line.trim().is_empty() {
479 return Err(self.err(
480 format!(
481 "`{word}` takes a heredoc: `{word} \"\"\"` alone on its line, the text, \
482 then `\"\"\"` alone on a line"
483 ),
484 start,
485 ));
486 }
487 self.pos += 3 + rest_of_line.len();
488 let body_start = (self.pos + 1).min(self.src.len());
489 let mut lines: Vec<&str> = Vec::new();
490 let mut at = body_start;
491 let closer = loop {
492 if at >= self.src.len() {
493 self.pos = self.src.len();
494 return Err(self.err(
495 format!("unterminated `{word}`: no `\"\"\"` line closes it"),
496 start,
497 ));
498 }
499 let end = self.src[at..].find('\n').map_or(self.src.len(), |i| at + i);
500 let line = &self.src[at..end];
501 if line.trim() == "\"\"\"" {
502 break (line, end);
503 }
504 lines.push(line);
505 at = (end + 1).min(self.src.len() + 1);
506 };
507 let indent = &closer.0[..closer.0.len() - closer.0.trim_start().len()];
508 let mut kept: Vec<&str> = Vec::with_capacity(lines.len());
509 for line in lines {
510 let line = line.trim_end();
511 if line.is_empty() {
512 kept.push("");
513 continue;
514 }
515 match line.strip_prefix(indent) {
516 Some(l) => kept.push(l),
517 None => {
518 return Err(self.err(
519 format!(
520 "a line of this `{word}` is indented less than its closing `\"\"\"`"
521 ),
522 start,
523 ))
524 }
525 }
526 }
527 while kept.first() == Some(&"") {
528 kept.remove(0);
529 }
530 while kept.last() == Some(&"") {
531 kept.pop();
532 }
533 let text = kept.join("\n");
534 if text.contains("\"\"\"") {
535 return Err(self.err(format!("a `{word}` text cannot hold `\"\"\"`"), start));
536 }
537 let close_at = closer.1 - closer.0.trim_start().len() + 3;
538 self.pos = close_at;
539 self.push(TokenKind::Doc { module, text }, start);
540 Ok(())
541 }
542
543 fn lex_number(&mut self, start: usize) -> Result<(), LexError> {
544 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
545 self.pos += 1;
546 }
547 let mut is_float =
551 self.peek() == Some(b'.') && matches!(self.peek_at(1), Some(b'0'..=b'9'));
552 if is_float {
553 self.pos += 1;
554 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
555 self.pos += 1;
556 }
557 }
558 let exponent_digits_at = match (self.peek(), self.peek_at(1)) {
562 (Some(b'e' | b'E'), Some(b'0'..=b'9')) => Some(1),
563 (Some(b'e' | b'E'), Some(b'+' | b'-'))
564 if matches!(self.peek_at(2), Some(b'0'..=b'9')) =>
565 {
566 Some(2)
567 }
568 _ => None,
569 };
570 if let Some(skip) = exponent_digits_at {
571 is_float = true;
572 self.pos += skip;
573 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
574 self.pos += 1;
575 }
576 }
577 let text: String = self.src[start..self.pos]
578 .chars()
579 .filter(|c| *c != '_')
580 .collect();
581 if is_float {
582 let v: f64 = text
583 .parse()
584 .map_err(|_| self.err(format!("invalid float literal {text:?}"), start))?;
585 self.push(TokenKind::Float(v), start);
586 } else {
587 let v: i64 = text
588 .parse()
589 .map_err(|_| self.err(format!("integer literal out of range: {text:?}"), start))?;
590 self.push(TokenKind::Int(v), start);
591 }
592 Ok(())
593 }
594
595 fn lex_colon(&mut self, start: usize) {
596 if self.peek_at(1) == Some(b':') {
600 self.pos += 2;
601 self.push(TokenKind::PathSep, start);
602 } else if matches!(self.peek_at(1), Some(c) if is_ident_start(c)) {
603 self.pos += 1;
604 let s = self.pos;
605 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
606 self.pos += 1;
607 }
608 if matches!(self.peek(), Some(b'?'))
613 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
614 {
615 self.pos += 1;
616 }
617 let name = self.src[s..self.pos].to_string();
618 self.push(TokenKind::Sym(name), start);
619 } else {
620 self.one(TokenKind::Colon, start);
621 }
622 }
623
624 fn lex_ident(&mut self, start: usize) {
625 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
626 self.pos += 1;
627 }
628
629 if matches!(self.peek(), Some(b'?'))
643 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
644 {
645 self.pos += 1;
646 }
647 let name = self.src[start..self.pos].to_string();
648
649 if self.peek() == Some(b':') && self.peek_at(1) != Some(b':') {
653 self.pos += 1;
654 self.push(TokenKind::Label(name), start);
655 return;
656 }
657
658 let kind = match name.as_str() {
659 "true" => TokenKind::True,
660 "false" => TokenKind::False,
661 "nil" => TokenKind::Nil,
662 _ => TokenKind::Ident(name),
663 };
664 self.push(kind, start);
665 }
666
667 fn lex_op(&mut self, start: usize) {
668 while matches!(self.peek(), Some(c) if OP_CHARS.as_bytes().contains(&c)) {
669 self.pos += 1;
670 }
671 let text = self.src[start..self.pos].to_string();
672 let kind = match text.as_str() {
673 "=>" => TokenKind::Rocket,
674 "|>" => TokenKind::Pipe,
675 _ => TokenKind::Op(text),
676 };
677 self.push(kind, start);
678 }
679}
680
681fn skip_interpolation(bytes: &[u8], mut i: usize) -> Option<usize> {
701 let mut depth = 1usize;
702 loop {
703 match *bytes.get(i)? {
704 b'{' => depth += 1,
705 b'}' => {
706 depth -= 1;
707 if depth == 0 {
708 return Some(i + 1);
709 }
710 }
711 b'"' => {
712 i = skip_code_string(bytes, i)?;
713 continue;
714 }
715 _ => {}
716 }
717 i += 1;
718 }
719}
720
721fn skip_code_string(bytes: &[u8], mut i: usize) -> Option<usize> {
722 i += 1;
723 loop {
724 match *bytes.get(i)? {
725 b'"' => return Some(i + 1),
726 b'\\' => i += 2,
727 b'#' if bytes.get(i + 1) == Some(&b'{') => i = skip_interpolation(bytes, i + 2)?,
728 _ => i += 1,
729 }
730 }
731}
732
733fn is_ident_start(c: u8) -> bool {
734 c.is_ascii_alphabetic() || c == b'_'
735}
736
737fn is_ident_continue(c: u8) -> bool {
738 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 fn kinds(src: &str) -> Vec<TokenKind> {
746 lex(src)
747 .expect("lex")
748 .into_iter()
749 .filter(|t| !t.is_trivia() && t.kind != TokenKind::Eof)
750 .map(|t| t.kind)
751 .collect()
752 }
753
754 #[test]
755 fn a_brace_inside_a_string_inside_interpolated_code_is_text() {
756 for (src, code) in [
757 ("\"a#{f(\"}\")}b\"", "f(\"}\")"),
758 ("\"a#{\"{\" |> \"}\"}b\"", "\"{\" |> \"}\""),
759 ("\"#{ {a: \"}\"} }\"", " {a: \"}\"} "),
760 ("\"#{f(\"x\\\"}\")}\"", "f(\"x\\\"}\")"),
761 ("\"#{\"in#{\"}\"}ner\"}\"", "\"in#{\"}\"}ner\""),
762 ] {
763 match kinds(src).as_slice() {
764 [TokenKind::InterpolatedStr { exprs, .. }] => assert_eq!(exprs, &[code], "{src}"),
765 other => panic!("{src}: {other:?}"),
766 }
767 }
768 assert!(lex("\"#{\"}\"").is_err());
769 }
770
771 #[test]
772 fn a_leading_byte_order_mark_is_stepped_over_and_one_inside_the_text_is_refused() {
773 assert_eq!(kinds("\u{feff}x"), kinds("x"));
774 let at = lex("\u{feff}x").expect("lex")[0].span;
775 assert_eq!(at.start, 3, "spans still index the source bytes");
776 assert!(lex("x\n\u{feff}y").is_err());
777 assert!(lex("\"a\u{feff}b\"").is_ok());
778 }
779
780 #[test]
781 fn lexes_integers_and_floats() {
782 assert_eq!(
783 kinds("1 2.5 1_000"),
784 vec![
785 TokenKind::Int(1),
786 TokenKind::Float(2.5),
787 TokenKind::Int(1000),
788 ]
789 );
790 }
791
792 #[test]
796 fn lexes_float_exponents() {
797 assert_eq!(
798 kinds("1e300 2.5E-3 7e+2"),
799 vec![
800 TokenKind::Float(1e300),
801 TokenKind::Float(2.5e-3),
802 TokenKind::Float(700.0),
803 ]
804 );
805 assert_eq!(
806 kinds("1.e"),
807 vec![
808 TokenKind::Int(1),
809 TokenKind::Dot,
810 TokenKind::Ident("e".into())
811 ]
812 );
813 }
814
815 #[test]
818 fn a_dot_after_a_digit_is_a_send_unless_a_digit_follows() {
819 assert_eq!(
820 kinds("1.foo"),
821 vec![
822 TokenKind::Int(1),
823 TokenKind::Dot,
824 TokenKind::Ident("foo".into()),
825 ]
826 );
827 }
828
829 #[test]
830 fn lexes_symbols_and_labels_distinctly() {
831 assert_eq!(kinds(":foo"), vec![TokenKind::Sym("foo".into())]);
832 assert_eq!(kinds("foo:"), vec![TokenKind::Label("foo".into())]);
833 }
834
835 #[test]
836 fn ruby_predicate_and_bang_suffixes_are_part_of_the_name() {
837 assert_eq!(
838 kinds("empty? save!"),
839 vec![
840 TokenKind::Ident("empty?".into()),
841 TokenKind::Ident("save!".into()),
842 ]
843 );
844 }
845
846 #[test]
854 fn a_bang_before_equals_is_the_operator_not_the_name() {
855 assert_eq!(
856 kinds("x!=3"),
857 vec![
858 TokenKind::Ident("x".into()),
859 TokenKind::Op("!=".into()),
860 TokenKind::Int(3),
861 ]
862 );
863 assert_eq!(
864 kinds("ok?! x"),
865 vec![
866 TokenKind::Ident("ok?".into()),
867 TokenKind::Op("!".into()),
868 TokenKind::Ident("x".into()),
869 ]
870 );
871 }
872
873 #[test]
874 fn lexes_strings_with_escapes() {
875 assert_eq!(kinds(r#""a\nb""#), vec![TokenKind::Str("a\nb".into())]);
876 }
877
878 #[test]
879 fn unterminated_string_is_an_error_with_a_span() {
880 let e = lex("\"oops").expect_err("must fail");
881 assert!(e.message.contains("unterminated"), "{}", e.message);
882 assert_eq!(e.span.start, 0);
883 }
884
885 #[test]
888 fn comments_and_newlines_are_emitted_as_trivia() {
889 let toks = lex("1 # hi\n2").expect("lex");
890 assert!(
891 toks.iter()
892 .any(|t| matches!(&t.kind, TokenKind::Comment(c) if c == "# hi")),
893 "comment was dropped: {toks:?}"
894 );
895 assert!(
896 toks.iter().any(|t| t.kind == TokenKind::Newline),
897 "newline was dropped"
898 );
899 }
900
901 #[test]
904 fn spans_point_at_the_actual_bytes() {
905 let src = "foo + 1";
906 let toks = lex(src).expect("lex");
907 let first = &toks[0];
908 assert_eq!(&src[first.span.start..first.span.end], "foo");
909 let last_int = toks
910 .iter()
911 .find(|t| matches!(t.kind, TokenKind::Int(_)))
912 .expect("an int token");
913 assert_eq!(&src[last_int.span.start..last_int.span.end], "1");
914 }
915
916 #[test]
917 fn lexes_pipeline_and_rocket() {
918 assert_eq!(kinds("|> =>"), vec![TokenKind::Pipe, TokenKind::Rocket]);
919 }
920}