use crate::error::LexerError;
use crate::token::{Operator, Sym, Symbol, Text, Token};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while};
use nom::character::complete::{alpha1, char, multispace1, satisfy};
use nom::character::one_of;
use nom::combinator::{eof, opt, recognize};
use nom::error::{Error, context};
use nom::multi::many0;
use nom::number::complete::double;
use nom::sequence::{delimited, pair};
use nom::{AsChar, IResult, Parser};
pub(crate) fn tokenize(input: &str) -> Result<Vec<Token<'_>>, LexerError> {
let mut input = Text::new(input);
let mut tokens = Vec::new();
loop {
let (remaining, token) = token(input).map_err(massage_nom_error)?;
input = remaining;
tokens.push(token);
if matches!(token.sym, Sym::Eof) {
break;
}
}
Ok(tokens)
}
fn massage_nom_error(err: nom::Err<Error<Text>>) -> LexerError {
match err {
nom::Err::Incomplete(_) => LexerError::IncompleteInput,
nom::Err::Error(err) => {
LexerError::InvalidSymbol(err.input.location_line(), err.input.get_column() as u32)
}
nom::Err::Failure(err) => {
LexerError::InvalidSymbol(err.input.location_line(), err.input.get_column() as u32)
}
}
}
fn token(input: Text) -> IResult<Text, Token> {
delimited(
skip_whitespace_and_comments,
parse_token,
skip_whitespace_and_comments,
)
.parse(input)
}
fn skip_whitespace_and_comments(input: Text) -> IResult<Text, Text> {
recognize(many0(alt((comment, multispace1)))).parse(input)
}
fn comment(input: Text) -> IResult<Text, Text> {
recognize(pair(
pair(tag("//"), take_while(|c: char| c != '\n' && c != '\r')),
opt(alt((tag("\r\n"), tag("\n"), tag("\r")))),
))
.parse(input)
}
fn parse_token(input: Text) -> IResult<Text, Token> {
alt((end_of_file, symbol, operator, ident, number, string)).parse(input)
}
fn symbol(input: Text) -> IResult<Text, Token> {
one_of("().,:[]{}")
.map(|c| match c {
'(' => Symbol::OpenParen,
')' => Symbol::CloseParen,
'.' => Symbol::Dot,
',' => Symbol::Comma,
':' => Symbol::Colon,
'[' => Symbol::OpenBracket,
']' => Symbol::CloseBracket,
'{' => Symbol::OpenBrace,
'}' => Symbol::CloseBrace,
_ => unreachable!(),
})
.map(move |sym| Token {
sym: Sym::Symbol(sym),
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}
fn end_of_file(input: Text) -> IResult<Text, Token> {
eof.map(|_| Token {
sym: Sym::Eof,
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}
fn operator(input: Text) -> IResult<Text, Token> {
alt((operator_1, operator_2)).parse(input)
}
fn operator_1(input: Text) -> IResult<Text, Token> {
one_of("+-*/^")
.map(|c| match c {
'+' => Operator::Add,
'-' => Operator::Sub,
'*' => Operator::Mul,
'/' => Operator::Div,
_ => unreachable!(),
})
.map(move |op| Token {
sym: Sym::Operator(op),
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}
fn operator_2(input: Text) -> IResult<Text, Token> {
one_of("<>!=")
.flat_map(|c| {
context(
"valid character when parsing an operator",
opt(char('=')).map_opt(move |eq_opt| match (c, eq_opt.is_some()) {
('<', false) => Some(Operator::Lt),
('<', true) => Some(Operator::Lte),
('>', false) => Some(Operator::Gt),
('>', true) => Some(Operator::Gte),
('!', true) => Some(Operator::Neq),
('=', true) => Some(Operator::Eq),
_ => None,
}),
)
})
.map(move |op| Token {
sym: Sym::Operator(op),
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}
fn ident(input: Text) -> IResult<Text, Token> {
recognize(pair(
alpha1,
many0(satisfy(|c| AsChar::is_alphanum(c) || c == '_')),
))
.map(|value: Text| {
let sym = if value.fragment().eq_ignore_ascii_case("and") {
Sym::Operator(Operator::And)
} else if value.fragment().eq_ignore_ascii_case("or") {
Sym::Operator(Operator::Or)
} else if value.fragment().eq_ignore_ascii_case("xor") {
Sym::Operator(Operator::Xor)
} else if value.fragment().eq_ignore_ascii_case("not") {
Sym::Operator(Operator::Not)
} else if value.fragment().eq_ignore_ascii_case("contains") {
Sym::Operator(Operator::Contains)
} else if value.fragment().eq_ignore_ascii_case("as") {
Sym::Operator(Operator::As)
} else {
Sym::Id(value.fragment())
};
Token {
sym,
line: value.location_line(),
col: value.get_column() as u32,
}
})
.parse(input)
}
fn number(input: Text) -> IResult<Text, Token> {
double
.map(|value| Token {
sym: Sym::Number(value),
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}
fn string(input: Text) -> IResult<Text, Token> {
delimited(char('"'), take_while(|c| c != '"'), char('"'))
.map(|value: Text| Token {
sym: Sym::String(value.fragment()),
line: input.location_line(),
col: input.get_column() as u32,
})
.parse(input)
}