use std::fmt::Display;
use crate::span::Span;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[rustfmt::skip]
pub enum TokenKind {
Unknown, Eof, Newline,
LParen, RParen, LCurly, RCurly,
LBrack, RBrack,
Eq, Comma, Colon, SemiColon,
Star, Slash, At,
Quote,
String, UnterminatedString,
Comment,
True, False, Null,
Name, Number, HexLit,
}
impl TokenKind {
pub fn is_string(&self) -> bool {
matches!(self, Self::String | Self::UnterminatedString)
}
pub fn from_punct_byte(b: u8) -> Option<Self> {
Some(match b {
b'(' => Self::LParen,
b')' => Self::RParen,
b'{' => Self::LCurly,
b'}' => Self::RCurly,
b'[' => Self::LBrack,
b']' => Self::RBrack,
b'=' => Self::Eq,
b',' => Self::Comma,
b':' => Self::Colon,
b';' => Self::SemiColon,
b'*' => Self::Star,
b'/' => Self::Slash,
b'@' => Self::At,
_ => return None,
})
}
pub fn print_value(&self) -> Option<&'static str> {
match self {
TokenKind::Unknown => None,
TokenKind::Eof => None,
TokenKind::Newline => None,
TokenKind::LParen => Some("("),
TokenKind::RParen => Some(")"),
TokenKind::LCurly => Some("{"),
TokenKind::RCurly => Some("}"),
TokenKind::LBrack => Some("["),
TokenKind::RBrack => Some("]"),
TokenKind::Eq => Some("="),
TokenKind::Comma => Some(","),
TokenKind::Colon => Some(":"),
TokenKind::SemiColon => Some(";"),
TokenKind::Star => Some("*"),
TokenKind::Slash => Some("/"),
TokenKind::At => Some("@"),
TokenKind::Quote => Some("\""),
TokenKind::String => None,
TokenKind::UnterminatedString => None,
TokenKind::Comment => None,
TokenKind::True => Some("true"),
TokenKind::False => Some("false"),
TokenKind::Null => Some("null"),
TokenKind::Name => None,
TokenKind::Number => None,
TokenKind::HexLit => None,
}
}
}
impl Display for TokenKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
TokenKind::Unknown => "unknown text",
TokenKind::Eof => "end of file",
TokenKind::Newline => "new line",
TokenKind::LParen => "'('",
TokenKind::RParen => "')'",
TokenKind::LCurly => "'{'",
TokenKind::RCurly => "'}'",
TokenKind::LBrack => "'['",
TokenKind::RBrack => "']'",
TokenKind::Eq => "'='",
TokenKind::Comma => "','",
TokenKind::Colon => "':'",
TokenKind::SemiColon => "';'",
TokenKind::Star => "'*'",
TokenKind::Slash => "'/'",
TokenKind::At => "'@'",
TokenKind::Quote => "'\"'",
TokenKind::String => "string literal",
TokenKind::UnterminatedString => "unterminated string literal",
TokenKind::True => "'true'",
TokenKind::False => "'false'",
TokenKind::Null => "'null'",
TokenKind::Name => "keyword",
TokenKind::Number => "number",
TokenKind::HexLit => "hexadecimal literal",
TokenKind::Comment => "comment",
})
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
pub fn lex(source: &str) -> Vec<Token> {
use TokenKind::*;
let mut result = Vec::with_capacity(source.len() / 6);
let mut cur = Cursor::new(source);
while !cur.at_end() {
let start = cur.pos;
if cur.consume_while(|b| b.is_ascii_whitespace()) > 0 {
if ends_line(result.last(), cur.consumed(start)) {
result.push(Token {
kind: Newline,
span: cur.span(start),
});
}
continue;
}
let mut kind = cur.consume_token();
assert!(cur.pos > start);
if kind == Name {
kind = keyword_or_name(&source[start..cur.pos]);
}
result.push(Token {
kind,
span: cur.span(start),
});
}
result
}
struct Cursor<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(source: &'a str) -> Self {
Cursor {
bytes: source.as_bytes(),
pos: 0,
}
}
fn at_end(&self) -> bool {
self.pos >= self.bytes.len()
}
fn rest(&self) -> &'a [u8] {
&self.bytes[self.pos..]
}
fn consumed(&self, start: usize) -> &'a [u8] {
&self.bytes[start..self.pos]
}
fn span(&self, start: usize) -> Span {
Span::new(start as u32, self.pos as u32)
}
fn starts_with(&self, prefix: &[u8]) -> bool {
self.rest().starts_with(prefix)
}
#[inline]
fn consume_while(&mut self, pred: impl Fn(&u8) -> bool) -> usize {
let n = scan_run(self.rest(), pred);
self.pos += n;
n
}
#[inline]
fn consume_token(&mut self) -> TokenKind {
use TokenKind::*;
let b = self.bytes[self.pos];
if let Some(k) = TokenKind::from_punct_byte(b) {
self.pos += 1;
return k;
}
if b == b'#' {
self.pos += 1;
self.consume_while(|b| !matches!(b, b'\n' | b'\r'));
return Comment;
}
if self.starts_with(b"0x") {
self.pos += 2;
self.consume_while(|b| b.is_ascii_hexdigit());
return HexLit;
}
if let Some(len) = scan_number(self.rest()) {
self.pos += len;
return Number;
}
if matches!(b, b'\'' | b'"') {
return self.consume_string();
}
if self.consume_while(is_name_char) > 0 {
return Name;
}
self.consume_while(|b| !b.is_ascii_whitespace());
Unknown
}
#[inline]
fn consume_string(&mut self) -> TokenKind {
use TokenKind::*;
let quote = self.bytes[self.pos];
self.pos += 1;
let mut escaped = false;
while let Some(&b) = self.bytes.get(self.pos) {
self.pos += 1;
match b {
b'\n' | b'\r' => return UnterminatedString,
_ if escaped => escaped = false,
b'\\' => escaped = true,
_ if b == quote => return String,
_ => {}
}
}
UnterminatedString
}
}
fn is_name_char(b: &u8) -> bool {
matches!(b, b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9')
}
fn keyword_or_name(word: &str) -> TokenKind {
use TokenKind::*;
match word {
"true" => True,
"false" => False,
"null" => Null,
_ => Name,
}
}
fn ends_value(kind: TokenKind) -> bool {
use TokenKind::*;
matches!(
kind,
Name | HexLit | True | False | Number | Null | RCurly | String | Eq | Comment | At
)
}
fn ends_line(last: Option<&Token>, ws: &[u8]) -> bool {
last.is_some_and(|t| ends_value(t.kind) || matches!(t.kind, TokenKind::LParen))
&& ws.iter().any(|&b| matches!(b, b'\n' | b'\r'))
}
#[inline]
fn scan_run(bytes: &[u8], pred: impl Fn(&u8) -> bool) -> usize {
bytes.iter().position(|b| !pred(b)).unwrap_or(bytes.len())
}
#[inline]
fn scan_number_segment(bytes: &[u8]) -> Option<usize> {
let len = scan_run(bytes, |b| matches!(b, b'0'..=b'9' | b'_'));
bytes[..len]
.split(|&b| b == b'_')
.all(|group| !group.is_empty())
.then_some(len)
}
#[inline]
fn scan_exponent(bytes: &[u8]) -> Option<usize> {
if !matches!(bytes.first(), Some(b'e' | b'E')) {
return Some(0);
}
let sign = 1 + matches!(bytes.get(1), Some(b'+' | b'-')) as usize;
Some(match scan_run(&bytes[sign..], u8::is_ascii_digit) {
0 => return None,
digits => sign + digits,
})
}
#[inline]
fn scan_number(bytes: &[u8]) -> Option<usize> {
let starts_with_minus = bytes.first() == Some(&b'-');
let mut i = starts_with_minus as usize;
if matches!(i, 0 | 1) {
let end = i + 3;
if bytes.len() >= end {
for word in [b"inf", b"nan"] {
if bytes[i..end].eq_ignore_ascii_case(word)
&& bytes.get(end).is_none_or(|c| !is_name_char(c))
{
return Some(end);
}
}
}
}
if bytes.get(i) == Some(&b'.') {
i += 1;
i += scan_number_segment(&bytes[i..])?;
i += scan_exponent(&bytes[i..])?;
return Some(i);
}
i += scan_number_segment(&bytes[i..])?;
if bytes.get(i) == Some(&b'.') {
i += 1;
i += scan_number_segment(&bytes[i..])?;
}
Some(i + scan_exponent(&bytes[i..])?)
}
#[cfg(test)]
mod tests {
use crate::parse::tokenizer::scan_number;
#[test]
fn number_scan() {
assert_eq!(scan_number(b"1234"), Some(4));
assert_eq!(scan_number(b"-1234"), Some(5));
assert_eq!(scan_number(b"-1234.212"), Some(9));
assert_eq!(scan_number(b"-1234.212e"), None);
assert_eq!(scan_number(b"-1234e120"), Some(9));
assert_eq!(scan_number(b"-1234.212E+120"), Some(14));
assert_eq!(scan_number(b"-1234.212e-120"), Some(14));
assert_eq!(scan_number(b".5e10"), Some(5));
assert_eq!(scan_number(b"NaN"), Some(3));
assert_eq!(scan_number(b"-nan"), Some(4));
assert_eq!(scan_number(b"Inf"), Some(3));
assert_eq!(scan_number(b"-inf"), Some(4));
}
}