use std::str::FromStr;
use crate::{
error::{EvalexprError, EvalexprResult},
value::numeric_types::{
default_numeric_types::DefaultNumericTypes, EvalexprInt, EvalexprNumericTypes,
},
};
mod display;
#[derive(Clone, PartialEq, Debug)]
pub enum Token<NumericTypes: EvalexprNumericTypes = DefaultNumericTypes> {
Plus,
Minus,
Star,
Slash,
Percent,
Hat,
Eq,
Neq,
Gt,
Lt,
Geq,
Leq,
And,
Or,
Not,
LBrace,
RBrace,
Assign,
PlusAssign,
MinusAssign,
StarAssign,
SlashAssign,
PercentAssign,
HatAssign,
AndAssign,
OrAssign,
Comma,
Semicolon,
Identifier(String),
Float(NumericTypes::Float),
Int(NumericTypes::Int),
Boolean(bool),
String(String),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PartialToken<NumericTypes: EvalexprNumericTypes = DefaultNumericTypes> {
Token(Token<NumericTypes>),
Literal(String),
Plus,
Minus,
Star,
Slash,
Percent,
Hat,
Whitespace,
Eq,
ExclamationMark,
Gt,
Lt,
Ampersand,
VerticalBar,
}
fn char_to_partial_token<NumericTypes: EvalexprNumericTypes>(
c: char,
) -> PartialToken<NumericTypes> {
match c {
'+' => PartialToken::Plus,
'-' => PartialToken::Minus,
'*' => PartialToken::Star,
'/' => PartialToken::Slash,
'%' => PartialToken::Percent,
'^' => PartialToken::Hat,
'(' => PartialToken::Token(Token::LBrace),
')' => PartialToken::Token(Token::RBrace),
',' => PartialToken::Token(Token::Comma),
';' => PartialToken::Token(Token::Semicolon),
'=' => PartialToken::Eq,
'!' => PartialToken::ExclamationMark,
'>' => PartialToken::Gt,
'<' => PartialToken::Lt,
'&' => PartialToken::Ampersand,
'|' => PartialToken::VerticalBar,
c => {
if c.is_whitespace() {
PartialToken::Whitespace
} else {
PartialToken::Literal(c.to_string())
}
},
}
}
impl<NumericTypes: EvalexprNumericTypes> Token<NumericTypes> {
#[cfg(not(tarpaulin_include))]
pub(crate) const fn is_leftsided_value(&self) -> bool {
match self {
Token::Plus => false,
Token::Minus => false,
Token::Star => false,
Token::Slash => false,
Token::Percent => false,
Token::Hat => false,
Token::Eq => false,
Token::Neq => false,
Token::Gt => false,
Token::Lt => false,
Token::Geq => false,
Token::Leq => false,
Token::And => false,
Token::Or => false,
Token::Not => false,
Token::LBrace => true,
Token::RBrace => false,
Token::Comma => false,
Token::Semicolon => false,
Token::Assign => false,
Token::PlusAssign => false,
Token::MinusAssign => false,
Token::StarAssign => false,
Token::SlashAssign => false,
Token::PercentAssign => false,
Token::HatAssign => false,
Token::AndAssign => false,
Token::OrAssign => false,
Token::Identifier(_) => true,
Token::Float(_) => true,
Token::Int(_) => true,
Token::Boolean(_) => true,
Token::String(_) => true,
}
}
#[cfg(not(tarpaulin_include))]
pub(crate) const fn is_rightsided_value(&self) -> bool {
match self {
Token::Plus => false,
Token::Minus => false,
Token::Star => false,
Token::Slash => false,
Token::Percent => false,
Token::Hat => false,
Token::Eq => false,
Token::Neq => false,
Token::Gt => false,
Token::Lt => false,
Token::Geq => false,
Token::Leq => false,
Token::And => false,
Token::Or => false,
Token::Not => false,
Token::LBrace => false,
Token::RBrace => true,
Token::Comma => false,
Token::Semicolon => false,
Token::Assign => false,
Token::PlusAssign => false,
Token::MinusAssign => false,
Token::StarAssign => false,
Token::SlashAssign => false,
Token::PercentAssign => false,
Token::HatAssign => false,
Token::AndAssign => false,
Token::OrAssign => false,
Token::Identifier(_) => true,
Token::Float(_) => true,
Token::Int(_) => true,
Token::Boolean(_) => true,
Token::String(_) => true,
}
}
#[cfg(not(tarpaulin_include))]
pub(crate) fn is_assignment(&self) -> bool {
use Token::*;
matches!(
self,
Assign
| PlusAssign
| MinusAssign
| StarAssign
| SlashAssign
| PercentAssign
| HatAssign
| AndAssign
| OrAssign
)
}
}
fn parse_escape_sequence<Iter: Iterator<Item = char>, NumericTypes: EvalexprNumericTypes>(
iter: &mut Iter,
) -> EvalexprResult<char, NumericTypes> {
match iter.next() {
Some('"') => Ok('"'),
Some('\\') => Ok('\\'),
Some(c) => Err(EvalexprError::IllegalEscapeSequence(format!("\\{}", c))),
None => Err(EvalexprError::IllegalEscapeSequence("\\".to_string())),
}
}
fn parse_string_literal<Iter: Iterator<Item = char>, NumericTypes: EvalexprNumericTypes>(
mut iter: &mut Iter,
) -> EvalexprResult<PartialToken<NumericTypes>, NumericTypes> {
let mut result = String::new();
while let Some(c) = iter.next() {
match c {
'"' => return Ok(PartialToken::Token(Token::String(result))),
'\\' => result.push(parse_escape_sequence(&mut iter)?),
c => result.push(c),
}
}
Err(EvalexprError::UnmatchedDoubleQuote)
}
fn try_skip_comment<NumericTypes: EvalexprNumericTypes>(
iter: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> EvalexprResult<bool, NumericTypes> {
let mut matched = false;
if let Some(lookahead) = iter.peek() {
if *lookahead == '/' {
matched = true;
iter.next();
for c in iter {
if c == '\n' {
break;
}
}
} else if *lookahead == '*' {
iter.next();
while let Some(c) = iter.next() {
if let Some(next) = iter.peek() {
if c == '*' && *next == '/' {
matched = true;
iter.next();
break;
}
}
}
if !matched {
return Err(EvalexprError::CustomMessage(
"unmatched inline comment".into(),
));
}
}
}
Ok(matched)
}
fn str_to_partial_tokens<NumericTypes: EvalexprNumericTypes>(
string: &str,
) -> EvalexprResult<Vec<PartialToken<NumericTypes>>, NumericTypes> {
let mut result = Vec::new();
let mut iter = string.chars().peekable();
while let Some(c) = iter.next() {
if c == '"' {
result.push(parse_string_literal(&mut iter)?);
} else {
let partial_token = char_to_partial_token(c);
if let PartialToken::Slash = partial_token {
if try_skip_comment(&mut iter)? {
continue;
}
}
let if_let_successful =
if let (Some(PartialToken::Literal(last)), PartialToken::Literal(literal)) =
(result.last_mut(), &partial_token)
{
last.push_str(literal);
true
} else {
false
};
if !if_let_successful {
result.push(partial_token);
}
}
}
Ok(result)
}
fn partial_tokens_to_tokens<NumericTypes: EvalexprNumericTypes>(
mut tokens: &[PartialToken<NumericTypes>],
) -> EvalexprResult<Vec<Token<NumericTypes>>, NumericTypes> {
let mut result = Vec::new();
while !tokens.is_empty() {
let first = tokens[0].clone();
let second = tokens.get(1).cloned();
let third = tokens.get(2).cloned();
let mut cutoff = 2;
result.extend(match first {
PartialToken::Token(token) => {
cutoff = 1;
Some(token)
},
PartialToken::Plus => match second {
Some(PartialToken::Eq) => Some(Token::PlusAssign),
_ => {
cutoff = 1;
Some(Token::Plus)
},
},
PartialToken::Minus => match second {
Some(PartialToken::Eq) => Some(Token::MinusAssign),
_ => {
cutoff = 1;
Some(Token::Minus)
},
},
PartialToken::Star => match second {
Some(PartialToken::Eq) => Some(Token::StarAssign),
_ => {
cutoff = 1;
Some(Token::Star)
},
},
PartialToken::Slash => match second {
Some(PartialToken::Eq) => Some(Token::SlashAssign),
_ => {
cutoff = 1;
Some(Token::Slash)
},
},
PartialToken::Percent => match second {
Some(PartialToken::Eq) => Some(Token::PercentAssign),
_ => {
cutoff = 1;
Some(Token::Percent)
},
},
PartialToken::Hat => match second {
Some(PartialToken::Eq) => Some(Token::HatAssign),
_ => {
cutoff = 1;
Some(Token::Hat)
},
},
PartialToken::Literal(literal) => {
cutoff = 1;
if let Ok(number) = parse_integer::<NumericTypes>(&literal) {
Some(Token::Int(number))
} else if let Ok(number) = literal.parse::<NumericTypes::Float>() {
Some(Token::Float(number))
} else if let Ok(boolean) = literal.parse::<bool>() {
Some(Token::Boolean(boolean))
} else {
match (second, third) {
(Some(second), Some(third))
if second == PartialToken::Minus || second == PartialToken::Plus =>
{
if let Ok(number) = format!("{}{}{}", literal, second, third)
.parse::<NumericTypes::Float>()
{
cutoff = 3;
Some(Token::Float(number))
} else {
Some(Token::Identifier(literal.to_string()))
}
},
_ => Some(Token::Identifier(literal.to_string())),
}
}
},
PartialToken::Whitespace => {
cutoff = 1;
None
},
PartialToken::Eq => match second {
Some(PartialToken::Eq) => Some(Token::Eq),
_ => {
cutoff = 1;
Some(Token::Assign)
},
},
PartialToken::ExclamationMark => match second {
Some(PartialToken::Eq) => Some(Token::Neq),
_ => {
cutoff = 1;
Some(Token::Not)
},
},
PartialToken::Gt => match second {
Some(PartialToken::Eq) => Some(Token::Geq),
_ => {
cutoff = 1;
Some(Token::Gt)
},
},
PartialToken::Lt => match second {
Some(PartialToken::Eq) => Some(Token::Leq),
_ => {
cutoff = 1;
Some(Token::Lt)
},
},
PartialToken::Ampersand => match second {
Some(PartialToken::Ampersand) => match third {
Some(PartialToken::Eq) => {
cutoff = 3;
Some(Token::AndAssign)
},
_ => Some(Token::And),
},
_ => return Err(EvalexprError::unmatched_partial_token(first, second)),
},
PartialToken::VerticalBar => match second {
Some(PartialToken::VerticalBar) => match third {
Some(PartialToken::Eq) => {
cutoff = 3;
Some(Token::OrAssign)
},
_ => Some(Token::Or),
},
_ => return Err(EvalexprError::unmatched_partial_token(first, second)),
},
});
tokens = &tokens[cutoff..];
}
Ok(result)
}
pub(crate) fn tokenize<NumericTypes: EvalexprNumericTypes>(
string: &str,
) -> EvalexprResult<Vec<Token<NumericTypes>>, NumericTypes> {
partial_tokens_to_tokens(&str_to_partial_tokens(string)?)
}
fn parse_integer<NumericTypes: EvalexprNumericTypes>(
literal: &str,
) -> Result<NumericTypes::Int, ()> {
if let Some(literal) = literal.strip_prefix("0x") {
NumericTypes::Int::from_hex_str(literal)
} else if let Some(literal) = literal.strip_prefix("0b") {
NumericTypes::Int::from_binary_str(literal)
} else if let Some(literal) = literal.strip_prefix("0o") {
NumericTypes::Int::from_octal_str(literal)
} else {
NumericTypes::Int::from_str(literal).map_err(|_| ())
}
}
#[cfg(test)]
mod tests {
use crate::{
token::{char_to_partial_token, tokenize, Token},
value::numeric_types::default_numeric_types::DefaultNumericTypes,
};
use std::fmt::Write;
#[test]
fn test_partial_token_display() {
let chars = vec![
'+', '-', '*', '/', '%', '^', '(', ')', ',', ';', '=', '!', '>', '<', '&', '|', ' ',
];
for char in chars {
assert_eq!(
format!("{}", char),
format!("{}", char_to_partial_token::<DefaultNumericTypes>(char))
);
}
}
#[test]
fn test_token_display() {
let token_string =
"+ - * / % ^ == != > < >= <= && || ! ( ) = += -= *= /= %= ^= &&= ||= , ; ";
let tokens = tokenize::<DefaultNumericTypes>(token_string).unwrap();
let mut result_string = String::new();
for token in tokens {
write!(result_string, "{} ", token).unwrap();
}
assert_eq!(token_string, result_string);
}
#[test]
fn test_skip_comment() {
let token_string =
"+ - * / % ^ == != > < >= <= && || ! ( ) = += -= *= /= %= ^= &&= ||= , ; ";
let token_string_with_comments = r"+ - * / % ^ == != >
< >= <= && /* inline comment */ || ! ( )
= += -= *= /= %= ^=
// line comment
&&= ||= , ;
";
let tokens = tokenize::<DefaultNumericTypes>(token_string_with_comments).unwrap();
let mut result_string = String::new();
for token in tokens {
write!(result_string, "{} ", token).unwrap();
}
assert_eq!(token_string, result_string);
}
#[test]
fn assignment_lhs_is_identifier() {
let tokens = tokenize::<DefaultNumericTypes>("a = 1").unwrap();
assert_eq!(
tokens.as_slice(),
[
Token::Identifier("a".to_string()),
Token::Assign,
Token::Int(1)
]
);
}
}