1use crate::compiler_error::{CompileError, CompilerFailure, CompilerStage};
2use crate::literal_units::{push_literal_char, push_lone_surrogate};
3use num_bigint::BigUint;
4use num_traits::Num;
5use unicode_ident::{is_xid_continue, is_xid_start};
6
7use crate::{Diagnostic, FileId, RawDoc, Severity, Span, Token, TokenKind};
8
9const VALID_ESCAPES: &str =
10 "valid escapes: \\\", \\', \\\\, \\/, \\n, \\t, \\r, \\b, \\f, \\v, \\0, \\u{HHHH}";
11
12const BOM: &[u8] = "\u{feff}".as_bytes();
14const MAX_LEXER_DIAGNOSTICS: usize = 20;
17
18pub struct Lexer<'a> {
19 source: &'a str,
20 bytes: &'a [u8],
21 file: FileId,
22 pos: u32,
23 diagnostics: Vec<Diagnostic>,
24 fatal: Option<CompilerFailure>,
25 pending_docs: Vec<RawDoc>,
27 template_frames: Vec<u32>,
31 last_significant_token: Option<TokenKind>,
33 last_bang_is_postfix: bool,
34}
35
36impl<'a> Lexer<'a> {
37 pub fn new(source: &'a str, file: FileId) -> Self {
42 Self {
43 source,
44 bytes: source.as_bytes(),
45 file,
46 pos: if source.as_bytes().starts_with(BOM) {
51 BOM.len() as u32
52 } else {
53 0
54 },
55 diagnostics: Vec::new(),
56 fatal: (source.len() > (u32::MAX - 4) as usize).then(|| CompilerFailure::Limit {
57 stage: CompilerStage::Parse,
58 span: None,
59 message: "source exceeds the 32-bit lexer offset limit".into(),
60 help: vec!["split the source into smaller modules".into()],
61 }),
62 pending_docs: Vec::new(),
63 template_frames: Vec::new(),
64 last_significant_token: None,
65 last_bang_is_postfix: false,
66 }
67 }
68
69 fn span(&self, start: u32, end: u32) -> Span {
70 Span {
71 file: self.file,
72 start,
73 end,
74 }
75 }
76
77 pub fn next_token(&mut self) -> Token {
78 let token = self.next_token_inner();
79 if self.fatal.is_none()
80 && let Err(error) = token.span.text(self.source, self.file)
81 {
82 self.fatal = Some(error.into_compiler_failure(CompilerStage::Parse));
83 return self.eof_token();
84 }
85 token
86 }
87
88 fn next_token_inner(&mut self) -> Token {
89 if self.fatal.is_some() {
90 return self.eof_token();
91 }
92 loop {
93 self.skip_trivia();
94 let Some(b) = self.peek() else {
95 let tok = self.eof_token();
96 return self.finalize(tok);
97 };
98 let tok = match b {
99 b'\n' | b'\r' => return self.lex_newline(),
101 b'0'..=b'9' => self.lex_number(),
102 b'.' if self.digit_at(1) => self.lex_number(),
103 b'"' | b'\'' => self.lex_string(b),
104 b'`' => {
105 let start = self.pos;
106 self.pos += 1;
107 self.lex_template_part(start, true)
108 }
109 b'=' | b'!' | b'<' | b'>' | b'+' | b'-' | b'*' | b'/' | b'%' | b'.' => {
110 self.lex_operator()
111 }
112 b'&' | b'^' | b'~' => self.lex_operator(),
113 b'|' => self.lex_operator(),
114 b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b':' | b';' | b'?' => {
115 self.lex_delimiter()
116 }
117 _ => {
118 if b.is_ascii_alphabetic() || b == b'_' || b == b'$' {
119 self.lex_ident()
120 } else if b >= 0x80
121 && let Some(c) = self.peek_char()
122 {
123 if is_xid_start(c) {
124 self.lex_ident()
125 } else {
126 self.diagnose_unexpected_char(c);
127 self.pos += c.len_utf8() as u32;
128 continue;
129 }
130 } else {
131 self.diagnose_unexpected_byte(b);
132 self.pos += 1;
133 continue;
134 }
135 }
136 };
137 return self.finalize(tok);
138 }
139 }
140
141 fn finalize(&mut self, tok: Token) -> Token {
142 let tok = self.attach_doc(tok);
143 match &tok.kind {
144 TokenKind::Newline => {}
145 other => {
146 self.last_bang_is_postfix =
147 matches!(other, TokenKind::Bang) && !self.regex_context();
148 self.last_significant_token = Some(other.clone());
149 }
150 }
151 tok
152 }
153
154 pub(crate) fn set_bang_is_postfix(&mut self, postfix: bool) {
157 self.last_bang_is_postfix = postfix;
158 }
159
160 fn regex_context(&self) -> bool {
161 !self.last_bang_is_postfix && is_regex_context(&self.last_significant_token)
162 }
163
164 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
165 let file = self.file;
166 self.finish()
167 .unwrap_or_else(|error| error.into_diagnostics(file))
168 }
169
170 pub fn finish(self) -> Result<Vec<Diagnostic>, CompileError> {
171 match self.fatal {
172 Some(fatal) => Err(CompileError {
173 diagnostics: self.diagnostics,
174 fatal: Some(fatal),
175 }),
176 None => Ok(self.diagnostics),
177 }
178 }
179
180 fn fail(&mut self, message: &str) -> Token {
181 self.fatal.get_or_insert_with(|| CompilerFailure::Internal {
182 stage: CompilerStage::Parse,
183 span: None,
184 message: message.into(),
185 });
186 self.eof_token()
187 }
188
189 fn peek(&self) -> Option<u8> {
190 if self.fatal.is_some() {
191 return None;
192 }
193 self.bytes.get(self.pos as usize).copied()
194 }
195
196 fn digit_at(&self, offset: usize) -> bool {
197 self.peek_at(offset).is_some_and(|b| b.is_ascii_digit())
198 }
199
200 fn peek_at(&self, offset: usize) -> Option<u8> {
201 (self.pos as usize)
202 .checked_add(offset)
203 .and_then(|index| self.bytes.get(index))
204 .copied()
205 }
206
207 fn peek_char(&self) -> Option<char> {
208 self.source.get(self.pos as usize..)?.chars().next()
209 }
210
211 fn eof_token(&self) -> Token {
212 Token::new(TokenKind::Eof, self.span(self.pos, self.pos))
213 }
214
215 fn error(&mut self, span: Span, message: impl Into<String>) {
216 if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
217 return;
218 }
219 self.diagnostics.push(Diagnostic {
220 severity: Severity::Error,
221 span,
222 message: message.into(),
223 help: vec![],
224 notes: vec![],
225 });
226 }
227
228 fn error_with_help(&mut self, span: Span, message: impl Into<String>, help: Vec<String>) {
229 if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
230 return;
231 }
232 self.diagnostics.push(Diagnostic {
233 severity: Severity::Error,
234 span,
235 message: message.into(),
236 help,
237 notes: vec![],
238 });
239 }
240
241 fn diagnose_unexpected_byte(&mut self, b: u8) {
242 if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
243 return;
244 }
245 let span = self.span(self.pos, self.pos + 1);
246 let message = if b.is_ascii() && !b.is_ascii_control() {
247 format!("unexpected character `{}`", b as char)
248 } else {
249 format!("unexpected byte 0x{b:02X}")
250 };
251 self.error(span, message);
252 }
253
254 fn diagnose_unexpected_char(&mut self, c: char) {
255 if self.diagnostics.len() >= MAX_LEXER_DIAGNOSTICS {
256 return;
257 }
258 let len = c.len_utf8() as u32;
259 self.error(
260 self.span(self.pos, self.pos + len),
261 format!("unexpected character `{c}`"),
262 );
263 }
264
265 fn skip_trivia(&mut self) {
266 loop {
267 match self.peek() {
268 Some(b' ' | b'\t' | 0x0B | 0x0C) => self.pos += 1,
270 Some(0x80..) if self.skip_space_separator() => {}
271 Some(b'/') => match self.peek_at(1) {
272 Some(b'/') => self.skip_line_comment(),
273 Some(b'*') => {
274 if self.peek_at(2) == Some(b'*') && self.peek_at(3) != Some(b'/') {
276 self.capture_doc_comment();
277 } else {
278 self.skip_block_comment();
279 }
280 }
281 _ => return,
282 },
283 _ => return,
284 }
285 }
286 }
287
288 fn skip_space_separator(&mut self) -> bool {
290 let Some(c) = self.peek_char().filter(|&c| is_space_separator(c)) else {
291 return false;
292 };
293 self.pos += c.len_utf8() as u32;
294 true
295 }
296
297 fn skip_line_comment(&mut self) {
298 self.pos += 2;
299 while let Some(b) = self.peek() {
300 if matches!(b, b'\n' | b'\r') {
301 break;
302 }
303 self.pos += 1;
304 }
305 }
306
307 fn skip_block_comment(&mut self) {
308 let start = self.pos;
309 self.pos += 2;
310 loop {
311 match self.peek() {
312 None => {
313 self.error(self.span(start, start + 2), "unterminated block comment");
314 return;
315 }
316 Some(b'*') if self.peek_at(1) == Some(b'/') => {
317 self.pos += 2;
318 return;
319 }
320 Some(_) => self.pos += 1,
321 }
322 }
323 }
324
325 fn capture_doc_comment(&mut self) {
327 let start = self.pos;
328 self.pos += 3; loop {
330 match self.peek() {
331 None => {
332 self.error(self.span(start, start + 3), "unterminated block comment");
333 return;
334 }
335 Some(b'*') if self.peek_at(1) == Some(b'/') => {
336 self.pos += 2;
337 let span = self.span(start, self.pos);
338 let Some(text) = self.source.get(start as usize..self.pos as usize) else {
339 self.fail("invalid documentation span");
340 return;
341 };
342 let text = text.to_string();
343 self.pending_docs.push(RawDoc { text, span });
344 return;
345 }
346 Some(_) => self.pos += 1,
347 }
348 }
349 }
350
351 fn attach_doc(&mut self, mut tok: Token) -> Token {
352 if !matches!(tok.kind, TokenKind::Newline) {
353 tok.leading_doc = self.pending_docs.pop();
354 self.pending_docs.clear();
355 }
356 tok
357 }
358
359 fn lex_newline(&mut self) -> Token {
361 let start = self.pos;
362 match self.peek() {
363 Some(b'\r') => {
364 self.pos += 1;
365 if self.peek() == Some(b'\n') {
366 self.pos += 1;
367 }
368 }
369 Some(b'\n') => self.pos += 1,
370 _ => unreachable!("newline dispatch requires a newline byte"),
371 }
372 Token::new(TokenKind::Newline, self.span(start, self.pos))
373 }
374
375 fn lex_number(&mut self) -> Token {
378 let start = self.pos;
379
380 if self.peek() == Some(b'0')
381 && let Some(radix) = self.peek_at(1).and_then(Radix::from_prefix)
382 {
383 return self.lex_radix_number(start, radix);
384 }
385
386 let token = self.lex_decimal_number(start);
387 if matches!(
388 token.kind,
389 TokenKind::NumberLiteral(_) | TokenKind::BigIntLiteral(_)
390 ) {
391 self.reject_leading_zero(token.span);
392 }
393 token
394 }
395
396 fn lex_decimal_number(&mut self, start: u32) -> Token {
397 let mut has_fraction_or_exponent = false;
398
399 self.scan_digits(|b| b.is_ascii_digit());
400
401 let Some(int_part) = self.source.get(start as usize..self.pos as usize) else {
402 return self.fail("invalid numeric literal span");
403 };
404 if self.peek() == Some(b'.') {
405 match self.decimal_point_role(int_part) {
406 DecimalPoint::Literal => {
407 has_fraction_or_exponent = true;
408 self.pos += 1;
409 self.scan_digits(|b| b.is_ascii_digit());
410 }
411 DecimalPoint::MemberAccess => {}
412 DecimalPoint::NameAfter => self.report_name_after_decimal_point(start, int_part),
413 }
414 }
415
416 if matches!(self.peek(), Some(b'e' | b'E')) {
417 has_fraction_or_exponent = true;
418 let exp_start = self.pos;
419 self.pos += 1;
420 if matches!(self.peek(), Some(b'+' | b'-')) {
421 self.pos += 1;
422 }
423 let digits_start = self.pos;
424 self.scan_digits(|b| b.is_ascii_digit());
425 if digits_start == self.pos {
426 self.error(self.span(exp_start, self.pos), "missing digits in exponent");
427 return Token::new(
428 TokenKind::NumberLiteral(f64::NAN),
429 self.span(start, self.pos),
430 );
431 }
432 }
433
434 if self.peek() == Some(b'n') {
435 self.pos += 1;
436 let span = self.span(start, self.pos);
437 if has_fraction_or_exponent {
438 self.error(
439 span,
440 "bigint literal cannot have a fractional or exponent part; \
441 remove the `.` / exponent or drop the `n` suffix",
442 );
443 }
444 return Token::new(TokenKind::BigIntLiteral(without_separators(int_part)), span);
447 }
448
449 let span = self.span(start, self.pos);
450 let Some(lexeme) = self.source.get(start as usize..self.pos as usize) else {
451 return self.fail("invalid token source span");
452 };
453 let lexeme = without_separators(lexeme);
454 let value = if let Ok(v) = lexeme.parse::<f64>() {
455 v
456 } else {
457 self.error(span, format!("invalid number literal `{lexeme}`"));
458 f64::NAN
459 };
460
461 Token::new(TokenKind::NumberLiteral(value), span)
462 }
463
464 fn decimal_point_role(&self, int_part: &str) -> DecimalPoint {
466 if self.digit_at(1) {
467 return DecimalPoint::Literal;
468 }
469 if is_legacy_octal_digits(int_part) {
470 return DecimalPoint::MemberAccess;
471 }
472 if !self.identifier_starts_at(1) || self.literal_continues_after_point() {
473 return DecimalPoint::Literal;
474 }
475 DecimalPoint::NameAfter
476 }
477
478 fn literal_continues_after_point(&self) -> bool {
482 match self.peek_at(1) {
483 Some(b'e' | b'E') => true,
484 Some(b'n') => !self.identifier_continues_at(2),
485 Some(b'_') => {
486 let mut offset = 1;
487 while self.peek_at(offset) == Some(b'_') {
488 offset += 1;
489 }
490 self.digit_at(offset) || !self.identifier_continues_at(offset)
491 }
492 _ => false,
493 }
494 }
495
496 fn report_name_after_decimal_point(&mut self, start: u32, int_part: &str) {
499 let help = match self.identifier_text_at(1) {
500 Some(name) if is_plain_decimal_integer(int_part) => format!(
501 "wrap the number in parentheses, `({int_part}).{name}`, or write `{int_part}..{name}`"
502 ),
503 _ => "wrap the number in parentheses, or write a second `.`".to_string(),
504 };
505 self.error_with_help(
506 self.span(start, self.pos + 1),
507 format!("`{int_part}.` is a complete number, so a name cannot follow it directly"),
508 vec![help],
509 );
510 }
511
512 fn reject_leading_zero(&mut self, span: Span) {
517 let Ok(literal) = span.text(self.source, self.file) else {
519 return;
520 };
521 let int_len = literal
522 .bytes()
523 .take_while(|b| b.is_ascii_digit() || *b == b'_')
524 .count();
525 let (int_part, rest) = literal.split_at(int_len);
526 let digits = without_separators(int_part);
527 if digits.len() < 2 || !digits.starts_with('0') {
528 return;
529 }
530 let significant = int_part.trim_start_matches(['0', '_']);
531 let suggested_literal = if significant.is_empty() {
532 format!("0{rest}")
533 } else {
534 format!("{significant}{rest}")
535 };
536 if int_part.as_bytes().get(1) == Some(&b'_') {
537 self.error_with_help(
538 span,
539 format!("a numeric separator cannot follow a leading `0` in `{literal}`"),
540 vec![format!("write `{suggested_literal}`")],
541 );
542 return;
543 }
544 if matches!(rest, "" | "n") && is_legacy_octal_digits(&digits) {
545 let help = if significant.is_empty() {
546 format!("write `{suggested_literal}`")
547 } else {
548 format!(
549 "write `0o{significant}{rest}` for octal, or `{suggested_literal}` for decimal"
550 )
551 };
552 self.error_with_help(
553 span,
554 format!("legacy octal literal `{literal}` is not allowed"),
555 vec![help],
556 );
557 return;
558 }
559 self.error_with_help(
560 span,
561 format!("decimal literal `{literal}` cannot have a leading zero"),
562 vec![format!("write `{suggested_literal}`")],
563 );
564 }
565
566 fn lex_radix_number(&mut self, start: u32, radix: Radix) -> Token {
570 self.pos += 2; let digits_start = self.pos;
572 self.scan_digits(|b| radix.accepts(b));
573 let Some(digits) = self.source.get(digits_start as usize..self.pos as usize) else {
574 return self.fail("invalid numeric literal span");
575 };
576 let digits = without_separators(digits);
577 if digits.is_empty() {
578 let span = self.span(start, self.pos);
579 self.error(span, format!("missing digits after `{}`", radix.prefix()));
580 return Token::new(TokenKind::NumberLiteral(f64::NAN), span);
581 }
582
583 if self.peek() == Some(b'n') {
584 self.pos += 1;
585 return Token::new(
586 TokenKind::BigIntLiteral(radix.to_decimal(&digits)),
587 self.span(start, self.pos),
588 );
589 }
590
591 Token::new(
592 TokenKind::NumberLiteral(radix.to_f64(&digits)),
593 self.span(start, self.pos),
594 )
595 }
596
597 fn scan_digits(&mut self, is_digit: impl Fn(u8) -> bool) {
602 let mut follows_digit = false;
603 loop {
604 match self.peek() {
605 Some(b) if is_digit(b) => {
606 self.pos += 1;
607 follows_digit = true;
608 }
609 Some(b'_') => {
610 let separator_start = self.pos;
611 while self.peek() == Some(b'_') {
612 self.pos += 1;
613 }
614 let precedes_digit = self.peek().is_some_and(&is_digit);
615 self.check_separator(separator_start, follows_digit && precedes_digit);
616 if !precedes_digit {
617 return;
618 }
619 follows_digit = false;
620 }
621 _ => return,
622 }
623 }
624 }
625
626 fn check_separator(&mut self, start: u32, is_between_digits: bool) {
627 let span = self.span(start, self.pos);
628 if !is_between_digits {
629 self.error_with_help(
630 span,
631 "numeric separators are only allowed between digits",
632 vec!["remove the `_`".to_string()],
633 );
634 } else if self.pos - start > 1 {
635 self.error_with_help(
636 span,
637 "only one numeric separator is allowed between digits",
638 vec!["remove the extra `_`".to_string()],
639 );
640 }
641 }
642
643 fn identifier_starts_at(&self, offset: usize) -> bool {
644 match self.peek_at(offset) {
645 Some(b) if b.is_ascii_alphabetic() || b == b'$' || b == b'_' => true,
646 Some(0x80..) => self.char_at(offset).is_some_and(is_xid_start),
647 _ => false,
648 }
649 }
650
651 fn identifier_continues_at(&self, offset: usize) -> bool {
652 self.char_at(offset).is_some_and(is_identifier_continue)
653 }
654
655 fn identifier_text_at(&self, offset: usize) -> Option<&'a str> {
657 let start = (self.pos as usize).checked_add(offset)?;
658 let rest = self.source.get(start..)?;
659 let len: usize = rest
660 .chars()
661 .take_while(|&c| is_identifier_continue(c))
662 .map(char::len_utf8)
663 .sum();
664 rest.get(..len).filter(|name| !name.is_empty())
665 }
666
667 fn char_at(&self, offset: usize) -> Option<char> {
668 let start = (self.pos as usize).checked_add(offset)?;
669 self.source.get(start..)?.chars().next()
670 }
671
672 fn lex_string(&mut self, quote: u8) -> Token {
673 let start = self.pos;
674 self.pos += 1; let mut value = String::new();
676
677 loop {
678 match self.peek() {
679 None => {
680 self.error(self.span(start, self.pos), "unterminated string literal");
681 return Token::new(TokenKind::StringLiteral(value), self.span(start, self.pos));
682 }
683 Some(b) if b == quote => {
684 self.pos += 1;
685 return Token::new(TokenKind::StringLiteral(value), self.span(start, self.pos));
686 }
687 Some(b'\\') => self.read_escape(&mut value),
688 Some(b'\r') => {
692 self.pos += 1;
693 if self.peek() == Some(b'\n') {
694 self.pos += 1;
695 }
696 value.push('\n');
697 }
698 Some(_) => {
699 let Some(c) = self.peek_char() else {
700 return self.fail("lexer cursor is not at a source character");
701 };
702 push_literal_char(&mut value, c);
703 self.pos += c.len_utf8() as u32;
704 }
705 }
706 }
707 }
708
709 fn lex_regex_literal(&mut self) -> Token {
712 let start = self.pos;
713 self.pos += 1; let mut source = String::new();
715 let mut in_class = false;
716
717 loop {
718 match self.peek() {
719 None | Some(b'\n' | b'\r') => {
720 self.error(self.span(start, self.pos), "unterminated regex literal");
721 return Token::new(
722 TokenKind::RegexLiteral {
723 source,
724 flags: String::new(),
725 },
726 self.span(start, self.pos),
727 );
728 }
729 Some(b'\\') => {
730 source.push('\\');
731 self.pos += 1;
732 if let Some(c) = self.peek_char() {
733 if matches!(c, '\n' | '\r') {
735 continue;
736 }
737 source.push(c);
738 self.pos += c.len_utf8() as u32;
739 }
740 }
741 Some(b'[') if !in_class => {
742 in_class = true;
743 source.push('[');
744 self.pos += 1;
745 }
746 Some(b']') if in_class => {
747 in_class = false;
748 source.push(']');
749 self.pos += 1;
750 }
751 Some(b'/') if !in_class => {
752 self.pos += 1;
753 let flags = self.lex_regex_flags();
754 return Token::new(
755 TokenKind::RegexLiteral { source, flags },
756 self.span(start, self.pos),
757 );
758 }
759 Some(_) => {
760 let Some(c) = self.peek_char() else {
761 return self.fail("lexer cursor is not at a source character");
762 };
763 source.push(c);
764 self.pos += c.len_utf8() as u32;
765 }
766 }
767 }
768 }
769
770 fn lex_regex_flags(&mut self) -> String {
772 let mut flags = String::new();
773 while let Some(b) = self.peek() {
774 if b.is_ascii_alphabetic() {
775 flags.push(b as char);
776 self.pos += 1;
777 } else {
778 break;
779 }
780 }
781 flags
782 }
783
784 fn read_escape(&mut self, out: &mut String) {
785 let esc_start = self.pos;
786 self.pos += 1;
787 match self.peek() {
788 None => {
789 self.error(
790 self.span(esc_start, self.pos),
791 "unterminated string literal",
792 );
793 }
794 Some(b'"') => {
795 out.push('"');
796 self.pos += 1;
797 }
798 Some(b'\'') => {
799 out.push('\'');
800 self.pos += 1;
801 }
802 Some(b'\\') => {
803 out.push('\\');
804 self.pos += 1;
805 }
806 Some(b'/') => {
808 out.push('/');
809 self.pos += 1;
810 }
811 Some(b'n') => {
812 out.push('\n');
813 self.pos += 1;
814 }
815 Some(b't') => {
816 out.push('\t');
817 self.pos += 1;
818 }
819 Some(b'r') => {
820 out.push('\r');
821 self.pos += 1;
822 }
823 Some(b'b') => {
824 out.push('\u{08}');
825 self.pos += 1;
826 }
827 Some(b'f') => {
828 out.push('\u{0C}');
829 self.pos += 1;
830 }
831 Some(b'v') => {
832 out.push('\u{0B}');
833 self.pos += 1;
834 }
835 Some(b'0') => {
836 out.push('\0');
837 self.pos += 1;
838 }
839 Some(b'u') => {
840 self.pos += 1;
841 self.read_unicode_escape(esc_start, out);
842 }
843 Some(_) => {
844 let Some(c) = self.peek_char() else {
845 self.fail("escape cursor is not at a source character");
846 return;
847 };
848 let end = self.pos + c.len_utf8() as u32;
849 self.error_with_help(
850 self.span(esc_start, end),
851 format!("unknown escape sequence `\\{c}`"),
852 vec![VALID_ESCAPES.to_string()],
853 );
854 push_literal_char(out, c);
855 self.pos = end;
856 }
857 }
858 }
859
860 fn read_unicode_escape(&mut self, esc_start: u32, out: &mut String) {
861 if self.peek() == Some(b'{') {
862 self.pos += 1;
863 let hex_start = self.pos;
864 while matches!(self.peek(), Some(b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F')) {
865 self.pos += 1;
866 }
867 let hex_end = self.pos;
868 if hex_end == hex_start {
869 self.error(
870 self.span(esc_start, self.pos),
871 "invalid unicode escape: expected hex digits",
872 );
873 return;
874 }
875 if self.peek() != Some(b'}') {
876 self.error(
877 self.span(esc_start, self.pos),
878 "invalid unicode escape: expected `}`",
879 );
880 return;
881 }
882 self.pos += 1;
883 let Some(hex) = self.source.get(hex_start as usize..hex_end as usize) else {
884 self.fail("invalid Unicode escape span");
885 return;
886 };
887 if hex.len() > 6 {
888 self.error(
889 self.span(esc_start, self.pos),
890 "invalid code point in `\\u{…}`: too many digits",
891 );
892 return;
893 }
894 let value = u32::from_str_radix(hex, 16).unwrap_or(0);
895 if value > 0x10FFFF {
896 self.error(
897 self.span(esc_start, self.pos),
898 "invalid code point in `\\u{…}`: exceeds U+10FFFF",
899 );
900 return;
901 }
902 if push_lone_surrogate(out, value) {
903 return;
904 }
905 if let Some(c) = char::from_u32(value) {
906 push_literal_char(out, c);
907 }
908 } else {
909 let Some(value) = self.read_four_hex(esc_start) else {
910 return;
911 };
912 if !push_lone_surrogate(out, value)
913 && let Some(c) = char::from_u32(value)
914 {
915 push_literal_char(out, c);
916 }
917 }
918 }
919
920 fn read_four_hex(&mut self, esc_start: u32) -> Option<u32> {
921 let mut value = 0u32;
922 for _ in 0..4 {
923 if let Some(d) = self.peek().and_then(|byte| char::from(byte).to_digit(16)) {
924 value = value * 16 + d;
925 self.pos += 1;
926 } else {
927 self.error(
928 self.span(esc_start, self.pos),
929 "invalid unicode escape: expected 4 hex digits",
930 );
931 return None;
932 }
933 }
934 Some(value)
935 }
936
937 fn lex_ident(&mut self) -> Token {
938 let start = self.pos;
939 let Some(first) = self.peek_char() else {
940 return self.fail("identifier lexer has no source character");
941 };
942 self.pos += first.len_utf8() as u32;
943 while let Some(c) = self.peek_char() {
944 if is_identifier_continue(c) {
945 self.pos += c.len_utf8() as u32;
946 } else {
947 break;
948 }
949 }
950 let span = self.span(start, self.pos);
951 let Some(lexeme) = self.source.get(start as usize..self.pos as usize) else {
952 return self.fail("invalid token source span");
953 };
954 let kind = match lexeme {
955 "true" => TokenKind::BooleanLiteral(true),
956 "false" => TokenKind::BooleanLiteral(false),
957 "null" => TokenKind::NullLiteral,
958 "let" => TokenKind::Let,
959 "const" => TokenKind::Const,
960 "function" => TokenKind::Function,
961 "if" => TokenKind::If,
962 "else" => TokenKind::Else,
963 "while" => TokenKind::While,
964 "do" => TokenKind::Do,
965 "for" => TokenKind::For,
966 "break" => TokenKind::Break,
967 "continue" => TokenKind::Continue,
968 "return" => TokenKind::Return,
969 "switch" => TokenKind::Switch,
970 "case" => TokenKind::Case,
971 "default" => TokenKind::Default,
972 "export" => TokenKind::Export,
973 "void" => TokenKind::Void,
974 "interface" => TokenKind::Interface,
975 "enum" => TokenKind::Enum,
976 "in" => TokenKind::In,
977 "typeof" => TokenKind::Typeof,
978 "import" => TokenKind::Import,
979 "instanceof" => TokenKind::Instanceof,
980 "new" => TokenKind::New,
981 "try" => TokenKind::Try,
982 "catch" => TokenKind::Catch,
983 "finally" => TokenKind::Finally,
984 "throw" => TokenKind::Throw,
985 "class" => TokenKind::Class,
986 "extends" => TokenKind::Extends,
987 "implements" => TokenKind::Implements,
988 "super" => TokenKind::Super,
989 "this" => TokenKind::This,
990 _ => TokenKind::Identifier,
991 };
992 Token::new(kind, span)
993 }
994
995 fn lex_operator(&mut self) -> Token {
997 let start = self.pos;
998 let b = self.peek().expect("dispatch matched a source byte");
999 let kind = match b {
1000 b'=' => {
1001 self.pos += 1;
1002 if self.peek() == Some(b'=') {
1003 self.pos += 1;
1004 if self.peek() == Some(b'=') {
1005 self.pos += 1;
1006 TokenKind::EqEqEq
1007 } else {
1008 TokenKind::EqEq
1009 }
1010 } else if self.peek() == Some(b'>') {
1011 self.pos += 1;
1012 TokenKind::Arrow
1013 } else {
1014 TokenKind::Equals
1015 }
1016 }
1017 b'!' => {
1018 self.pos += 1;
1019 if self.peek() == Some(b'=') {
1020 self.pos += 1;
1021 if self.peek() == Some(b'=') {
1022 self.pos += 1;
1023 TokenKind::BangEqEq
1024 } else {
1025 TokenKind::BangEq
1026 }
1027 } else {
1028 TokenKind::Bang
1029 }
1030 }
1031 b'<' => {
1032 self.pos += 1;
1033 if self.peek() == Some(b'=') {
1034 self.pos += 1;
1035 TokenKind::LessEquals
1036 } else {
1037 TokenKind::LessThan
1038 }
1039 }
1040 b'>' => {
1041 self.pos += 1;
1042 if self.peek() == Some(b'=') {
1043 self.pos += 1;
1044 TokenKind::GreaterEquals
1045 } else {
1046 TokenKind::GreaterThan
1047 }
1048 }
1049 b'+' => {
1050 self.pos += 1;
1051 match self.peek() {
1052 Some(b'+') => {
1053 self.pos += 1;
1054 TokenKind::PlusPlus
1055 }
1056 Some(b'=') => {
1057 self.pos += 1;
1058 TokenKind::PlusEquals
1059 }
1060 _ => TokenKind::Plus,
1061 }
1062 }
1063 b'-' => {
1064 self.pos += 1;
1065 match self.peek() {
1066 Some(b'-') => {
1067 self.pos += 1;
1068 TokenKind::MinusMinus
1069 }
1070 Some(b'=') => {
1071 self.pos += 1;
1072 TokenKind::MinusEquals
1073 }
1074 _ => TokenKind::Minus,
1075 }
1076 }
1077 b'*' => {
1078 self.pos += 1;
1079 match self.peek() {
1080 Some(b'*') => {
1081 self.pos += 1;
1082 if self.peek() == Some(b'=') {
1084 self.pos += 1;
1085 TokenKind::StarStarEquals
1086 } else {
1087 TokenKind::StarStar
1088 }
1089 }
1090 Some(b'=') => {
1091 self.pos += 1;
1092 TokenKind::StarEquals
1093 }
1094 _ => TokenKind::Star,
1095 }
1096 }
1097 b'/' => {
1098 if self.regex_context() {
1100 return self.lex_regex_literal();
1101 }
1102 self.pos += 1;
1103 if self.peek() == Some(b'=') {
1104 self.pos += 1;
1105 TokenKind::SlashEquals
1106 } else {
1107 TokenKind::Slash
1108 }
1109 }
1110 b'%' => {
1111 self.pos += 1;
1112 if self.peek() == Some(b'=') {
1113 self.pos += 1;
1114 TokenKind::PercentEquals
1115 } else {
1116 TokenKind::Percent
1117 }
1118 }
1119 b'&' => {
1120 self.pos += 1;
1121 match self.peek() {
1122 Some(b'&') => {
1123 self.pos += 1;
1124 TokenKind::AmpAmp
1125 }
1126 Some(b'=') => {
1127 self.pos += 1;
1128 TokenKind::AmpEquals
1129 }
1130 _ => TokenKind::Amp,
1131 }
1132 }
1133 b'^' => {
1134 self.pos += 1;
1135 if self.peek() == Some(b'=') {
1136 self.pos += 1;
1137 TokenKind::CaretEquals
1138 } else {
1139 TokenKind::Caret
1140 }
1141 }
1142 b'~' => {
1143 self.pos += 1;
1144 TokenKind::Tilde
1145 }
1146 b'|' => {
1147 self.pos += 1;
1148 if self.peek() == Some(b'|') {
1149 self.pos += 1;
1150 TokenKind::PipePipe
1151 } else if self.peek() == Some(b'=') {
1152 self.pos += 1;
1153 TokenKind::PipeEquals
1154 } else {
1155 TokenKind::Pipe
1156 }
1157 }
1158 b'.' => {
1159 self.pos += 1;
1160 if self.peek() == Some(b'.') && self.peek_at(1) == Some(b'.') {
1162 self.pos += 2;
1163 TokenKind::DotDotDot
1164 } else {
1165 TokenKind::Dot
1166 }
1167 }
1168 _ => unreachable!("operator dispatch requires an operator byte"),
1169 };
1170 Token::new(kind, self.span(start, self.pos))
1171 }
1172
1173 fn lex_delimiter(&mut self) -> Token {
1175 let start = self.pos;
1176 let b = self.peek().expect("dispatch matched a source byte");
1177 if let Some(depth) = self.template_frames.last_mut() {
1178 if b == b'{' {
1179 let Some(next) = depth.checked_add(1) else {
1180 return self.fail("template brace depth overflow");
1181 };
1182 *depth = next;
1183 } else if b == b'}' {
1184 if *depth == 0 {
1185 self.template_frames.pop();
1186 self.pos += 1;
1187 return self.lex_template_part(start, false);
1188 }
1189 *depth -= 1;
1190 }
1191 }
1192 self.pos += 1;
1193 let kind = match b {
1194 b'(' => TokenKind::LeftParen,
1195 b')' => TokenKind::RightParen,
1196 b'{' => TokenKind::LeftBrace,
1197 b'}' => TokenKind::RightBrace,
1198 b'[' => TokenKind::LeftBracket,
1199 b']' => TokenKind::RightBracket,
1200 b',' => TokenKind::Comma,
1201 b':' => TokenKind::Colon,
1202 b';' => TokenKind::Semicolon,
1203 b'?' => match self.peek() {
1204 Some(b'.') if !self.digit_at(1) => {
1206 self.pos += 1;
1207 TokenKind::QuestionDot
1208 }
1209 Some(b'?') => {
1210 self.pos += 1;
1211 TokenKind::QuestionQuestion
1212 }
1213 _ => TokenKind::Question,
1214 },
1215 _ => unreachable!("delimiter dispatch requires a delimiter byte"),
1216 };
1217 Token::new(kind, self.span(start, self.pos))
1218 }
1219
1220 fn lex_template_part(&mut self, start: u32, is_head: bool) -> Token {
1224 let mut value = String::new();
1225 loop {
1226 match self.peek() {
1227 None => {
1228 self.error(self.span(start, self.pos), "unterminated template literal");
1229 let kind = if is_head {
1230 TokenKind::TemplateNoSubstitution(value)
1231 } else {
1232 TokenKind::TemplateTail(value)
1233 };
1234 return Token::new(kind, self.span(start, self.pos));
1235 }
1236 Some(b'`') => {
1237 self.pos += 1;
1238 let kind = if is_head {
1239 TokenKind::TemplateNoSubstitution(value)
1240 } else {
1241 TokenKind::TemplateTail(value)
1242 };
1243 return Token::new(kind, self.span(start, self.pos));
1244 }
1245 Some(b'$') if self.peek_at(1) == Some(b'{') => {
1246 self.pos += 2; self.template_frames.push(0);
1248 let kind = if is_head {
1249 TokenKind::TemplateHead(value)
1250 } else {
1251 TokenKind::TemplateMiddle(value)
1252 };
1253 return Token::new(kind, self.span(start, self.pos));
1254 }
1255 Some(b'\\') => self.read_template_escape(&mut value),
1256 Some(b'\r') => {
1257 self.pos += 1;
1259 if self.peek() == Some(b'\n') {
1260 self.pos += 1;
1261 }
1262 value.push('\n');
1263 }
1264 Some(_) => {
1265 let Some(c) = self.peek_char() else {
1266 return self.fail("lexer cursor is not at a source character");
1267 };
1268 push_literal_char(&mut value, c);
1269 self.pos += c.len_utf8() as u32;
1270 }
1271 }
1272 }
1273 }
1274
1275 fn read_template_escape(&mut self, out: &mut String) {
1277 match self.peek_at(1) {
1278 Some(b'`') => {
1279 out.push('`');
1280 self.pos += 2;
1281 }
1282 Some(b'$') => {
1283 out.push('$');
1284 self.pos += 2;
1285 }
1286 Some(_) => self.read_escape(out),
1287 None => self.pos += 1,
1288 }
1289 }
1290}
1291
1292fn is_identifier_continue(c: char) -> bool {
1293 c == '_' || c == '$' || is_xid_continue(c)
1294}
1295
1296enum DecimalPoint {
1298 Literal,
1300 MemberAccess,
1303 NameAfter,
1305}
1306
1307fn is_plain_decimal_integer(int_part: &str) -> bool {
1309 let has_leading_zero = int_part.len() > 1 && int_part.starts_with('0');
1310 !has_leading_zero && !int_part.ends_with('_') && !int_part.contains("__")
1311}
1312
1313fn is_legacy_octal_digits(int_part: &str) -> bool {
1315 int_part.len() >= 2
1316 && int_part.starts_with('0')
1317 && int_part.bytes().all(|digit| matches!(digit, b'0'..=b'7'))
1318}
1319
1320fn without_separators(digits: &str) -> String {
1321 digits.replace('_', "")
1322}
1323
1324#[derive(Clone, Copy)]
1326enum Radix {
1327 Hex,
1328 Binary,
1329 Octal,
1330}
1331
1332impl Radix {
1333 fn from_prefix(b: u8) -> Option<Self> {
1334 match b {
1335 b'x' | b'X' => Some(Self::Hex),
1336 b'b' | b'B' => Some(Self::Binary),
1337 b'o' | b'O' => Some(Self::Octal),
1338 _ => None,
1339 }
1340 }
1341
1342 fn accepts(self, b: u8) -> bool {
1343 match self {
1344 Self::Hex => b.is_ascii_hexdigit(),
1345 Self::Binary => matches!(b, b'0' | b'1'),
1346 Self::Octal => matches!(b, b'0'..=b'7'),
1347 }
1348 }
1349
1350 fn base(self) -> u32 {
1351 match self {
1352 Self::Hex => 16,
1353 Self::Binary => 2,
1354 Self::Octal => 8,
1355 }
1356 }
1357
1358 fn prefix(self) -> &'static str {
1359 match self {
1360 Self::Hex => "0x",
1361 Self::Binary => "0b",
1362 Self::Octal => "0o",
1363 }
1364 }
1365
1366 fn to_f64(self, digits: &str) -> f64 {
1369 let base = f64::from(self.base());
1370 digits
1371 .bytes()
1372 .map(|b| f64::from(hex_digit_value(b)))
1373 .fold(0.0, |acc, d| acc * base + d)
1374 }
1375
1376 fn to_decimal(self, digits: &str) -> String {
1377 BigUint::from_str_radix(digits, self.base())
1378 .map_or_else(|_| "0".to_string(), |v| v.to_str_radix(10))
1379 }
1380}
1381
1382fn is_space_separator(c: char) -> bool {
1386 matches!(
1387 c,
1388 '\u{a0}' | '\u{1680}' | '\u{2000}'..='\u{200a}' | '\u{202f}' | '\u{205f}' | '\u{3000}'
1389 )
1390}
1391
1392fn hex_digit_value(b: u8) -> u32 {
1395 match b {
1396 b'0'..=b'9' => u32::from(b - b'0'),
1397 b'a'..=b'f' => u32::from(b - b'a' + 10),
1398 b'A'..=b'F' => u32::from(b - b'A' + 10),
1399 _ => 0,
1400 }
1401}
1402
1403#[allow(clippy::match_like_matches_macro)] pub fn is_regex_context(prev: &Option<TokenKind>) -> bool {
1407 let Some(kind) = prev else {
1408 return true;
1409 };
1410 match kind {
1411 TokenKind::Plus
1412 | TokenKind::Minus
1413 | TokenKind::Star
1414 | TokenKind::Slash
1415 | TokenKind::Percent
1416 | TokenKind::Equals
1417 | TokenKind::PlusEquals
1418 | TokenKind::MinusEquals
1419 | TokenKind::StarEquals
1420 | TokenKind::SlashEquals
1421 | TokenKind::PercentEquals
1422 | TokenKind::StarStar
1423 | TokenKind::StarStarEquals
1424 | TokenKind::EqEqEq
1425 | TokenKind::EqEq
1426 | TokenKind::BangEqEq
1427 | TokenKind::BangEq
1428 | TokenKind::LessThan
1429 | TokenKind::GreaterThan
1430 | TokenKind::LessEquals
1431 | TokenKind::GreaterEquals
1432 | TokenKind::Bang
1433 | TokenKind::AmpAmp
1434 | TokenKind::Amp
1435 | TokenKind::AmpEquals
1436 | TokenKind::PipeEquals
1437 | TokenKind::Caret
1438 | TokenKind::CaretEquals
1439 | TokenKind::Tilde
1440 | TokenKind::Pipe
1441 | TokenKind::PipePipe
1442 | TokenKind::Question
1443 | TokenKind::QuestionDot
1444 | TokenKind::QuestionQuestion
1445 | TokenKind::Arrow
1446 | TokenKind::DotDotDot => true,
1447
1448 TokenKind::LeftParen
1449 | TokenKind::LeftBrace
1450 | TokenKind::LeftBracket
1451 | TokenKind::Comma
1452 | TokenKind::Colon
1453 | TokenKind::Semicolon => true,
1454
1455 TokenKind::Return
1456 | TokenKind::Throw
1457 | TokenKind::Typeof
1458 | TokenKind::In
1459 | TokenKind::New
1460 | TokenKind::Case
1461 | TokenKind::Default
1462 | TokenKind::Else
1463 | TokenKind::Do
1464 | TokenKind::Void => true,
1465
1466 _ => false,
1467 }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472 use super::Lexer;
1473 use crate::source::Sources;
1474 use crate::{Diagnostic, FileId, Span, Token, TokenKind, diagnostics};
1475
1476 const F: FileId = FileId(0);
1477
1478 #[test]
1479 fn invalid_unicode_cursor_and_template_depth_return_fatal_errors() {
1480 let mut lexer = Lexer::new("é", F);
1481 lexer.pos = 1;
1482 assert!(matches!(lexer.lex_ident().kind, TokenKind::Eof));
1483 assert!(
1484 lexer
1485 .finish()
1486 .expect_err("invalid UTF-8 cursor")
1487 .fatal
1488 .is_some()
1489 );
1490
1491 let mut lexer = Lexer::new("{", F);
1492 lexer.template_frames.push(u32::MAX);
1493 assert!(matches!(lexer.lex_delimiter().kind, TokenKind::Eof));
1494 assert!(
1495 lexer
1496 .finish()
1497 .expect_err("template depth overflow")
1498 .fatal
1499 .is_some()
1500 );
1501 }
1502
1503 fn sources(text: &str) -> Sources {
1504 let (sources, _) = Sources::single("script.subm", text).unwrap();
1505 sources
1506 }
1507
1508 fn tokenize_all(source: &str) -> (Vec<Token>, Vec<Diagnostic>) {
1509 let mut lx = Lexer::new(source, crate::FileId(0));
1510 let mut tokens = Vec::new();
1511 loop {
1512 let tok = lx.next_token();
1513 let is_eof = tok.kind == TokenKind::Eof;
1514 tokens.push(tok);
1515 if is_eof {
1516 break;
1517 }
1518 }
1519 (tokens, lx.into_diagnostics())
1520 }
1521
1522 fn tokenize_one(source: &str) -> (Token, Token, Vec<Diagnostic>) {
1523 let mut lx = Lexer::new(source, crate::FileId(0));
1524 let first = lx.next_token();
1525 let second = lx.next_token();
1526 (first, second, lx.into_diagnostics())
1527 }
1528
1529 fn expect_number(source: &str, value: f64, span: Span) {
1530 let (tok, eof, diags) = tokenize_one(source);
1531 assert_eq!(tok.span, span, "span mismatch for {source:?}");
1532 match tok.kind {
1533 TokenKind::NumberLiteral(v) => assert_eq!(v, value, "value mismatch for {source:?}"),
1534 other => panic!("expected NumberLiteral for {source:?}, got {other:?}"),
1535 }
1536 assert_eq!(eof.kind, TokenKind::Eof);
1537 assert!(
1538 diags.is_empty(),
1539 "unexpected diagnostics for {source:?}: {diags:?}"
1540 );
1541 }
1542
1543 fn expect_string(source: &str, value: &str) {
1544 let (tok, eof, diags) = tokenize_one(source);
1545 match tok.kind {
1546 TokenKind::StringLiteral(ref s) => {
1547 assert_eq!(s, value, "string mismatch for {source:?}");
1548 }
1549 other => panic!("expected StringLiteral for {source:?}, got {other:?}"),
1550 }
1551 assert_eq!(eof.kind, TokenKind::Eof);
1552 assert!(
1553 diags.is_empty(),
1554 "unexpected diagnostics for {source:?}: {diags:?}"
1555 );
1556 }
1557
1558 fn expect_single_token(source: &str, expected: TokenKind, span: Span) {
1559 let (tok, eof, diags) = tokenize_one(source);
1560 assert_eq!(tok.kind, expected, "kind mismatch for {source:?}");
1561 assert_eq!(tok.span, span, "span mismatch for {source:?}");
1562 assert_eq!(eof.kind, TokenKind::Eof);
1563 assert!(
1564 diags.is_empty(),
1565 "unexpected diagnostics for {source:?}: {diags:?}"
1566 );
1567 }
1568
1569 #[test]
1570 fn slash_after_non_null_assertion_is_division() {
1571 for prefix in ["a!", "f()!", "a[0]!", "o?.b!.count!", "a!!"] {
1572 for (operator, expected) in [("/", TokenKind::Slash), ("/=", TokenKind::SlashEquals)] {
1573 let (tokens, diagnostics) = tokenize_all(&format!("{prefix} {operator} 10"));
1574 assert!(
1575 diagnostics.is_empty(),
1576 "{prefix} {operator}: {diagnostics:?}"
1577 );
1578 assert!(tokens.iter().any(|token| token.kind == expected));
1579 }
1580 }
1581 for source in ["!/x/.test(s)", "!!/x/.test(s)", "a! / /x/.test(s)"] {
1582 let (tokens, diagnostics) = tokenize_all(source);
1583 assert!(diagnostics.is_empty(), "{source}: {diagnostics:?}");
1584 assert!(
1585 tokens
1586 .iter()
1587 .any(|token| matches!(token.kind, TokenKind::RegexLiteral { .. }))
1588 );
1589 }
1590 }
1591
1592 #[test]
1593 fn lex_integer() {
1594 expect_number("42", 42.0, Span::new(F, 0, 2).unwrap());
1595 }
1596
1597 #[test]
1598 fn lex_zero() {
1599 expect_number("0", 0.0, Span::new(F, 0, 1).unwrap());
1600 }
1601
1602 #[test]
1603 #[allow(clippy::approx_constant)]
1604 fn lex_decimal() {
1605 expect_number("3.14", 3.14, Span::new(F, 0, 4).unwrap());
1606 }
1607
1608 #[test]
1609 fn lex_leading_zero_decimal() {
1610 expect_number("0.5", 0.5, Span::new(F, 0, 3).unwrap());
1611 }
1612
1613 #[test]
1614 fn lex_exponent() {
1615 expect_number("1e10", 1e10, Span::new(F, 0, 4).unwrap());
1616 }
1617
1618 #[test]
1619 fn lex_decimal_with_exponent() {
1620 expect_number("1.5e10", 1.5e10, Span::new(F, 0, 6).unwrap());
1621 }
1622
1623 #[test]
1624 fn lex_uppercase_exponent_with_negative_sign() {
1625 expect_number("2E-3", 2e-3, Span::new(F, 0, 4).unwrap());
1626 }
1627
1628 #[test]
1629 fn lex_positive_exponent() {
1630 expect_number("1e+2", 1e2, Span::new(F, 0, 4).unwrap());
1631 }
1632
1633 #[test]
1634 fn lex_number_with_trailing_whitespace() {
1635 let mut lx = Lexer::new("42 ", crate::FileId(0));
1636 let tok = lx.next_token();
1637 assert_eq!(tok.span, Span::new(F, 0, 2).unwrap());
1638 assert_eq!(tok.kind, TokenKind::NumberLiteral(42.0));
1639 assert_eq!(lx.next_token().kind, TokenKind::Eof);
1640 assert!(lx.into_diagnostics().is_empty());
1641 }
1642
1643 #[test]
1644 fn unicode_space_separators_are_whitespace() {
1645 let source = "a\u{a0}b\u{1680}c\u{2000}d\u{200a}e\u{202f}f\u{205f}g\u{3000}h\u{0b}i\u{0c}j";
1646 let (tokens, diags) = tokenize_all(source);
1647 let names: Vec<_> = tokens
1648 .iter()
1649 .filter(|t| t.kind == TokenKind::Identifier)
1650 .map(|t| &source[t.span.start as usize..t.span.end as usize])
1651 .collect();
1652 assert_eq!(names, ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]);
1653 assert!(diags.is_empty());
1654 }
1655
1656 #[test]
1657 fn zero_width_space_is_not_whitespace() {
1658 let (_tokens, diags) = tokenize_all("a\u{200b}b");
1660 assert_eq!(diags.len(), 1);
1661 assert!(diags[0].message.starts_with("unexpected character"));
1662 }
1663
1664 #[test]
1665 fn empty_input_is_eof() {
1666 let mut lx = Lexer::new("", crate::FileId(0));
1667 let tok = lx.next_token();
1668 assert_eq!(tok.kind, TokenKind::Eof);
1669 assert_eq!(tok.span, Span::new(F, 0, 0).unwrap());
1670 assert!(lx.into_diagnostics().is_empty());
1671 }
1672
1673 #[test]
1674 fn bare_exponent_is_nan_with_diagnostic() {
1675 let mut lx = Lexer::new("1e", crate::FileId(0));
1676 let tok = lx.next_token();
1677 assert_eq!(tok.span, Span::new(F, 0, 2).unwrap());
1678 match tok.kind {
1679 TokenKind::NumberLiteral(v) => assert!(v.is_nan()),
1680 other => panic!("expected NumberLiteral(NaN), got {other:?}"),
1681 }
1682 let diags = lx.into_diagnostics();
1683 assert_eq!(diags.len(), 1);
1684 assert_eq!(diags[0].span, Span::new(F, 1, 2).unwrap());
1685 assert_eq!(diags[0].message, "missing digits in exponent");
1686 }
1687
1688 #[test]
1689 fn signed_exponent_with_no_digits_is_nan_with_diagnostic() {
1690 let mut lx = Lexer::new("3.14e+", crate::FileId(0));
1691 let tok = lx.next_token();
1692 assert_eq!(tok.span, Span::new(F, 0, 6).unwrap());
1693 match tok.kind {
1694 TokenKind::NumberLiteral(v) => assert!(v.is_nan()),
1695 other => panic!("expected NumberLiteral(NaN), got {other:?}"),
1696 }
1697 let diags = lx.into_diagnostics();
1698 assert_eq!(diags.len(), 1);
1699 assert_eq!(diags[0].span, Span::new(F, 4, 6).unwrap());
1700 }
1701
1702 #[test]
1703 fn renders_bare_exponent_diagnostic() {
1704 let source = "1e";
1705 let mut lx = Lexer::new(source, crate::FileId(0));
1706 let _ = lx.next_token();
1707 let diags = lx.into_diagnostics();
1708 let rendered = diagnostics::render(&diags[0], &sources(source));
1709 insta::assert_snapshot!(rendered);
1710 }
1711
1712 fn expect_bigint(source: &str, digits: &str, span: Span) {
1713 let (tok, eof, diags) = tokenize_one(source);
1714 assert_eq!(tok.span, span, "span mismatch for {source:?}");
1715 match tok.kind {
1716 TokenKind::BigIntLiteral(ref s) => {
1717 assert_eq!(s, digits, "digits mismatch for {source:?}");
1718 }
1719 other => panic!("expected BigIntLiteral for {source:?}, got {other:?}"),
1720 }
1721 assert_eq!(eof.kind, TokenKind::Eof);
1722 assert!(
1723 diags.is_empty(),
1724 "unexpected diagnostics for {source:?}: {diags:?}"
1725 );
1726 }
1727
1728 #[test]
1729 fn lex_bigint_simple() {
1730 expect_bigint("42n", "42", Span::new(F, 0, 3).unwrap());
1731 }
1732
1733 #[test]
1734 fn lex_bigint_zero() {
1735 expect_bigint("0n", "0", Span::new(F, 0, 2).unwrap());
1736 }
1737
1738 #[test]
1739 fn lex_bigint_large_beyond_u64() {
1740 expect_bigint(
1742 "1267650600228229401496703205376n",
1743 "1267650600228229401496703205376",
1744 Span::new(F, 0, 32).unwrap(),
1745 );
1746 }
1747
1748 #[test]
1749 fn lex_bigint_fraction_rejected() {
1750 let mut lx = Lexer::new("3.14n", crate::FileId(0));
1751 let tok = lx.next_token();
1752 assert_eq!(tok.span, Span::new(F, 0, 5).unwrap());
1753 match tok.kind {
1754 TokenKind::BigIntLiteral(ref s) => assert_eq!(s, "3"),
1755 other => panic!("expected BigIntLiteral (recovery), got {other:?}"),
1756 }
1757 let diags = lx.into_diagnostics();
1758 assert_eq!(diags.len(), 1);
1759 assert_eq!(diags[0].span, Span::new(F, 0, 5).unwrap());
1760 assert!(
1761 diags[0]
1762 .message
1763 .contains("bigint literal cannot have a fractional or exponent part"),
1764 "unexpected message: {:?}",
1765 diags[0].message
1766 );
1767 }
1768
1769 #[test]
1770 fn lex_bigint_exponent_rejected() {
1771 let mut lx = Lexer::new("1e10n", crate::FileId(0));
1772 let tok = lx.next_token();
1773 assert_eq!(tok.span, Span::new(F, 0, 5).unwrap());
1774 match tok.kind {
1775 TokenKind::BigIntLiteral(ref s) => assert_eq!(s, "1"),
1776 other => panic!("expected BigIntLiteral (recovery), got {other:?}"),
1777 }
1778 let diags = lx.into_diagnostics();
1779 assert_eq!(diags.len(), 1);
1780 assert!(
1781 diags[0]
1782 .message
1783 .contains("bigint literal cannot have a fractional or exponent part"),
1784 "unexpected message: {:?}",
1785 diags[0].message
1786 );
1787 }
1788
1789 #[test]
1790 fn lex_hex_literal() {
1791 expect_number("0xff", 255.0, Span::new(F, 0, 4).unwrap());
1792 expect_number("0XFF", 255.0, Span::new(F, 0, 4).unwrap());
1793 expect_number("0x0", 0.0, Span::new(F, 0, 3).unwrap());
1794 expect_number("0x10", 16.0, Span::new(F, 0, 4).unwrap());
1795 }
1796
1797 #[test]
1798 fn lex_binary_literal() {
1799 expect_number("0b1010", 10.0, Span::new(F, 0, 6).unwrap());
1800 expect_number("0B1", 1.0, Span::new(F, 0, 3).unwrap());
1801 expect_number("0b0", 0.0, Span::new(F, 0, 3).unwrap());
1802 }
1803
1804 #[test]
1805 fn lex_octal_literal() {
1806 expect_number("0o17", 15.0, Span::new(F, 0, 4).unwrap());
1807 expect_number("0O7", 7.0, Span::new(F, 0, 3).unwrap());
1808 expect_number("0o0", 0.0, Span::new(F, 0, 3).unwrap());
1809 }
1810
1811 #[test]
1812 fn lex_radix_bigint() {
1813 expect_bigint("0xffn", "255", Span::new(F, 0, 5).unwrap());
1814 expect_bigint("0b101n", "5", Span::new(F, 0, 6).unwrap());
1815 expect_bigint("0o17n", "15", Span::new(F, 0, 5).unwrap());
1816 }
1817
1818 #[test]
1819 fn lex_hex_bigint_beyond_u64() {
1820 expect_bigint(
1821 "0xffffffffffffffffn",
1822 "18446744073709551615",
1823 Span::new(F, 0, 19).unwrap(),
1824 );
1825 }
1826
1827 #[test]
1828 fn lex_radix_missing_digits() {
1829 for src in ["0x", "0b", "0o"] {
1830 let mut lx = Lexer::new(src, crate::FileId(0));
1831 let tok = lx.next_token();
1832 assert_eq!(tok.span, Span::new(F, 0, 2).unwrap(), "span for {src:?}");
1833 assert!(
1834 matches!(tok.kind, TokenKind::NumberLiteral(v) if v.is_nan()),
1835 "expected NaN recovery for {src:?}, got {:?}",
1836 tok.kind
1837 );
1838 let diags = lx.into_diagnostics();
1839 assert_eq!(diags.len(), 1, "diags for {src:?}");
1840 assert!(
1841 diags[0].message.contains("missing digits after"),
1842 "unexpected message for {src:?}: {:?}",
1843 diags[0].message
1844 );
1845 }
1846 }
1847
1848 fn expect_single_leading_zero_error(source: &str, message: &str, help: &str) {
1849 let (_, eof, diags) = tokenize_one(source);
1850 assert_eq!(
1851 eof.kind,
1852 TokenKind::Eof,
1853 "{source:?} should lex as one token"
1854 );
1855 assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
1856 assert_eq!(diags[0].message, message, "message for {source:?}");
1857 assert_eq!(diags[0].help, vec![help.to_string()], "help for {source:?}");
1858 let source_len = u32::try_from(source.len()).unwrap();
1859 assert_eq!(diags[0].span, Span::new(F, 0, source_len).unwrap());
1860 }
1861
1862 #[test]
1863 fn lex_legacy_octal_rejected() {
1864 expect_single_leading_zero_error(
1865 "010",
1866 "legacy octal literal `010` is not allowed",
1867 "write `0o10` for octal, or `10` for decimal",
1868 );
1869 expect_single_leading_zero_error(
1870 "010n",
1871 "legacy octal literal `010n` is not allowed",
1872 "write `0o10n` for octal, or `10n` for decimal",
1873 );
1874 expect_single_leading_zero_error(
1875 "000",
1876 "legacy octal literal `000` is not allowed",
1877 "write `0`",
1878 );
1879 }
1880
1881 #[test]
1882 fn lex_leading_zero_decimal_rejected() {
1883 expect_single_leading_zero_error(
1884 "09",
1885 "decimal literal `09` cannot have a leading zero",
1886 "write `9`",
1887 );
1888 expect_single_leading_zero_error(
1889 "08.5",
1890 "decimal literal `08.5` cannot have a leading zero",
1891 "write `8.5`",
1892 );
1893 expect_single_leading_zero_error(
1894 "00.5",
1895 "decimal literal `00.5` cannot have a leading zero",
1896 "write `0.5`",
1897 );
1898 expect_single_leading_zero_error(
1899 "07e1",
1900 "decimal literal `07e1` cannot have a leading zero",
1901 "write `7e1`",
1902 );
1903 expect_single_leading_zero_error(
1904 "08n",
1905 "decimal literal `08n` cannot have a leading zero",
1906 "write `8n`",
1907 );
1908 }
1909
1910 #[test]
1911 fn lex_single_leading_zero_accepted() {
1912 expect_number("0", 0.0, Span::new(F, 0, 1).unwrap());
1913 expect_number("0.5", 0.5, Span::new(F, 0, 3).unwrap());
1914 expect_number("0e1", 0.0, Span::new(F, 0, 3).unwrap());
1915 expect_number("0.010", 0.01, Span::new(F, 0, 5).unwrap());
1916 expect_number("1e010", 1e10, Span::new(F, 0, 5).unwrap());
1917 expect_number("0x010", 16.0, Span::new(F, 0, 5).unwrap());
1918 expect_bigint("0n", "0", Span::new(F, 0, 2).unwrap());
1919 }
1920
1921 #[test]
1922 fn lex_numeric_separators() {
1923 expect_number("1_000", 1000.0, Span::new(F, 0, 5).unwrap());
1924 expect_number("1_000.5_5", 1000.55, Span::new(F, 0, 9).unwrap());
1925 expect_number("1e1_0", 1e10, Span::new(F, 0, 5).unwrap());
1926 expect_number("0.0_1", 0.01, Span::new(F, 0, 5).unwrap());
1927 expect_number("0xF_F", 255.0, Span::new(F, 0, 5).unwrap());
1928 expect_number("0b1_0", 2.0, Span::new(F, 0, 5).unwrap());
1929 expect_number("0o1_7", 15.0, Span::new(F, 0, 5).unwrap());
1930 expect_bigint("1_000n", "1000", Span::new(F, 0, 6).unwrap());
1931 expect_bigint("0xF_Fn", "255", Span::new(F, 0, 6).unwrap());
1932 expect_number("0XF_F", 255.0, Span::new(F, 0, 5).unwrap());
1933 expect_number("0B1_0", 2.0, Span::new(F, 0, 5).unwrap());
1934 expect_number("0O1_7", 15.0, Span::new(F, 0, 5).unwrap());
1935 expect_number("1E1_0", 1e10, Span::new(F, 0, 5).unwrap());
1936 expect_number("1e+1_0", 1e10, Span::new(F, 0, 6).unwrap());
1937 expect_number("1e-1_0", 1e-10, Span::new(F, 0, 6).unwrap());
1938 }
1939
1940 #[test]
1941 fn lex_misplaced_numeric_separator_rejected() {
1942 for source in [
1943 "1_", "1_.5", "1._5", "1_e5", "1e_5", "1e+_5", "1_n", "0x_1", "0x1_",
1944 ] {
1945 let (_, eof, diags) = tokenize_one(source);
1946 assert_eq!(
1947 eof.kind,
1948 TokenKind::Eof,
1949 "{source:?} should lex as one token"
1950 );
1951 assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
1952 assert_eq!(
1953 diags[0].message, "numeric separators are only allowed between digits",
1954 "message for {source:?}"
1955 );
1956 }
1957 let (_, _, diags) = tokenize_one("1__0");
1958 assert_eq!(diags.len(), 1, "diags: {diags:?}");
1959 assert_eq!(
1960 diags[0].message,
1961 "only one numeric separator is allowed between digits"
1962 );
1963 assert_eq!(diags[0].span, Span::new(F, 1, 3).unwrap());
1964 expect_single_leading_zero_error(
1965 "0_1",
1966 "a numeric separator cannot follow a leading `0` in `0_1`",
1967 "write `1`",
1968 );
1969 }
1970
1971 #[test]
1972 fn lex_number_ending_in_decimal_point() {
1973 expect_number("1.", 1.0, Span::new(F, 0, 2).unwrap());
1974 expect_number("1.e5", 1e5, Span::new(F, 0, 4).unwrap());
1975 expect_number("0.", 0.0, Span::new(F, 0, 2).unwrap());
1976 let (tokens, diags) = tokenize_all("1..toString");
1977 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1978 assert!(matches!(tokens[0].kind, TokenKind::NumberLiteral(v) if v == 1.0));
1979 assert_eq!(tokens[0].span, Span::new(F, 0, 2).unwrap());
1980 assert_eq!(tokens[1].kind, TokenKind::Dot);
1981 }
1982
1983 #[test]
1984 fn lex_number_starting_with_decimal_point() {
1985 expect_number(".5", 0.5, Span::new(F, 0, 2).unwrap());
1986 expect_number(".5e1", 5.0, Span::new(F, 0, 4).unwrap());
1987 expect_number(".5_5", 0.55, Span::new(F, 0, 4).unwrap());
1988 let (tokens, diags) = tokenize_all("c?.5:1");
1990 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
1991 assert_eq!(tokens[1].kind, TokenKind::Question);
1992 assert!(matches!(tokens[2].kind, TokenKind::NumberLiteral(v) if v == 0.5));
1993 let (tokens, _) = tokenize_all("c?.x");
1994 assert_eq!(tokens[1].kind, TokenKind::QuestionDot);
1995 let (_, _, diags) = tokenize_one(".5n");
1997 assert_eq!(diags.len(), 1, "diags: {diags:?}");
1998 assert!(
1999 diags[0]
2000 .message
2001 .contains("bigint literal cannot have a fractional")
2002 );
2003 let (_, _, diags) = tokenize_one(".5_");
2004 assert_eq!(diags.len(), 1, "diags: {diags:?}");
2005 assert_eq!(
2006 diags[0].message,
2007 "numeric separators are only allowed between digits"
2008 );
2009 }
2010
2011 #[test]
2012 fn lex_name_after_decimal_point_rejected() {
2013 for (source, literal_end) in [
2015 ("1.toString", 2),
2016 ("1_0.x", 4),
2017 ("1.$", 2),
2018 ("1.name", 2),
2019 ("1._x", 2),
2020 ("1.\u{e4}", 2),
2021 ] {
2022 let (tokens, diags) = tokenize_all(source);
2023 assert_eq!(diags.len(), 1, "diags for {source:?}: {diags:?}");
2024 assert!(
2025 diags[0].message.contains("is a complete number"),
2026 "message for {source:?}: {diags:?}"
2027 );
2028 assert_eq!(diags[0].span, Span::new(F, 0, literal_end).unwrap());
2029 assert_eq!(tokens[1].kind, TokenKind::Dot, "tokens for {source:?}");
2031 }
2032 let (_, diags) = tokenize_all("1.toString");
2033 assert_eq!(
2034 diags[0].help,
2035 vec!["wrap the number in parentheses, `(1).toString`, or write `1..toString`"]
2036 );
2037 let (_, diags) = tokenize_all("1.\u{e4}");
2038 assert_eq!(
2039 diags[0].help,
2040 vec!["wrap the number in parentheses, `(1).\u{e4}`, or write `1..\u{e4}`"]
2041 );
2042 let (_, diags) = tokenize_all("1_.x");
2044 assert_eq!(diags.len(), 2, "diags: {diags:?}");
2045 assert_eq!(
2046 diags[1].help,
2047 vec!["wrap the number in parentheses, or write a second `.`"]
2048 );
2049 }
2050
2051 #[test]
2052 fn lex_legacy_octal_leaves_decimal_point() {
2053 let (tokens, diags) = tokenize_all("010.toString");
2054 assert_eq!(diags.len(), 1, "diags: {diags:?}");
2055 assert!(diags[0].message.contains("legacy octal literal `010`"));
2056 assert_eq!(tokens[1].kind, TokenKind::Dot);
2057 }
2058
2059 #[test]
2060 fn lex_hex_trailing_junk_splits() {
2061 let (tokens, diags) = tokenize_all("0xfg");
2063 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2064 assert!(matches!(tokens[0].kind, TokenKind::NumberLiteral(v) if v == 15.0));
2065 assert_eq!(tokens[0].span, Span::new(F, 0, 3).unwrap());
2066 assert_eq!(tokens[1].kind, TokenKind::Identifier);
2067 assert_eq!(tokens[1].span, Span::new(F, 3, 4).unwrap());
2068 }
2069
2070 #[test]
2071 fn lex_bigint_then_dot_method() {
2072 let (tokens, diags) = tokenize_all("42n.toString()");
2073 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2074 let kinds: Vec<_> = tokens.iter().map(|t| &t.kind).collect();
2075 assert!(matches!(kinds[0], TokenKind::BigIntLiteral(s) if s == "42"));
2076 assert_eq!(tokens[0].span, Span::new(F, 0, 3).unwrap());
2077 assert_eq!(tokens[1].kind, TokenKind::Dot);
2078 }
2079
2080 #[test]
2081 fn string_double_quoted() {
2082 expect_string("\"hello\"", "hello");
2083 }
2084
2085 #[test]
2086 fn string_single_quoted() {
2087 expect_string("'world'", "world");
2088 }
2089
2090 #[test]
2091 fn string_escaped_double_quote() {
2092 expect_string("\"with \\\"escape\\\"\"", "with \"escape\"");
2093 }
2094
2095 #[test]
2096 fn string_escaped_single_quote() {
2097 expect_string("'it\\'s'", "it's");
2098 }
2099
2100 #[test]
2101 fn string_newline_and_tab_escapes() {
2102 expect_string("\"\\n\\t\"", "\n\t");
2103 }
2104
2105 #[test]
2106 fn string_carriage_return_escape() {
2107 expect_string("\"a\\rb\"", "a\rb");
2108 }
2109
2110 #[test]
2111 fn string_null_and_other_escapes() {
2112 expect_string("\"\\0\\b\\f\\v\"", "\0\u{08}\u{0C}\u{0B}");
2113 }
2114
2115 #[test]
2116 fn string_backslash_escape() {
2117 expect_string("\"a\\\\b\"", "a\\b");
2118 }
2119
2120 #[test]
2121 fn string_unicode_4hex_escape() {
2122 expect_string("\"\\u00e9\"", "é");
2123 }
2124
2125 #[test]
2126 fn string_unicode_brace_escape() {
2127 expect_string("\"\\u{1F600}\"", "\u{1F600}");
2128 }
2129
2130 #[test]
2131 fn string_surrogate_pair_joins() {
2132 expect_string("\"\\uD83D\\uDE00\"", "\u{1F600}");
2134 }
2135
2136 #[test]
2137 fn string_contains_multibyte_literal() {
2138 expect_string("\"café\"", "café");
2139 }
2140
2141 #[test]
2142 fn string_unterminated_at_eof() {
2143 let (tok, _eof, diags) = tokenize_one("\"hello");
2144 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "hello"));
2145 assert_eq!(diags.len(), 1);
2146 assert_eq!(diags[0].message, "unterminated string literal");
2147 }
2148
2149 #[test]
2150 fn string_raw_newline_is_part_of_the_value() {
2151 let (tok, _eof, diags) = tokenize_one("\"a\nb\"");
2152 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a\nb"));
2153 assert!(diags.is_empty());
2154 }
2155
2156 #[test]
2157 fn string_raw_crlf_normalised_to_lf() {
2158 let (tok, _eof, diags) = tokenize_one("\"a\r\nb\"");
2159 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a\nb"));
2160 assert!(diags.is_empty());
2161 }
2162
2163 #[test]
2164 fn string_still_unterminated_when_newline_reaches_eof() {
2165 let (tok, _eof, diags) = tokenize_one("\"abc\n");
2166 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "abc\n"));
2167 assert_eq!(diags.len(), 1);
2168 assert_eq!(diags[0].message, "unterminated string literal");
2169 }
2170
2171 #[test]
2172 fn string_unknown_escape_diagnoses_but_keeps_character() {
2173 let (tok, _eof, diags) = tokenize_one("\"\\q\"");
2174 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "q"));
2175 assert_eq!(diags.len(), 1);
2176 assert_eq!(diags[0].message, "unknown escape sequence `\\q`");
2177 }
2178
2179 #[test]
2180 fn string_escaped_slash_is_a_slash() {
2181 let (tok, _eof, diags) = tokenize_one("\"a\\/b\"");
2182 assert!(matches!(tok.kind, TokenKind::StringLiteral(ref s) if s == "a/b"));
2183 assert!(diags.is_empty());
2184 }
2185
2186 #[test]
2187 fn string_lone_surrogates_are_kept() {
2188 for (source, unit) in [
2189 ("\"\\uD83D\"", 0xD83D),
2190 ("\"\\uDE00\"", 0xDE00),
2191 ("\"\\u{D800}\"", 0xD800),
2192 ] {
2193 let (tok, _, diags) = tokenize_one(source);
2194 assert!(diags.is_empty(), "{source}: {diags:?}");
2195 let TokenKind::StringLiteral(text) = tok.kind else {
2196 panic!("{source}: expected a string literal");
2197 };
2198 assert_eq!(crate::literal_units::literal_units(&text), vec![unit]);
2199 }
2200 }
2201
2202 #[test]
2203 fn string_marker_code_point_is_kept() {
2204 let (tok, _, diags) = tokenize_one("\"\\u{10FFFE}\u{10FFFE}\"");
2205 assert!(diags.is_empty());
2206 let TokenKind::StringLiteral(text) = tok.kind else {
2207 panic!("expected a string literal");
2208 };
2209 assert_eq!(
2210 crate::literal_units::literal_units(&text),
2211 "\u{10FFFE}\u{10FFFE}".encode_utf16().collect::<Vec<_>>()
2212 );
2213 }
2214
2215 #[test]
2216 fn string_brace_escape_empty_diagnosed() {
2217 let (_, _, diags) = tokenize_one("\"\\u{}\"");
2218 assert_eq!(diags.len(), 1);
2219 assert!(diags[0].message.starts_with("invalid unicode escape"));
2220 }
2221
2222 #[test]
2223 fn string_brace_escape_too_big_diagnosed() {
2224 let (_, _, diags) = tokenize_one("\"\\u{110000}\"");
2225 assert_eq!(diags.len(), 1);
2226 assert!(diags[0].message.contains("exceeds U+10FFFF"));
2227 }
2228
2229 #[test]
2230 fn string_brace_escape_unclosed_diagnosed() {
2231 let (_, _, diags) = tokenize_one("\"\\u{1F600\"");
2232 assert!(!diags.is_empty());
2234 assert!(diags[0].message.contains("invalid unicode escape"));
2235 }
2236
2237 #[test]
2238 fn renders_unterminated_string_diagnostic() {
2239 let source = "let s = \"hello";
2240 let (_, diags) = tokenize_all(source);
2241 let unterminated = diags
2242 .iter()
2243 .find(|d| d.message == "unterminated string literal")
2244 .expect("expected an unterminated-string diagnostic");
2245 let rendered = diagnostics::render(unterminated, &sources(source));
2246 insta::assert_snapshot!(rendered);
2247 }
2248
2249 fn expect_template(source: &str, expected: &[TokenKind]) {
2250 let (tokens, diags) = tokenize_all(source);
2251 assert!(
2252 diags.is_empty(),
2253 "unexpected diagnostics for {source:?}: {diags:?}"
2254 );
2255 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind.clone()).collect();
2256 let mut want = expected.to_vec();
2257 want.push(TokenKind::Eof);
2258 assert_eq!(kinds, want, "token stream mismatch for {source:?}");
2259 }
2260
2261 #[test]
2262 fn template_no_substitution_plain() {
2263 expect_template(
2264 "`hello`",
2265 &[TokenKind::TemplateNoSubstitution("hello".to_string())],
2266 );
2267 }
2268
2269 #[test]
2270 fn template_no_substitution_empty() {
2271 expect_template("``", &[TokenKind::TemplateNoSubstitution(String::new())]);
2272 }
2273
2274 #[test]
2275 fn template_no_substitution_with_dollar_not_followed_by_brace() {
2276 expect_template(
2277 "`a$b`",
2278 &[TokenKind::TemplateNoSubstitution("a$b".to_string())],
2279 );
2280 }
2281
2282 #[test]
2283 fn template_single_interpolation() {
2284 expect_template(
2285 "`a${x}b`",
2286 &[
2287 TokenKind::TemplateHead("a".to_string()),
2288 TokenKind::Identifier,
2289 TokenKind::TemplateTail("b".to_string()),
2290 ],
2291 );
2292 }
2293
2294 #[test]
2295 fn template_two_interpolations() {
2296 expect_template(
2297 "`a${x}b${y}c`",
2298 &[
2299 TokenKind::TemplateHead("a".to_string()),
2300 TokenKind::Identifier,
2301 TokenKind::TemplateMiddle("b".to_string()),
2302 TokenKind::Identifier,
2303 TokenKind::TemplateTail("c".to_string()),
2304 ],
2305 );
2306 }
2307
2308 #[test]
2309 fn template_empty_head_and_tail() {
2310 expect_template(
2311 "`${x}`",
2312 &[
2313 TokenKind::TemplateHead(String::new()),
2314 TokenKind::Identifier,
2315 TokenKind::TemplateTail(String::new()),
2316 ],
2317 );
2318 }
2319
2320 #[test]
2321 fn template_object_literal_inside_interpolation_does_not_close() {
2322 let (tokens, diags) = tokenize_all("`x=${ {a:1} }`");
2324 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
2325 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind.clone()).collect();
2326 assert_eq!(
2327 kinds,
2328 vec![
2329 TokenKind::TemplateHead("x=".to_string()),
2330 TokenKind::LeftBrace,
2331 TokenKind::Identifier,
2332 TokenKind::Colon,
2333 TokenKind::NumberLiteral(1.0),
2334 TokenKind::RightBrace,
2335 TokenKind::TemplateTail(String::new()),
2336 TokenKind::Eof,
2337 ]
2338 );
2339 }
2340
2341 #[test]
2342 fn template_nested_in_interpolation() {
2343 expect_template(
2346 "`out${`in${x}`}end`",
2347 &[
2348 TokenKind::TemplateHead("out".to_string()),
2349 TokenKind::TemplateHead("in".to_string()),
2350 TokenKind::Identifier,
2351 TokenKind::TemplateTail(String::new()),
2352 TokenKind::TemplateTail("end".to_string()),
2353 ],
2354 );
2355 }
2356
2357 #[test]
2358 fn template_escapes_inside_part() {
2359 expect_template(
2361 "`a\\nb\\`c\\${d}\\\\e\\u{1F600}`",
2362 &[TokenKind::TemplateNoSubstitution(
2363 "a\nb`c${d}\\e\u{1F600}".to_string(),
2364 )],
2365 );
2366 }
2367
2368 #[test]
2369 fn template_multiline_lf_preserved() {
2370 expect_template(
2371 "`line1\nline2`",
2372 &[TokenKind::TemplateNoSubstitution(
2373 "line1\nline2".to_string(),
2374 )],
2375 );
2376 }
2377
2378 #[test]
2379 fn template_multiline_crlf_normalised_to_lf() {
2380 expect_template(
2381 "`a\r\nb`",
2382 &[TokenKind::TemplateNoSubstitution("a\nb".to_string())],
2383 );
2384 }
2385
2386 #[test]
2387 fn template_unterminated_no_substitution_diagnosed() {
2388 let (tokens, diags) = tokenize_all("`hello");
2389 assert_eq!(diags.len(), 1, "diags: {diags:?}");
2390 assert_eq!(diags[0].message, "unterminated template literal");
2391 assert!(matches!(
2392 &tokens[0].kind,
2393 TokenKind::TemplateNoSubstitution(s) if s == "hello"
2394 ));
2395 }
2396
2397 #[test]
2398 fn template_unterminated_after_head_diagnosed() {
2399 let (_, diags) = tokenize_all("`a${x");
2400 assert!(diags.is_empty(), "unexpected lexer diags: {diags:?}");
2403 }
2404
2405 #[test]
2406 fn template_unterminated_after_interpolation_diagnosed() {
2407 let (_, diags) = tokenize_all("`a${x}b");
2408 assert_eq!(diags.len(), 1);
2409 assert_eq!(diags[0].message, "unterminated template literal");
2410 }
2411
2412 #[test]
2413 fn renders_unterminated_template_diagnostic() {
2414 let source = "let s = `hello";
2415 let (_, diags) = tokenize_all(source);
2416 let unterminated = diags
2417 .iter()
2418 .find(|d| d.message == "unterminated template literal")
2419 .expect("expected an unterminated-template diagnostic");
2420 let rendered = diagnostics::render(unterminated, &sources(source));
2421 insta::assert_snapshot!(rendered);
2422 }
2423
2424 #[test]
2425 fn lex_true_false_null() {
2426 let (tokens, diags) = tokenize_all("true false null");
2427 assert!(diags.is_empty());
2428 assert_eq!(tokens[0].kind, TokenKind::BooleanLiteral(true));
2429 assert_eq!(tokens[1].kind, TokenKind::BooleanLiteral(false));
2430 assert_eq!(tokens[2].kind, TokenKind::NullLiteral);
2431 assert_eq!(tokens[3].kind, TokenKind::Eof);
2432 }
2433
2434 #[test]
2435 fn literal_prefix_words_are_identifiers() {
2436 for src in ["trueValue", "falseness", "nullable"] {
2437 let (tok, _eof, diags) = tokenize_one(src);
2438 assert!(diags.is_empty(), "diagnostics for {src:?}");
2439 assert_eq!(
2440 tok.kind,
2441 TokenKind::Identifier,
2442 "expected Identifier for {src:?}"
2443 );
2444 }
2445 }
2446
2447 #[test]
2448 fn lex_all_mvp_keywords() {
2449 let keywords = [
2450 ("let", TokenKind::Let),
2451 ("const", TokenKind::Const),
2452 ("function", TokenKind::Function),
2453 ("if", TokenKind::If),
2454 ("else", TokenKind::Else),
2455 ("while", TokenKind::While),
2456 ("return", TokenKind::Return),
2457 ("void", TokenKind::Void),
2458 ("interface", TokenKind::Interface),
2459 ("export", TokenKind::Export),
2460 ("typeof", TokenKind::Typeof),
2461 ("import", TokenKind::Import),
2462 ("new", TokenKind::New),
2463 ];
2464 for (src, expected) in keywords {
2465 let (tok, _eof, diags) = tokenize_one(src);
2466 assert!(diags.is_empty(), "diagnostics for {src:?}");
2467 assert_eq!(tok.kind, expected, "kind mismatch for {src:?}");
2468 assert_eq!(tok.span, Span::new(F, 0, src.len() as u32).unwrap());
2469 }
2470 }
2471
2472 #[test]
2473 fn contextual_keywords_lex_as_identifiers() {
2474 for src in ["type", "is", "from", "as", "of"] {
2475 let (tok, _eof, diags) = tokenize_one(src);
2476 assert!(diags.is_empty(), "diagnostics for {src:?}");
2477 assert_eq!(tok.kind, TokenKind::Identifier, "kind mismatch for {src:?}");
2478 }
2479 }
2480
2481 #[test]
2482 fn lex_ascii_identifiers() {
2483 for src in ["foo", "_bar", "$baz", "x1", "_123", "camelCase"] {
2484 let (tok, _eof, diags) = tokenize_one(src);
2485 assert!(diags.is_empty(), "diagnostics for {src:?}");
2486 assert_eq!(tok.kind, TokenKind::Identifier, "kind mismatch for {src:?}");
2487 assert_eq!(tok.span, Span::new(F, 0, src.len() as u32).unwrap());
2488 }
2489 }
2490
2491 #[test]
2492 fn lex_unicode_identifier() {
2493 let (tok, _eof, diags) = tokenize_one("café");
2494 assert!(diags.is_empty());
2495 assert_eq!(tok.kind, TokenKind::Identifier);
2496 assert_eq!(tok.span, Span::new(F, 0, "café".len() as u32).unwrap());
2497 }
2498
2499 #[test]
2500 fn keyword_prefix_is_identifier() {
2501 let (tok, _eof, diags) = tokenize_one("letx");
2502 assert!(diags.is_empty());
2503 assert_eq!(tok.kind, TokenKind::Identifier);
2504 }
2505
2506 #[test]
2507 fn lex_arithmetic_operators() {
2508 expect_single_token("+", TokenKind::Plus, Span::new(F, 0, 1).unwrap());
2509 expect_single_token("-", TokenKind::Minus, Span::new(F, 0, 1).unwrap());
2510 expect_single_token("*", TokenKind::Star, Span::new(F, 0, 1).unwrap());
2511 let (toks, diags) = tokenize_all("a / b");
2513 assert!(diags.is_empty(), "diags: {diags:?}");
2514 assert_eq!(toks[1].kind, TokenKind::Slash);
2515 expect_single_token("%", TokenKind::Percent, Span::new(F, 0, 1).unwrap());
2516 }
2517
2518 #[test]
2519 fn lex_compound_assignment_operators() {
2520 expect_single_token("+=", TokenKind::PlusEquals, Span::new(F, 0, 2).unwrap());
2521 expect_single_token("-=", TokenKind::MinusEquals, Span::new(F, 0, 2).unwrap());
2522 expect_single_token("*=", TokenKind::StarEquals, Span::new(F, 0, 2).unwrap());
2523 let (toks, diags) = tokenize_all("a /= 2");
2525 assert!(diags.is_empty(), "diags: {diags:?}");
2526 assert_eq!(toks[1].kind, TokenKind::SlashEquals);
2527 expect_single_token("%=", TokenKind::PercentEquals, Span::new(F, 0, 2).unwrap());
2528 expect_single_token("**", TokenKind::StarStar, Span::new(F, 0, 2).unwrap());
2529 expect_single_token(
2530 "**=",
2531 TokenKind::StarStarEquals,
2532 Span::new(F, 0, 3).unwrap(),
2533 );
2534 let (toks, diags) = tokenize_all("a **= 2");
2536 assert!(diags.is_empty(), "diags: {diags:?}");
2537 assert_eq!(toks[1].kind, TokenKind::StarStarEquals);
2538 let (toks, diags) = tokenize_all("a ** b");
2539 assert!(diags.is_empty(), "diags: {diags:?}");
2540 assert_eq!(toks[1].kind, TokenKind::StarStar);
2541 }
2542
2543 #[test]
2544 fn lex_bitwise_operators() {
2545 for (source, kind) in [
2546 ("&", TokenKind::Amp),
2547 ("&=", TokenKind::AmpEquals),
2548 ("|", TokenKind::Pipe),
2549 ("|=", TokenKind::PipeEquals),
2550 ("^", TokenKind::Caret),
2551 ("^=", TokenKind::CaretEquals),
2552 ("~", TokenKind::Tilde),
2553 ] {
2554 expect_single_token(source, kind, Span::new(F, 0, source.len() as u32).unwrap());
2555 }
2556 let (tokens, diags) = tokenize_all("~ /x/.test('x')");
2557 assert!(diags.is_empty(), "{diags:?}");
2558 assert!(matches!(tokens[1].kind, TokenKind::RegexLiteral { .. }));
2559 }
2560
2561 #[test]
2562 fn lex_regex_literal_basic() {
2563 expect_single_token(
2565 "/abc/",
2566 TokenKind::RegexLiteral {
2567 source: "abc".to_string(),
2568 flags: String::new(),
2569 },
2570 Span::new(F, 0, 5).unwrap(),
2571 );
2572 }
2573
2574 #[test]
2575 fn lex_regex_literal_with_flags() {
2576 let (toks, diags) = tokenize_all("/foo/gi");
2577 assert!(diags.is_empty(), "diags: {diags:?}");
2578 assert_eq!(
2579 toks[0].kind,
2580 TokenKind::RegexLiteral {
2581 source: "foo".to_string(),
2582 flags: "gi".to_string(),
2583 }
2584 );
2585 }
2586
2587 #[test]
2588 fn lex_regex_after_open_paren_is_literal() {
2589 let (toks, diags) = tokenize_all("f(/x/)");
2590 assert!(diags.is_empty(), "diags: {diags:?}");
2591 assert_eq!(toks[0].kind, TokenKind::Identifier);
2593 assert_eq!(toks[1].kind, TokenKind::LeftParen);
2594 assert!(matches!(toks[2].kind, TokenKind::RegexLiteral { .. }));
2595 assert_eq!(toks[3].kind, TokenKind::RightParen);
2596 }
2597
2598 #[test]
2599 fn lex_regex_after_equals_is_literal() {
2600 let (toks, diags) = tokenize_all("let r = /a/g;");
2601 assert!(diags.is_empty(), "diags: {diags:?}");
2602 assert!(toks.iter().any(|t| matches!(
2603 &t.kind,
2604 TokenKind::RegexLiteral { source, flags } if source == "a" && flags == "g"
2605 )));
2606 }
2607
2608 #[test]
2609 fn lex_regex_after_return_is_literal() {
2610 let (toks, diags) = tokenize_all("return /x/;");
2611 assert!(diags.is_empty(), "diags: {diags:?}");
2612 assert_eq!(toks[0].kind, TokenKind::Return);
2613 assert!(matches!(toks[1].kind, TokenKind::RegexLiteral { .. }));
2614 }
2615
2616 #[test]
2617 fn lex_regex_after_typeof_is_literal() {
2618 let (toks, diags) = tokenize_all("typeof /x/");
2619 assert!(diags.is_empty(), "diags: {diags:?}");
2620 assert_eq!(toks[0].kind, TokenKind::Typeof);
2621 assert!(matches!(toks[1].kind, TokenKind::RegexLiteral { .. }));
2622 }
2623
2624 #[test]
2625 fn lex_division_after_identifier_stays_division() {
2626 let (toks, diags) = tokenize_all("a / b");
2627 assert!(diags.is_empty(), "diags: {diags:?}");
2628 assert_eq!(toks[0].kind, TokenKind::Identifier);
2629 assert_eq!(toks[1].kind, TokenKind::Slash);
2630 assert_eq!(toks[2].kind, TokenKind::Identifier);
2631 }
2632
2633 #[test]
2634 fn lex_division_after_close_paren_stays_division() {
2635 let (toks, diags) = tokenize_all("(x) / 2");
2636 assert!(diags.is_empty(), "diags: {diags:?}");
2637 assert!(toks.iter().any(|t| t.kind == TokenKind::Slash));
2638 assert!(
2639 !toks
2640 .iter()
2641 .any(|t| matches!(t.kind, TokenKind::RegexLiteral { .. }))
2642 );
2643 }
2644
2645 #[test]
2646 fn lex_division_chain_a_div_b_div_c() {
2647 let (toks, diags) = tokenize_all("a / b / c");
2648 assert!(diags.is_empty(), "diags: {diags:?}");
2649 let kinds: Vec<&TokenKind> = toks.iter().map(|t| &t.kind).collect();
2650 assert_eq!(
2651 kinds,
2652 vec![
2653 &TokenKind::Identifier,
2654 &TokenKind::Slash,
2655 &TokenKind::Identifier,
2656 &TokenKind::Slash,
2657 &TokenKind::Identifier,
2658 &TokenKind::Eof,
2659 ]
2660 );
2661 }
2662
2663 #[test]
2664 fn lex_regex_escaped_slash_does_not_close() {
2665 let (toks, diags) = tokenize_all("/a\\/b/");
2666 assert!(diags.is_empty(), "diags: {diags:?}");
2667 assert_eq!(
2668 toks[0].kind,
2669 TokenKind::RegexLiteral {
2670 source: "a\\/b".to_string(),
2671 flags: String::new(),
2672 }
2673 );
2674 }
2675
2676 #[test]
2677 fn lex_regex_slash_inside_char_class_does_not_close() {
2678 let (toks, diags) = tokenize_all("/[/]/");
2679 assert!(diags.is_empty(), "diags: {diags:?}");
2680 assert_eq!(
2681 toks[0].kind,
2682 TokenKind::RegexLiteral {
2683 source: "[/]".to_string(),
2684 flags: String::new(),
2685 }
2686 );
2687 }
2688
2689 #[test]
2690 fn lex_regex_unterminated_at_newline_is_diagnosed() {
2691 let (toks, diags) = tokenize_all("/abc\n");
2692 assert!(
2693 !diags.is_empty()
2694 && diags
2695 .iter()
2696 .any(|d| d.message.contains("unterminated regex literal")),
2697 "expected unterminated diagnostic, got {diags:?}"
2698 );
2699 assert!(matches!(toks[0].kind, TokenKind::RegexLiteral { .. }));
2700 }
2701
2702 #[test]
2703 fn lex_regex_unterminated_at_eof_is_diagnosed() {
2704 let (_toks, diags) = tokenize_all("/abc");
2705 assert!(
2706 diags
2707 .iter()
2708 .any(|d| d.message.contains("unterminated regex literal")),
2709 "expected unterminated diagnostic, got {diags:?}"
2710 );
2711 }
2712
2713 #[test]
2714 fn lex_regex_after_regex_is_division() {
2715 let (toks, diags) = tokenize_all("/x/ / 2");
2717 assert!(diags.is_empty(), "diags: {diags:?}");
2718 assert!(matches!(toks[0].kind, TokenKind::RegexLiteral { .. }));
2719 assert_eq!(toks[1].kind, TokenKind::Slash);
2720 }
2721
2722 #[test]
2723 fn lex_block_comment_remains_unaffected() {
2724 let (toks, diags) = tokenize_all("/* a / b */ x");
2725 assert!(diags.is_empty(), "diags: {diags:?}");
2726 assert_eq!(toks[0].kind, TokenKind::Identifier);
2727 }
2728
2729 #[test]
2730 fn lex_line_comment_remains_unaffected() {
2731 let (toks, diags) = tokenize_all("// /x/\n");
2732 assert!(diags.is_empty(), "diags: {diags:?}");
2733 assert!(
2735 toks.iter()
2736 .all(|t| matches!(t.kind, TokenKind::Newline | TokenKind::Eof))
2737 );
2738 assert!(
2739 !toks
2740 .iter()
2741 .any(|t| matches!(t.kind, TokenKind::RegexLiteral { .. }))
2742 );
2743 }
2744
2745 #[test]
2746 fn lex_postfix_increment_decrement() {
2747 expect_single_token("++", TokenKind::PlusPlus, Span::new(F, 0, 2).unwrap());
2749 expect_single_token("--", TokenKind::MinusMinus, Span::new(F, 0, 2).unwrap());
2750 let (toks, diags) = tokenize_all("+=");
2751 assert!(diags.is_empty());
2752 assert_eq!(toks[0].kind, TokenKind::PlusEquals);
2753 let (toks, diags) = tokenize_all("-=");
2754 assert!(diags.is_empty());
2755 assert_eq!(toks[0].kind, TokenKind::MinusEquals);
2756 let (toks, diags) = tokenize_all("+++");
2758 assert!(diags.is_empty());
2759 assert_eq!(toks[0].kind, TokenKind::PlusPlus);
2760 assert_eq!(toks[1].kind, TokenKind::Plus);
2761 }
2762
2763 #[test]
2764 fn lex_equality_operators_longest_match() {
2765 expect_single_token("=", TokenKind::Equals, Span::new(F, 0, 1).unwrap());
2766 expect_single_token("==", TokenKind::EqEq, Span::new(F, 0, 2).unwrap());
2767 expect_single_token("===", TokenKind::EqEqEq, Span::new(F, 0, 3).unwrap());
2768 expect_single_token("!", TokenKind::Bang, Span::new(F, 0, 1).unwrap());
2769 expect_single_token("!=", TokenKind::BangEq, Span::new(F, 0, 2).unwrap());
2770 expect_single_token("!==", TokenKind::BangEqEq, Span::new(F, 0, 3).unwrap());
2771 }
2772
2773 #[test]
2774 fn lex_arrow_token() {
2775 expect_single_token("=>", TokenKind::Arrow, Span::new(F, 0, 2).unwrap());
2776 }
2777
2778 #[test]
2779 fn lex_comparison_operators() {
2780 expect_single_token("<", TokenKind::LessThan, Span::new(F, 0, 1).unwrap());
2781 expect_single_token(">", TokenKind::GreaterThan, Span::new(F, 0, 1).unwrap());
2782 expect_single_token("<=", TokenKind::LessEquals, Span::new(F, 0, 2).unwrap());
2783 expect_single_token(">=", TokenKind::GreaterEquals, Span::new(F, 0, 2).unwrap());
2784 }
2785
2786 #[test]
2787 fn lex_logical_operators() {
2788 expect_single_token("&&", TokenKind::AmpAmp, Span::new(F, 0, 2).unwrap());
2789 expect_single_token("||", TokenKind::PipePipe, Span::new(F, 0, 2).unwrap());
2790 }
2791
2792 #[test]
2793 fn lex_pipe_longest_match() {
2794 expect_single_token("|", TokenKind::Pipe, Span::new(F, 0, 1).unwrap());
2795 expect_single_token("||", TokenKind::PipePipe, Span::new(F, 0, 2).unwrap());
2796 }
2797
2798 #[test]
2799 fn lex_dot_standalone_and_after_identifier() {
2800 expect_single_token(".", TokenKind::Dot, Span::new(F, 0, 1).unwrap());
2801 let (tokens, diags) = tokenize_all("foo.bar");
2802 assert!(diags.is_empty());
2803 assert_eq!(tokens[0].kind, TokenKind::Identifier);
2804 assert_eq!(tokens[1].kind, TokenKind::Dot);
2805 assert_eq!(tokens[2].kind, TokenKind::Identifier);
2806 }
2807
2808 #[test]
2809 fn single_amp_is_bitwise_and() {
2810 expect_single_token("&", TokenKind::Amp, Span::new(F, 0, 1).unwrap());
2811 }
2812
2813 #[test]
2814 fn lex_all_delimiters() {
2815 expect_single_token("(", TokenKind::LeftParen, Span::new(F, 0, 1).unwrap());
2816 expect_single_token(")", TokenKind::RightParen, Span::new(F, 0, 1).unwrap());
2817 expect_single_token("{", TokenKind::LeftBrace, Span::new(F, 0, 1).unwrap());
2818 expect_single_token("}", TokenKind::RightBrace, Span::new(F, 0, 1).unwrap());
2819 expect_single_token("[", TokenKind::LeftBracket, Span::new(F, 0, 1).unwrap());
2820 expect_single_token("]", TokenKind::RightBracket, Span::new(F, 0, 1).unwrap());
2821 expect_single_token(",", TokenKind::Comma, Span::new(F, 0, 1).unwrap());
2822 expect_single_token(":", TokenKind::Colon, Span::new(F, 0, 1).unwrap());
2823 expect_single_token(";", TokenKind::Semicolon, Span::new(F, 0, 1).unwrap());
2824 expect_single_token("?", TokenKind::Question, Span::new(F, 0, 1).unwrap());
2825 }
2826
2827 #[test]
2828 fn lex_brackets_combined() {
2829 let (tokens, diags) = tokenize_all("({[]})");
2830 assert!(diags.is_empty());
2831 let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2832 assert_eq!(
2833 kinds,
2834 vec![
2835 TokenKind::LeftParen,
2836 TokenKind::LeftBrace,
2837 TokenKind::LeftBracket,
2838 TokenKind::RightBracket,
2839 TokenKind::RightBrace,
2840 TokenKind::RightParen,
2841 TokenKind::Eof,
2842 ]
2843 );
2844 }
2845
2846 #[test]
2847 fn lex_newline_lf() {
2848 expect_single_token("\n", TokenKind::Newline, Span::new(F, 0, 1).unwrap());
2849 }
2850
2851 #[test]
2852 fn lex_newline_crlf_is_one_token() {
2853 expect_single_token("\r\n", TokenKind::Newline, Span::new(F, 0, 2).unwrap());
2854 }
2855
2856 #[test]
2857 fn lex_newline_cr() {
2858 expect_single_token("\r", TokenKind::Newline, Span::new(F, 0, 1).unwrap());
2859 }
2860
2861 #[test]
2862 fn lex_mixed_newlines() {
2863 let (tokens, diags) = tokenize_all("a\nb\r\nc\rd");
2864 assert!(diags.is_empty());
2865 let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2866 assert_eq!(
2867 kinds,
2868 vec![
2869 TokenKind::Identifier,
2870 TokenKind::Newline,
2871 TokenKind::Identifier,
2872 TokenKind::Newline,
2873 TokenKind::Identifier,
2874 TokenKind::Newline,
2875 TokenKind::Identifier,
2876 TokenKind::Eof,
2877 ]
2878 );
2879 }
2880
2881 #[test]
2882 fn line_comment_skipped_but_newline_preserved() {
2883 let (tokens, diags) = tokenize_all("a // comment\nb");
2884 assert!(diags.is_empty());
2885 let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2886 assert_eq!(
2887 kinds,
2888 vec![
2889 TokenKind::Identifier,
2890 TokenKind::Newline,
2891 TokenKind::Identifier,
2892 TokenKind::Eof,
2893 ]
2894 );
2895 }
2896
2897 #[test]
2898 fn block_comment_skipped() {
2899 let (tokens, diags) = tokenize_all("a /* block */ b");
2900 assert!(diags.is_empty());
2901 let kinds: Vec<_> = tokens.iter().map(|t| t.kind.clone()).collect();
2902 assert_eq!(
2903 kinds,
2904 vec![TokenKind::Identifier, TokenKind::Identifier, TokenKind::Eof]
2905 );
2906 }
2907
2908 #[test]
2909 fn empty_block_comment_is_fine() {
2910 let (tokens, diags) = tokenize_all("/**/");
2911 assert!(diags.is_empty());
2912 assert_eq!(tokens.len(), 1);
2913 assert_eq!(tokens[0].kind, TokenKind::Eof);
2914 }
2915
2916 #[test]
2917 fn unterminated_block_comment_diagnosed() {
2918 let (_, diags) = tokenize_all("/* unterminated");
2919 assert_eq!(diags.len(), 1);
2920 assert_eq!(diags[0].message, "unterminated block comment");
2921 assert_eq!(diags[0].span, Span::new(F, 0, 2).unwrap());
2922 }
2923
2924 #[test]
2925 fn single_slash_star_not_block_comment_start() {
2926 let (_, diags) = tokenize_all("/*/");
2928 assert_eq!(diags.len(), 1);
2929 assert_eq!(diags[0].message, "unterminated block comment");
2930 }
2931
2932 #[test]
2933 fn doc_comment_attaches_to_next_token() {
2934 let (tok, eof, diags) = tokenize_one("/** Summary. */ foo");
2935 let doc = tok.leading_doc.as_ref().expect("doc attached");
2936 assert_eq!(doc.text, "/** Summary. */");
2937 assert_eq!(doc.span, Span::new(F, 0, 15).unwrap());
2938 assert_eq!(tok.kind, TokenKind::Identifier);
2939 assert!(eof.leading_doc.is_none());
2940 assert!(diags.is_empty());
2941 }
2942
2943 #[test]
2944 fn doc_comment_survives_intervening_newlines() {
2945 let (tokens, diags) = tokenize_all("/** doc */\n\nfoo");
2946 assert!(diags.is_empty());
2947 let ident = tokens
2948 .iter()
2949 .find(|t| t.kind == TokenKind::Identifier)
2950 .expect("identifier present");
2951 assert!(ident.leading_doc.is_some());
2952 for nl in tokens.iter().filter(|t| t.kind == TokenKind::Newline) {
2953 assert!(nl.leading_doc.is_none(), "newline shouldn't claim the doc");
2954 }
2955 }
2956
2957 #[test]
2958 fn empty_doc_comment_captured() {
2959 let (tok, _eof, _diags) = tokenize_one("/** */ x");
2960 let doc = tok.leading_doc.as_ref().expect("doc attached");
2961 assert_eq!(doc.text, "/** */");
2962 }
2963
2964 #[test]
2965 fn regular_block_comment_not_captured() {
2966 let (tok, _eof, _diags) = tokenize_one("/* not a doc */ foo");
2967 assert!(tok.leading_doc.is_none());
2968 }
2969
2970 #[test]
2971 fn empty_block_comment_not_a_doc() {
2972 let (tok, _eof, _diags) = tokenize_one("/**/ foo");
2973 assert!(tok.leading_doc.is_none());
2974 }
2975
2976 #[test]
2977 fn line_comment_not_captured() {
2978 let (tok, _eof, _diags) = tokenize_one("// line\nfoo");
2979 assert!(tok.leading_doc.is_none());
2980 }
2981
2982 #[test]
2983 fn last_doc_wins_when_multiple_in_a_row() {
2984 let (tok, _eof, _diags) = tokenize_one("/** first */ /** second */ foo");
2985 let doc = tok.leading_doc.as_ref().expect("doc attached");
2986 assert_eq!(doc.text, "/** second */");
2987 }
2988
2989 #[test]
2990 fn unterminated_doc_emits_diagnostic() {
2991 let (tok, _eof, diags) = tokenize_one("/** unterminated");
2992 assert_eq!(tok.kind, TokenKind::Eof);
2993 assert_eq!(diags.len(), 1);
2994 assert_eq!(diags[0].message, "unterminated block comment");
2995 }
2996
2997 #[test]
2999 fn leading_bom_is_skipped() {
3000 let (tokens, diags) = tokenize_all("\u{feff}const x = 1;");
3001 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
3002 assert_eq!(tokens[0].kind, TokenKind::Const);
3003 }
3004
3005 #[test]
3008 fn leading_bom_keeps_spans_aligned_to_the_file() {
3009 let source = "\u{feff}const x = 1;";
3010 let (tokens, _diags) = tokenize_all(source);
3011 let start = tokens[0].span.start as usize;
3012 assert_eq!(&source[start..start + 5], "const");
3013 }
3014
3015 #[test]
3017 fn bom_after_the_first_byte_is_still_an_error() {
3018 let (_tokens, diags) = tokenize_all("const \u{feff}x = 1;");
3019 assert_eq!(diags.len(), 1);
3020 assert!(
3021 diags[0].message.contains("unexpected character"),
3022 "got: {}",
3023 diags[0].message
3024 );
3025 }
3026
3027 #[test]
3028 fn malformed_bytes_stop_accumulating_diagnostics_at_the_cap() {
3029 let source = "@".repeat(10_000);
3030 let mut lexer = Lexer::new(&source, F);
3031 assert!(matches!(lexer.next_token().kind, TokenKind::Eof));
3032 let diagnostics = lexer.finish().unwrap();
3033 assert_eq!(diagnostics.len(), super::MAX_LEXER_DIAGNOSTICS);
3034 assert!(
3035 diagnostics
3036 .iter()
3037 .all(|diagnostic| diagnostic.message.contains("unexpected character"))
3038 );
3039 }
3040
3041 #[test]
3042 fn tokenize_mvp_fixture() {
3043 let source = "function greet(name: string): string {\n return \"Hello, \" + name;\n}\n\nfunction main(): string {\n const msg = greet(\"world\");\n console.log(msg);\n assert(msg === \"Hello, world\", \"greeting should match\");\n return msg;\n}\n";
3044 let (tokens, diags) = tokenize_all(source);
3045 assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
3046 let kinds: Vec<String> = tokens.iter().map(|t| format!("{:?}", t.kind)).collect();
3047 insta::assert_debug_snapshot!(kinds);
3048 }
3049}