#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Number(f64),
Str(String),
Ident(String),
Regex(String, String),
True,
False,
Null,
Plus,
Minus,
Star,
Slash,
Percent,
EqEq,
NotEq,
Lt,
Gt,
LtEq,
GtEq,
AndAnd,
OrOr,
Bang,
Dot,
Comma,
LParen,
RParen,
LBracket,
RBracket,
Error(char),
Eof,
}
pub fn tokenize(src: &str) -> Vec<Token> {
let chars: Vec<char> = src.chars().collect();
let mut tokens: Vec<Token> = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
match c {
'0'..='9' => {
let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let text: String = chars[start..i].iter().collect();
match text.parse::<f64>() {
Ok(n) => tokens.push(Token::Number(n)),
Err(_) => tokens.push(Token::Error(c)),
}
}
'"' | '\'' => {
let quote = c;
i += 1;
let mut s = String::new();
while i < chars.len() && chars[i] != quote {
if chars[i] == '\\' && i + 1 < chars.len() {
i += 1;
s.push(match chars[i] {
'n' => '\n',
't' => '\t',
'r' => '\r',
'\\' => '\\',
'"' => '"',
'\'' => '\'',
other => other,
});
} else {
s.push(chars[i]);
}
i += 1;
}
if i < chars.len() {
i += 1; }
tokens.push(Token::Str(s));
}
c if c.is_ascii_alphabetic() || c == '_' || c == '$' => {
let start = i;
while i < chars.len()
&& (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '$')
{
i += 1;
}
let word: String = chars[start..i].iter().collect();
tokens.push(match word.as_str() {
"true" => Token::True,
"false" => Token::False,
"null" => Token::Null,
_ => Token::Ident(word),
});
}
'/' if regex_allowed(&tokens) => {
i += 1;
let mut pat = String::new();
let mut in_class = false;
while i < chars.len() && (chars[i] != '/' || in_class) {
match chars[i] {
'\\' if i + 1 < chars.len() => {
pat.push(chars[i]);
pat.push(chars[i + 1]);
i += 2;
continue;
}
'[' => in_class = true,
']' => in_class = false,
_ => {}
}
pat.push(chars[i]);
i += 1;
}
if i < chars.len() {
i += 1; }
let flag_start = i;
while i < chars.len() && chars[i].is_ascii_alphabetic() {
i += 1;
}
let flags: String = chars[flag_start..i].iter().collect();
tokens.push(Token::Regex(pat, flags));
}
_ => {
let two: String = chars[i..(i + 2).min(chars.len())].iter().collect();
let (tok, len) = match two.as_str() {
"==" => (Token::EqEq, 2),
"!=" => (Token::NotEq, 2),
"<=" => (Token::LtEq, 2),
">=" => (Token::GtEq, 2),
"&&" => (Token::AndAnd, 2),
"||" => (Token::OrOr, 2),
_ => match c {
'+' => (Token::Plus, 1),
'-' => (Token::Minus, 1),
'*' => (Token::Star, 1),
'/' => (Token::Slash, 1),
'%' => (Token::Percent, 1),
'<' => (Token::Lt, 1),
'>' => (Token::Gt, 1),
'!' => (Token::Bang, 1),
'.' => (Token::Dot, 1),
',' => (Token::Comma, 1),
'(' => (Token::LParen, 1),
')' => (Token::RParen, 1),
'[' => (Token::LBracket, 1),
']' => (Token::RBracket, 1),
other => (Token::Error(other), 1),
},
};
tokens.push(tok);
i += len;
}
}
}
tokens.push(Token::Eof);
tokens
}
fn regex_allowed(tokens: &[Token]) -> bool {
match tokens.last() {
None => true,
Some(t) => matches!(
t,
Token::Plus
| Token::Minus
| Token::Star
| Token::Slash
| Token::Percent
| Token::EqEq
| Token::NotEq
| Token::Lt
| Token::Gt
| Token::LtEq
| Token::GtEq
| Token::AndAnd
| Token::OrOr
| Token::Bang
| Token::LParen
| Token::Comma
| Token::LBracket
| Token::Dot
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbers_strings_idents() {
let t = tokenize(r#"foo == "bar" + 3.5"#);
assert_eq!(
t,
vec![
Token::Ident("foo".into()),
Token::EqEq,
Token::Str("bar".into()),
Token::Plus,
Token::Number(3.5),
Token::Eof
]
);
}
#[test]
fn single_quoted_string() {
let t = tokenize("'hello world'");
assert_eq!(t[0], Token::Str("hello world".into()));
}
#[test]
fn keywords() {
assert_eq!(tokenize("true")[0], Token::True);
assert_eq!(tokenize("false")[0], Token::False);
assert_eq!(tokenize("null")[0], Token::Null);
}
#[test]
fn two_char_operators() {
let t = tokenize("a && b || c >= d <= e != f");
assert!(t.contains(&Token::AndAnd));
assert!(t.contains(&Token::OrOr));
assert!(t.contains(&Token::GtEq));
assert!(t.contains(&Token::LtEq));
assert!(t.contains(&Token::NotEq));
}
#[test]
fn member_and_call() {
let t = tokenize("file.hasTag(\"x\")");
assert_eq!(
t,
vec![
Token::Ident("file".into()),
Token::Dot,
Token::Ident("hasTag".into()),
Token::LParen,
Token::Str("x".into()),
Token::RParen,
Token::Eof
]
);
}
#[test]
fn regex_literal_vs_division() {
let t = tokenize("/ab.c/gi");
assert_eq!(t[0], Token::Regex("ab.c".into(), "gi".into()));
let t2 = tokenize("price / age");
assert!(t2.contains(&Token::Slash));
assert!(!t2.iter().any(|x| matches!(x, Token::Regex(_, _))));
}
#[test]
fn regex_with_char_class_containing_slash() {
let t = tokenize(r"/[a/b]/");
assert_eq!(t[0], Token::Regex("[a/b]".into(), "".into()));
}
#[test]
fn escapes_in_string() {
let t = tokenize(r#""a\"b\n""#);
assert_eq!(t[0], Token::Str("a\"b\n".into()));
}
}