use crate::diag::{thresholds, Diagnostic, Repair, RepairKind, Severity, Span};
pub const ESCAPED_DOLLAR: char = '\u{E000}';
pub const SENTINEL_POS: (u32, u32) = (u32::MAX, u32::MAX);
pub struct Lexed {
pub tokens: Vec<Token>,
pub repairs: Vec<Diagnostic>,
pub errors: Vec<Diagnostic>,
pub comments: Vec<(String, Span)>,
}
fn unclosed_string_repair(line: u32, col: u32, delimiter: &str) -> Diagnostic {
Diagnostic {
severity: Severity::Info, code: "H001".into(),
message: format!(
"string literal reached end of input without its closing {}; inserted",
delimiter
),
path: None,
span: Some(Span {
line,
column: col,
end_line: line,
end_column: col + 1,
}),
repair: Some(Repair {
original: String::new(),
suggested: delimiter.to_string(),
kind: RepairKind::InsertQuote,
safe: true,
}),
confidence: thresholds::AUTO_APPLY + 0.03,
file: None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Eof,
Newline,
Indent,
Dedent,
Ident,
String(Vec<(u32, u32)>),
SingleString,
MlString(Vec<(u32, u32)>),
MlSingleString,
Integer,
Float,
True,
False,
Null,
Let,
Env,
Secret,
Assign, Dot, Comma, QuestionColon, Plus, Minus, Star, Slash, EqEq, NotEq, Lt, LtEq, Gt, GtEq, AndAnd, OrOr, Bang,
LBracket, RBracket, LBrace, RBrace, LParen, RParen, Colon, At,
Unit(String),
}
#[derive(Debug, Clone)]
pub struct Token {
pub kind: TokenKind,
pub text: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineClass {
Blank,
Content,
}
pub struct Lexer {
chars: Vec<char>,
pos: usize,
line: u32,
col: u32,
open_delims: Vec<char>,
indent_stack: Vec<usize>,
at_line_start: bool,
}
impl Lexer {
pub fn new(src: &str) -> Self {
let src = src.strip_prefix('\u{FEFF}').unwrap_or(src);
Self {
chars: src.chars().collect(),
pos: 0,
line: 1,
col: 1,
open_delims: Vec::new(),
indent_stack: vec![0],
at_line_start: true,
}
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn peek_ahead(&self, n: usize) -> Option<char> {
self.chars.get(self.pos + n).copied()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += 1;
if c == '\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
Some(c)
}
fn make_token(&self, kind: TokenKind, text: String, start_line: u32, start_col: u32) -> Token {
Token {
kind,
text,
line: start_line,
col: start_col,
end_line: self.line,
end_col: self.col,
}
}
fn lex_ml_string(&mut self, quote: char) -> (String, bool, Vec<(u32, u32)>) {
let mut s = String::new();
let mut pos: Vec<(u32, u32)> = Vec::new();
let mut real_off: u32 = 0;
let mut escaped = false;
while let Some(ch) = self.peek() {
if escaped {
let at = (real_off, self.col);
match ch {
'n' => {
s.push('\n');
pos.push(at);
}
'\n' => {
s.push('\\');
pos.push(at);
s.push('\n');
pos.push((real_off, self.col + 1));
real_off += 1;
}
't' => {
s.push('\t');
pos.push(at);
}
'r' => {
s.push('\r');
pos.push(at);
}
'\\' => {
s.push('\\');
pos.push(at);
}
'"' => {
s.push('"');
pos.push(at);
}
'0' => {
s.push('\0');
pos.push(at);
}
'$' => {
s.push(ESCAPED_DOLLAR);
pos.push(SENTINEL_POS);
}
other => {
s.push('\\');
pos.push(at);
s.push(other);
pos.push((real_off, self.col + 1));
}
}
escaped = false;
self.bump();
} else if ch == '\\' {
escaped = true;
self.bump();
} else if ch == quote
&& self.peek_ahead(1) == Some(quote)
&& self.peek_ahead(2) == Some(quote)
{
self.bump();
self.bump();
self.bump();
return (s, true, pos);
} else if ch == '\r' {
if self.peek_ahead(1) == Some('\n') {
self.bump();
continue;
}
pos.push((real_off, self.col));
s.push('\n');
self.bump();
} else {
if ch == '\n' {
pos.push((real_off, self.col));
real_off += 1;
} else {
pos.push((real_off, self.col));
}
s.push(ch);
self.bump();
}
}
if escaped {
s.push('\\');
pos.push((real_off, self.col));
}
(s, false, pos)
}
fn lex_ml_literal(&mut self, quote: char) -> (String, bool) {
let mut s = String::new();
while let Some(ch) = self.peek() {
if ch == quote && self.peek_ahead(1) == Some(quote) && self.peek_ahead(2) == Some(quote)
{
self.bump();
self.bump();
self.bump();
return (s, true);
}
if ch == '\r' {
if self.peek_ahead(1) == Some('\n') {
self.bump();
continue;
}
s.push('\n');
self.bump();
continue;
}
s.push(ch);
self.bump();
}
(s, false)
}
fn measure_indentation(
&mut self,
tokens: &mut Vec<Token>,
errors: &mut Vec<Diagnostic>,
repairs: &mut Vec<Diagnostic>,
) -> LineClass {
let line = self.line;
let start_col = self.col;
let mut column: usize = 0;
let mut had_tab = false;
let mut tab_span: Option<(u32, u32)> = None;
while let Some(ch) = self.peek() {
match ch {
' ' => {
column += 1;
self.bump();
}
'\t' => {
if !had_tab {
had_tab = true;
tab_span = Some((self.line, self.col));
}
column += 1;
self.bump();
}
_ => break,
}
}
if let Some((l, col)) = tab_span {
errors.push(Diagnostic {
severity: Severity::Error,
code: "E006".into(),
message: "tab character in leading whitespace; use spaces".into(),
path: None,
span: Some(Span {
line: l,
column: col,
end_line: l,
end_column: col + 1,
}),
repair: None,
confidence: 0.0,
file: None,
});
}
match self.peek() {
None | Some('\n') | Some('#') => return LineClass::Blank,
_ => {}
}
let top = *self.indent_stack.last().unwrap_or(&0);
if column > top {
self.indent_stack.push(column);
tokens.push(self.make_token(TokenKind::Indent, column.to_string(), line, start_col));
} else if column < top {
while let Some(&level) = self.indent_stack.last() {
if level <= column {
break;
}
self.indent_stack.pop();
let target = self.indent_stack.last().copied().unwrap_or(0);
tokens.push(self.make_token(
TokenKind::Dedent,
target.to_string(),
line,
start_col,
));
}
if self.indent_stack.last().copied() != Some(column) {
errors.push(Diagnostic {
severity: Severity::Error,
code: "E007".into(),
message: format!("inconsistent dedentation to column {column}"),
path: None,
span: Some(Span {
line,
column: start_col,
end_line: line,
end_column: start_col + column as u32,
}),
repair: None,
confidence: 0.0,
file: None,
});
}
}
let _ = repairs; LineClass::Content
}
pub fn tokenize(&mut self) -> Lexed {
let mut tokens = Vec::new();
let mut repairs: Vec<Diagnostic> = Vec::new();
let mut errors: Vec<Diagnostic> = Vec::new();
let mut comments: Vec<(String, Span)> = Vec::new();
loop {
if self.at_line_start {
self.at_line_start = false;
if self.open_delims.is_empty() {
let class = self.measure_indentation(&mut tokens, &mut errors, &mut repairs);
if class == LineClass::Blank {
self.at_line_start = true;
}
}
}
let c = match self.peek() {
Some(c) => c,
None => break,
};
match c {
' ' | '\t' | '\r' => {
self.bump();
}
'\n' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Newline, "\n".into(), l, col));
self.at_line_start = true;
}
'#' => {
let (l, col) = (self.line, self.col);
let mut text = String::new();
while let Some(ch) = self.peek() {
if ch == '\n' {
break;
}
text.push(ch);
self.bump();
}
let stripped = text.ends_with('\r');
if stripped {
text.pop();
}
comments.push((
text,
Span {
line: l,
column: col,
end_line: self.line,
end_column: if stripped {
self.col.saturating_sub(1)
} else {
self.col
},
},
));
}
'?' if self.peek_ahead(1) == Some(':') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::QuestionColon, "?:".into(), l, col));
}
'=' if self.peek_ahead(1) == Some('=') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::EqEq, "==".into(), l, col));
}
'!' if self.peek_ahead(1) == Some('=') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::NotEq, "!=".into(), l, col));
}
'<' if self.peek_ahead(1) == Some('=') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::LtEq, "<=".into(), l, col));
}
'>' if self.peek_ahead(1) == Some('=') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::GtEq, ">=".into(), l, col));
}
'&' if self.peek_ahead(1) == Some('&') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::AndAnd, "&&".into(), l, col));
}
'|' if self.peek_ahead(1) == Some('|') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
tokens.push(self.make_token(TokenKind::OrOr, "||".into(), l, col));
}
'=' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Assign, "=".into(), l, col));
}
'.' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Dot, ".".into(), l, col));
}
',' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Comma, ",".into(), l, col));
}
'+' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Plus, "+".into(), l, col));
}
'-' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Minus, "-".into(), l, col));
}
'*' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Star, "*".into(), l, col));
}
'/' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Slash, "/".into(), l, col));
}
'<' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Lt, "<".into(), l, col));
}
'>' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Gt, ">".into(), l, col));
}
'!' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Bang, "!".into(), l, col));
}
'[' => {
let (l, col) = (self.line, self.col);
self.bump();
self.open_delims.push('[');
tokens.push(self.make_token(TokenKind::LBracket, "[".into(), l, col));
}
']' => {
let (l, col) = (self.line, self.col);
self.bump();
if self.open_delims.last() == Some(&'[') {
self.open_delims.pop();
}
tokens.push(self.make_token(TokenKind::RBracket, "]".into(), l, col));
}
'{' => {
let (l, col) = (self.line, self.col);
self.bump();
self.open_delims.push('{');
tokens.push(self.make_token(TokenKind::LBrace, "{".into(), l, col));
}
'}' => {
let (l, col) = (self.line, self.col);
self.bump();
if self.open_delims.last() == Some(&'{') {
self.open_delims.pop();
}
tokens.push(self.make_token(TokenKind::RBrace, "}".into(), l, col));
}
'(' => {
let (l, col) = (self.line, self.col);
self.bump();
self.open_delims.push('(');
tokens.push(self.make_token(TokenKind::LParen, "(".into(), l, col));
}
')' => {
let (l, col) = (self.line, self.col);
self.bump();
if self.open_delims.last() == Some(&'(') {
self.open_delims.pop();
}
tokens.push(self.make_token(TokenKind::RParen, ")".into(), l, col));
}
':' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::Colon, ":".into(), l, col));
}
'@' => {
let (l, col) = (self.line, self.col);
self.bump();
tokens.push(self.make_token(TokenKind::At, "@".into(), l, col));
}
'"' if self.peek_ahead(1) == Some('"') && self.peek_ahead(2) == Some('"') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
self.bump(); let (raw, terminated, src_pos) = self.lex_ml_string('"');
if !terminated {
repairs.push(unclosed_string_repair(l, col, "\"\"\""));
}
tokens.push(self.make_token(TokenKind::MlString(src_pos), raw, l, col));
}
'\'' if self.peek_ahead(1) == Some('\'') && self.peek_ahead(2) == Some('\'') => {
let (l, col) = (self.line, self.col);
self.bump();
self.bump();
self.bump(); let (raw, terminated) = self.lex_ml_literal('\'');
if !terminated {
repairs.push(unclosed_string_repair(l, col, "'''"));
}
tokens.push(self.make_token(TokenKind::MlSingleString, raw, l, col));
}
'"' => {
let (l, col) = (self.line, self.col);
self.bump(); let mut s = String::new();
let mut pos: Vec<(u32, u32)> = Vec::new();
let mut real_off: u32 = 0;
let mut escaped = false;
let mut terminated = false;
while let Some(ch) = self.peek() {
if escaped {
let at = (real_off, self.col);
match ch {
'n' => {
s.push('\n');
pos.push(at);
}
'\n' => {
s.push('\\');
pos.push(at);
s.push('\n');
pos.push((real_off, self.col + 1));
real_off += 1;
}
't' => {
s.push('\t');
pos.push(at);
}
'r' => {
s.push('\r');
pos.push(at);
}
'\\' => {
s.push('\\');
pos.push(at);
}
'"' => {
s.push('"');
pos.push(at);
}
'0' => {
s.push('\0');
pos.push(at);
}
'$' => {
s.push(ESCAPED_DOLLAR);
pos.push(SENTINEL_POS);
}
other => {
s.push('\\');
pos.push(at);
s.push(other);
pos.push((real_off, self.col + 1));
}
}
escaped = false;
self.bump();
} else if ch == '\\' {
escaped = true;
self.bump();
} else if ch == '"' {
self.bump();
terminated = true;
break;
} else if ch == '\r' {
if self.peek_ahead(1) == Some('\n') {
self.bump();
continue;
}
pos.push((real_off, self.col));
s.push('\n');
self.bump();
} else {
if ch == '\n' {
pos.push((real_off, self.col));
real_off += 1;
} else {
pos.push((real_off, self.col));
}
s.push(ch);
self.bump();
}
}
if !terminated {
if escaped {
s.push('\\');
pos.push((real_off, self.col));
}
repairs.push(unclosed_string_repair(l, col, "\""));
}
tokens.push(self.make_token(TokenKind::String(pos), s, l, col));
}
'\'' => {
let (l, col) = (self.line, self.col);
self.bump();
let mut s = String::new();
let mut escaped = false;
let mut terminated = false;
while let Some(ch) = self.peek() {
if escaped {
match ch {
'\\' => s.push('\\'),
'\'' => s.push('\''),
other => {
s.push('\\');
s.push(other);
}
}
escaped = false;
self.bump();
} else if ch == '\\' {
escaped = true;
self.bump();
} else if ch == '\'' {
self.bump();
terminated = true;
break;
} else if ch == '\r' {
if self.peek_ahead(1) == Some('\n') {
self.bump();
continue;
}
s.push('\n');
self.bump();
} else {
s.push(ch);
self.bump();
}
}
if !terminated {
if escaped {
s.push('\\');
}
repairs.push(unclosed_string_repair(l, col, "'"));
}
tokens.push(self.make_token(TokenKind::SingleString, s, l, col));
}
c if c.is_ascii_digit() => {
let (l, col) = (self.line, self.col);
let mut s = String::new();
let mut is_float = false;
while let Some(ch) = self.peek() {
if ch.is_ascii_digit() || ch == '_' {
s.push(ch);
self.bump();
} else if ch == '.' && !is_float {
if let Some(next) = self.peek_ahead(1) {
if next.is_ascii_digit() {
is_float = true;
s.push(ch);
self.bump();
continue;
}
}
break;
} else {
break;
}
}
let cleaned: String = s.chars().filter(|c| *c != '_').collect();
let kind = if is_float {
TokenKind::Float
} else {
TokenKind::Integer
};
tokens.push(self.make_token(kind, cleaned, l, col));
if let Some(u) = self.try_consume_unit() {
let (ul, uc) = (self.line, self.col);
tokens.push(self.make_token(TokenKind::Unit(u.clone()), u, ul, uc));
}
}
c if c.is_ascii_alphabetic() || c == '_' => {
let (l, col) = (self.line, self.col);
let mut s = String::new();
while let Some(ch) = self.peek() {
if ch.is_ascii_alphanumeric() || ch == '_' {
s.push(ch);
self.bump();
} else {
break;
}
}
let kind = match s.as_str() {
"let" => TokenKind::Let,
"true" => TokenKind::True,
"false" => TokenKind::False,
"null" => TokenKind::Null,
"env" => TokenKind::Env,
"secret" => TokenKind::Secret,
_ => TokenKind::Ident,
};
tokens.push(self.make_token(kind, s, l, col));
}
_ => {
let (l, col) = (self.line, self.col);
self.bump();
errors.push(Diagnostic {
severity: Severity::Error,
code: "E005".into(),
message: format!("unexpected character {:?}", c),
path: None,
span: Some(Span {
line: l,
column: col,
end_line: l,
end_column: col + 1,
}),
repair: None,
confidence: 0.0,
file: None,
});
}
}
}
while self.indent_stack.len() > 1 {
self.indent_stack.pop();
let target = self.indent_stack.last().copied().unwrap_or(0);
tokens.push(self.make_token(
TokenKind::Dedent,
target.to_string(),
self.line,
self.col,
));
}
tokens.push(self.make_token(TokenKind::Eof, String::new(), self.line, self.col));
Lexed {
tokens,
repairs,
errors,
comments,
}
}
fn try_consume_unit(&mut self) -> Option<String> {
let start_pos = self.pos;
let start_line = self.line;
let start_col = self.col;
let mut s = String::new();
while let Some(ch) = self.peek() {
if ch.is_alphanumeric() || ch == '/' || ch == '_' || ch == '%' {
s.push(ch);
self.bump();
} else {
break;
}
}
if s.is_empty() {
return None;
}
const KNOWN: &[&str] = &[
"ns", "us", "µs", "ms", "s", "m", "h", "d", "B", "KiB", "MiB", "GiB", "TiB", "KB",
"MB", "GB", "TB", "/s", "/m", "/h", "per_s", "per_m", "per_h", "req/s", "req_s",
"/req/s", "/req/m", "/req/h", "req/m", "req_m", "req/h", "req_h", "%", "Hz", "kHz",
"MHz", "GHz", "THz", "b", "Kb", "Mb", "Gb", "bps", "Kbps", "Mbps", "Gbps", "MB/s",
"week", "sec", "min", "tokens", "ktokens", "Mtokens", "tok/s", "FLOPS", "TFLOPS",
"PFLOPS", "Pa", "bar", "psi", "V", "A", "W", "J", "kWh", "N", "deg", "rad", "rpm",
"meter", "km", "kg", "g", "PiB", "EiB", "K", "Cel", "Fahr", "dB",
];
if KNOWN.contains(&s.as_str()) {
Some(s)
} else {
self.pos = start_pos;
self.line = start_line;
self.col = start_col;
None
}
}
}