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 Words {
63 flavor: crate::words::Flavor,
64 words: Vec<(String, Span)>,
65 },
66 True,
67 False,
68 Nil,
69
70 Ident(String),
73
74 LParen,
76 RParen,
77 LBracket,
78 RBracket,
79 LBrace,
80 RBrace,
81 Comma,
82 Dot,
83 Colon,
86 PathSep,
90 Label(String),
94 Rocket,
96 Pipe,
98
99 Op(String),
101
102 Doc {
106 module: bool,
107 text: String,
108 },
109
110 Comment(String),
112 Newline,
113
114 Eof,
115}
116
117impl fmt::Display for TokenKind {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match self {
128 Self::Int(v) => write!(f, "the number `{v}`"),
129 Self::Float(v) => write!(f, "the number `{v}`"),
130 Self::Str(_) | Self::InterpolatedStr { .. } => f.write_str("a string"),
131 Self::Sym(s) => write!(f, "the symbol `:{s}`"),
132 Self::Words { flavor, .. } => write!(f, "the word list `{}[…]`", flavor.sigil()),
133 Self::True => f.write_str("`true`"),
134 Self::False => f.write_str("`false`"),
135 Self::Nil => f.write_str("`nil`"),
136 Self::Ident(n) => write!(f, "`{n}`"),
137 Self::LParen => f.write_str("`(`"),
138 Self::RParen => f.write_str("`)`"),
139 Self::LBracket => f.write_str("`[`"),
140 Self::RBracket => f.write_str("`]`"),
141 Self::LBrace => f.write_str("`{`"),
142 Self::RBrace => f.write_str("`}`"),
143 Self::Comma => f.write_str("`,`"),
144 Self::Dot => f.write_str("`.`"),
145 Self::Colon => f.write_str("`:`"),
146 Self::PathSep => f.write_str("`::`"),
147 Self::Label(n) => write!(f, "the label `{n}:`"),
148 Self::Rocket => f.write_str("`=>`"),
149 Self::Pipe => f.write_str("`|>`"),
150 Self::Op(o) => write!(f, "`{o}`"),
151 Self::Doc { module: false, .. } => f.write_str("`@doc`"),
152 Self::Doc { module: true, .. } => f.write_str("`@moduledoc`"),
153 Self::Comment(_) => f.write_str("a comment"),
154 Self::Newline => f.write_str("the end of the line"),
155 Self::Eof => f.write_str("the end of the file"),
156 }
157 }
158}
159
160#[derive(Clone, Debug, PartialEq)]
161pub struct Token {
162 pub kind: TokenKind,
163 pub span: Span,
164}
165
166impl Token {
167 pub fn is_trivia(&self) -> bool {
169 matches!(self.kind, TokenKind::Comment(_) | TokenKind::Newline)
170 }
171}
172
173#[derive(Clone, Debug, PartialEq)]
174pub struct LexError {
175 pub message: String,
176 pub span: Span,
177}
178
179impl fmt::Display for LexError {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 write!(
182 f,
183 "{} at {}..{}",
184 self.message, self.span.start, self.span.end
185 )
186 }
187}
188
189impl std::error::Error for LexError {}
190
191const OP_CHARS: &str = "+-*/=<>!%&|";
193
194pub fn lex(src: &str) -> Result<Vec<Token>, LexError> {
196 Lexer::new(src).run()
197}
198
199struct Lexer<'a> {
200 src: &'a str,
201 bytes: &'a [u8],
202 pos: usize,
203 out: Vec<Token>,
204}
205
206impl<'a> Lexer<'a> {
207 fn new(src: &'a str) -> Self {
208 Self {
209 src,
210 bytes: src.as_bytes(),
211 pos: if src.starts_with('\u{feff}') {
212 '\u{feff}'.len_utf8()
213 } else {
214 0
215 },
216 out: Vec::new(),
217 }
218 }
219
220 fn decode_char(&self) -> Option<(char, usize)> {
229 let rest = self.src.get(self.pos..)?;
230 let ch = rest.chars().next()?;
231 Some((ch, ch.len_utf8()))
232 }
233
234 fn peek(&self) -> Option<u8> {
235 self.bytes.get(self.pos).copied()
236 }
237
238 fn peek_at(&self, n: usize) -> Option<u8> {
239 self.bytes.get(self.pos + n).copied()
240 }
241
242 fn push(&mut self, kind: TokenKind, start: usize) {
243 self.out.push(Token {
244 kind,
245 span: Span::new(start, self.pos),
246 });
247 }
248
249 fn err(&self, message: impl Into<String>, start: usize) -> LexError {
250 LexError {
251 message: message.into(),
252 span: Span::new(start, self.pos.max(start + 1)),
253 }
254 }
255
256 fn run(mut self) -> Result<Vec<Token>, LexError> {
257 while let Some(c) = self.peek() {
258 let start = self.pos;
259 match c {
260 b'\n' => {
261 self.pos += 1;
262 self.push(TokenKind::Newline, start);
263 }
264 b' ' | b'\t' | b'\r' => {
268 self.pos += 1;
269 }
270 b'#' => {
271 while let Some(c) = self.peek() {
272 if c == b'\n' {
273 break;
274 }
275 self.pos += 1;
276 }
277 let text = self.src[start..self.pos].to_string();
278 self.push(TokenKind::Comment(text), start);
279 }
280 b'"' => self.lex_string(start)?,
281 b'@' => self.lex_doc(start)?,
282 b'0'..=b'9' => self.lex_number(start)?,
283 b':' => self.lex_colon(start),
284 b'(' => self.one(TokenKind::LParen, start),
285 b')' => self.one(TokenKind::RParen, start),
286 b'[' => self.one(TokenKind::LBracket, start),
287 b']' => self.one(TokenKind::RBracket, start),
288 b'{' => self.one(TokenKind::LBrace, start),
289 b'}' => self.one(TokenKind::RBrace, start),
290 b',' => self.one(TokenKind::Comma, start),
291 b'.' => self.one(TokenKind::Dot, start),
292 c if c >= 0x80 => {
297 let Some((ch, width)) = self.decode_char() else {
298 self.pos += 1;
299 return Err(self.err("invalid UTF-8 in source", start));
300 };
301 match crate::kigou::classify(ch) {
302 crate::kigou::Class::Operator(op) => {
303 self.pos += width;
304 self.push(TokenKind::Op(op.to_owned()), start);
305 }
306 crate::kigou::Class::Word => self.lex_ident(start),
307 crate::kigou::Class::Reject => {
308 self.pos += width;
309 return Err(self.err(
310 format!(
311 "the character {ch:?} (U+{:04X}) is not legal in blue source \
312 outside a string — invisible and control characters are \
313 rejected because source that looks correct and is not costs \
314 far more than a clear error here",
315 ch as u32
316 ),
317 start,
318 ));
319 }
320 }
321 }
322 c if is_ident_start(c) => self.lex_ident(start),
323 b'%' if self.at_word_list() => {
324 self.lex_words(start)?;
325 }
326 c if OP_CHARS.as_bytes().contains(&c) => self.lex_op(start),
327 b';' => {
331 self.pos += 1;
332 return Err(self.err(
333 "unexpected `;`: blue ends a statement at the end of the line, \
334 so put the next statement on a new line",
335 start,
336 ));
337 }
338 _ => {
339 self.pos += 1;
340 return Err(self.err(format!("unexpected character `{}`", c as char), start));
341 }
342 }
343 }
344 let end = self.pos;
345 self.out.push(Token {
346 kind: TokenKind::Eof,
347 span: Span::new(end, end),
348 });
349 Ok(self.out)
350 }
351
352 fn one(&mut self, kind: TokenKind, start: usize) {
353 self.pos += 1;
354 self.push(kind, start);
355 }
356
357 fn lex_string(&mut self, start: usize) -> Result<(), LexError> {
358 self.pos += 1; let mut buf = String::new();
360 let mut parts: Vec<String> = Vec::new();
363 let mut exprs: Vec<String> = Vec::new();
364 loop {
365 if self.peek() == Some(b'#') && self.src.as_bytes().get(self.pos + 1) == Some(&b'{') {
368 self.pos += 2;
369 let expr_start = self.pos;
370 let Some(after) = skip_interpolation(self.bytes, self.pos) else {
371 return Err(self.err("unterminated `#{` interpolation", start));
372 };
373 self.pos = after - 1;
374 exprs.push(self.src[expr_start..self.pos].to_string());
375 self.pos += 1; parts.push(std::mem::take(&mut buf));
377 continue;
378 }
379 match self.peek() {
380 None => return Err(self.err("unterminated string literal", start)),
381 Some(b'"') => {
382 self.pos += 1;
383 break;
384 }
385 Some(b'\\') => {
386 self.pos += 1;
387 let esc = self
388 .peek()
389 .ok_or_else(|| self.err("unterminated escape", start))?;
390 let ch = match esc {
391 b'n' => '\n',
392 b't' => '\t',
393 b'r' => '\r',
394 b'\\' => '\\',
395 b'"' => '"',
396 b'0' => '\0',
397 b'u' => {
404 self.pos += 1; if self.peek() != Some(b'{') {
406 return Err(self.err("expected `{` after \\u", start));
407 }
408 self.pos += 1; let hex_start = self.pos;
410 while self.peek().is_some_and(|c| c != b'}') {
411 self.pos += 1;
412 }
413 if self.peek() != Some(b'}') {
414 return Err(self.err("unterminated \\u{...} escape", start));
415 }
416 let hex = &self.src[hex_start..self.pos];
417 let code = u32::from_str_radix(hex, 16).map_err(|_| {
418 self.err(format!("`{hex}` is not hexadecimal"), start)
419 })?;
420 let ch = char::from_u32(code).ok_or_else(|| {
425 self.err(format!("`{hex}` is not a Unicode scalar value"), start)
426 })?;
427 buf.push(ch);
428 self.pos += 1; continue;
430 }
431 other => {
432 return Err(
433 self.err(format!("unknown escape \\{}", other as char), start)
434 )
435 }
436 };
437 buf.push(ch);
438 self.pos += 1;
439 }
440 Some(_) => {
441 let ch = self.src[self.pos..]
442 .chars()
443 .next()
444 .expect("peek said there is a byte");
445 buf.push(ch);
446 self.pos += ch.len_utf8();
447 }
448 }
449 }
450 if exprs.is_empty() {
451 self.push(TokenKind::Str(buf), start);
452 } else {
453 parts.push(buf);
454 self.push(TokenKind::InterpolatedStr { parts, exprs }, start);
455 }
456 Ok(())
457 }
458
459 fn lex_doc(&mut self, start: usize) -> Result<(), LexError> {
460 self.pos += 1;
461 let word_start = self.pos;
462 while self.peek().is_some_and(is_ident_continue) {
463 self.pos += 1;
464 }
465 let module = match &self.src[word_start..self.pos] {
466 "doc" => false,
467 "moduledoc" => true,
468 other => {
469 return Err(self.err(
470 format!("unexpected `@{other}`: blue's attributes are `@doc` and `@moduledoc`"),
471 start,
472 ))
473 }
474 };
475 let word = if module { "@moduledoc" } else { "@doc" };
476 while matches!(self.peek(), Some(b' ' | b'\t')) {
477 self.pos += 1;
478 }
479 let opens = self.src[self.pos..].starts_with("\"\"\"");
480 let rest_of_line = self.src[self.pos..]
481 .split('\n')
482 .next()
483 .unwrap_or("")
484 .get(3..)
485 .unwrap_or("x");
486 if !opens || !rest_of_line.trim().is_empty() {
487 return Err(self.err(
488 format!(
489 "`{word}` takes a heredoc: `{word} \"\"\"` alone on its line, the text, \
490 then `\"\"\"` alone on a line"
491 ),
492 start,
493 ));
494 }
495 self.pos += 3 + rest_of_line.len();
496 let body_start = (self.pos + 1).min(self.src.len());
497 let mut lines: Vec<&str> = Vec::new();
498 let mut at = body_start;
499 let closer = loop {
500 if at >= self.src.len() {
501 self.pos = self.src.len();
502 return Err(self.err(
503 format!("unterminated `{word}`: no `\"\"\"` line closes it"),
504 start,
505 ));
506 }
507 let end = self.src[at..].find('\n').map_or(self.src.len(), |i| at + i);
508 let line = &self.src[at..end];
509 if line.trim() == "\"\"\"" {
510 break (line, end);
511 }
512 lines.push(line);
513 at = (end + 1).min(self.src.len() + 1);
514 };
515 let indent = &closer.0[..closer.0.len() - closer.0.trim_start().len()];
516 let mut kept: Vec<&str> = Vec::with_capacity(lines.len());
517 for line in lines {
518 let line = line.trim_end();
519 if line.is_empty() {
520 kept.push("");
521 continue;
522 }
523 match line.strip_prefix(indent) {
524 Some(l) => kept.push(l),
525 None => {
526 return Err(self.err(
527 format!(
528 "a line of this `{word}` is indented less than its closing `\"\"\"`"
529 ),
530 start,
531 ))
532 }
533 }
534 }
535 while kept.first() == Some(&"") {
536 kept.remove(0);
537 }
538 while kept.last() == Some(&"") {
539 kept.pop();
540 }
541 let text = kept.join("\n");
542 if text.contains("\"\"\"") {
543 return Err(self.err(format!("a `{word}` text cannot hold `\"\"\"`"), start));
544 }
545 let close_at = closer.1 - closer.0.trim_start().len() + 3;
546 self.pos = close_at;
547 self.push(TokenKind::Doc { module, text }, start);
548 Ok(())
549 }
550
551 fn lex_words(&mut self, start: usize) -> Result<(), LexError> {
552 use crate::words::Flavor;
553 let flavor = if self.peek_at(1) == Some(b'w') {
554 Flavor::Strings
555 } else {
556 Flavor::Keywords
557 };
558 self.pos += 3;
559 let mut words = Vec::new();
560 loop {
561 while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
562 self.pos += 1;
563 }
564 match self.peek() {
565 None => {
566 return Err(self.err(
567 format!("unterminated `{}[` word list", flavor.sigil()),
568 start,
569 ))
570 }
571 Some(b']') => {
572 self.pos += 1;
573 break;
574 }
575 Some(_) => {}
576 }
577 let w = self.pos;
578 while let Some((ch, width)) = self.decode_char() {
579 if matches!(ch, ']' | ' ' | '\t' | '\r' | '\n') {
580 break;
581 }
582 self.pos += width;
583 }
584 let word = &self.src[w..self.pos];
585 if let Some(why) = crate::words::refusal(flavor, word) {
586 return Err(LexError {
587 message: why,
588 span: Span::new(w, self.pos),
589 });
590 }
591 words.push((word.to_string(), Span::new(w, self.pos)));
592 }
593 self.push(TokenKind::Words { flavor, words }, start);
594 Ok(())
595 }
596
597 fn lex_number(&mut self, start: usize) -> Result<(), LexError> {
598 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
599 self.pos += 1;
600 }
601 let mut is_float =
605 self.peek() == Some(b'.') && matches!(self.peek_at(1), Some(b'0'..=b'9'));
606 if is_float {
607 self.pos += 1;
608 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
609 self.pos += 1;
610 }
611 }
612 let exponent_digits_at = match (self.peek(), self.peek_at(1)) {
616 (Some(b'e' | b'E'), Some(b'0'..=b'9')) => Some(1),
617 (Some(b'e' | b'E'), Some(b'+' | b'-'))
618 if matches!(self.peek_at(2), Some(b'0'..=b'9')) =>
619 {
620 Some(2)
621 }
622 _ => None,
623 };
624 if let Some(skip) = exponent_digits_at {
625 is_float = true;
626 self.pos += skip;
627 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
628 self.pos += 1;
629 }
630 }
631 let text: String = self.src[start..self.pos]
632 .chars()
633 .filter(|c| *c != '_')
634 .collect();
635 if is_float {
636 let v: f64 = text
637 .parse()
638 .map_err(|_| self.err(format!("invalid float literal {text:?}"), start))?;
639 self.push(TokenKind::Float(v), start);
640 } else {
641 let v: i64 = text
642 .parse()
643 .map_err(|_| self.err(format!("integer literal out of range: {text:?}"), start))?;
644 self.push(TokenKind::Int(v), start);
645 }
646 Ok(())
647 }
648
649 fn lex_colon(&mut self, start: usize) {
650 if self.peek_at(1) == Some(b':') {
654 self.pos += 2;
655 self.push(TokenKind::PathSep, start);
656 } else if matches!(self.peek_at(1), Some(c) if is_ident_start(c)) {
657 self.pos += 1;
658 let s = self.pos;
659 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
660 self.pos += 1;
661 }
662 if matches!(self.peek(), Some(b'?'))
667 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
668 {
669 self.pos += 1;
670 }
671 let name = self.src[s..self.pos].to_string();
672 self.push(TokenKind::Sym(name), start);
673 } else {
674 self.one(TokenKind::Colon, start);
675 }
676 }
677
678 fn lex_ident(&mut self, start: usize) {
679 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
680 self.pos += 1;
681 }
682
683 if matches!(self.peek(), Some(b'?'))
697 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
698 {
699 self.pos += 1;
700 }
701 let name = self.src[start..self.pos].to_string();
702
703 if self.peek() == Some(b':') && self.peek_at(1) != Some(b':') {
707 self.pos += 1;
708 self.push(TokenKind::Label(name), start);
709 return;
710 }
711
712 let kind = match name.as_str() {
713 "true" => TokenKind::True,
714 "false" => TokenKind::False,
715 "nil" => TokenKind::Nil,
716 _ => TokenKind::Ident(name),
717 };
718 self.push(kind, start);
719 }
720
721 fn at_word_list(&self) -> bool {
722 self.peek() == Some(b'%')
723 && matches!(self.peek_at(1), Some(b'w' | b'i'))
724 && self.peek_at(2) == Some(b'[')
725 }
726
727 fn lex_op(&mut self, start: usize) {
728 while matches!(self.peek(), Some(c) if OP_CHARS.as_bytes().contains(&c)) {
729 if self.pos > start && self.at_word_list() {
730 break;
731 }
732 self.pos += 1;
733 }
734 let text = self.src[start..self.pos].to_string();
735 let kind = match text.as_str() {
736 "=>" => TokenKind::Rocket,
737 "|>" => TokenKind::Pipe,
738 _ => TokenKind::Op(text),
739 };
740 self.push(kind, start);
741 }
742}
743
744fn skip_interpolation(bytes: &[u8], mut i: usize) -> Option<usize> {
764 let mut depth = 1usize;
765 loop {
766 match *bytes.get(i)? {
767 b'{' => depth += 1,
768 b'}' => {
769 depth -= 1;
770 if depth == 0 {
771 return Some(i + 1);
772 }
773 }
774 b'"' => {
775 i = skip_code_string(bytes, i)?;
776 continue;
777 }
778 b'%' if matches!(bytes.get(i + 1), Some(b'w' | b'i'))
779 && bytes.get(i + 2) == Some(&b'[') =>
780 {
781 i += 3 + bytes.get(i + 3..)?.iter().position(|b| *b == b']')?;
782 }
783 _ => {}
784 }
785 i += 1;
786 }
787}
788
789fn skip_code_string(bytes: &[u8], mut i: usize) -> Option<usize> {
790 i += 1;
791 loop {
792 match *bytes.get(i)? {
793 b'"' => return Some(i + 1),
794 b'\\' => i += 2,
795 b'#' if bytes.get(i + 1) == Some(&b'{') => i = skip_interpolation(bytes, i + 2)?,
796 _ => i += 1,
797 }
798 }
799}
800
801fn is_ident_start(c: u8) -> bool {
802 c.is_ascii_alphabetic() || c == b'_'
803}
804
805fn is_ident_continue(c: u8) -> bool {
806 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 fn kinds(src: &str) -> Vec<TokenKind> {
814 lex(src)
815 .expect("lex")
816 .into_iter()
817 .filter(|t| !t.is_trivia() && t.kind != TokenKind::Eof)
818 .map(|t| t.kind)
819 .collect()
820 }
821
822 #[test]
823 fn a_brace_inside_a_string_inside_interpolated_code_is_text() {
824 for (src, code) in [
825 ("\"a#{f(\"}\")}b\"", "f(\"}\")"),
826 ("\"a#{\"{\" |> \"}\"}b\"", "\"{\" |> \"}\""),
827 ("\"#{ {a: \"}\"} }\"", " {a: \"}\"} "),
828 ("\"#{f(\"x\\\"}\")}\"", "f(\"x\\\"}\")"),
829 ("\"#{\"in#{\"}\"}ner\"}\"", "\"in#{\"}\"}ner\""),
830 ] {
831 match kinds(src).as_slice() {
832 [TokenKind::InterpolatedStr { exprs, .. }] => assert_eq!(exprs, &[code], "{src}"),
833 other => panic!("{src}: {other:?}"),
834 }
835 }
836 assert!(lex("\"#{\"}\"").is_err());
837 }
838
839 #[test]
840 fn a_leading_byte_order_mark_is_stepped_over_and_one_inside_the_text_is_refused() {
841 assert_eq!(kinds("\u{feff}x"), kinds("x"));
842 let at = lex("\u{feff}x").expect("lex")[0].span;
843 assert_eq!(at.start, 3, "spans still index the source bytes");
844 assert!(lex("x\n\u{feff}y").is_err());
845 assert!(lex("\"a\u{feff}b\"").is_ok());
846 }
847
848 #[test]
849 fn lexes_integers_and_floats() {
850 assert_eq!(
851 kinds("1 2.5 1_000"),
852 vec![
853 TokenKind::Int(1),
854 TokenKind::Float(2.5),
855 TokenKind::Int(1000),
856 ]
857 );
858 }
859
860 #[test]
864 fn lexes_float_exponents() {
865 assert_eq!(
866 kinds("1e300 2.5E-3 7e+2"),
867 vec![
868 TokenKind::Float(1e300),
869 TokenKind::Float(2.5e-3),
870 TokenKind::Float(700.0),
871 ]
872 );
873 assert_eq!(
874 kinds("1.e"),
875 vec![
876 TokenKind::Int(1),
877 TokenKind::Dot,
878 TokenKind::Ident("e".into())
879 ]
880 );
881 }
882
883 #[test]
886 fn a_dot_after_a_digit_is_a_send_unless_a_digit_follows() {
887 assert_eq!(
888 kinds("1.foo"),
889 vec![
890 TokenKind::Int(1),
891 TokenKind::Dot,
892 TokenKind::Ident("foo".into()),
893 ]
894 );
895 }
896
897 #[test]
898 fn lexes_symbols_and_labels_distinctly() {
899 assert_eq!(kinds(":foo"), vec![TokenKind::Sym("foo".into())]);
900 assert_eq!(kinds("foo:"), vec![TokenKind::Label("foo".into())]);
901 }
902
903 #[test]
904 fn ruby_predicate_and_bang_suffixes_are_part_of_the_name() {
905 assert_eq!(
906 kinds("empty? save!"),
907 vec![
908 TokenKind::Ident("empty?".into()),
909 TokenKind::Ident("save!".into()),
910 ]
911 );
912 }
913
914 #[test]
922 fn a_bang_before_equals_is_the_operator_not_the_name() {
923 assert_eq!(
924 kinds("x!=3"),
925 vec![
926 TokenKind::Ident("x".into()),
927 TokenKind::Op("!=".into()),
928 TokenKind::Int(3),
929 ]
930 );
931 assert_eq!(
932 kinds("ok?! x"),
933 vec![
934 TokenKind::Ident("ok?".into()),
935 TokenKind::Op("!".into()),
936 TokenKind::Ident("x".into()),
937 ]
938 );
939 }
940
941 #[test]
942 fn lexes_strings_with_escapes() {
943 assert_eq!(kinds(r#""a\nb""#), vec![TokenKind::Str("a\nb".into())]);
944 }
945
946 #[test]
947 fn unterminated_string_is_an_error_with_a_span() {
948 let e = lex("\"oops").expect_err("must fail");
949 assert!(e.message.contains("unterminated"), "{}", e.message);
950 assert_eq!(e.span.start, 0);
951 }
952
953 #[test]
956 fn comments_and_newlines_are_emitted_as_trivia() {
957 let toks = lex("1 # hi\n2").expect("lex");
958 assert!(
959 toks.iter()
960 .any(|t| matches!(&t.kind, TokenKind::Comment(c) if c == "# hi")),
961 "comment was dropped: {toks:?}"
962 );
963 assert!(
964 toks.iter().any(|t| t.kind == TokenKind::Newline),
965 "newline was dropped"
966 );
967 }
968
969 #[test]
972 fn spans_point_at_the_actual_bytes() {
973 let src = "foo + 1";
974 let toks = lex(src).expect("lex");
975 let first = &toks[0];
976 assert_eq!(&src[first.span.start..first.span.end], "foo");
977 let last_int = toks
978 .iter()
979 .find(|t| matches!(t.kind, TokenKind::Int(_)))
980 .expect("an int token");
981 assert_eq!(&src[last_int.span.start..last_int.span.end], "1");
982 }
983
984 #[test]
985 fn lexes_word_lists() {
986 use crate::words::Flavor;
987 let src = "%w[a --force\n x/y] %i[pair? b]";
988 let toks = lex(src).expect("lex");
989 let TokenKind::Words { flavor, words } = &toks[0].kind else {
990 panic!("{toks:?}");
991 };
992 assert_eq!(*flavor, Flavor::Strings);
993 let spelled: Vec<&str> = words.iter().map(|(_, at)| &src[at.start..at.end]).collect();
994 assert_eq!(spelled, ["a", "--force", "x/y"]);
995 assert_eq!(
996 words.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>(),
997 spelled
998 );
999 assert_eq!(
1000 &src[toks[0].span.start..toks[0].span.end],
1001 "%w[a --force\n x/y]"
1002 );
1003 assert!(
1004 matches!(&toks[1].kind, TokenKind::Words { flavor: Flavor::Keywords, words } if words.len() == 2)
1005 );
1006 assert_eq!(
1007 kinds("%w[]"),
1008 vec![TokenKind::Words {
1009 flavor: Flavor::Strings,
1010 words: vec![]
1011 }]
1012 );
1013 }
1014
1015 #[test]
1016 fn a_percent_is_still_modulo_unless_w_or_i_and_a_bracket_follow() {
1017 assert_eq!(
1018 kinds("a %w(b)"),
1019 vec![
1020 TokenKind::Ident("a".into()),
1021 TokenKind::Op("%".into()),
1022 TokenKind::Ident("w".into()),
1023 TokenKind::LParen,
1024 TokenKind::Ident("b".into()),
1025 TokenKind::RParen,
1026 ]
1027 );
1028 assert_eq!(kinds("x%i")[1], TokenKind::Op("%".into()));
1029 }
1030
1031 #[test]
1032 fn an_operator_before_a_word_list_ends_where_the_list_begins() {
1033 for (src, op) in [("*%i[a a]", "*"), ("x ==%w[b]", "=="), ("-%i[a]", "-")] {
1034 let ks = kinds(src);
1035 assert!(ks.contains(&TokenKind::Op(op.into())), "{src}: {ks:?}");
1036 assert!(
1037 ks.iter().any(|k| matches!(k, TokenKind::Words { .. })),
1038 "{src}: {ks:?}"
1039 );
1040 }
1041 assert_eq!(kinds("a %% b")[1], TokenKind::Op("%%".into()));
1042 }
1043
1044 #[test]
1045 fn a_word_list_inside_an_interpolation_keeps_its_braces() {
1046 let src = "\"a#{join_lines(%w[} {])}b\"";
1047 assert!(lex(src).is_ok(), "{:?}", lex(src));
1048 assert!(lex("\"#{%w[/x/{id} y]}\"").is_ok());
1049 assert!(
1050 lex("\"#{%w[a b\"").is_err(),
1051 "an open word list is still refused"
1052 );
1053 }
1054
1055 #[test]
1056 fn a_word_list_refuses_commas_quotes_colons_and_an_open_end() {
1057 for (src, says) in [
1058 ("%w[a, b]", "not commas"),
1059 ("%w[\"a\" b]", "without quotes"),
1060 ("%i[:a b]", "without its colon"),
1061 ("%i[a-b]", "not a name"),
1062 ("%w[a # b]", "cannot be part of a word"),
1063 ("%w[a b", "unterminated"),
1064 ("%w[a\u{a0}b]", "cannot be part of a word"),
1065 ] {
1066 let e = lex(src).expect_err(src);
1067 assert!(e.message.contains(says), "{src}: {}", e.message);
1068 }
1069 }
1070
1071 #[test]
1072 fn lexes_pipeline_and_rocket() {
1073 assert_eq!(kinds("|> =>"), vec![TokenKind::Pipe, TokenKind::Rocket]);
1074 }
1075}