use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub kind: TokenKind,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
Ident(String),
Str(String),
Num(i64),
LBrace,
RBrace,
Colon,
Assign,
Eq,
Ne,
Lt,
Gt,
Le,
Ge,
Newline,
Eof,
}
impl fmt::Display for TokenKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TokenKind::Ident(s) => write!(f, "identifier `{s}`"),
TokenKind::Str(s) => write!(f, "string \"{s}\""),
TokenKind::Num(n) => write!(f, "number {n}"),
TokenKind::LBrace => f.write_str("`{`"),
TokenKind::RBrace => f.write_str("`}`"),
TokenKind::Colon => f.write_str("`:`"),
TokenKind::Assign => f.write_str("`=`"),
TokenKind::Eq => f.write_str("`==`"),
TokenKind::Ne => f.write_str("`!=`"),
TokenKind::Lt => f.write_str("`<`"),
TokenKind::Gt => f.write_str("`>`"),
TokenKind::Le => f.write_str("`<=`"),
TokenKind::Ge => f.write_str("`>=`"),
TokenKind::Newline => f.write_str("end of line"),
TokenKind::Eof => f.write_str("end of file"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LexError {
pub message: String,
pub line: usize,
}
impl fmt::Display for LexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "line {}: {}", self.line, self.message)
}
}
impl std::error::Error for LexError {}
pub fn tokenize(src: &str) -> Result<Vec<Token>, LexError> {
let src = src.strip_prefix('\u{feff}').unwrap_or(src);
let chars: Vec<char> = src.chars().collect();
let mut tokens: Vec<Token> = Vec::new();
let mut i = 0;
let mut line = 1usize;
let mut pending_newline = false;
macro_rules! push {
($kind:expr) => {{
tokens.push(Token { kind: $kind, line });
pending_newline = false;
}};
}
while i < chars.len() {
let c = chars[i];
match c {
'\n' => {
if !pending_newline && !tokens.is_empty() {
tokens.push(Token {
kind: TokenKind::Newline,
line,
});
pending_newline = true;
}
line += 1;
i += 1;
}
' ' | '\t' | '\r' => {
i += 1;
}
'#' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
}
'/' if i + 1 < chars.len() && chars[i + 1] == '/' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
}
'{' => {
push!(TokenKind::LBrace);
i += 1;
}
'}' => {
push!(TokenKind::RBrace);
i += 1;
}
':' => {
push!(TokenKind::Colon);
i += 1;
}
'=' => {
if i + 1 < chars.len() && chars[i + 1] == '=' {
push!(TokenKind::Eq);
i += 2;
} else {
push!(TokenKind::Assign);
i += 1;
}
}
'!' if i + 1 < chars.len() && chars[i + 1] == '=' => {
push!(TokenKind::Ne);
i += 2;
}
'<' => {
if i + 1 < chars.len() && chars[i + 1] == '=' {
push!(TokenKind::Le);
i += 2;
} else {
push!(TokenKind::Lt);
i += 1;
}
}
'>' => {
if i + 1 < chars.len() && chars[i + 1] == '=' {
push!(TokenKind::Ge);
i += 2;
} else {
push!(TokenKind::Gt);
i += 1;
}
}
'"' => {
let (s, next) = lex_string(&chars, i, line)?;
push!(TokenKind::Str(s));
i = next;
}
c if c.is_ascii_digit() => {
let (n, next) = lex_number(&chars, i);
push!(TokenKind::Num(n));
i = next;
}
'-' if i + 1 < chars.len() && chars[i + 1].is_ascii_digit() => {
let (n, next) = lex_number(&chars, i + 1);
push!(TokenKind::Num(-n));
i = next;
}
c if is_ident_start(c) => {
let (s, next) = lex_ident(&chars, i);
push!(TokenKind::Ident(s));
i = next;
}
other => {
return Err(LexError {
message: format!("unexpected character '{other}'"),
line,
});
}
}
}
tokens.push(Token {
kind: TokenKind::Eof,
line,
});
Ok(tokens)
}
fn lex_string(chars: &[char], start: usize, line: usize) -> Result<(String, usize), LexError> {
let mut out = String::new();
let mut i = start + 1;
while i < chars.len() {
let c = chars[i];
match c {
'\\' if i + 1 < chars.len() => {
let esc = chars[i + 1];
out.push(match esc {
'n' => '\n',
't' => '\t',
'r' => '\r',
'\\' => '\\',
'"' => '"',
'0' => '\0',
other => other,
});
i += 2;
}
'"' => {
return Ok((out, i + 1));
}
'\n' => {
return Err(LexError {
message: "unterminated string literal".to_string(),
line,
});
}
_ => {
out.push(c);
i += 1;
}
}
}
Err(LexError {
message: "unterminated string literal".to_string(),
line,
})
}
fn lex_number(chars: &[char], start: usize) -> (i64, usize) {
let mut i = start;
let mut n: i64 = 0;
while i < chars.len() && chars[i].is_ascii_digit() {
n = n
.saturating_mul(10)
.saturating_add((chars[i] as u8 - b'0') as i64);
i += 1;
}
(n, i)
}
fn lex_ident(chars: &[char], start: usize) -> (String, usize) {
let mut i = start;
let mut out = String::new();
while i < chars.len() && is_ident_continue(chars[i]) {
out.push(chars[i]);
i += 1;
}
(out, i)
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_ident_continue(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/' || c == '-'
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(src: &str) -> Vec<TokenKind> {
tokenize(src).unwrap().into_iter().map(|t| t.kind).collect()
}
#[test]
fn lexes_operators() {
let k = kinds("status != 200");
assert_eq!(
k,
vec![
TokenKind::Ident("status".into()),
TokenKind::Ne,
TokenKind::Num(200),
TokenKind::Eof,
]
);
}
#[test]
fn lexes_assignment_and_string() {
let k = kinds("username = \"' OR 1=1\"");
assert_eq!(
k,
vec![
TokenKind::Ident("username".into()),
TokenKind::Assign,
TokenKind::Str("' OR 1=1".into()),
TokenKind::Eof,
]
);
}
#[test]
fn collapses_newlines_and_skips_comments() {
let src = "a\n\n# comment\n\nb\n";
let k = kinds(src);
assert_eq!(
k,
vec![
TokenKind::Ident("a".into()),
TokenKind::Newline,
TokenKind::Ident("b".into()),
TokenKind::Newline,
TokenKind::Eof,
]
);
}
#[test]
fn lexes_braces_and_colon() {
let k = kinds("send { }: ");
assert_eq!(
k,
vec![
TokenKind::Ident("send".into()),
TokenKind::LBrace,
TokenKind::RBrace,
TokenKind::Colon,
TokenKind::Eof,
]
);
}
#[test]
fn comparison_variants() {
assert_eq!(
kinds("<= >= == < >"),
vec![
TokenKind::Le,
TokenKind::Ge,
TokenKind::Eq,
TokenKind::Lt,
TokenKind::Gt,
TokenKind::Eof
]
);
}
#[test]
fn error_on_unterminated_string() {
let err = tokenize("x = \"oops\n").unwrap_err();
assert!(err.message.contains("unterminated"));
}
#[test]
fn skips_leading_bom() {
let k = kinds("\u{feff}attack");
assert_eq!(k, vec![TokenKind::Ident("attack".into()), TokenKind::Eof]);
}
}