pub mod source_map;
mod token;
pub use source_map::{SourceBlock, SourceMap, extract_pht_blocks};
pub use token::{Keyword, Span, SpannedToken, Token};
use crate::diagnostic::{Diagnostic, Severity};
pub fn lex(source: &str, file: &str) -> (Vec<SpannedToken>, Vec<Diagnostic>) {
let mut lexer = LexerState::new(source, file);
let mut tokens = Vec::new();
let mut diagnostics = Vec::new();
loop {
lexer.skip_whitespace();
if lexer.is_eof() {
tokens.push(lexer.make_token(Token::Eof, 0));
break;
}
match lexer.next_token() {
Ok(Some(tok)) => tokens.push(tok),
Ok(None) => { }
Err(diag) => {
diagnostics.push(diag);
lexer.advance();
}
}
}
(tokens, diagnostics)
}
struct LexerState<'a> {
source: &'a str,
file: String,
pos: usize,
line: usize,
col: usize,
}
impl<'a> LexerState<'a> {
fn new(source: &'a str, file: &str) -> Self {
Self {
source,
file: file.to_string(),
pos: 0,
line: 1,
col: 1,
}
}
fn is_eof(&self) -> bool {
self.pos >= self.source.len()
}
fn peek(&self) -> Option<char> {
self.source[self.pos..].chars().next()
}
fn peek_next(&self) -> Option<char> {
let mut chars = self.source[self.pos..].chars();
chars.next();
chars.next()
}
fn advance(&mut self) -> Option<char> {
let ch = self.peek()?;
self.pos += ch.len_utf8();
if ch == '\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
Some(ch)
}
fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_ascii_whitespace() {
self.advance();
} else {
break;
}
}
}
fn make_token(&self, kind: Token, len: usize) -> SpannedToken {
let end_col = if len > 0 { self.col - 1 } else { self.col };
SpannedToken {
token: kind,
span: Span {
file: self.file.clone(),
start_line: self.line,
start_col: if len > 0 {
self.col.saturating_sub(len)
} else {
self.col
},
end_line: self.line,
end_col,
},
}
}
fn make_span_from(&self, start_line: usize, start_col: usize) -> Span {
Span {
file: self.file.clone(),
start_line,
start_col,
end_line: self.line,
end_col: if self.col > 1 { self.col - 1 } else { 1 },
}
}
fn error_at(&self, message: impl Into<String>) -> Diagnostic {
Diagnostic {
severity: Severity::Error,
summary: message.into(),
file: self.file.clone(),
line: self.line,
col: self.col,
explanation: None,
suggestion: None,
}
}
fn next_token(&mut self) -> Result<Option<SpannedToken>, Diagnostic> {
let start_line = self.line;
let start_col = self.col;
let ch = self.peek().unwrap();
match ch {
'"' => self.lex_string_literal(start_line, start_col).map(Some),
'/' => {
match self.peek_next() {
Some('/') => {
self.lex_line_comment();
Ok(None)
}
Some('*') => {
self.lex_block_comment()?;
Ok(None)
}
_ => {
self.advance();
Ok(Some(SpannedToken {
token: Token::Slash,
span: self.make_span_from(start_line, start_col),
}))
}
}
}
':' => self.single_char_token(Token::Colon, start_line, start_col),
';' => self.single_char_token(Token::Semicolon, start_line, start_col),
',' => self.single_char_token(Token::Comma, start_line, start_col),
'{' => self.single_char_token(Token::LeftBrace, start_line, start_col),
'}' => self.single_char_token(Token::RightBrace, start_line, start_col),
'[' => self.single_char_token(Token::LeftBracket, start_line, start_col),
']' => self.single_char_token(Token::RightBracket, start_line, start_col),
'(' => self.single_char_token(Token::LeftParen, start_line, start_col),
')' => self.single_char_token(Token::RightParen, start_line, start_col),
'@' => self.single_char_token(Token::At, start_line, start_col),
'+' => self.single_char_token(Token::Plus, start_line, start_col),
'*' => self.single_char_token(Token::Star, start_line, start_col),
c if is_ident_start(c) => Ok(Some(self.lex_identifier(start_line, start_col))),
_ => Err(self.error_at(format!("unexpected character: '{ch}'"))),
}
}
fn single_char_token(
&mut self,
kind: Token,
start_line: usize,
start_col: usize,
) -> Result<Option<SpannedToken>, Diagnostic> {
self.advance();
Ok(Some(SpannedToken {
token: kind,
span: self.make_span_from(start_line, start_col),
}))
}
fn lex_identifier(&mut self, start_line: usize, start_col: usize) -> SpannedToken {
let start_pos = self.pos;
while let Some(ch) = self.peek() {
if is_ident_continue(ch) {
self.advance();
} else {
break;
}
}
let text = &self.source[start_pos..self.pos];
let token = match classify_keyword(text) {
Some(kw) => Token::Keyword(kw),
None => Token::Identifier(text.to_string()),
};
SpannedToken {
token,
span: self.make_span_from(start_line, start_col),
}
}
fn lex_string_literal(
&mut self,
start_line: usize,
start_col: usize,
) -> Result<SpannedToken, Diagnostic> {
self.advance();
let mut value = String::new();
loop {
match self.peek() {
None => {
return Err(Diagnostic {
severity: Severity::Error,
summary: "unterminated string literal".to_string(),
file: self.file.clone(),
line: start_line,
col: start_col,
explanation: Some(
"string literal was opened here but never closed".to_string(),
),
suggestion: Some("add a closing '\"'".to_string()),
});
}
Some('"') => {
self.advance(); return Ok(SpannedToken {
token: Token::StringLiteral(value),
span: self.make_span_from(start_line, start_col),
});
}
Some('\\') => {
self.advance(); match self.peek() {
Some('"') => {
value.push('"');
self.advance();
}
Some('\\') => {
value.push('\\');
self.advance();
}
Some('n') => {
value.push('\n');
self.advance();
}
Some('r') => {
value.push('\r');
self.advance();
}
Some('t') => {
value.push('\t');
self.advance();
}
Some(c) => {
return Err(self.error_at(format!("unknown escape sequence: '\\{c}'")));
}
None => {
return Err(
self.error_at("unterminated escape sequence at end of input")
);
}
}
}
Some('\n') => {
return Err(Diagnostic {
severity: Severity::Error,
summary: "unterminated string literal (newline in string)".to_string(),
file: self.file.clone(),
line: start_line,
col: start_col,
explanation: Some("string literals cannot span multiple lines".to_string()),
suggestion: Some(
"use \\n for newlines, or close the string before the line break"
.to_string(),
),
});
}
Some(ch) => {
value.push(ch);
self.advance();
}
}
}
}
fn lex_line_comment(&mut self) {
self.advance();
self.advance();
while let Some(ch) = self.peek() {
if ch == '\n' {
self.advance();
break;
}
self.advance();
}
}
fn lex_block_comment(&mut self) -> Result<(), Diagnostic> {
let start_line = self.line;
let start_col = self.col;
self.advance();
self.advance();
loop {
match self.peek() {
None => {
return Err(Diagnostic {
severity: Severity::Error,
summary: "unterminated block comment".to_string(),
file: self.file.clone(),
line: start_line,
col: start_col,
explanation: Some(
"block comment was opened here but never closed".to_string(),
),
suggestion: Some("add a closing '*/'".to_string()),
});
}
Some('*') => {
if self.peek_next() == Some('/') {
self.advance(); self.advance(); return Ok(());
}
self.advance();
}
_ => {
self.advance();
}
}
}
}
}
fn is_ident_start(ch: char) -> bool {
ch.is_ascii_alphabetic() || ch == '_'
}
fn is_ident_continue(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}
fn classify_keyword(text: &str) -> Option<Keyword> {
match text {
"namespace" => Some(Keyword::Namespace),
"uses" => Some(Keyword::Uses),
"type" => Some(Keyword::Type),
"plural" => Some(Keyword::Plural),
"required" => Some(Keyword::Required),
"optional" => Some(Keyword::Optional),
"string" => Some(Keyword::String),
"int64" => Some(Keyword::Int64),
"real64" => Some(Keyword::Real64),
"bool" => Some(Keyword::Bool),
"date" => Some(Keyword::Date),
"time" => Some(Keyword::Time),
"datetime" => Some(Keyword::DateTime),
_ => None,
}
}
#[cfg(test)]
mod tests;