mod error;
mod token;
pub use error::LexError;
pub use token::Token;
pub const KEYWORDS: [&str; 32] = [
"let", "mut", "const", "fn", "return", "if", "else", "while", "for", "in", "break", "continue",
"match", "type", "record", "trait", "impl", "import", "module", "pub", "async", "await",
"extern", "unsafe", "true", "false", "as", "is", "where", "with", "self", "Self",
];
use nova_diagnostics::{FileId, Span, Spanned};
pub fn lex(source: &str, file: FileId) -> (Vec<Spanned<Token>>, Vec<LexError>) {
let mut lexer = Lexer::new(source, file);
lexer.tokenize()
}
pub fn lex_with_comments(
source: &str,
file: FileId,
) -> (Vec<Spanned<Token>>, Vec<Comment>, Vec<LexError>) {
let mut lexer = Lexer::new(source, file);
let (tokens, errors) = lexer.tokenize();
(tokens, lexer.comments, errors)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Comment {
pub kind: CommentKind,
pub span: Span,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommentKind {
Line,
Block,
}
struct InterpFrame {
open: usize,
braces: usize,
}
pub struct Lexer<'src> {
source: &'src str,
file: FileId,
pos: usize,
interp: Vec<InterpFrame>,
in_string: bool,
comments: Vec<Comment>,
}
impl<'src> Lexer<'src> {
pub fn new(source: &'src str, file: FileId) -> Self {
Self {
source,
file,
pos: 0,
interp: Vec::new(),
in_string: false,
comments: Vec::new(),
}
}
pub fn tokenize(&mut self) -> (Vec<Spanned<Token>>, Vec<LexError>) {
let mut tokens = Vec::new();
let mut errors = Vec::new();
while self.pos < self.source.len() {
match self.next_token() {
Ok(Some(tok)) => tokens.push(tok),
Ok(None) => {}
Err(e) => errors.push(e),
}
}
if let Some(frame) = self.interp.last() {
let end = (frame.open + 2).min(self.source.len());
errors.push(LexError::UnterminatedInterpolation(
self.span(frame.open, end),
));
}
tokens.push(Spanned::new(
Token::Eof,
Span::point(self.source.len() as u32, self.file),
));
(tokens, errors)
}
fn span(&self, start: usize, end: usize) -> Span {
Span::new(start as u32, end as u32, self.file)
}
fn skip_trivia(&mut self) -> Result<(), LexError> {
let b = self.source.as_bytes();
loop {
while self.pos < b.len() {
match b[self.pos] {
b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
_ => break,
}
}
if self.pos + 1 < b.len() && b[self.pos] == b'/' && b[self.pos + 1] == b'/' {
let doc = b.get(self.pos + 2) == Some(&b'/') && b.get(self.pos + 3) != Some(&b'/');
if doc {
break;
}
let start = self.pos;
while self.pos < b.len() && b[self.pos] != b'\n' {
self.pos += 1;
}
let end = if self.pos > start && b[self.pos - 1] == b'\r' {
self.pos - 1
} else {
self.pos
};
self.comments.push(Comment {
kind: CommentKind::Line,
span: self.span(start, end),
});
continue;
}
if self.pos + 1 < b.len() && b[self.pos] == b'/' && b[self.pos + 1] == b'*' {
let start = self.pos;
self.pos += 2;
while self.pos + 1 < b.len() && !(b[self.pos] == b'*' && b[self.pos + 1] == b'/') {
self.pos += 1;
}
if self.pos + 1 >= b.len() {
self.pos = b.len();
return Err(LexError::UnterminatedBlockComment(
self.span(start, b.len()),
));
}
self.pos += 2;
self.comments.push(Comment {
kind: CommentKind::Block,
span: self.span(start, self.pos),
});
continue;
}
break;
}
Ok(())
}
fn next_token(&mut self) -> Result<Option<Spanned<Token>>, LexError> {
if self.in_string {
if self.pos >= self.source.len() {
return Ok(None);
}
return self.lex_string_content();
}
self.skip_trivia()?;
if self.pos >= self.source.len() {
return Ok(None);
}
if self.source.as_bytes()[self.pos] == b'"' {
return self.lex_string_start();
}
if self.pos + 1 < self.source.len()
&& self.source.as_bytes()[self.pos] == b'r'
&& (self.source.as_bytes()[self.pos + 1] == b'"'
|| self.source.as_bytes()[self.pos + 1] == b'#')
{
return self.lex_raw_string();
}
self.lex_logos()
}
fn lex_string_start(&mut self) -> Result<Option<Spanned<Token>>, LexError> {
let start = self.pos;
self.pos += 1; self.in_string = true;
Ok(Some(Spanned::new(
Token::StrStart,
self.span(start, self.pos),
)))
}
fn lex_string_content(&mut self) -> Result<Option<Spanned<Token>>, LexError> {
let src = self.source.as_bytes();
let start = self.pos;
if src[self.pos] == b'"' {
self.pos += 1;
self.in_string = false;
return Ok(Some(Spanned::new(
Token::StrEnd,
self.span(start, self.pos),
)));
}
if self.pos + 1 < src.len() && src[self.pos] == b'$' && src[self.pos + 1] == b'{' {
self.pos += 2;
self.in_string = false; self.interp.push(InterpFrame {
open: start,
braces: 0,
});
return Ok(Some(Spanned::new(
Token::InterpOpen,
self.span(start, self.pos),
)));
}
let mut buf = String::new();
while self.pos < src.len() && src[self.pos] != b'"' {
if self.pos + 1 < src.len() && src[self.pos] == b'$' && src[self.pos + 1] == b'{' {
break;
}
if src[self.pos] == b'\\' {
self.pos += 1;
if self.pos >= src.len() {
return Err(LexError::UnterminatedString(self.span(start, self.pos)));
}
match src[self.pos] {
b'n' => {
buf.push('\n');
self.pos += 1;
}
b't' => {
buf.push('\t');
self.pos += 1;
}
b'r' => {
buf.push('\r');
self.pos += 1;
}
b'"' => {
buf.push('"');
self.pos += 1;
}
b'\\' => {
buf.push('\\');
self.pos += 1;
}
b'0' => {
buf.push('\0');
self.pos += 1;
}
b'u' => {
self.pos += 1;
if self.pos < src.len() && src[self.pos] == b'{' {
self.pos += 1;
let hex_start = self.pos;
while self.pos < src.len() && src[self.pos] != b'}' {
self.pos += 1;
}
let hex = &self.source[hex_start..self.pos];
if self.pos < src.len() {
self.pos += 1;
} match u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) {
Some(c) => buf.push(c),
None => {
return Err(LexError::InvalidUnicodeEscape(
self.span(start, self.pos),
))
}
}
} else {
return Err(LexError::InvalidEscapeSequence(
'u',
self.span(start, self.pos),
));
}
}
c => {
return Err(LexError::InvalidEscapeSequence(
c as char,
self.span(start, self.pos),
));
}
}
} else {
let ch_len = utf8_char_width(src[self.pos]);
let slice = &self.source[self.pos..self.pos + ch_len];
buf.push_str(slice);
self.pos += ch_len;
}
}
if self.pos >= src.len() && !buf.is_empty() {
return Err(LexError::UnterminatedString(self.span(start, self.pos)));
}
let end = self.pos;
Ok(Some(Spanned::new(
Token::StrPart(buf),
self.span(start, end),
)))
}
fn lex_raw_string(&mut self) -> Result<Option<Spanned<Token>>, LexError> {
let start = self.pos;
self.pos += 1; let mut hash_count = 0usize;
let src = self.source.as_bytes();
while self.pos < src.len() && src[self.pos] == b'#' {
hash_count += 1;
self.pos += 1;
}
if self.pos >= src.len() || src[self.pos] != b'"' {
return Err(LexError::InvalidNumberLiteral(self.span(start, self.pos)));
}
self.pos += 1;
let mut content = String::new();
loop {
if self.pos >= src.len() {
return Err(LexError::UnterminatedString(self.span(start, self.pos)));
}
if src[self.pos] == b'"' {
let mut end_hashes = 0;
let mut look = self.pos + 1;
while look < src.len() && src[look] == b'#' && end_hashes < hash_count {
end_hashes += 1;
look += 1;
}
if end_hashes == hash_count {
self.pos = look;
break;
}
}
let ch_len = utf8_char_width(src[self.pos]);
content.push_str(&self.source[self.pos..self.pos + ch_len]);
self.pos += ch_len;
}
Ok(Some(Spanned::new(
Token::RawStr(content),
self.span(start, self.pos),
)))
}
fn maybe_close_interp(&mut self, start: usize) -> Spanned<Token> {
let span = self.span(start, self.pos);
match self.interp.last_mut() {
Some(frame) if frame.braces > 0 => {
frame.braces -= 1;
Spanned::new(Token::RBrace, span)
}
Some(_) => {
self.interp.pop();
self.in_string = true;
Spanned::new(Token::InterpClose, span)
}
None => Spanned::new(Token::RBrace, span),
}
}
fn lex_logos(&mut self) -> Result<Option<Spanned<Token>>, LexError> {
use logos::Logos;
let remaining = &self.source[self.pos..];
let mut lex = RawToken::lexer(remaining);
match lex.next() {
None => {
self.pos = self.source.len();
Ok(None)
}
Some(Ok(raw)) => {
let tok_span = lex.span();
let tok_start = self.pos + tok_span.start;
let tok_end = self.pos + tok_span.end;
self.pos = tok_end;
let span = self.span(tok_start, tok_end);
match raw {
RawToken::LBrace => {
if let Some(frame) = self.interp.last_mut() {
frame.braces += 1;
}
}
RawToken::RBrace => return Ok(Some(self.maybe_close_interp(tok_start))),
_ => {}
}
let tok = raw_to_token(raw, lex.slice());
Ok(Some(Spanned::new(tok, span)))
}
Some(Err(())) => {
let err_offset = lex.span().start;
let ch = remaining[err_offset..].chars().next().unwrap_or('?');
let err_start = self.pos + err_offset;
self.pos = err_start + ch.len_utf8();
Err(LexError::UnexpectedCharacter(
ch,
self.span(err_start, self.pos),
))
}
}
}
}
fn utf8_char_width(b: u8) -> usize {
match b {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xFF => 4,
_ => 1,
}
}
fn raw_to_token(raw: RawToken, slice: &str) -> Token {
match raw {
RawToken::Let => Token::Let,
RawToken::Mut => Token::Mut,
RawToken::Const => Token::Const,
RawToken::Fn => Token::Fn,
RawToken::Return => Token::Return,
RawToken::If => Token::If,
RawToken::Else => Token::Else,
RawToken::While => Token::While,
RawToken::For => Token::For,
RawToken::In => Token::In,
RawToken::Break => Token::Break,
RawToken::Continue => Token::Continue,
RawToken::Match => Token::Match,
RawToken::Type => Token::Type,
RawToken::Record => Token::Record,
RawToken::Trait => Token::Trait,
RawToken::Impl => Token::Impl,
RawToken::Import => Token::Import,
RawToken::Module => Token::Module,
RawToken::Pub => Token::Pub,
RawToken::Async => Token::Async,
RawToken::Await => Token::Await,
RawToken::Extern => Token::Extern,
RawToken::Unsafe => Token::Unsafe,
RawToken::True => Token::True,
RawToken::False => Token::False,
RawToken::As => Token::As,
RawToken::Is => Token::Is,
RawToken::Where => Token::Where,
RawToken::With => Token::With,
RawToken::SelfLower => Token::SelfLower,
RawToken::SelfUpper => Token::SelfUpper,
RawToken::LParen => Token::LParen,
RawToken::RParen => Token::RParen,
RawToken::LBracket => Token::LBracket,
RawToken::RBracket => Token::RBracket,
RawToken::LBrace => Token::LBrace,
RawToken::RBrace => Token::RBrace, RawToken::Comma => Token::Comma,
RawToken::Semicolon => Token::Semicolon,
RawToken::Colon => Token::Colon,
RawToken::ColonColon => Token::ColonColon,
RawToken::Arrow => Token::Arrow,
RawToken::FatArrow => Token::FatArrow,
RawToken::Dot => Token::Dot,
RawToken::DotDot => Token::DotDot,
RawToken::DotDotEq => Token::DotDotEq,
RawToken::At => Token::At,
RawToken::Question => Token::Question,
RawToken::Eq => Token::Eq,
RawToken::EqEq => Token::EqEq,
RawToken::BangEq => Token::BangEq,
RawToken::Lt => Token::Lt,
RawToken::LtEq => Token::LtEq,
RawToken::Gt => Token::Gt,
RawToken::GtEq => Token::GtEq,
RawToken::AmpAmp => Token::AmpAmp,
RawToken::PipePipe => Token::PipePipe,
RawToken::Bang => Token::Bang,
RawToken::Plus => Token::Plus,
RawToken::Minus => Token::Minus,
RawToken::Star => Token::Star,
RawToken::Slash => Token::Slash,
RawToken::Percent => Token::Percent,
RawToken::PlusEq => Token::PlusEq,
RawToken::MinusEq => Token::MinusEq,
RawToken::StarEq => Token::StarEq,
RawToken::SlashEq => Token::SlashEq,
RawToken::PercentEq => Token::PercentEq,
RawToken::Amp => Token::Amp,
RawToken::Pipe => Token::Pipe,
RawToken::Caret => Token::Caret,
RawToken::Tilde => Token::Tilde,
RawToken::LtLt => Token::LtLt,
RawToken::GtGt => Token::GtGt,
RawToken::AmpEq => Token::AmpEq,
RawToken::PipeEq => Token::PipeEq,
RawToken::CaretEq => Token::CaretEq,
RawToken::LtLtEq => Token::LtLtEq,
RawToken::GtGtEq => Token::GtGtEq,
RawToken::Float(f) => Token::Float(f),
RawToken::Int(n) => Token::Int(n),
RawToken::Char(c) => Token::Char(c),
RawToken::DocComment => Token::DocComment(slice[3..].trim_end().to_owned()),
RawToken::Ident => Token::Ident(slice.to_owned()),
}
}
#[derive(logos::Logos, Debug, Clone, PartialEq)]
#[logos(skip r"[ \t\n\r]+")]
#[logos(skip r"//[^\n]*")] #[logos(skip r"/\*([^*]|\*[^/])*\*/")] enum RawToken {
#[token("let")]
Let,
#[token("mut")]
Mut,
#[token("const")]
Const,
#[token("fn")]
Fn,
#[token("return")]
Return,
#[token("if")]
If,
#[token("else")]
Else,
#[token("while")]
While,
#[token("for")]
For,
#[token("in")]
In,
#[token("break")]
Break,
#[token("continue")]
Continue,
#[token("match")]
Match,
#[token("type")]
Type,
#[token("record")]
Record,
#[token("trait")]
Trait,
#[token("impl")]
Impl,
#[token("import")]
Import,
#[token("module")]
Module,
#[token("pub")]
Pub,
#[token("async")]
Async,
#[token("await")]
Await,
#[token("extern")]
Extern,
#[token("unsafe")]
Unsafe,
#[token("true")]
True,
#[token("false")]
False,
#[token("as")]
As,
#[token("is")]
Is,
#[token("where")]
Where,
#[token("with")]
With,
#[token("self")]
SelfLower,
#[token("Self")]
SelfUpper,
#[token("::")]
ColonColon,
#[token("->")]
Arrow,
#[token("=>")]
FatArrow,
#[token("..")]
DotDot,
#[token("..=")]
DotDotEq,
#[token("==")]
EqEq,
#[token("!=")]
BangEq,
#[token("<=")]
LtEq,
#[token(">=")]
GtEq,
#[token("&&")]
AmpAmp,
#[token("||")]
PipePipe,
#[token("+=")]
PlusEq,
#[token("-=")]
MinusEq,
#[token("*=")]
StarEq,
#[token("/=")]
SlashEq,
#[token("%=")]
PercentEq,
#[token("&=")]
AmpEq,
#[token("|=")]
PipeEq,
#[token("^=")]
CaretEq,
#[token("<<=")]
LtLtEq,
#[token(">>=")]
GtGtEq,
#[token("<<")]
LtLt,
#[token(">>")]
GtGt,
#[token("(")]
LParen,
#[token(")")]
RParen,
#[token("[")]
LBracket,
#[token("]")]
RBracket,
#[token("{")]
LBrace,
#[token("}")]
RBrace,
#[token(",")]
Comma,
#[token(";")]
Semicolon,
#[token(":")]
Colon,
#[token(".")]
Dot,
#[token("@")]
At,
#[token("?")]
Question,
#[token("=")]
Eq,
#[token("<")]
Lt,
#[token(">")]
Gt,
#[token("!")]
Bang,
#[token("+")]
Plus,
#[token("-")]
Minus,
#[token("*")]
Star,
#[token("/")]
Slash,
#[token("%")]
Percent,
#[token("&")]
Amp,
#[token("|")]
Pipe,
#[token("^")]
Caret,
#[token("~")]
Tilde,
#[regex(r"[0-9][0-9_]*\.[0-9][0-9_]*([eE][+-]?[0-9]+)?", lex_float)]
#[regex(r"[0-9][0-9_]*[eE][+-]?[0-9]+", lex_float)]
Float(f64),
#[regex(r"0x[0-9a-fA-F][0-9a-fA-F_]*", lex_hex_int)]
#[regex(r"0b[01][01_]*", lex_bin_int)]
#[regex(r"0o[0-7][0-7_]*", lex_oct_int)]
#[regex(r"[0-9][0-9_]*", lex_dec_int)]
Int(i64),
#[regex(r"'([^'\\]|\\.)'", lex_char)]
Char(char),
#[regex(r"///[^\n]*")]
DocComment,
#[regex(r"[a-zA-Z_][a-zA-Z0-9_]*")]
Ident,
}
fn lex_hex_int(lex: &mut logos::Lexer<RawToken>) -> Option<i64> {
let s = lex.slice()[2..].replace('_', "");
i64::from_str_radix(&s, 16).ok()
}
fn lex_bin_int(lex: &mut logos::Lexer<RawToken>) -> Option<i64> {
let s = lex.slice()[2..].replace('_', "");
i64::from_str_radix(&s, 2).ok()
}
fn lex_oct_int(lex: &mut logos::Lexer<RawToken>) -> Option<i64> {
let s = lex.slice()[2..].replace('_', "");
i64::from_str_radix(&s, 8).ok()
}
fn lex_dec_int(lex: &mut logos::Lexer<RawToken>) -> Option<i64> {
lex.slice().replace('_', "").parse().ok()
}
fn lex_float(lex: &mut logos::Lexer<RawToken>) -> Option<f64> {
lex.slice().replace('_', "").parse().ok()
}
fn lex_char(lex: &mut logos::Lexer<RawToken>) -> Option<char> {
let s = lex.slice();
let inner = &s[1..s.len() - 1];
if inner.starts_with('\\') {
match inner.as_bytes().get(1)? {
b'n' => Some('\n'),
b't' => Some('\t'),
b'r' => Some('\r'),
b'\\' => Some('\\'),
b'\'' => Some('\''),
b'0' => Some('\0'),
_ => None,
}
} else {
inner.chars().next()
}
}