use super::ExprError;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Token {
Integer(i64),
Float(f64),
Text(String),
Date(String),
Ident(String),
Bracket(String),
Plus,
Minus,
Star,
Slash,
Backslash,
Caret,
Amp,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
LParen,
RParen,
Comma,
Dot,
And,
Or,
Not,
Like,
Is,
NullKw,
True,
False,
Between,
In,
Mod,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Spanned {
pub(crate) token: Token,
pub(crate) start: usize,
}
pub(crate) fn lex(source: &str) -> Result<Vec<Spanned>, ExprError> {
let chars: Vec<(usize, char)> = source.char_indices().collect();
let mut tokens = Vec::new();
let mut i = 0;
while i < chars.len() {
let (start, c) = chars[i];
i += 1;
if c.is_whitespace() {
continue;
}
if c.is_ascii_digit() {
let number = scan_number(&chars, &mut i, start, c)?;
tokens.push(number);
continue;
}
let token = match c {
'"' | '\'' => scan_string(&chars, &mut i, c, start)?,
'#' => Token::Date(scan_delimited(&chars, &mut i, '#', start)?),
'[' => Token::Bracket(scan_delimited(&chars, &mut i, ']', start)?),
'+' => Token::Plus,
'-' => Token::Minus,
'*' => Token::Star,
'/' => Token::Slash,
'\\' => Token::Backslash,
'^' => Token::Caret,
'&' => Token::Amp,
'(' => Token::LParen,
')' => Token::RParen,
',' => Token::Comma,
'.' => Token::Dot,
'<' => match chars.get(i).map(|&(_, c)| c) {
Some('=') => {
i += 1;
Token::Le
}
Some('>') => {
i += 1;
Token::Ne
}
_ => Token::Lt,
},
'>' => {
if chars.get(i).map(|&(_, c)| c) == Some('=') {
i += 1;
Token::Ge
} else {
Token::Gt
}
}
'=' => Token::Eq,
c if c.is_ascii_alphabetic() || c == '_' => {
let word: String = std::iter::once(c)
.chain(
chars[i..]
.iter()
.take_while(|&&(_, c)| c.is_ascii_alphanumeric() || c == '_')
.map(|&(_, c)| c),
)
.collect();
i += word.chars().count() - 1;
keyword_or_ident(&word)
}
other => {
return Err(ExprError::Lex {
position: start,
detail: format!("unexpected character {other:?}"),
});
}
};
tokens.push(Spanned { token, start });
}
Ok(tokens)
}
fn scan_number(
chars: &[(usize, char)],
i: &mut usize,
start: usize,
first: char,
) -> Result<Spanned, ExprError> {
let mut digits = String::from(first);
while matches!(chars.get(*i), Some((_, '0'..='9'))) {
digits.push(chars[*i].1);
*i += 1;
}
let is_float = matches!(chars.get(*i), Some((_, '.')))
&& matches!(chars.get(*i + 1), Some((_, '0'..='9')));
if is_float {
digits.push('.');
*i += 1;
while matches!(chars.get(*i), Some((_, '0'..='9'))) {
digits.push(chars[*i].1);
*i += 1;
}
}
let token = if is_float {
match digits.parse::<f64>() {
Ok(value) => Token::Float(value),
Err(_) => {
return Err(ExprError::Lex {
position: start,
detail: "float literal is not representable".to_owned(),
});
}
}
} else {
match digits.parse::<i64>() {
Ok(value) => Token::Integer(value),
Err(_) => {
return Err(ExprError::Lex {
position: start,
detail: "integer literal exceeds 64 bits".to_owned(),
});
}
}
};
Ok(Spanned { token, start })
}
fn scan_string(
chars: &[(usize, char)],
i: &mut usize,
quote: char,
start: usize,
) -> Result<Token, ExprError> {
let mut text = String::new();
loop {
match chars.get(*i) {
Some((_, c)) if *c == quote => {
*i += 1;
if chars.get(*i).map(|&(_, c)| c) == Some(quote) {
text.push(quote);
*i += 1;
} else {
return Ok(Token::Text(text));
}
}
Some((_, c)) => {
text.push(*c);
*i += 1;
}
None => {
return Err(ExprError::Lex {
position: start,
detail: "unterminated string".to_owned(),
});
}
}
}
}
fn scan_delimited(
chars: &[(usize, char)],
i: &mut usize,
delimiter: char,
start: usize,
) -> Result<String, ExprError> {
let mut text = String::new();
loop {
match chars.get(*i) {
Some((_, c)) if *c == delimiter => {
*i += 1;
return Ok(text);
}
Some((_, c)) => {
text.push(*c);
*i += 1;
}
None => {
return Err(ExprError::Lex {
position: start,
detail: format!("missing closing {delimiter:?}"),
});
}
}
}
}
fn keyword_or_ident(word: &str) -> Token {
match word.to_lowercase().as_str() {
"and" => Token::And,
"or" => Token::Or,
"not" => Token::Not,
"like" => Token::Like,
"is" => Token::Is,
"null" => Token::NullKw,
"true" => Token::True,
"false" => Token::False,
"between" => Token::Between,
"in" => Token::In,
"mod" => Token::Mod,
_ => Token::Ident(word.to_owned()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(source: &str) -> Vec<Token> {
lex(source)
.expect("source must lex")
.into_iter()
.map(|spanned| spanned.token)
.collect()
}
#[test]
fn operators_lex_in_longest_match_order() {
assert_eq!(
kinds("< <= <> > >= = + - * / \\ ^ & ( ) , ."),
vec![
Token::Lt,
Token::Le,
Token::Ne,
Token::Gt,
Token::Ge,
Token::Eq,
Token::Plus,
Token::Minus,
Token::Star,
Token::Slash,
Token::Backslash,
Token::Caret,
Token::Amp,
Token::LParen,
Token::RParen,
Token::Comma,
Token::Dot,
]
);
}
#[test]
fn literals_and_keywords_lex_case_insensitively() {
assert_eq!(
kinds("42 2.5 \"a\"\"b\" 'c' #2025-01-05# [Order Details] between TRUE null Mod"),
vec![
Token::Integer(42),
Token::Float(2.5),
Token::Text("a\"b".into()),
Token::Text("c".into()),
Token::Date("2025-01-05".into()),
Token::Bracket("Order Details".into()),
Token::Between,
Token::True,
Token::NullKw,
Token::Mod,
]
);
}
#[test]
fn unknown_characters_are_lexical_errors() {
let error = lex("1 @ 2").expect_err("@ must fail");
assert!(matches!(error, ExprError::Lex { position: 2, .. }));
}
#[test]
fn unterminated_strings_and_brackets_fail() {
assert!(lex("\"open").is_err());
assert!(lex("'open").is_err());
assert!(lex("#2025-01-05").is_err());
assert!(lex("[open").is_err());
}
#[test]
fn oversized_integer_literals_fail_precisely() {
let error = lex("9223372036854775808").expect_err("2^63 does not fit i64");
assert!(error.to_string().contains("exceeds 64 bits"), "{error}");
}
}