1use cove_diag::{Diagnostic, FileId, SourceMap, Span};
9
10use crate::token::{Keyword, StringPart, Token, TokenKind};
11
12enum Unclosed {
15 Brace,
16 Quote,
17 Apostrophe,
18}
19
20pub fn lex(sources: &SourceMap, file: FileId) -> Result<Vec<Token>, Vec<Diagnostic>> {
27 let (tokens, diagnostics) = lex_recovered(sources, file);
28 if diagnostics.is_empty() {
29 Ok(tokens)
30 } else {
31 Err(diagnostics)
32 }
33}
34
35pub fn lex_recovered(sources: &SourceMap, file: FileId) -> (Vec<Token>, Vec<Diagnostic>) {
57 let text = sources.get(file).text.as_str();
58 let mut lexer = Lexer {
59 text,
60 file,
61 pos: 0,
62 tokens: Vec::new(),
63 diagnostics: Vec::new(),
64 pending_newline: false,
65 };
66 lexer.run();
67 (lexer.tokens, lexer.diagnostics)
68}
69
70struct Lexer<'a> {
71 text: &'a str,
72 file: FileId,
73 pos: usize,
74 tokens: Vec<Token>,
75 diagnostics: Vec<Diagnostic>,
76 pending_newline: bool,
79}
80
81fn escaped_char(escaped: char) -> Option<char> {
95 Some(match escaped {
96 '\\' => '\\',
97 '"' => '"',
98 '\'' => '\'',
99 'n' => '\n',
100 't' => '\t',
101 'r' => '\r',
102 '0' => '\0',
103 '{' => '{',
104 '}' => '}',
105 _ => return None,
106 })
107}
108
109fn is_ident_start(c: char) -> bool {
110 c.is_ascii_alphabetic() || c == '_'
111}
112
113fn is_ident_continue(c: char) -> bool {
114 c.is_ascii_alphanumeric() || c == '_'
115}
116
117fn duration_factor(unit: &str) -> Option<i64> {
120 Some(match unit {
121 "ns" => 1,
122 "us" => 1_000,
123 "ms" => 1_000_000,
124 "s" => 1_000_000_000,
125 "m" => 60_000_000_000,
126 "h" => 3_600_000_000_000,
127 _ => return None,
128 })
129}
130
131impl<'a> Lexer<'a> {
132 fn peek_char(&self) -> Option<char> {
133 self.text[self.pos..].chars().next()
134 }
135
136 fn peek_char_at(&self, n: usize) -> Option<char> {
137 self.text[self.pos..].chars().nth(n)
138 }
139
140 fn bump(&mut self) -> Option<char> {
141 let c = self.peek_char()?;
142 self.pos += c.len_utf8();
143 Some(c)
144 }
145
146 fn push_token(&mut self, kind: TokenKind, start: usize) {
149 let span = Span::new(self.file, start as u32, self.pos as u32);
150 let preceded_by_newline = std::mem::take(&mut self.pending_newline);
151 self.tokens.push(Token {
152 kind,
153 span,
154 preceded_by_newline,
155 });
156 }
157
158 fn run(&mut self) {
159 loop {
160 self.skip_whitespace();
161 let start = self.pos;
162 let Some(c) = self.peek_char() else { break };
163
164 if c == '/' {
165 if let Some(kind) = self.handle_slash() {
166 self.push_token(kind, start);
167 }
168 continue;
169 }
170
171 if c == '"' {
172 self.bump();
173 if let Some(kind) = self.lex_string(start) {
174 self.push_token(kind, start);
175 }
176 continue;
177 }
178
179 if c == '\'' {
180 self.bump();
181 if let Some(kind) = self.lex_code_point(start) {
182 self.push_token(kind, start);
183 }
184 continue;
185 }
186
187 if c.is_ascii_digit() {
188 let kind = self.lex_number(start);
189 self.push_token(kind, start);
190 continue;
191 }
192
193 if is_ident_start(c) {
194 let kind = self.lex_ident(start);
195 self.push_token(kind, start);
196 continue;
197 }
198
199 if let Some(kind) = self.lex_operator() {
200 self.push_token(kind, start);
201 continue;
202 }
203
204 if c == '@' {
205 self.bump();
206 self.reserved_annotation(start);
207 continue;
208 }
209
210 self.bump();
211 self.unexpected_character(c, start);
212 }
213
214 let eof = self.pos as u32;
215 self.push_token(TokenKind::Eof, eof as usize);
216 }
217
218 fn skip_whitespace(&mut self) {
219 while let Some(c) = self.peek_char() {
220 match c {
221 '\n' => self.pending_newline = true,
222 ' ' | '\t' | '\r' => {}
223 _ => return,
224 }
225 self.bump();
226 }
227 }
228
229 fn unexpected_character(&mut self, c: char, start: usize) {
230 let span = Span::new(self.file, start as u32, self.pos as u32);
231 let mut diag = Diagnostic::error(
232 "cove::lex::unexpected_character",
233 format!("unexpected character `{c}`"),
234 )
235 .at(span);
236 if c == ';' {
237 diag = diag
238 .rule("Cove statements are not terminated by `;`.")
239 .help("Remove the `;`; the next token or a newline ends the statement.");
240 }
241 self.diagnostics.push(diag);
242 }
243
244 fn reserved_annotation(&mut self, start: usize) {
250 let span = Span::new(self.file, start as u32, self.pos as u32);
251 self.diagnostics.push(
252 Diagnostic::error(
253 "cove::parse::reserved_annotation",
254 "`@` is reserved decorator syntax",
255 )
256 .at(span)
257 .rule(
258 "Decorator syntax is reserved for behavior with specified compiler or runtime \
259 semantics; the MVP defines no annotations, so an unknown annotation is an error.",
260 )
261 .help("remove the `@...`; there is no annotation the MVP recognizes yet"),
262 );
263 }
264
265 fn find_line_end(&self) -> usize {
266 match self.text[self.pos..].find('\n') {
267 Some(i) => self.pos + i,
268 None => self.text.len(),
269 }
270 }
271
272 fn handle_slash(&mut self) -> Option<TokenKind> {
278 let rest = &self.text[self.pos..];
279
280 if rest.starts_with("///") && !rest.starts_with("////") {
281 self.pos += 3;
282 let content_start = self.pos;
283 let line_end = self.find_line_end();
284 let content = &self.text[content_start..line_end];
285 let content = content.strip_prefix(' ').unwrap_or(content);
286 let text = content.trim_end().to_string();
287 self.pos = line_end;
288 return Some(TokenKind::DocComment(text));
289 }
290
291 if rest.starts_with("//") {
292 self.pos = self.find_line_end();
293 return None;
294 }
295
296 if rest.starts_with("/*") {
297 let start = self.pos;
298 self.pos += 2;
299 self.skip_block_comment(start);
300 if self.text[start..self.pos].contains('\n') {
301 self.pending_newline = true;
302 }
303 return None;
304 }
305
306 if rest.starts_with("/=") {
307 self.pos += 2;
308 return Some(TokenKind::SlashEq);
309 }
310
311 self.pos += 1;
312 Some(TokenKind::Slash)
313 }
314
315 fn skip_block_comment(&mut self, start: usize) {
319 let mut depth = 1u32;
320 loop {
321 if self.text[self.pos..].starts_with("*/") {
322 self.pos += 2;
323 depth -= 1;
324 if depth == 0 {
325 return;
326 }
327 continue;
328 }
329 if self.text[self.pos..].starts_with("/*") {
330 self.pos += 2;
331 depth += 1;
332 continue;
333 }
334 if self.bump().is_none() {
335 let span = Span::new(self.file, start as u32, self.pos as u32);
336 self.diagnostics.push(
337 Diagnostic::error(
338 "cove::lex::unterminated_block_comment",
339 "block comment is never closed",
340 )
341 .at(span)
342 .help("Add a matching `*/`."),
343 );
344 return;
345 }
346 }
347 }
348
349 fn lex_ident(&mut self, start: usize) -> TokenKind {
350 while let Some(c) = self.peek_char() {
351 if is_ident_continue(c) {
352 self.bump();
353 } else {
354 break;
355 }
356 }
357 let text = &self.text[start..self.pos];
358 match text {
359 "_" => TokenKind::Underscore,
360 "true" => TokenKind::Bool(true),
361 "false" => TokenKind::Bool(false),
362 _ => match Keyword::from_text(text) {
363 Some(keyword) => TokenKind::Keyword(keyword),
364 None => TokenKind::Ident(text.to_string()),
365 },
366 }
367 }
368
369 fn lex_number(&mut self, start: usize) -> TokenKind {
370 if self.text[start..].starts_with("0x") {
371 let digits_start = start + 2;
372 if matches!(self.text[digits_start..].chars().next(), Some(c) if c.is_ascii_hexdigit())
373 {
374 self.pos = digits_start;
375 return self.lex_radix_integer(start, 16, |c| c.is_ascii_hexdigit());
376 }
377 } else if self.text[start..].starts_with("0b") {
378 let digits_start = start + 2;
379 if matches!(self.text[digits_start..].chars().next(), Some('0' | '1')) {
380 self.pos = digits_start;
381 return self.lex_radix_integer(start, 2, |c| c == '0' || c == '1');
382 }
383 }
384 self.lex_decimal_number(start)
385 }
386
387 fn lex_radix_integer(
388 &mut self,
389 start: usize,
390 radix: u32,
391 pred: impl Fn(char) -> bool,
392 ) -> TokenKind {
393 let digits_start = self.pos;
394 while let Some(c) = self.peek_char() {
395 if pred(c) || c == '_' {
396 self.bump();
397 } else {
398 break;
399 }
400 }
401 let raw = self.text[digits_start..self.pos].replace('_', "");
402 self.finish_integer_or_duration(start, &raw, radix)
403 }
404
405 fn lex_decimal_number(&mut self, start: usize) -> TokenKind {
406 while let Some(c) = self.peek_char() {
407 if c.is_ascii_digit() || c == '_' {
408 self.bump();
409 } else {
410 break;
411 }
412 }
413
414 let mut is_float = false;
415
416 if self.peek_char() == Some('.')
417 && matches!(self.peek_char_at(1), Some(d) if d.is_ascii_digit())
418 {
419 is_float = true;
420 self.bump();
421 while let Some(c) = self.peek_char() {
422 if c.is_ascii_digit() || c == '_' {
423 self.bump();
424 } else {
425 break;
426 }
427 }
428 }
429
430 if matches!(self.peek_char(), Some('e' | 'E')) {
431 let mark = self.pos;
432 self.bump();
433 if matches!(self.peek_char(), Some('+' | '-')) {
434 self.bump();
435 }
436 if matches!(self.peek_char(), Some(d) if d.is_ascii_digit()) {
437 is_float = true;
438 while let Some(c) = self.peek_char() {
439 if c.is_ascii_digit() || c == '_' {
440 self.bump();
441 } else {
442 break;
443 }
444 }
445 } else {
446 self.pos = mark;
447 }
448 }
449
450 if is_float {
451 let text = self.text[start..self.pos].replace('_', "");
452 let value: f64 = text
453 .parse()
454 .expect("lexer produced a malformed float literal");
455 TokenKind::Float(value)
456 } else {
457 let raw = self.text[start..self.pos].replace('_', "");
458 self.finish_integer_or_duration(start, &raw, 10)
459 }
460 }
461
462 fn finish_integer_or_duration(
466 &mut self,
467 start: usize,
468 raw_digits: &str,
469 radix: u32,
470 ) -> TokenKind {
471 let suffix_start = self.pos;
472 while let Some(c) = self.peek_char() {
473 if is_ident_continue(c) {
474 self.bump();
475 } else {
476 break;
477 }
478 }
479 let suffix = self.text[suffix_start..self.pos].to_string();
480 let span = Span::new(self.file, start as u32, self.pos as u32);
481
482 if suffix.is_empty() {
483 return match i64::from_str_radix(raw_digits, radix) {
484 Ok(value) => TokenKind::Int(value),
485 Err(_) => {
486 let message = format!(
487 "integer literal `{}` does not fit in a 64-bit integer",
488 &self.text[start..self.pos]
489 );
490 self.diagnostics.push(
491 Diagnostic::error("cove::lex::integer_out_of_range", message)
492 .at(span)
493 .help("Use a smaller value, or split the computation across multiple steps."),
494 );
495 TokenKind::Int(0)
496 }
497 };
498 }
499
500 if let Some(factor) = duration_factor(&suffix) {
501 let ns = i64::from_str_radix(raw_digits, radix)
502 .ok()
503 .and_then(|value| value.checked_mul(factor));
504 match ns {
505 Some(ns) => TokenKind::Duration(ns),
506 None => {
507 let message = format!(
508 "duration literal `{}` overflows a 64-bit nanosecond count",
509 &self.text[start..self.pos]
510 );
511 self.diagnostics.push(
512 Diagnostic::error("cove::lex::duration_out_of_range", message)
513 .at(span)
514 .help("Use a smaller value or a coarser unit."),
515 );
516 TokenKind::Duration(0)
517 }
518 }
519 } else {
520 let message = format!("`{suffix}` is not a valid literal suffix");
521 self.diagnostics.push(
522 Diagnostic::error("cove::lex::invalid_number_suffix", message)
523 .at(span)
524 .help("Valid duration suffixes are `ns`, `us`, `ms`, `s`, `m`, and `h`."),
525 );
526 TokenKind::Int(0)
527 }
528 }
529
530 fn lex_string(&mut self, quote_start: usize) -> Option<TokenKind> {
534 let mut parts = Vec::new();
535 let mut current = String::new();
536
537 loop {
538 match self.peek_char() {
539 None => {
540 self.unterminated_string(quote_start);
541 return None;
542 }
543 Some('"') => {
544 self.bump();
545 if !current.is_empty() {
546 parts.push(StringPart::Text(current));
547 }
548 return Some(TokenKind::Str(parts));
549 }
550 Some('\\') => {
551 let esc_start = self.pos;
552 self.bump();
553 match self.peek_char() {
554 None => {
555 self.unterminated_string(quote_start);
556 return None;
557 }
558 Some(escaped) => {
559 self.bump();
560 match escaped_char(escaped) {
561 Some(character) => current.push(character),
562 None => self.unknown_escape(escaped, esc_start),
563 }
564 }
565 }
566 }
567 Some('{') => {
568 let brace_start = self.pos;
569 self.bump();
570 if !current.is_empty() {
571 parts.push(StringPart::Text(std::mem::take(&mut current)));
572 }
573 let interp_start = self.pos;
574 match self.skip_interpolation_body() {
575 Ok(()) => {
576 let interp_end = self.pos - 1;
577 let source = self.text[interp_start..interp_end].to_string();
578 let span = Span::new(self.file, interp_start as u32, interp_end as u32);
579 parts.push(StringPart::Interpolation { source, span });
580 }
581 Err(()) => {
582 let span = Span::new(self.file, brace_start as u32, self.pos as u32);
583 self.diagnostics.push(
584 Diagnostic::error(
585 "cove::lex::unterminated_interpolation",
586 "string interpolation is never closed",
587 )
588 .at(span)
589 .help("Add a matching `}`."),
590 );
591 return None;
592 }
593 }
594 }
595 Some(c) => {
596 self.bump();
597 current.push(c);
598 }
599 }
600 }
601 }
602
603 fn lex_code_point(&mut self, quote_start: usize) -> Option<TokenKind> {
617 let mut scalars = String::new();
618 loop {
619 match self.peek_char() {
620 None => {
621 let span = Span::new(self.file, quote_start as u32, self.pos as u32);
622 self.diagnostics.push(
623 Diagnostic::error(
624 "cove::lex::unterminated_code_point",
625 "code-point literal is never closed",
626 )
627 .at(span)
628 .help("Add a closing `'`."),
629 );
630 return None;
631 }
632 Some('\'') => {
633 self.bump();
634 let span = Span::new(self.file, quote_start as u32, self.pos as u32);
635 let mut characters = scalars.chars();
636 let Some(first) = characters.next() else {
637 self.diagnostics.push(
638 Diagnostic::error(
639 "cove::lex::empty_code_point",
640 "code-point literal names no character",
641 )
642 .at(span)
643 .rule("A code-point literal holds exactly one Unicode scalar value.")
644 .help("Write the character between the quotes, as `'a'`. The empty string is `\"\"`."),
645 );
646 return Some(TokenKind::Int(0));
647 };
648 if characters.next().is_some() {
649 let count = scalars.chars().count();
650 self.diagnostics.push(
651 Diagnostic::error(
652 "cove::lex::code_point_is_not_one_scalar",
653 format!("code-point literal holds {count} scalar values, not one"),
654 )
655 .at(span)
656 .rule(
657 "A code-point literal holds exactly one Unicode scalar value, so a \
658 combining pair or an emoji sequence is more than one and is not a \
659 code point.",
660 )
661 .help("Write one scalar, or use a `\"...\"` string for text of any length."),
662 );
663 }
664 return Some(TokenKind::Int(first as i64));
665 }
666 Some('\\') => {
667 let esc_start = self.pos;
668 self.bump();
669 match self.peek_char() {
670 None => {
671 let span = Span::new(self.file, quote_start as u32, self.pos as u32);
672 self.diagnostics.push(
673 Diagnostic::error(
674 "cove::lex::unterminated_code_point",
675 "code-point literal is never closed",
676 )
677 .at(span)
678 .help("Add a closing `'`."),
679 );
680 return None;
681 }
682 Some(escaped) => {
683 self.bump();
684 match escaped_char(escaped) {
685 Some(character) => scalars.push(character),
686 None => self.unknown_escape(escaped, esc_start),
687 }
688 }
689 }
690 }
691 Some(c) => {
692 self.bump();
693 scalars.push(c);
694 }
695 }
696 }
697 }
698
699 fn unknown_escape(&mut self, escaped: char, esc_start: usize) {
701 let span = Span::new(self.file, esc_start as u32, self.pos as u32);
702 let message = format!("unknown escape sequence `\\{escaped}`");
703 self.diagnostics.push(
704 Diagnostic::error("cove::lex::unknown_escape", message)
705 .at(span)
706 .help(
707 "Use one of the supported escapes: \\\\, \\\", \\', \\n, \\t, \\r, \\0, \\{, \\}.",
708 ),
709 );
710 }
711
712 fn unterminated_string(&mut self, quote_start: usize) {
713 let span = Span::new(self.file, quote_start as u32, self.pos as u32);
714 self.diagnostics.push(
715 Diagnostic::error(
716 "cove::lex::unterminated_string",
717 "string literal is never closed",
718 )
719 .at(span)
720 .help("Add a closing `\"`."),
721 );
722 }
723
724 fn skip_interpolation_body(&mut self) -> Result<(), ()> {
739 let mut unclosed = vec![Unclosed::Brace];
740 while let Some(innermost) = unclosed.last() {
741 let inside_scalar = matches!(innermost, Unclosed::Apostrophe);
748 let escaping = matches!(innermost, Unclosed::Quote | Unclosed::Apostrophe);
749 match self.peek_char() {
750 None => return Err(()),
751 Some('\\') if escaping => {
752 self.bump();
753 if self.peek_char().is_none() {
754 return Err(());
755 }
756 self.bump();
757 }
758 Some('"') if !inside_scalar => {
759 self.bump();
760 if matches!(innermost, Unclosed::Quote) {
761 unclosed.pop();
762 } else {
763 unclosed.push(Unclosed::Quote);
764 }
765 }
766 Some('\'') if !matches!(innermost, Unclosed::Quote) => {
769 self.bump();
770 if inside_scalar {
771 unclosed.pop();
772 } else {
773 unclosed.push(Unclosed::Apostrophe);
774 }
775 }
776 Some('{') if !inside_scalar => {
777 self.bump();
778 unclosed.push(Unclosed::Brace);
779 }
780 Some('}') if matches!(innermost, Unclosed::Brace) => {
781 self.bump();
782 unclosed.pop();
783 }
784 Some(_) => {
785 self.bump();
786 }
787 }
788 }
789 Ok(())
790 }
791
792 fn lex_operator(&mut self) -> Option<TokenKind> {
796 let rest = &self.text[self.pos..];
797
798 macro_rules! op {
799 ($lit:literal, $kind:expr) => {
800 if rest.starts_with($lit) {
801 self.pos += $lit.len();
802 return Some($kind);
803 }
804 };
805 }
806
807 op!("...", TokenKind::Ellipsis);
808 op!("..<", TokenKind::DotDotLt);
809 op!("..", TokenKind::DotDot);
810 op!(".", TokenKind::Dot);
811 op!("->", TokenKind::Arrow);
812 op!("=>", TokenKind::FatArrow);
813 op!("==", TokenKind::EqEq);
814 op!("!=", TokenKind::BangEq);
815 op!("<=", TokenKind::LtEq);
816 op!(">=", TokenKind::GtEq);
817 op!("+=", TokenKind::PlusEq);
818 op!("-=", TokenKind::MinusEq);
819 op!("*=", TokenKind::StarEq);
820 op!("%=", TokenKind::PercentEq);
821 op!("&&", TokenKind::AmpAmp);
822 op!("||", TokenKind::PipePipe);
823 op!("=", TokenKind::Eq);
824 op!("!", TokenKind::Bang);
825 op!("<", TokenKind::Lt);
826 op!(">", TokenKind::Gt);
827 op!("+", TokenKind::Plus);
828 op!("-", TokenKind::Minus);
829 op!("*", TokenKind::Star);
830 op!("%", TokenKind::Percent);
831 op!("?", TokenKind::Question);
832 op!(",", TokenKind::Comma);
833 op!(":", TokenKind::Colon);
834 op!("(", TokenKind::LParen);
835 op!(")", TokenKind::RParen);
836 op!("{", TokenKind::LBrace);
837 op!("}", TokenKind::RBrace);
838 op!("[", TokenKind::LBracket);
839 op!("]", TokenKind::RBracket);
840
841 None
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848
849 fn lex_ok(src: &str) -> Vec<Token> {
850 let mut sources = SourceMap::new();
851 let file = sources.add("test.cove", src);
852 lex(&sources, file).unwrap_or_else(|diags| {
853 panic!("expected `{src}` to lex successfully, got errors: {diags:?}")
854 })
855 }
856
857 fn lex_err(src: &str) -> Vec<Diagnostic> {
858 let mut sources = SourceMap::new();
859 let file = sources.add("test.cove", src);
860 match lex(&sources, file) {
861 Ok(tokens) => panic!("expected `{src}` to fail to lex, got tokens: {tokens:?}"),
862 Err(diags) => diags,
863 }
864 }
865
866 fn kinds(src: &str) -> Vec<TokenKind> {
867 let tokens = lex_ok(src);
868 assert!(
869 matches!(tokens.last().unwrap().kind, TokenKind::Eof),
870 "token stream must end with Eof, got {tokens:?}"
871 );
872 tokens[..tokens.len() - 1]
873 .iter()
874 .map(|t| t.kind.clone())
875 .collect()
876 }
877
878 #[test]
879 fn empty_file_is_just_eof() {
880 let tokens = lex_ok("");
881 assert_eq!(tokens.len(), 1);
882 assert_eq!(tokens[0].kind, TokenKind::Eof);
883 assert_eq!(tokens[0].span.start, 0);
884 assert_eq!(tokens[0].span.end, 0);
885 }
886
887 #[test]
888 fn whitespace_produces_no_tokens() {
889 assert_eq!(kinds(" \t\r\n foo"), vec![TokenKind::Ident("foo".into())]);
890 }
891
892 #[test]
895 fn tokens_record_a_preceding_line_break() {
896 let tokens = lex_ok("a b\nc /* x\ny */ d // e\nf");
897 let flags: Vec<bool> = tokens.iter().map(|t| t.preceded_by_newline).collect();
898 assert_eq!(flags, vec![false, false, true, true, true, false]);
900 }
901
902 #[test]
903 fn a_block_comment_on_one_line_is_not_a_line_break() {
904 let tokens = lex_ok("a /* x */ b");
905 assert!(!tokens[1].preceded_by_newline);
906 }
907
908 #[test]
909 fn keywords_vs_identifiers() {
910 assert_eq!(
911 kinds("fn foo let bar self"),
912 vec![
913 TokenKind::Keyword(Keyword::Fn),
914 TokenKind::Ident("foo".into()),
915 TokenKind::Keyword(Keyword::Let),
916 TokenKind::Ident("bar".into()),
917 TokenKind::Keyword(Keyword::SelfValue),
918 ]
919 );
920 assert_eq!(kinds("forever"), vec![TokenKind::Ident("forever".into())]);
922 assert_eq!(
923 kinds("true false"),
924 vec![TokenKind::Bool(true), TokenKind::Bool(false)]
925 );
926 }
927
928 #[test]
929 fn is_is_a_keyword_and_maximal_munch_still_applies() {
930 assert_eq!(
931 kinds("a is b"),
932 vec![
933 TokenKind::Ident("a".into()),
934 TokenKind::Keyword(Keyword::Is),
935 TokenKind::Ident("b".into()),
936 ]
937 );
938 assert_eq!(kinds("island"), vec![TokenKind::Ident("island".into())]);
940 }
941
942 #[test]
943 fn underscore_is_its_own_token() {
944 assert_eq!(kinds("_"), vec![TokenKind::Underscore]);
945 assert_eq!(kinds("_foo"), vec![TokenKind::Ident("_foo".into())]);
946 assert_eq!(kinds("foo_"), vec![TokenKind::Ident("foo_".into())]);
947 }
948
949 #[test]
950 fn doc_comments_strip_one_leading_space_and_trailing_whitespace() {
951 assert_eq!(
952 kinds("/// hello\n/// world"),
953 vec![
954 TokenKind::DocComment("hello".into()),
955 TokenKind::DocComment("world".into()),
956 ]
957 );
958 assert_eq!(
959 kinds("///no-space"),
960 vec![TokenKind::DocComment("no-space".into())]
961 );
962 assert_eq!(
963 kinds("/// two spaces"),
964 vec![TokenKind::DocComment(" two spaces".into())]
965 );
966 assert_eq!(
967 kinds("///trailing \nfn"),
968 vec![
969 TokenKind::DocComment("trailing".into()),
970 TokenKind::Keyword(Keyword::Fn)
971 ]
972 );
973 }
974
975 #[test]
976 fn four_slashes_is_a_plain_comment_not_a_doc_comment() {
977 assert_eq!(kinds("//// not a doc\n42"), vec![TokenKind::Int(42)]);
978 }
979
980 #[test]
981 fn line_comments_are_discarded() {
982 assert_eq!(kinds("// hello\n42"), vec![TokenKind::Int(42)]);
983 }
984
985 #[test]
986 fn nested_block_comments_are_discarded() {
987 assert_eq!(
988 kinds("/* outer /* inner */ still outer */ 42"),
989 vec![TokenKind::Int(42)]
990 );
991 }
992
993 #[test]
994 fn unterminated_block_comment_is_an_error() {
995 let diags = lex_err("/* never closed");
996 assert_eq!(diags.len(), 1);
997 assert_eq!(diags[0].code, "cove::lex::unterminated_block_comment");
998 }
999
1000 #[test]
1001 fn decimal_integers_and_underscores() {
1002 assert_eq!(kinds("123"), vec![TokenKind::Int(123)]);
1003 assert_eq!(kinds("1_000"), vec![TokenKind::Int(1000)]);
1004 assert_eq!(kinds("0"), vec![TokenKind::Int(0)]);
1005 }
1006
1007 #[test]
1008 fn hex_and_binary_integers() {
1009 assert_eq!(kinds("0xFF"), vec![TokenKind::Int(255)]);
1010 assert_eq!(kinds("0b1010"), vec![TokenKind::Int(10)]);
1011 }
1012
1013 #[test]
1014 fn floats() {
1015 assert_eq!(kinds("1.5"), vec![TokenKind::Float(1.5)]);
1016 assert_eq!(kinds("1.5e10"), vec![TokenKind::Float(1.5e10)]);
1017 assert_eq!(kinds("1e-3"), vec![TokenKind::Float(1e-3)]);
1018 }
1019
1020 #[test]
1021 fn range_dot_is_only_part_of_a_float_before_a_digit() {
1022 assert_eq!(
1023 kinds("0..<10"),
1024 vec![TokenKind::Int(0), TokenKind::DotDotLt, TokenKind::Int(10)]
1025 );
1026 assert_eq!(
1027 kinds("0..n"),
1028 vec![
1029 TokenKind::Int(0),
1030 TokenKind::DotDot,
1031 TokenKind::Ident("n".into())
1032 ]
1033 );
1034 }
1035
1036 #[test]
1037 fn every_duration_unit() {
1038 assert_eq!(kinds("1ns"), vec![TokenKind::Duration(1)]);
1039 assert_eq!(kinds("1us"), vec![TokenKind::Duration(1_000)]);
1040 assert_eq!(kinds("1ms"), vec![TokenKind::Duration(1_000_000)]);
1041 assert_eq!(kinds("1s"), vec![TokenKind::Duration(1_000_000_000)]);
1042 assert_eq!(kinds("1m"), vec![TokenKind::Duration(60_000_000_000)]);
1043 assert_eq!(kinds("1h"), vec![TokenKind::Duration(3_600_000_000_000)]);
1044 assert_eq!(kinds("500ms"), vec![TokenKind::Duration(500_000_000)]);
1045 assert_eq!(kinds("60s"), vec![TokenKind::Duration(60_000_000_000)]);
1046 assert_eq!(kinds("5s"), vec![TokenKind::Duration(5_000_000_000)]);
1047 }
1048
1049 #[test]
1050 fn integer_out_of_range_is_an_error() {
1051 let diags = lex_err("99999999999999999999");
1052 assert_eq!(diags.len(), 1);
1053 assert_eq!(diags[0].code, "cove::lex::integer_out_of_range");
1054 }
1055
1056 #[test]
1057 fn duration_out_of_range_is_an_error() {
1058 let diags = lex_err("9999999999h");
1059 assert_eq!(diags.len(), 1);
1060 assert_eq!(diags[0].code, "cove::lex::duration_out_of_range");
1061 }
1062
1063 #[test]
1064 fn invalid_number_suffix_is_an_error() {
1065 let diags = lex_err("5sx");
1066 assert_eq!(diags.len(), 1);
1067 assert_eq!(diags[0].code, "cove::lex::invalid_number_suffix");
1068 }
1069
1070 #[test]
1071 fn string_escapes() {
1072 let tokens = kinds(r#""\\ \" \n \t \r \0 \{ \}""#);
1073 assert_eq!(
1074 tokens,
1075 vec![TokenKind::Str(vec![StringPart::Text(
1076 "\\ \" \n \t \r \0 { }".into()
1077 )])]
1078 );
1079 }
1080
1081 #[test]
1088 fn code_point_literals_are_int_tokens() {
1089 assert_eq!(kinds("'a'"), vec![TokenKind::Int(97)]);
1090 assert_eq!(kinds("'0'"), vec![TokenKind::Int(48)]);
1091 assert_eq!(kinds("' '"), vec![TokenKind::Int(32)]);
1092 assert_eq!(kinds("'{'"), vec![TokenKind::Int(123)]);
1093 assert_eq!(kinds("'\u{e9}'"), vec![TokenKind::Int(233)]);
1094 assert_eq!(kinds("'\u{3042}'"), vec![TokenKind::Int(12354)]);
1095 assert_eq!(kinds("'\u{1F600}'"), vec![TokenKind::Int(128512)]);
1096 }
1097
1098 #[test]
1100 fn code_point_literals_take_the_escapes_a_string_takes() {
1101 assert_eq!(kinds(r"'\n'"), vec![TokenKind::Int(10)]);
1102 assert_eq!(kinds(r"'\t'"), vec![TokenKind::Int(9)]);
1103 assert_eq!(kinds(r"'\r'"), vec![TokenKind::Int(13)]);
1104 assert_eq!(kinds(r"'\0'"), vec![TokenKind::Int(0)]);
1105 assert_eq!(kinds(r"'\\'"), vec![TokenKind::Int(92)]);
1106 assert_eq!(kinds(r"'\''"), vec![TokenKind::Int(39)]);
1107 assert_eq!(kinds(r#"'\"'"#), vec![TokenKind::Int(34)]);
1108 assert_eq!(kinds(r"'\{'"), vec![TokenKind::Int(123)]);
1109 assert_eq!(kinds(r"'\}'"), vec![TokenKind::Int(125)]);
1110 }
1111
1112 #[test]
1115 fn a_string_may_escape_an_apostrophe() {
1116 assert_eq!(
1117 kinds(r#""\'""#),
1118 vec![TokenKind::Str(vec![StringPart::Text("'".to_string())])]
1119 );
1120 }
1121
1122 #[test]
1123 fn an_empty_code_point_literal_is_an_error() {
1124 let diags = lex_err("''");
1125 assert_eq!(diags.len(), 1);
1126 assert_eq!(diags[0].code, "cove::lex::empty_code_point");
1127 }
1128
1129 #[test]
1133 fn a_code_point_literal_holding_more_than_one_scalar_is_an_error() {
1134 let diags = lex_err("'ab'");
1135 assert_eq!(diags.len(), 1);
1136 assert_eq!(diags[0].code, "cove::lex::code_point_is_not_one_scalar");
1137 assert_eq!(
1138 diags[0].message,
1139 "code-point literal holds 2 scalar values, not one"
1140 );
1141
1142 let diags = lex_err("'\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F466}'");
1143 assert_eq!(diags.len(), 1);
1144 assert_eq!(
1145 diags[0].message,
1146 "code-point literal holds 5 scalar values, not one"
1147 );
1148 }
1149
1150 #[test]
1151 fn an_unterminated_code_point_literal_is_an_error() {
1152 let diags = lex_err("'a");
1153 assert_eq!(diags.len(), 1);
1154 assert_eq!(diags[0].code, "cove::lex::unterminated_code_point");
1155 }
1156
1157 #[test]
1161 fn a_code_point_literal_inside_an_interpolation_may_hold_a_brace() {
1162 let tokens = kinds(r#""{ head == '{' }""#);
1163 assert_eq!(tokens.len(), 1);
1164 let TokenKind::Str(parts) = &tokens[0] else {
1165 panic!("expected a string token, got {tokens:?}");
1166 };
1167 assert_eq!(parts.len(), 1);
1168 let StringPart::Interpolation { source, .. } = &parts[0] else {
1169 panic!("expected one interpolation, got {parts:?}");
1170 };
1171 assert_eq!(source.trim(), "head == '{'");
1172 }
1173
1174 #[test]
1175 fn unknown_escape_is_an_error() {
1176 let diags = lex_err(r#""\q""#);
1177 assert_eq!(diags.len(), 1);
1178 assert_eq!(diags[0].code, "cove::lex::unknown_escape");
1179 }
1180
1181 #[test]
1182 fn unterminated_string_is_an_error() {
1183 let diags = lex_err("\"never closed");
1184 assert_eq!(diags.len(), 1);
1185 assert_eq!(diags[0].code, "cove::lex::unterminated_string");
1186 }
1187
1188 #[test]
1189 fn simple_interpolation() {
1190 let tokens = kinds(r#""a{b}c""#);
1191 assert_eq!(
1192 tokens,
1193 vec![TokenKind::Str(vec![
1194 StringPart::Text("a".into()),
1195 StringPart::Interpolation {
1196 source: "b".into(),
1197 span: Span::new(FileId(0), 3, 4),
1198 },
1199 StringPart::Text("c".into()),
1200 ])]
1201 );
1202 }
1203
1204 #[test]
1205 fn interpolation_with_nested_braces_and_nested_strings() {
1206 let tokens = kinds("\"{f(\"}\")}\"");
1211 assert_eq!(
1212 tokens,
1213 vec![TokenKind::Str(vec![StringPart::Interpolation {
1214 source: "f(\"}\")".into(),
1215 span: Span::new(FileId(0), 2, 8),
1216 }])]
1217 );
1218 }
1219
1220 #[test]
1221 fn multiple_interpolations_in_one_string() {
1222 let tokens = kinds(r#""{a}-{b}""#);
1223 assert_eq!(
1224 tokens,
1225 vec![TokenKind::Str(vec![
1226 StringPart::Interpolation {
1227 source: "a".into(),
1228 span: Span::new(FileId(0), 2, 3),
1229 },
1230 StringPart::Text("-".into()),
1231 StringPart::Interpolation {
1232 source: "b".into(),
1233 span: Span::new(FileId(0), 6, 7),
1234 },
1235 ])]
1236 );
1237 }
1238
1239 #[test]
1240 fn unterminated_interpolation_is_an_error() {
1241 let diags = lex_err(r#""{a"#);
1242 assert_eq!(diags.len(), 1);
1243 assert_eq!(diags[0].code, "cove::lex::unterminated_interpolation");
1244 }
1245
1246 #[test]
1247 fn longest_match_operators() {
1248 assert_eq!(
1249 kinds("... ..< .. ."),
1250 vec![
1251 TokenKind::Ellipsis,
1252 TokenKind::DotDotLt,
1253 TokenKind::DotDot,
1254 TokenKind::Dot,
1255 ]
1256 );
1257 assert_eq!(
1258 kinds("-> => == != <= >= += -= *= /= %= && ||"),
1259 vec![
1260 TokenKind::Arrow,
1261 TokenKind::FatArrow,
1262 TokenKind::EqEq,
1263 TokenKind::BangEq,
1264 TokenKind::LtEq,
1265 TokenKind::GtEq,
1266 TokenKind::PlusEq,
1267 TokenKind::MinusEq,
1268 TokenKind::StarEq,
1269 TokenKind::SlashEq,
1270 TokenKind::PercentEq,
1271 TokenKind::AmpAmp,
1272 TokenKind::PipePipe,
1273 ]
1274 );
1275 assert_eq!(
1276 kinds("= ! < > + - * / % ? , : ( ) { } [ ]"),
1277 vec![
1278 TokenKind::Eq,
1279 TokenKind::Bang,
1280 TokenKind::Lt,
1281 TokenKind::Gt,
1282 TokenKind::Plus,
1283 TokenKind::Minus,
1284 TokenKind::Star,
1285 TokenKind::Slash,
1286 TokenKind::Percent,
1287 TokenKind::Question,
1288 TokenKind::Comma,
1289 TokenKind::Colon,
1290 TokenKind::LParen,
1291 TokenKind::RParen,
1292 TokenKind::LBrace,
1293 TokenKind::RBrace,
1294 TokenKind::LBracket,
1295 TokenKind::RBracket,
1296 ]
1297 );
1298 assert_eq!(kinds("<=="), vec![TokenKind::LtEq, TokenKind::Eq]);
1300 }
1301
1302 #[test]
1303 fn semicolon_is_an_error_with_the_cove_rule() {
1304 let diags = lex_err(";");
1305 assert_eq!(diags.len(), 1);
1306 assert_eq!(diags[0].code, "cove::lex::unexpected_character");
1307 assert!(diags[0].rule.as_deref().unwrap().contains(';'));
1308 }
1309
1310 #[test]
1311 fn unexpected_character_is_an_error() {
1312 let diags = lex_err("`");
1313 assert_eq!(diags.len(), 1);
1314 assert_eq!(diags[0].code, "cove::lex::unexpected_character");
1315 }
1316
1317 #[test]
1318 fn at_sign_is_reserved_decorator_syntax() {
1319 let diags = lex_err("@decorate");
1320 assert_eq!(diags.len(), 1);
1321 assert_eq!(diags[0].code, "cove::parse::reserved_annotation");
1322 assert!(diags[0].rule.as_deref().unwrap().contains("Decorator"));
1323 }
1324
1325 #[test]
1326 fn all_errors_in_a_file_are_collected() {
1327 let diags = lex_err("` ~ #");
1328 assert_eq!(diags.len(), 3);
1329 for diag in &diags {
1330 assert_eq!(diag.code, "cove::lex::unexpected_character");
1331 }
1332 }
1333
1334 #[test]
1338 fn recovery_answers_the_tokens_before_an_error_as_well_as_the_error() {
1339 let mut sources = SourceMap::new();
1340 let file = sources.add("test.cove", "let n = 1\nlet greeting = \"open");
1341 let (tokens, diagnostics) = lex_recovered(&sources, file);
1342
1343 assert_eq!(diagnostics.len(), 1);
1344 assert_eq!(diagnostics[0].code, "cove::lex::unterminated_string");
1345 assert!(
1346 lex(&sources, file).is_err(),
1347 "`lex` still refuses what it always refused"
1348 );
1349
1350 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind.clone()).collect();
1351 assert_eq!(
1352 kinds,
1353 vec![
1354 TokenKind::Keyword(Keyword::Let),
1355 TokenKind::Ident("n".into()),
1356 TokenKind::Eq,
1357 TokenKind::Int(1),
1358 TokenKind::Keyword(Keyword::Let),
1359 TokenKind::Ident("greeting".into()),
1360 TokenKind::Eq,
1361 TokenKind::Eof,
1362 ],
1363 "everything up to the open quote, and nothing invented for it"
1364 );
1365 }
1366
1367 #[test]
1371 fn recovery_and_lex_agree_when_there_is_no_error() {
1372 let source = "export fn main() -> Int { 1 + 2 }";
1373 let mut sources = SourceMap::new();
1374 let file = sources.add("test.cove", source);
1375 let (tokens, diagnostics) = lex_recovered(&sources, file);
1376 assert!(diagnostics.is_empty());
1377 assert_eq!(tokens, lex(&sources, file).expect("it lexes"));
1378 }
1379
1380 #[test]
1381 fn duration_example_program_lexes() {
1382 let tokens =
1383 kinds("clock.timeout(500ms) { retry(3, attempts) }\nfor attempt in 0..<attempts {}");
1384 assert!(tokens.contains(&TokenKind::Duration(500_000_000)));
1385 assert!(tokens.contains(&TokenKind::DotDotLt));
1386 assert!(tokens.contains(&TokenKind::Keyword(Keyword::For)));
1387 }
1388}