use super::comments;
use super::escapes;
use super::ident::{is_id_continue, is_id_start};
use super::token::{Token, TokenKind, WordLookup, keyword_at, keyword_at_in_word, long_keyword};
use tsv_lang::Error;
const LEGACY_SEPARATOR_MSG: &str =
"Numeric separators are not allowed in legacy octal-like literals";
#[inline]
const fn utf8_len(lead: u8) -> usize {
match lead {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
_ => 4,
}
}
const ID_START_LUT: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0;
while i < 256 {
let b = i as u8;
t[i] = b.is_ascii_alphabetic() || b == b'_' || b == b'$';
i += 1;
}
t
};
const ID_CONTINUE_LUT: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0;
while i < 256 {
let b = i as u8;
t[i] = b.is_ascii_alphanumeric() || b == b'_' || b == b'$';
i += 1;
}
t
};
#[inline]
const fn is_ascii_id_start(b: u8) -> bool {
ID_START_LUT[b as usize]
}
#[inline]
const fn is_ascii_id_continue(b: u8) -> bool {
ID_CONTINUE_LUT[b as usize]
}
#[inline]
fn ascii_id_run_end(bytes: &[u8], start: usize) -> usize {
const CHUNK: usize = 8;
let mut end = start + 1;
while let Some(chunk) = bytes.get(end..end + CHUNK) {
for (i, &b) in chunk.iter().enumerate() {
if !is_ascii_id_continue(b) {
return end + i;
}
}
end += CHUNK;
}
while end < bytes.len() && is_ascii_id_continue(bytes[end]) {
end += 1;
}
end
}
#[inline]
pub(crate) fn unicode_escape_len_at(bytes: &[u8], pos: usize) -> usize {
match try_decode_unicode_escape(bytes, pos) {
Some((_, len)) => len,
None => 0,
}
}
fn try_decode_unicode_escape(bytes: &[u8], start: usize) -> Option<(char, usize)> {
if start + 2 > bytes.len() || bytes[start] != b'\\' || bytes[start + 1] != b'u' {
return None;
}
let after_u = start + 2;
if after_u < bytes.len() && bytes[after_u] == b'{' {
let content_start = after_u + 1;
let mut end = content_start;
while end < bytes.len() && bytes[end] != b'}' {
if !bytes[end].is_ascii_hexdigit() {
return None;
}
end += 1;
}
if end >= bytes.len() || end == content_start {
return None;
}
let mut code: u64 = 0;
for &b in &bytes[content_start..end] {
if code <= 0x10FFFF {
code = code * 16 + u64::from((b as char).to_digit(16)?);
}
}
if code > 0x10FFFF {
return None;
}
let ch = char::from_u32(code as u32)?;
Some((ch, end + 1 - start)) } else {
if after_u + 4 > bytes.len() {
return None;
}
let mut code: u32 = 0;
for &b in &bytes[after_u..after_u + 4] {
code = code * 16 + (b as char).to_digit(16)?;
}
let ch = char::from_u32(code)?;
Some((ch, 6)) }
}
enum TemplateStop {
Backtick,
Interpolation,
Eof,
}
#[derive(Clone, Copy)]
pub(crate) struct LexerCheckpoint {
position: usize,
template_depth: u32,
had_line_terminator: bool,
nonplain_ident_starts: [u32; 2],
}
pub(crate) struct Lexer<'a> {
source: &'a str,
bytes: &'a [u8],
position: usize,
template_depth: u32,
had_line_terminator: bool,
decode_scratch: String,
has_decoded: bool,
nonplain_ident_starts: [u32; 2],
base_offset: usize,
}
pub(crate) const fn is_es_whitespace(c: char) -> bool {
matches!(
c,
'\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{FEFF}' | '\u{0020}' | '\u{00A0}' | '\u{1680}'
| '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
)
}
pub(crate) const fn is_es_line_terminator(c: char) -> bool {
matches!(c, '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}')
}
#[inline]
pub(crate) fn is_es_line_terminator_at(bytes: &[u8], pos: usize) -> bool {
match bytes[pos] {
b'\n' | b'\r' => true,
0xE2 => {
pos + 2 < bytes.len() && bytes[pos + 1] == 0x80 && matches!(bytes[pos + 2], 0xA8 | 0xA9)
}
_ => false,
}
}
pub(crate) const ES_LINE_TERMINATOR_LEADS: [u8; 3] = [b'\n', b'\r', 0xE2];
#[cold]
#[inline(never)]
fn skip_whitespace_general(source: &str, mut pos: usize) -> (usize, bool) {
let mut crossed = false;
loop {
match source[pos..].chars().next() {
Some(c) if is_es_line_terminator(c) => {
crossed = true;
pos += c.len_utf8();
}
Some(c) if is_es_whitespace(c) => pos += c.len_utf8(),
_ => return (pos, crossed),
}
}
}
impl<'a> Lexer<'a> {
pub(crate) fn at_offset(source: &'a str, base_offset: usize) -> Self {
let bytes = source.as_bytes();
let position = tsv_lang::leading_bom_len(source);
Self {
source,
bytes,
position,
template_depth: 0,
had_line_terminator: false,
decode_scratch: String::new(),
has_decoded: false,
nonplain_ident_starts: [u32::MAX; 2],
base_offset,
}
}
#[cold]
#[inline(never)]
fn host_err(&self, err: Error) -> Error {
err.shift_position(self.base_offset)
}
fn hashbang_at(&self, start: usize) -> bool {
self.base_offset == 0 && start == tsv_lang::leading_bom_len(self.source)
}
#[inline]
fn cur_byte(&self) -> Option<u8> {
self.bytes.get(self.position).copied()
}
#[inline]
fn byte_ahead(&self, offset: usize) -> Option<u8> {
self.bytes.get(self.position + offset).copied()
}
#[inline]
fn cur_char(&self) -> Option<char> {
self.source[self.position..].chars().next()
}
#[inline]
fn at_line_separator(&self) -> bool {
matches!(self.cur_char(), Some('\u{2028}' | '\u{2029}'))
}
#[inline]
pub(crate) fn decoded_str(&self) -> Option<&str> {
if self.has_decoded {
Some(&self.decode_scratch)
} else {
None
}
}
#[inline]
pub(crate) fn ident_is_plain_ascii(&self, start: u32) -> bool {
let [k0, k1] = self.nonplain_ident_starts;
start != k0 && start != k1
}
#[cold]
#[inline(never)]
fn note_nonplain_ident(&mut self, start: usize) {
let start = start as u32;
if self.nonplain_ident_starts[0] != start {
self.nonplain_ident_starts[1] = self.nonplain_ident_starts[0];
self.nonplain_ident_starts[0] = start;
}
}
pub(crate) fn had_line_terminator(&self) -> bool {
self.had_line_terminator
}
#[must_use]
pub(crate) fn checkpoint(&self) -> LexerCheckpoint {
LexerCheckpoint {
position: self.position,
template_depth: self.template_depth,
had_line_terminator: self.had_line_terminator,
nonplain_ident_starts: self.nonplain_ident_starts,
}
}
pub(crate) fn rewind(&mut self, checkpoint: LexerCheckpoint) {
let LexerCheckpoint {
position,
template_depth,
had_line_terminator,
nonplain_ident_starts,
} = checkpoint;
self.position = position;
self.template_depth = template_depth;
self.had_line_terminator = had_line_terminator;
self.nonplain_ident_starts = nonplain_ident_starts;
self.has_decoded = false;
}
pub(crate) fn seek_and_next_token(&mut self, position: usize) -> Result<Token, Error> {
self.set_position(position);
self.next_token()
}
#[inline]
fn set_position(&mut self, position: usize) {
self.position = position;
}
#[expect(clippy::inline_always)]
#[inline(always)]
fn advance(&mut self) {
if let Some(&b) = self.bytes.get(self.position) {
self.position += utf8_len(b);
}
}
#[inline]
fn advance_ascii(&mut self) {
debug_assert!(self.bytes[self.position].is_ascii());
self.position += 1;
}
#[inline]
fn make_token(&self, kind: TokenKind, start: usize) -> Token {
Token {
kind,
start: start as u32,
end: self.position as u32,
}
}
#[inline]
fn scan_ascii_identifier_into(
&mut self,
bytes: &'a [u8],
start: usize,
dst: &mut Token,
) -> Result<(), Error> {
let first = bytes[start];
debug_assert!(is_ascii_id_start(first));
let end = ascii_id_run_end(bytes, start);
let lookup = match bytes.get(end) {
Some(&stop) if stop >= 0x80 || stop == b'\\' => WordLookup::Deferred,
_ => keyword_at_in_word(bytes, start, end - start),
};
let keyword = match lookup {
WordLookup::Settled(keyword) => keyword,
WordLookup::Long => return self.finish_long_name_into(start, end, dst),
WordLookup::Deferred => return self.next_token_dispatch_at(start, dst),
};
self.position = end;
*dst = Token {
kind: match keyword {
Some(kw) => TokenKind::Keyword(kw),
None => TokenKind::Identifier,
},
start: start as u32,
end: end as u32,
};
Ok(())
}
#[inline(never)]
fn finish_long_name_into(
&mut self,
start: usize,
end: usize,
dst: &mut Token,
) -> Result<(), Error> {
let Some(name) = self.bytes.get(start..end) else {
return self.next_token_dispatch_at(start, dst);
};
self.position = end;
*dst = Token {
kind: match long_keyword(name) {
Some(kw) => TokenKind::Keyword(kw),
None => TokenKind::Identifier,
},
start: start as u32,
end: end as u32,
};
Ok(())
}
#[cold]
#[inline(never)]
fn next_token_dispatch_at(&mut self, start: usize, dst: &mut Token) -> Result<(), Error> {
let bytes = self.bytes;
self.next_token_dispatch(bytes, start, bytes[start], dst)
}
#[cold]
#[inline(never)]
fn finish_ascii_identifier_into(
&mut self,
bytes: &'a [u8],
start: usize,
end: usize,
dst: &mut Token,
) {
self.set_position(end);
if bytes
.get(end)
.is_some_and(|&stop| stop >= 0x80 || stop == b'\\')
{
let decoded = self.scan_identifier_tail(start, None);
if self.position != end {
self.finish_general_identifier_into(start, decoded, dst);
return;
}
}
*dst = Token {
kind: match keyword_at(bytes, start, end - start) {
Some(kw) => TokenKind::Keyword(kw),
None => TokenKind::Identifier,
},
start: start as u32,
end: end as u32,
};
}
fn scan_identifier_into(&mut self, first_char: char, dst: &mut Token) -> Result<(), Error> {
let start = self.position;
let mut decoded: Option<String> = None;
if first_char == '\\' {
if let Some((ch, len)) = try_decode_unicode_escape(self.bytes, self.position) {
if !is_id_start(ch) {
return Err(Error::lex(
format!("Invalid identifier start character from unicode escape: '{ch}'"),
start,
));
}
decoded = Some(String::from(ch));
for _ in 0..len {
self.advance();
}
} else {
return Err(Error::lex("Invalid unicode escape in identifier", start));
}
} else {
self.advance();
self.note_nonplain_ident(start);
}
let decoded = self.scan_identifier_tail(start, decoded);
self.finish_general_identifier_into(start, decoded, dst);
Ok(())
}
fn scan_identifier_tail(
&mut self,
start: usize,
mut decoded: Option<String>,
) -> Option<String> {
loop {
match self.cur_byte() {
Some(b'\\') => {
if let Some((ch, len)) = try_decode_unicode_escape(self.bytes, self.position) {
if !is_id_continue(ch) {
break;
}
decoded
.get_or_insert_with(|| self.source[start..self.position].to_string())
.push(ch);
for _ in 0..len {
self.advance();
}
} else {
break;
}
}
Some(b) if b < 0x80 => {
if is_ascii_id_continue(b) {
if let Some(d) = &mut decoded {
d.push(b as char);
}
self.advance();
} else {
break;
}
}
Some(_) => match self.cur_char() {
Some(ch) if is_id_continue(ch) => {
self.note_nonplain_ident(start);
if let Some(d) = &mut decoded {
d.push(ch);
}
self.advance();
}
_ => break,
},
None => break,
}
}
decoded
}
fn finish_general_identifier_into(
&mut self,
start: usize,
decoded: Option<String>,
dst: &mut Token,
) {
if let Some(s) = decoded {
self.decode_scratch.clear();
self.decode_scratch.push_str(&s);
self.has_decoded = true;
}
*dst = Token {
kind: TokenKind::Identifier,
start: start as u32,
end: self.position as u32,
};
}
fn scan_digits(&mut self, is_valid_digit: impl Fn(char) -> bool) -> Result<bool, Error> {
let mut saw_digit = false;
let mut prev_was_digit = false;
while let Some(b) = self.cur_byte() {
if b == b'_' {
let next_is_digit = self
.byte_ahead(1)
.is_some_and(|n| is_valid_digit(n as char));
if !prev_was_digit || !next_is_digit {
return Err(Error::lex(
"Numeric separator '_' must appear between two digits",
self.position,
));
}
self.advance();
prev_was_digit = false;
} else if is_valid_digit(b as char) {
self.advance();
saw_digit = true;
prev_was_digit = true;
} else {
break;
}
}
Ok(saw_digit)
}
fn scan_decimal_number(&mut self) -> Result<bool, Error> {
fn is_exponent_start(s: &str) -> bool {
let mut chars = s.chars();
if !matches!(chars.next(), Some('e' | 'E')) {
return false;
}
let mut c = chars.next();
if matches!(c, Some('+' | '-')) {
c = chars.next();
}
c.is_some_and(|c| c.is_ascii_digit())
}
let mut is_integer = true;
self.scan_digits(|c| c.is_ascii_digit())?;
if self.cur_byte() == Some(b'.') {
if self.byte_ahead(1).is_some_and(|b| b.is_ascii_digit()) {
is_integer = false;
self.advance(); self.scan_digits(|c| c.is_ascii_digit())?;
} else if is_exponent_start(&self.source[self.position + 1..]) {
is_integer = false;
self.advance(); } else {
is_integer = false;
self.advance(); }
}
if matches!(self.cur_byte(), Some(b'e' | b'E')) {
is_integer = false;
self.advance(); if matches!(self.cur_byte(), Some(b'+' | b'-')) {
self.advance();
}
self.scan_digits(|c| c.is_ascii_digit())?;
}
Ok(is_integer)
}
fn scan_number_into(&mut self, start: usize, first: u8, dst: &mut Token) -> Result<(), Error> {
let mut bigint_allowed = true;
if first == b'0' {
match self.byte_ahead(1) {
Some(b'x' | b'X') => {
self.advance(); self.advance(); if !self.scan_digits(|c| c.is_ascii_hexdigit())? {
return Err(Error::lex("Missing hexadecimal digits after '0x'", start));
}
}
Some(b'b' | b'B') => {
self.advance(); self.advance(); if !self.scan_digits(|c| c == '0' || c == '1')? {
return Err(Error::lex("Missing binary digits after '0b'", start));
}
}
Some(b'o' | b'O') => {
self.advance(); self.advance(); if !self.scan_digits(|c| ('0'..='7').contains(&c))? {
return Err(Error::lex("Missing octal digits after '0o'", start));
}
}
Some(b'0'..=b'9') => {
self.advance(); let mut all_octal = true;
while let Some(b) = self.cur_byte() {
if b.is_ascii_digit() {
if b >= b'8' {
all_octal = false;
}
self.advance();
} else {
break;
}
}
if self.cur_byte() == Some(b'_') {
return Err(Error::lex(LEGACY_SEPARATOR_MSG, start));
}
if !all_octal {
self.scan_decimal_number()?;
}
bigint_allowed = false;
}
Some(b'_') => {
return Err(Error::lex(LEGACY_SEPARATOR_MSG, start));
}
_ => {
bigint_allowed = self.scan_decimal_number()?;
}
}
} else {
bigint_allowed = self.scan_decimal_number()?;
}
if bigint_allowed && self.cur_byte() == Some(b'n') {
self.advance();
}
let id_start_follows = match self.cur_byte() {
Some(b) if b < 0x80 => is_ascii_id_start(b),
Some(_) => self.cur_char().is_some_and(is_id_start),
None => false,
};
if id_start_follows {
return Err(Error::lex(
"Identifier directly after number",
self.position,
));
}
*dst = self.make_token(TokenKind::Number, start);
Ok(())
}
fn scan_string_into(&mut self, start: usize, quote: u8, dst: &mut Token) -> Result<(), Error> {
self.advance(); let content_start = self.position;
let bytes = self.bytes;
let len = bytes.len();
let mut p = content_start;
let mut has_escapes = false;
loop {
p = tsv_lang::swar::next_byte_of(bytes, p, [quote, b'\\', b'\n', b'\r']);
if p >= len || bytes[p] == b'\n' || bytes[p] == b'\r' {
self.position = p;
return Err(Error::lex("Unterminated string literal", start));
}
if bytes[p] == quote {
let content_end = p;
p += 1; self.position = p;
let content = &self.source[content_start..content_end];
if has_escapes {
escapes::decode_string_escapes_into(content, &mut self.decode_scratch)
.map_err(|e| e.shift_position(content_start))?;
self.has_decoded = true;
}
*dst = Token {
kind: TokenKind::String,
start: start as u32,
end: p as u32,
};
return Ok(());
}
has_escapes = true;
p += 1;
if p < len {
if bytes[p] == b'\r' && bytes.get(p + 1) == Some(&b'\n') {
p += 2;
} else {
p += utf8_len(bytes[p]);
}
}
}
}
#[inline]
fn skip_ascii_whitespace(&mut self, bytes: &'a [u8]) -> Option<u8> {
let mut pos = self.position;
while let Some(&b) = bytes.get(pos) {
match b {
b' ' | b'\t' | 0x0B | 0x0C => {}
b'\n' | b'\r' => self.had_line_terminator = true,
_ => {
self.position = pos;
return Some(b);
}
}
pos += 1;
}
self.position = pos;
None
}
#[inline]
pub(crate) fn next_token_into(&mut self, dst: &mut Token) -> Result<(), Error> {
self.has_decoded = false;
self.had_line_terminator = false;
self.next_token_into_local(dst)
.map_err(|err| self.host_err(err))
}
fn next_token_into_local(&mut self, dst: &mut Token) -> Result<(), Error> {
let bytes = self.bytes;
let Some(first) = self.skip_ascii_whitespace(bytes) else {
let end = self.position as u32;
*dst = Token {
kind: TokenKind::Eof,
start: end,
end,
};
return Ok(());
};
let start = self.position;
*dst = match first {
b if is_ascii_id_start(b) => {
return self.scan_ascii_identifier_into(bytes, start, dst);
}
b';' => {
self.advance_ascii();
self.make_token(TokenKind::Semicolon, start)
}
b':' => {
self.advance_ascii();
self.make_token(TokenKind::Colon, start)
}
b'=' => {
self.advance_ascii();
match self.cur_byte() {
Some(b'>') => {
self.advance_ascii();
self.make_token(TokenKind::Arrow, start)
}
Some(b'=') => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::EqualsEqualsEquals, start)
} else {
self.make_token(TokenKind::EqualsEquals, start)
}
}
_ => {
self.make_token(TokenKind::Equals, start)
}
}
}
b',' => {
self.advance_ascii();
self.make_token(TokenKind::Comma, start)
}
b'{' => {
self.advance_ascii();
self.make_token(TokenKind::BraceOpen, start)
}
b'}' => {
self.advance_ascii();
self.make_token(TokenKind::BraceClose, start)
}
b'[' => {
self.advance_ascii();
self.make_token(TokenKind::BracketOpen, start)
}
b']' => {
self.advance_ascii();
self.make_token(TokenKind::BracketClose, start)
}
b'(' => {
self.advance_ascii();
self.make_token(TokenKind::ParenOpen, start)
}
b')' => {
self.advance_ascii();
self.make_token(TokenKind::ParenClose, start)
}
b'.' => {
if self.byte_ahead(1) == Some(b'.') && self.byte_ahead(2) == Some(b'.') {
self.advance_ascii(); self.advance_ascii(); self.advance_ascii(); self.make_token(TokenKind::DotDotDot, start)
} else if self.byte_ahead(1).is_some_and(|b| b.is_ascii_digit()) {
return self.next_token_dispatch(bytes, start, b'.', dst);
} else {
self.advance_ascii();
self.make_token(TokenKind::Dot, start)
}
}
b'-' => {
self.advance_ascii();
if self.cur_byte() == Some(b'-') {
self.advance_ascii();
self.make_token(TokenKind::MinusMinus, start)
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::MinusEquals, start)
} else {
self.make_token(TokenKind::Minus, start)
}
}
b'+' => {
self.advance_ascii();
if self.cur_byte() == Some(b'+') {
self.advance_ascii();
self.make_token(TokenKind::PlusPlus, start)
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::PlusEquals, start)
} else {
self.make_token(TokenKind::Plus, start)
}
}
b'/' => {
match self.byte_ahead(1) {
Some(b'/' | b'*') => return self.next_token_dispatch(bytes, start, b'/', dst),
Some(b'=') => {
self.advance_ascii();
self.advance_ascii();
self.make_token(TokenKind::SlashEquals, start)
}
_ => {
self.advance_ascii();
self.make_token(TokenKind::Slash, start)
}
}
}
b'*' => {
self.advance_ascii();
if self.cur_byte() == Some(b'*') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::StarStarEquals, start)
} else {
self.make_token(TokenKind::StarStar, start)
}
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::StarEquals, start)
} else {
self.make_token(TokenKind::Star, start)
}
}
b'%' => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::PercentEquals, start)
} else {
self.make_token(TokenKind::Percent, start)
}
}
b'^' => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::CaretEquals, start)
} else {
self.make_token(TokenKind::Caret, start)
}
}
b'~' => {
self.advance_ascii();
self.make_token(TokenKind::Tilde, start)
}
b'<' => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::LessThanEquals, start)
} else if self.cur_byte() == Some(b'<') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::LeftShiftEquals, start)
} else {
self.make_token(TokenKind::LeftShift, start)
}
} else {
self.make_token(TokenKind::LessThan, start)
}
}
b'>' => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::GreaterThanEquals, start)
} else if self.cur_byte() == Some(b'>') {
self.advance_ascii();
if self.cur_byte() == Some(b'>') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::UnsignedRightShiftEquals, start)
} else {
self.make_token(TokenKind::UnsignedRightShift, start)
}
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::RightShiftEquals, start)
} else {
self.make_token(TokenKind::RightShift, start)
}
} else {
self.make_token(TokenKind::GreaterThan, start)
}
}
b'!' => {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::BangEqualsEquals, start)
} else {
self.make_token(TokenKind::BangEquals, start)
}
} else {
self.make_token(TokenKind::Bang, start)
}
}
b'&' => {
self.advance_ascii();
if self.cur_byte() == Some(b'&') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::AmpersandAmpersandEquals, start)
} else {
self.make_token(TokenKind::AmpersandAmpersand, start)
}
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::AmpersandEquals, start)
} else {
self.make_token(TokenKind::Ampersand, start)
}
}
b'|' => {
self.advance_ascii();
if self.cur_byte() == Some(b'|') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::PipePipeEquals, start)
} else {
self.make_token(TokenKind::PipePipe, start)
}
} else if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::PipeEquals, start)
} else {
self.make_token(TokenKind::Pipe, start)
}
}
b'?' => {
self.advance_ascii();
if self.cur_byte() == Some(b'?') {
self.advance_ascii();
if self.cur_byte() == Some(b'=') {
self.advance_ascii();
self.make_token(TokenKind::QuestionQuestionEquals, start)
} else {
self.make_token(TokenKind::QuestionQuestion, start)
}
} else if self.cur_byte() == Some(b'.') {
let next = self.byte_ahead(1);
if next.is_none_or(|b| !b.is_ascii_digit()) {
self.advance_ascii();
self.make_token(TokenKind::QuestionDot, start)
} else {
self.make_token(TokenKind::Question, start)
}
} else {
self.make_token(TokenKind::Question, start)
}
}
b'@' => {
self.advance_ascii();
self.make_token(TokenKind::At, start)
}
b'#' => {
if self.byte_ahead(1) == Some(b'!') {
return self.next_token_dispatch(bytes, start, b'#', dst);
}
self.advance_ascii();
self.make_token(TokenKind::Hash, start)
}
0x80.. => return self.next_token_after_non_ascii(bytes, start, dst),
b => return self.next_token_dispatch(bytes, start, b, dst),
};
Ok(())
}
#[cold]
#[inline(never)]
fn next_token_after_non_ascii(
&mut self,
bytes: &'a [u8],
pos: usize,
dst: &mut Token,
) -> Result<(), Error> {
let (start, crossed) = skip_whitespace_general(self.source, pos);
if start == pos {
return self.next_token_dispatch(bytes, pos, bytes[pos], dst);
}
if crossed {
self.had_line_terminator = true;
}
self.position = start;
self.next_token_into_local(dst)
}
#[inline(never)]
fn next_token_dispatch(
&mut self,
bytes: &'a [u8],
start: usize,
first: u8,
dst: &mut Token,
) -> Result<(), Error> {
match first {
b if is_ascii_id_start(b) => {
let end = ascii_id_run_end(bytes, start);
self.finish_ascii_identifier_into(bytes, start, end, dst);
Ok(())
}
b if b.is_ascii_digit() => self.scan_number_into(start, b, dst),
b'.' => {
debug_assert!(self.byte_ahead(1).is_some_and(|b| b.is_ascii_digit()));
self.scan_number_into(start, b'.', dst)
}
b'\\' => {
if let Some((ch, _)) = try_decode_unicode_escape(self.bytes, self.position)
&& is_id_start(ch)
{
return self.scan_identifier_into('\\', dst);
}
Err(Error::lex("Unexpected character: '\\'", start))
}
quote @ (b'\'' | b'"') => self.scan_string_into(start, quote, dst),
b'/' => {
if self.byte_ahead(1) == Some(b'/') {
let end = comments::line_comment_end(bytes, start);
self.comment_into(start, end, false, dst);
} else {
debug_assert_eq!(self.byte_ahead(1), Some(b'*'));
let end = comments::block_comment_end(bytes, start)?;
self.comment_into(start, end, true, dst);
}
Ok(())
}
b'`' => self.read_template_into(start, dst),
b'#' => {
debug_assert_eq!(self.byte_ahead(1), Some(b'!'));
if self.hashbang_at(start) {
self.read_hashbang_into(start, dst)
} else {
self.advance_ascii();
*dst = self.make_token(TokenKind::Hash, start);
Ok(())
}
}
b if b >= 0x80 => match self.cur_char() {
Some(ch) if is_id_start(ch) => self.scan_identifier_into(ch, dst),
Some(ch) => Err(Error::lex(format!("Unexpected character: '{ch}'"), start)),
None => Err(Error::lex("Unexpected character", start)),
},
b => Err(Error::lex(
format!("Unexpected character: '{}'", b as char),
start,
)),
}
}
#[inline]
fn comment_into(&mut self, start: usize, end: usize, is_block: bool, dst: &mut Token) {
self.position = end;
*dst = Token {
kind: TokenKind::Comment {
is_block,
content_start: (start + 2) as u32,
},
start: start as u32,
end: end as u32,
};
}
pub(crate) fn next_token(&mut self) -> Result<Token, Error> {
let mut tok = Token {
kind: TokenKind::Eof,
start: 0,
end: 0,
};
self.next_token_into(&mut tok)?;
Ok(tok)
}
fn scan_template_body(&mut self, content_start: usize) -> (usize, TemplateStop, bool) {
let bytes = self.bytes;
let len = bytes.len();
let mut p = content_start;
let mut has_escapes = false;
loop {
p = tsv_lang::swar::next_byte_of(bytes, p, [b'`', b'$', b'\\']);
if p >= len {
self.position = p;
return (p, TemplateStop::Eof, has_escapes);
}
match bytes[p] {
b'`' => {
let content_end = p;
self.position = p + 1; return (content_end, TemplateStop::Backtick, has_escapes);
}
b'$' if bytes.get(p + 1) == Some(&b'{') => {
let content_end = p;
self.position = p + 2; return (content_end, TemplateStop::Interpolation, has_escapes);
}
b'$' => p += 1, _ => {
has_escapes = true;
p += 1;
if p < len {
p += utf8_len(bytes[p]);
}
}
}
}
}
fn read_template_into(&mut self, start: usize, dst: &mut Token) -> Result<(), Error> {
self.advance();
let content_start = self.position;
let (content_end, stop, has_escapes) = self.scan_template_body(content_start);
let kind = match stop {
TemplateStop::Eof => {
return Err(Error::lex("Unterminated template literal", start));
}
TemplateStop::Backtick => {
if self.source[start..].starts_with('`') {
TokenKind::NoSubstitutionTemplate
} else {
TokenKind::TemplateTail
}
}
TemplateStop::Interpolation => {
self.template_depth += 1;
if self.source[start..].starts_with('`') {
TokenKind::TemplateHead
} else {
TokenKind::TemplateMiddle
}
}
};
let content = &self.source[content_start..content_end];
if has_escapes
&& escapes::decode_string_escapes_into(content, &mut self.decode_scratch).is_ok()
{
self.has_decoded = true;
}
*dst = Token {
kind,
start: start as u32,
end: self.position as u32,
};
Ok(())
}
#[inline]
pub(crate) fn read_regex_literal(
&mut self,
slash_start: usize,
) -> Result<(Token, usize), Error> {
self.read_regex_literal_local(slash_start)
.map_err(|err| self.host_err(err))
}
fn read_regex_literal_local(&mut self, slash_start: usize) -> Result<(Token, usize), Error> {
self.has_decoded = false;
self.set_position(slash_start + 1);
let pattern_start = self.position;
let mut in_class = false; let mut escaped = false;
loop {
match self.cur_byte() {
None => {
return Err(Error::lex(
"Unterminated regular expression literal",
slash_start,
));
}
Some(b'\n' | b'\r') => {
return Err(Error::lex(
"Unterminated regular expression literal",
slash_start,
));
}
Some(b) if b >= 0x80 && self.at_line_separator() => {
return Err(Error::lex(
"Unterminated regular expression literal",
slash_start,
));
}
Some(_) if escaped => {
escaped = false;
self.advance();
}
Some(b'\\') => {
escaped = true;
self.advance();
}
Some(b'[') if !in_class => {
in_class = true;
self.advance();
}
Some(b']') if in_class => {
in_class = false;
self.advance();
}
Some(b'/') if !in_class => {
break;
}
Some(_) => {
self.advance();
}
}
}
let pattern_end = self.position;
let pattern = &self.source[pattern_start..pattern_end];
if pattern.is_empty() {
return Err(Error::lex(
"Regular expression literal cannot be empty (use /(?:)/ for empty pattern)",
slash_start,
));
}
self.advance();
while let Some(b) = self.cur_byte() {
let Some(ch) = (if b < 0x80 {
Some(b as char)
} else {
self.cur_char()
}) else {
break;
};
if is_id_continue(ch) {
self.advance();
} else {
break;
}
}
Ok((
Token {
kind: TokenKind::RegexLiteral,
start: slash_start as u32,
end: self.position as u32,
},
pattern_end,
))
}
#[inline]
pub(crate) fn continue_template_from_brace(
&mut self,
brace_end: usize,
) -> Result<Token, Error> {
self.continue_template_from_brace_local(brace_end)
.map_err(|err| self.host_err(err))
}
fn continue_template_from_brace_local(&mut self, brace_end: usize) -> Result<Token, Error> {
self.has_decoded = false;
if self.template_depth == 0 {
return Err(Error::lex(
"continue_template called outside template context",
self.position,
));
}
self.template_depth -= 1;
self.set_position(brace_end);
let content_start = brace_end;
let brace_start = brace_end - 1; let (content_end, stop, has_escapes) = self.scan_template_body(content_start);
let kind = match stop {
TemplateStop::Eof => {
return Err(Error::lex("Unterminated template literal", brace_start));
}
TemplateStop::Backtick => TokenKind::TemplateTail,
TemplateStop::Interpolation => {
self.template_depth += 1;
TokenKind::TemplateMiddle
}
};
let content = &self.source[content_start..content_end];
if has_escapes
&& escapes::decode_string_escapes_into(content, &mut self.decode_scratch).is_ok()
{
self.has_decoded = true;
}
Ok(Token {
kind,
start: brace_start as u32,
end: self.position as u32,
})
}
fn read_hashbang_into(&mut self, start: usize, dst: &mut Token) -> Result<(), Error> {
self.advance(); self.advance();
loop {
match self.cur_byte() {
None | Some(b'\n' | b'\r') => break,
Some(b) if b >= 0x80 && self.at_line_separator() => {
break;
}
Some(_) => {
self.advance();
}
}
}
*dst = Token {
kind: TokenKind::Comment {
is_block: false,
content_start: start as u32,
},
start: start as u32,
end: self.position as u32,
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::token::KEYWORDS;
#[test]
fn ascii_id_luts_match_unicode_predicates() {
for b in 0u8..0x80 {
assert_eq!(
is_ascii_id_start(b),
is_id_start(b as char),
"id_start mismatch at byte {b:#x}"
);
assert_eq!(
is_ascii_id_continue(b),
is_id_continue(b as char),
"id_continue mismatch at byte {b:#x}"
);
}
}
#[test]
fn line_terminator_leads_cover_every_first_byte_of_the_exact_class() {
for b in 0u8..0x80 {
let bytes = [b];
assert!(
!is_es_line_terminator_at(&bytes, 0) || ES_LINE_TERMINATOR_LEADS.contains(&b),
"byte {b:#x} terminates a line but is not a scan lead"
);
}
for s in ["\u{2028}", "\u{2029}", "\u{2014}"] {
let bytes = s.as_bytes();
assert!(
!is_es_line_terminator_at(bytes, 0) || ES_LINE_TERMINATOR_LEADS.contains(&bytes[0]),
"{s:?} terminates a line but its lead is not a scan lead"
);
}
assert!(!is_es_line_terminator_at("\u{2014}".as_bytes(), 0));
}
fn model_identifier(src: &str) -> (TokenKind, usize, Option<String>, bool) {
let bytes = src.as_bytes();
let mut decoded = None;
let mut end = if bytes[0] == b'\\' {
let (ch, len) = try_decode_unicode_escape(bytes, 0).expect("an escape-led case");
assert!(
is_id_start(ch),
"an escape-led case must start an identifier"
);
decoded = Some(String::from(ch));
len
} else {
let ch = src.chars().next().expect("a non-empty case");
assert!(is_id_start(ch), "a case must start an identifier");
ch.len_utf8()
};
loop {
if bytes.get(end) == Some(&b'\\') {
match try_decode_unicode_escape(bytes, end) {
Some((ch, len)) if is_id_continue(ch) => {
decoded
.get_or_insert_with(|| src[..end].to_string())
.push(ch);
end += len;
}
_ => break,
}
} else {
match src[end..].chars().next() {
Some(ch) if is_id_continue(ch) => {
if let Some(d) = &mut decoded {
d.push(ch);
}
end += ch.len_utf8();
}
_ => break,
}
}
}
let kind = match decoded {
Some(_) => TokenKind::Identifier,
None => KEYWORDS
.iter()
.find(|(kw, _)| *kw == &src[..end])
.map_or(TokenKind::Identifier, |&(_, kw)| TokenKind::Keyword(kw)),
};
(kind, end, decoded, src[..end].is_ascii())
}
#[track_caller]
fn assert_identifier_matches_model(src: &str) {
const PREFIX: &str = "; ";
let (kind, len, decoded, plain) = model_identifier(src);
for prefix in ["", PREFIX] {
let source = format!("{prefix}{src}");
let start = prefix.len();
let end = start + len;
let mut lexer = Lexer::at_offset(&source, 0);
if !prefix.is_empty() {
let semicolon = lexer.next_token().expect("the prefix lexes");
assert_eq!(semicolon.kind, TokenKind::Semicolon, "prefix of {source:?}");
}
let token = lexer
.next_token()
.unwrap_or_else(|e| panic!("{source:?} failed to lex: {e}"));
assert_eq!(
(token.kind, token.start, token.end),
(kind.clone(), start as u32, end as u32),
"token of {source:?}"
);
assert_eq!(lexer.position, end, "cursor after {source:?}");
assert_eq!(
lexer.decoded_str(),
decoded.as_deref(),
"decoded name of {source:?}"
);
assert_eq!(
lexer.ident_is_plain_ascii(start as u32),
plain,
"plain-ASCII record of {source:?}"
);
}
}
fn identifier_followers() -> Vec<String> {
let mut out: Vec<String> = (0u8..0x80).map(|b| char::from(b).to_string()).collect();
let strided = (0x80..0x800)
.chain((0x800..0x1_0000).step_by(61))
.chain((0x1_0000..0x11_0000).step_by(1021));
let special = [
0x200C, 0x200D, 0x00A0, 0xFEFF, 0x2028, 0x2029, 0x3000, 0x1680, 0x0301, 0x00B7, 0x2118,
0x309B, 0x0085, 0x2014, 0xFFFF, 0x10_FFFF,
];
out.extend(
strided
.chain(special)
.filter_map(char::from_u32)
.map(String::from),
);
out
}
#[test]
fn every_ascii_led_identifier_stop_matches_the_grammar() {
let followers = identifier_followers();
let tails = ["", "x", " ", "u0061", "u{62}", "u0020", "u00", "u{110000}"];
let mut cases = 0_u32;
for first in (0u8..0x80).filter(|&b| is_ascii_id_start(b)) {
for body in ["", "b", "b9$_Z"] {
let head = format!("{}{body}", char::from(first));
assert_identifier_matches_model(&head);
cases += 1;
for follower in &followers {
let tails: &[&str] = if follower == "\\" {
&tails
} else {
&tails[..2]
};
for tail in tails {
assert_identifier_matches_model(&format!("{head}{follower}{tail}"));
cases += 1;
}
}
}
}
assert!(
cases > 500_000,
"the identifier sweep collapsed to {cases} cases"
);
}
#[test]
fn every_keyword_stop_matches_the_oracle() {
let followers = identifier_followers();
let mut cases = 0_u32;
for &(kw, _) in KEYWORDS {
for word in [
kw.to_string(),
kw[..kw.len() - 1].to_string(),
format!("{kw}s"),
] {
for follower in &followers {
let tails: &[&str] = if follower == "\\" {
&["", "x", "u0061", "u0020"]
} else {
&["", "x"]
};
for tail in tails {
assert_identifier_matches_model(&format!("{word}{follower}{tail}"));
cases += 1;
}
}
}
}
assert!(
cases > 100_000,
"the keyword sweep collapsed to {cases} cases"
);
}
#[test]
fn every_identifier_length_matches_the_grammar() {
let class: Vec<char> = ('a'..='z')
.chain('A'..='Z')
.chain('0'..='9')
.chain(['_', '$'])
.collect();
let stops = [
"",
" ",
"(",
".",
";",
"\\",
"\\x",
"\\u0061",
"\\u0020",
"\u{e9}",
"\u{a0}",
"\u{2028}",
"\u{200c}b",
"\t\n",
];
let mut names: Vec<String> = Vec::new();
for len in 1..=20 {
for (k, &c) in class.iter().enumerate() {
let name: String = (0..len)
.map(|i| match i {
0 => ['x', '_', '$', 'Q'][k % 4],
i if i == k % len => c,
_ => 'e',
})
.collect();
names.push(name);
}
}
for &(kw, _) in KEYWORDS {
names.push(kw.to_string());
for position in 0..kw.len() {
let mut near = kw.as_bytes().to_vec();
near[position] = if near[position] == b'z' { b'y' } else { b'z' };
names.push(String::from_utf8(near).unwrap());
}
names.push(format!("{kw}s"));
names.push(kw[..kw.len() - 1].to_string());
}
let mut cases = 0_u32;
for name in &names {
for stop in stops {
assert_identifier_matches_model(&format!("{name}{stop}"));
cases += 1;
}
}
assert!(
cases > 15_000,
"the identifier length sweep collapsed to {cases} cases"
);
}
#[test]
fn identifier_special_cases_match_the_grammar() {
for src in [
"ab",
"if\\x",
"if\\u0061",
"i\\u0066",
"\\u0069f",
"aé",
"a\u{200C}b",
"a\u{200D}b",
"return\u{00A0}x",
"return\u{2028}x",
"return\u{FEFF}x",
"returné",
"$a",
"_a",
"$",
"_",
"é",
"éa",
"é\\u0061",
"\\u0061",
"\\u{61}bc",
"\\u0061\\u0062",
"\u{2118}x",
] {
assert_identifier_matches_model(src);
}
for (src, first_end, second) in [("\\u0061 b", 6, 7..8), ("a\\u0062 c", 7, 8..9)] {
let mut lexer = Lexer::at_offset(src, 0);
let first = lexer.next_token().expect("the escaped name lexes");
assert_eq!((first.kind, first.end), (TokenKind::Identifier, first_end));
assert!(
lexer.decoded_str().is_some(),
"{src:?}: the escape is decoded"
);
let plain = lexer.next_token().expect("the plain name lexes");
assert_eq!(
(plain.kind, plain.start, plain.end),
(TokenKind::Identifier, second.start, second.end),
"{src:?}"
);
assert_eq!(
lexer.decoded_str(),
None,
"{src:?}: the plain name decodes nothing"
);
}
let mut lexer = Lexer::at_offset("#private", 0);
let hash = lexer.next_token().expect("`#` lexes");
assert_eq!((hash.kind, hash.start, hash.end), (TokenKind::Hash, 0, 1));
let name = lexer.next_token().expect("the private name lexes");
assert_eq!(
(name.kind, name.start, name.end),
(model_identifier("private").0, 1, 8)
);
}
type TriviaItem = Result<(TokenKind, u32, u32, bool), Error>;
fn model_trivia(src: &str, mut pos: usize) -> (Vec<TriviaItem>, usize, bool) {
let line_end = |from: usize| {
src[from..]
.char_indices()
.find(|&(_, c)| is_es_line_terminator(c))
.map_or(src.len(), |(i, _)| from + i)
};
let mut items = Vec::new();
if pos == 0 && src.starts_with("#!") {
let kind = TokenKind::Comment {
is_block: false,
content_start: 0,
};
pos = line_end(0);
items.push(Ok((kind, 0, pos as u32, false)));
}
loop {
let mut crossed = false;
while let Some(c) = src[pos..].chars().next() {
if is_es_line_terminator(c) {
crossed = true;
} else if !is_es_whitespace(c) {
break;
}
pos += c.len_utf8();
}
let (is_block, end) = if src[pos..].starts_with("//") {
(false, line_end(pos))
} else if src[pos..].starts_with("/*") {
let Some(close) = src[pos + 2..].find("*/") else {
items.push(Err(Error::lex("Unterminated block comment", pos)));
return (items, pos, crossed);
};
(true, pos + 2 + close + 2)
} else {
return (items, pos, crossed);
};
let kind = TokenKind::Comment {
is_block,
content_start: (pos + 2) as u32,
};
items.push(Ok((kind, pos as u32, end as u32, crossed)));
pos = end;
}
}
const TRIVIA_PIECES: &[&str] = &[
" ",
"\t",
"\u{0B}",
"\u{0C}",
"\n",
"\r",
"\r\n",
"\u{A0}",
"\u{FEFF}",
"\u{1680}",
"\u{2000}",
"\u{2001}",
"\u{2002}",
"\u{2003}",
"\u{2004}",
"\u{2005}",
"\u{2006}",
"\u{2007}",
"\u{2008}",
"\u{2009}",
"\u{200A}",
"\u{202F}",
"\u{205F}",
"\u{3000}",
"\u{2028}",
"\u{2029}",
"//",
"//x",
"/**/",
"/*x*/",
"/*\n*/",
"/*\u{2028}* /*/",
"/*",
];
fn trivia_followers() -> Vec<(&'static str, Option<(TokenKind, usize)>)> {
vec![
("", Some((TokenKind::Eof, 0))),
("a", Some((TokenKind::Identifier, 1))),
("if", Some((model_identifier("if").0, 2))),
("é", Some((TokenKind::Identifier, 2))),
(";", Some((TokenKind::Semicolon, 1))),
("/", Some((TokenKind::Slash, 1))),
("/=", Some((TokenKind::SlashEquals, 2))),
("=>", Some((TokenKind::Arrow, 2))),
("<!--", Some((TokenKind::LessThan, 1))),
("-->", Some((TokenKind::MinusMinus, 2))),
("#", Some((TokenKind::Hash, 1))),
("\u{85}", None),
("\u{200B}", None),
("\u{180E}", None),
]
}
#[track_caller]
fn assert_trivia_matches_model(
prefix: &str,
trivia: &str,
follower: &str,
token: Option<&(TokenKind, usize)>,
) {
let source = format!("{prefix}{trivia}{follower}");
let mut lexer = Lexer::at_offset(&source, 0);
let from = if prefix.is_empty() {
tsv_lang::leading_bom_len(&source)
} else {
let first = lexer.next_token().expect("the prefix lexes");
assert_eq!(first.end as usize, prefix.len(), "prefix of {source:?}");
prefix.len()
};
let (items, pos, crossed) = model_trivia(&source, from);
for item in items {
match item {
Ok((kind, start, end, crossed)) => {
let comment = lexer
.next_token()
.unwrap_or_else(|e| panic!("{source:?} failed to lex: {e}"));
assert_eq!(
(comment.kind, comment.start, comment.end),
(kind, start, end),
"comment of {source:?}"
);
assert_eq!(
lexer.had_line_terminator(),
crossed,
"line terminator ahead of the comment at {start} in {source:?}"
);
}
Err(expected) => {
assert_eq!(
lexer.next_token().map(|t| t.kind),
Err(expected),
"{source:?}"
);
return;
}
}
}
let result = lexer.next_token();
let token = if pos == source.len() {
Some(&(TokenKind::Eof, 0))
} else if &source[pos..] == follower {
token
} else {
let next = result.unwrap_or_else(|e| panic!("{source:?} failed to lex: {e}"));
assert_eq!(
next.start as usize, pos,
"token inside the trivia of {source:?}"
);
assert_eq!(
lexer.had_line_terminator(),
crossed,
"line terminator ahead of the token in {source:?}"
);
return;
};
match token {
Some((kind, len)) => {
let next = result.unwrap_or_else(|e| panic!("{source:?} failed to lex: {e}"));
assert_eq!(
(next.kind, next.start as usize, next.end as usize),
(kind.clone(), pos, pos + len),
"token after the trivia of {source:?}"
);
assert_eq!(
lexer.had_line_terminator(),
crossed,
"line terminator ahead of the token in {source:?}"
);
assert_eq!(lexer.position, pos + len, "cursor after {source:?}");
}
None => {
let ch = follower
.chars()
.next()
.expect("a rejected follower is a char");
assert_eq!(
result.map(|t| t.kind),
Err(Error::lex(format!("Unexpected character: '{ch}'"), pos)),
"{source:?}"
);
}
}
}
#[test]
fn every_trivia_run_matches_the_grammar() {
let followers = trivia_followers();
let mut runs = vec![String::new()];
let mut frontier = runs.clone();
for _ in 0..3 {
frontier = frontier
.iter()
.flat_map(|run| {
TRIVIA_PIECES
.iter()
.map(move |piece| format!("{run}{piece}"))
})
.collect();
runs.extend(frontier.iter().cloned());
}
let mut cases = 0_u32;
for run in &runs {
for (follower, token) in &followers {
for prefix in ["", ";"] {
assert_trivia_matches_model(prefix, run, follower, token.as_ref());
cases += 1;
}
}
}
assert!(
cases > 1_000_000,
"the trivia sweep collapsed to {cases} cases"
);
}
#[test]
fn byte_zero_trivia_matches_the_grammar() {
let followers = trivia_followers();
for head in [
"#!",
"#!x",
"#!x\n",
"#!x\r\n",
"#!x\u{2028}",
"\u{FEFF}",
"\u{FEFF}#!x\n",
] {
for piece in ["", " ", "\n", "//x", "/**/", "/*"] {
for (follower, token) in &followers {
assert_trivia_matches_model(
"",
&format!("{head}{piece}"),
follower,
token.as_ref(),
);
}
}
}
}
}