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 matches!(self.peek_at(1), Some(b'w' | b'i'))
324 && self.peek_at(2) == Some(b'[') =>
325 {
326 self.lex_words(start)?;
327 }
328 c if OP_CHARS.as_bytes().contains(&c) => self.lex_op(start),
329 b';' => {
333 self.pos += 1;
334 return Err(self.err(
335 "unexpected `;`: blue ends a statement at the end of the line, \
336 so put the next statement on a new line",
337 start,
338 ));
339 }
340 _ => {
341 self.pos += 1;
342 return Err(self.err(format!("unexpected character `{}`", c as char), start));
343 }
344 }
345 }
346 let end = self.pos;
347 self.out.push(Token {
348 kind: TokenKind::Eof,
349 span: Span::new(end, end),
350 });
351 Ok(self.out)
352 }
353
354 fn one(&mut self, kind: TokenKind, start: usize) {
355 self.pos += 1;
356 self.push(kind, start);
357 }
358
359 fn lex_string(&mut self, start: usize) -> Result<(), LexError> {
360 self.pos += 1; let mut buf = String::new();
362 let mut parts: Vec<String> = Vec::new();
365 let mut exprs: Vec<String> = Vec::new();
366 loop {
367 if self.peek() == Some(b'#') && self.src.as_bytes().get(self.pos + 1) == Some(&b'{') {
370 self.pos += 2;
371 let expr_start = self.pos;
372 let Some(after) = skip_interpolation(self.bytes, self.pos) else {
373 return Err(self.err("unterminated `#{` interpolation", start));
374 };
375 self.pos = after - 1;
376 exprs.push(self.src[expr_start..self.pos].to_string());
377 self.pos += 1; parts.push(std::mem::take(&mut buf));
379 continue;
380 }
381 match self.peek() {
382 None => return Err(self.err("unterminated string literal", start)),
383 Some(b'"') => {
384 self.pos += 1;
385 break;
386 }
387 Some(b'\\') => {
388 self.pos += 1;
389 let esc = self
390 .peek()
391 .ok_or_else(|| self.err("unterminated escape", start))?;
392 let ch = match esc {
393 b'n' => '\n',
394 b't' => '\t',
395 b'r' => '\r',
396 b'\\' => '\\',
397 b'"' => '"',
398 b'0' => '\0',
399 b'u' => {
406 self.pos += 1; if self.peek() != Some(b'{') {
408 return Err(self.err("expected `{` after \\u", start));
409 }
410 self.pos += 1; let hex_start = self.pos;
412 while self.peek().is_some_and(|c| c != b'}') {
413 self.pos += 1;
414 }
415 if self.peek() != Some(b'}') {
416 return Err(self.err("unterminated \\u{...} escape", start));
417 }
418 let hex = &self.src[hex_start..self.pos];
419 let code = u32::from_str_radix(hex, 16).map_err(|_| {
420 self.err(format!("`{hex}` is not hexadecimal"), start)
421 })?;
422 let ch = char::from_u32(code).ok_or_else(|| {
427 self.err(format!("`{hex}` is not a Unicode scalar value"), start)
428 })?;
429 buf.push(ch);
430 self.pos += 1; continue;
432 }
433 other => {
434 return Err(
435 self.err(format!("unknown escape \\{}", other as char), start)
436 )
437 }
438 };
439 buf.push(ch);
440 self.pos += 1;
441 }
442 Some(_) => {
443 let ch = self.src[self.pos..]
444 .chars()
445 .next()
446 .expect("peek said there is a byte");
447 buf.push(ch);
448 self.pos += ch.len_utf8();
449 }
450 }
451 }
452 if exprs.is_empty() {
453 self.push(TokenKind::Str(buf), start);
454 } else {
455 parts.push(buf);
456 self.push(TokenKind::InterpolatedStr { parts, exprs }, start);
457 }
458 Ok(())
459 }
460
461 fn lex_doc(&mut self, start: usize) -> Result<(), LexError> {
462 self.pos += 1;
463 let word_start = self.pos;
464 while self.peek().is_some_and(is_ident_continue) {
465 self.pos += 1;
466 }
467 let module = match &self.src[word_start..self.pos] {
468 "doc" => false,
469 "moduledoc" => true,
470 other => {
471 return Err(self.err(
472 format!("unexpected `@{other}`: blue's attributes are `@doc` and `@moduledoc`"),
473 start,
474 ))
475 }
476 };
477 let word = if module { "@moduledoc" } else { "@doc" };
478 while matches!(self.peek(), Some(b' ' | b'\t')) {
479 self.pos += 1;
480 }
481 let opens = self.src[self.pos..].starts_with("\"\"\"");
482 let rest_of_line = self.src[self.pos..]
483 .split('\n')
484 .next()
485 .unwrap_or("")
486 .get(3..)
487 .unwrap_or("x");
488 if !opens || !rest_of_line.trim().is_empty() {
489 return Err(self.err(
490 format!(
491 "`{word}` takes a heredoc: `{word} \"\"\"` alone on its line, the text, \
492 then `\"\"\"` alone on a line"
493 ),
494 start,
495 ));
496 }
497 self.pos += 3 + rest_of_line.len();
498 let body_start = (self.pos + 1).min(self.src.len());
499 let mut lines: Vec<&str> = Vec::new();
500 let mut at = body_start;
501 let closer = loop {
502 if at >= self.src.len() {
503 self.pos = self.src.len();
504 return Err(self.err(
505 format!("unterminated `{word}`: no `\"\"\"` line closes it"),
506 start,
507 ));
508 }
509 let end = self.src[at..].find('\n').map_or(self.src.len(), |i| at + i);
510 let line = &self.src[at..end];
511 if line.trim() == "\"\"\"" {
512 break (line, end);
513 }
514 lines.push(line);
515 at = (end + 1).min(self.src.len() + 1);
516 };
517 let indent = &closer.0[..closer.0.len() - closer.0.trim_start().len()];
518 let mut kept: Vec<&str> = Vec::with_capacity(lines.len());
519 for line in lines {
520 let line = line.trim_end();
521 if line.is_empty() {
522 kept.push("");
523 continue;
524 }
525 match line.strip_prefix(indent) {
526 Some(l) => kept.push(l),
527 None => {
528 return Err(self.err(
529 format!(
530 "a line of this `{word}` is indented less than its closing `\"\"\"`"
531 ),
532 start,
533 ))
534 }
535 }
536 }
537 while kept.first() == Some(&"") {
538 kept.remove(0);
539 }
540 while kept.last() == Some(&"") {
541 kept.pop();
542 }
543 let text = kept.join("\n");
544 if text.contains("\"\"\"") {
545 return Err(self.err(format!("a `{word}` text cannot hold `\"\"\"`"), start));
546 }
547 let close_at = closer.1 - closer.0.trim_start().len() + 3;
548 self.pos = close_at;
549 self.push(TokenKind::Doc { module, text }, start);
550 Ok(())
551 }
552
553 fn lex_words(&mut self, start: usize) -> Result<(), LexError> {
554 use crate::words::Flavor;
555 let flavor = if self.peek_at(1) == Some(b'w') {
556 Flavor::Strings
557 } else {
558 Flavor::Keywords
559 };
560 self.pos += 3;
561 let mut words = Vec::new();
562 loop {
563 while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
564 self.pos += 1;
565 }
566 match self.peek() {
567 None => {
568 return Err(self.err(
569 format!("unterminated `{}[` word list", flavor.sigil()),
570 start,
571 ))
572 }
573 Some(b']') => {
574 self.pos += 1;
575 break;
576 }
577 Some(_) => {}
578 }
579 let w = self.pos;
580 while let Some((ch, width)) = self.decode_char() {
581 if matches!(ch, ']' | ' ' | '\t' | '\r' | '\n') {
582 break;
583 }
584 self.pos += width;
585 }
586 let word = &self.src[w..self.pos];
587 if let Some(why) = crate::words::refusal(flavor, word) {
588 return Err(LexError {
589 message: why,
590 span: Span::new(w, self.pos),
591 });
592 }
593 words.push((word.to_string(), Span::new(w, self.pos)));
594 }
595 self.push(TokenKind::Words { flavor, words }, start);
596 Ok(())
597 }
598
599 fn lex_number(&mut self, start: usize) -> Result<(), LexError> {
600 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
601 self.pos += 1;
602 }
603 let mut is_float =
607 self.peek() == Some(b'.') && matches!(self.peek_at(1), Some(b'0'..=b'9'));
608 if is_float {
609 self.pos += 1;
610 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
611 self.pos += 1;
612 }
613 }
614 let exponent_digits_at = match (self.peek(), self.peek_at(1)) {
618 (Some(b'e' | b'E'), Some(b'0'..=b'9')) => Some(1),
619 (Some(b'e' | b'E'), Some(b'+' | b'-'))
620 if matches!(self.peek_at(2), Some(b'0'..=b'9')) =>
621 {
622 Some(2)
623 }
624 _ => None,
625 };
626 if let Some(skip) = exponent_digits_at {
627 is_float = true;
628 self.pos += skip;
629 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
630 self.pos += 1;
631 }
632 }
633 let text: String = self.src[start..self.pos]
634 .chars()
635 .filter(|c| *c != '_')
636 .collect();
637 if is_float {
638 let v: f64 = text
639 .parse()
640 .map_err(|_| self.err(format!("invalid float literal {text:?}"), start))?;
641 self.push(TokenKind::Float(v), start);
642 } else {
643 let v: i64 = text
644 .parse()
645 .map_err(|_| self.err(format!("integer literal out of range: {text:?}"), start))?;
646 self.push(TokenKind::Int(v), start);
647 }
648 Ok(())
649 }
650
651 fn lex_colon(&mut self, start: usize) {
652 if self.peek_at(1) == Some(b':') {
656 self.pos += 2;
657 self.push(TokenKind::PathSep, start);
658 } else if matches!(self.peek_at(1), Some(c) if is_ident_start(c)) {
659 self.pos += 1;
660 let s = self.pos;
661 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
662 self.pos += 1;
663 }
664 if matches!(self.peek(), Some(b'?'))
669 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
670 {
671 self.pos += 1;
672 }
673 let name = self.src[s..self.pos].to_string();
674 self.push(TokenKind::Sym(name), start);
675 } else {
676 self.one(TokenKind::Colon, start);
677 }
678 }
679
680 fn lex_ident(&mut self, start: usize) {
681 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
682 self.pos += 1;
683 }
684
685 if matches!(self.peek(), Some(b'?'))
699 || (matches!(self.peek(), Some(b'!')) && !matches!(self.peek_at(1), Some(b'=')))
700 {
701 self.pos += 1;
702 }
703 let name = self.src[start..self.pos].to_string();
704
705 if self.peek() == Some(b':') && self.peek_at(1) != Some(b':') {
709 self.pos += 1;
710 self.push(TokenKind::Label(name), start);
711 return;
712 }
713
714 let kind = match name.as_str() {
715 "true" => TokenKind::True,
716 "false" => TokenKind::False,
717 "nil" => TokenKind::Nil,
718 _ => TokenKind::Ident(name),
719 };
720 self.push(kind, start);
721 }
722
723 fn lex_op(&mut self, start: usize) {
724 while matches!(self.peek(), Some(c) if OP_CHARS.as_bytes().contains(&c)) {
725 self.pos += 1;
726 }
727 let text = self.src[start..self.pos].to_string();
728 let kind = match text.as_str() {
729 "=>" => TokenKind::Rocket,
730 "|>" => TokenKind::Pipe,
731 _ => TokenKind::Op(text),
732 };
733 self.push(kind, start);
734 }
735}
736
737fn skip_interpolation(bytes: &[u8], mut i: usize) -> Option<usize> {
757 let mut depth = 1usize;
758 loop {
759 match *bytes.get(i)? {
760 b'{' => depth += 1,
761 b'}' => {
762 depth -= 1;
763 if depth == 0 {
764 return Some(i + 1);
765 }
766 }
767 b'"' => {
768 i = skip_code_string(bytes, i)?;
769 continue;
770 }
771 _ => {}
772 }
773 i += 1;
774 }
775}
776
777fn skip_code_string(bytes: &[u8], mut i: usize) -> Option<usize> {
778 i += 1;
779 loop {
780 match *bytes.get(i)? {
781 b'"' => return Some(i + 1),
782 b'\\' => i += 2,
783 b'#' if bytes.get(i + 1) == Some(&b'{') => i = skip_interpolation(bytes, i + 2)?,
784 _ => i += 1,
785 }
786 }
787}
788
789fn is_ident_start(c: u8) -> bool {
790 c.is_ascii_alphabetic() || c == b'_'
791}
792
793fn is_ident_continue(c: u8) -> bool {
794 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 fn kinds(src: &str) -> Vec<TokenKind> {
802 lex(src)
803 .expect("lex")
804 .into_iter()
805 .filter(|t| !t.is_trivia() && t.kind != TokenKind::Eof)
806 .map(|t| t.kind)
807 .collect()
808 }
809
810 #[test]
811 fn a_brace_inside_a_string_inside_interpolated_code_is_text() {
812 for (src, code) in [
813 ("\"a#{f(\"}\")}b\"", "f(\"}\")"),
814 ("\"a#{\"{\" |> \"}\"}b\"", "\"{\" |> \"}\""),
815 ("\"#{ {a: \"}\"} }\"", " {a: \"}\"} "),
816 ("\"#{f(\"x\\\"}\")}\"", "f(\"x\\\"}\")"),
817 ("\"#{\"in#{\"}\"}ner\"}\"", "\"in#{\"}\"}ner\""),
818 ] {
819 match kinds(src).as_slice() {
820 [TokenKind::InterpolatedStr { exprs, .. }] => assert_eq!(exprs, &[code], "{src}"),
821 other => panic!("{src}: {other:?}"),
822 }
823 }
824 assert!(lex("\"#{\"}\"").is_err());
825 }
826
827 #[test]
828 fn a_leading_byte_order_mark_is_stepped_over_and_one_inside_the_text_is_refused() {
829 assert_eq!(kinds("\u{feff}x"), kinds("x"));
830 let at = lex("\u{feff}x").expect("lex")[0].span;
831 assert_eq!(at.start, 3, "spans still index the source bytes");
832 assert!(lex("x\n\u{feff}y").is_err());
833 assert!(lex("\"a\u{feff}b\"").is_ok());
834 }
835
836 #[test]
837 fn lexes_integers_and_floats() {
838 assert_eq!(
839 kinds("1 2.5 1_000"),
840 vec![
841 TokenKind::Int(1),
842 TokenKind::Float(2.5),
843 TokenKind::Int(1000),
844 ]
845 );
846 }
847
848 #[test]
852 fn lexes_float_exponents() {
853 assert_eq!(
854 kinds("1e300 2.5E-3 7e+2"),
855 vec![
856 TokenKind::Float(1e300),
857 TokenKind::Float(2.5e-3),
858 TokenKind::Float(700.0),
859 ]
860 );
861 assert_eq!(
862 kinds("1.e"),
863 vec![
864 TokenKind::Int(1),
865 TokenKind::Dot,
866 TokenKind::Ident("e".into())
867 ]
868 );
869 }
870
871 #[test]
874 fn a_dot_after_a_digit_is_a_send_unless_a_digit_follows() {
875 assert_eq!(
876 kinds("1.foo"),
877 vec![
878 TokenKind::Int(1),
879 TokenKind::Dot,
880 TokenKind::Ident("foo".into()),
881 ]
882 );
883 }
884
885 #[test]
886 fn lexes_symbols_and_labels_distinctly() {
887 assert_eq!(kinds(":foo"), vec![TokenKind::Sym("foo".into())]);
888 assert_eq!(kinds("foo:"), vec![TokenKind::Label("foo".into())]);
889 }
890
891 #[test]
892 fn ruby_predicate_and_bang_suffixes_are_part_of_the_name() {
893 assert_eq!(
894 kinds("empty? save!"),
895 vec![
896 TokenKind::Ident("empty?".into()),
897 TokenKind::Ident("save!".into()),
898 ]
899 );
900 }
901
902 #[test]
910 fn a_bang_before_equals_is_the_operator_not_the_name() {
911 assert_eq!(
912 kinds("x!=3"),
913 vec![
914 TokenKind::Ident("x".into()),
915 TokenKind::Op("!=".into()),
916 TokenKind::Int(3),
917 ]
918 );
919 assert_eq!(
920 kinds("ok?! x"),
921 vec![
922 TokenKind::Ident("ok?".into()),
923 TokenKind::Op("!".into()),
924 TokenKind::Ident("x".into()),
925 ]
926 );
927 }
928
929 #[test]
930 fn lexes_strings_with_escapes() {
931 assert_eq!(kinds(r#""a\nb""#), vec![TokenKind::Str("a\nb".into())]);
932 }
933
934 #[test]
935 fn unterminated_string_is_an_error_with_a_span() {
936 let e = lex("\"oops").expect_err("must fail");
937 assert!(e.message.contains("unterminated"), "{}", e.message);
938 assert_eq!(e.span.start, 0);
939 }
940
941 #[test]
944 fn comments_and_newlines_are_emitted_as_trivia() {
945 let toks = lex("1 # hi\n2").expect("lex");
946 assert!(
947 toks.iter()
948 .any(|t| matches!(&t.kind, TokenKind::Comment(c) if c == "# hi")),
949 "comment was dropped: {toks:?}"
950 );
951 assert!(
952 toks.iter().any(|t| t.kind == TokenKind::Newline),
953 "newline was dropped"
954 );
955 }
956
957 #[test]
960 fn spans_point_at_the_actual_bytes() {
961 let src = "foo + 1";
962 let toks = lex(src).expect("lex");
963 let first = &toks[0];
964 assert_eq!(&src[first.span.start..first.span.end], "foo");
965 let last_int = toks
966 .iter()
967 .find(|t| matches!(t.kind, TokenKind::Int(_)))
968 .expect("an int token");
969 assert_eq!(&src[last_int.span.start..last_int.span.end], "1");
970 }
971
972 #[test]
973 fn lexes_word_lists() {
974 use crate::words::Flavor;
975 let src = "%w[a --force\n x/y] %i[pair? b]";
976 let toks = lex(src).expect("lex");
977 let TokenKind::Words { flavor, words } = &toks[0].kind else {
978 panic!("{toks:?}");
979 };
980 assert_eq!(*flavor, Flavor::Strings);
981 let spelled: Vec<&str> = words.iter().map(|(_, at)| &src[at.start..at.end]).collect();
982 assert_eq!(spelled, ["a", "--force", "x/y"]);
983 assert_eq!(
984 words.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>(),
985 spelled
986 );
987 assert_eq!(
988 &src[toks[0].span.start..toks[0].span.end],
989 "%w[a --force\n x/y]"
990 );
991 assert!(
992 matches!(&toks[1].kind, TokenKind::Words { flavor: Flavor::Keywords, words } if words.len() == 2)
993 );
994 assert_eq!(
995 kinds("%w[]"),
996 vec![TokenKind::Words {
997 flavor: Flavor::Strings,
998 words: vec![]
999 }]
1000 );
1001 }
1002
1003 #[test]
1004 fn a_percent_is_still_modulo_unless_w_or_i_and_a_bracket_follow() {
1005 assert_eq!(
1006 kinds("a %w(b)"),
1007 vec![
1008 TokenKind::Ident("a".into()),
1009 TokenKind::Op("%".into()),
1010 TokenKind::Ident("w".into()),
1011 TokenKind::LParen,
1012 TokenKind::Ident("b".into()),
1013 TokenKind::RParen,
1014 ]
1015 );
1016 assert_eq!(kinds("x%i")[1], TokenKind::Op("%".into()));
1017 }
1018
1019 #[test]
1020 fn a_word_list_refuses_commas_quotes_colons_and_an_open_end() {
1021 for (src, says) in [
1022 ("%w[a, b]", "not commas"),
1023 ("%w[\"a\" b]", "without quotes"),
1024 ("%i[:a b]", "without its colon"),
1025 ("%i[a-b]", "not a name"),
1026 ("%w[a # b]", "cannot be part of a word"),
1027 ("%w[a b", "unterminated"),
1028 ("%w[a\u{a0}b]", "cannot be part of a word"),
1029 ] {
1030 let e = lex(src).expect_err(src);
1031 assert!(e.message.contains(says), "{src}: {}", e.message);
1032 }
1033 }
1034
1035 #[test]
1036 fn lexes_pipeline_and_rocket() {
1037 assert_eq!(kinds("|> =>"), vec![TokenKind::Pipe, TokenKind::Rocket]);
1038 }
1039}