pub mod ast;
pub mod tokens;
pub use ast::Expr;
use ast::{BinaryOp, Span, UnaryOp};
use crate::types::ParseError;
use nom::{IResult, character::complete::multispace0};
use tokens::{bool_literal, identifier, number_literal, offset, string_literal};
struct Parser<'a> {
full: &'a str,
}
impl<'a> Parser<'a> {
fn new(full: &'a str) -> Self {
Self { full }
}
fn span(&self, before: &str, after: &str) -> Span {
let start = offset(self.full, before);
let end = offset(self.full, after);
Span::new(start, end - start)
}
fn parse_primary(&self, i: &'a str) -> IResult<&'a str, Expr> {
let i = multispace0(i)?.0;
if let Ok((rest, n)) = number_literal(i) {
return Ok((rest, Expr::Number(n, self.span(i, rest))));
}
if let Ok((rest, text)) = string_literal(i) {
return Ok((rest, Expr::Text(text, self.span(i, rest))));
}
if let Some(inner) = i.strip_prefix('(') {
let (rest, expr) = self.parse_comparison(inner)?;
let rest = multispace0(rest)?.0;
if let Some(after) = rest.strip_prefix(')') {
return Ok((after, expr));
}
return Err(nom::Err::Error(nom::error::Error::new(
rest,
nom::error::ErrorKind::Char,
)));
}
if let Ok((rest, b)) = bool_literal(i) {
return Ok((rest, Expr::Bool(b, self.span(i, rest))));
}
if let Ok((rest, name)) = identifier(i) {
let rest_ws = multispace0(rest)?.0;
if let Some(args_input) = rest_ws.strip_prefix('(') {
let (rest2, args) = self.parse_arg_list(args_input)?;
let rest2 = multispace0(rest2)?.0;
if let Some(after) = rest2.strip_prefix(')') {
return Ok((after, Expr::FunctionCall {
name: name.to_uppercase(),
args,
span: self.span(i, after),
}));
}
return Err(nom::Err::Error(nom::error::Error::new(
rest2,
nom::error::ErrorKind::Char,
)));
}
return Ok((rest, Expr::Variable(name.to_string(), self.span(i, rest))));
}
Err(nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Alt)))
}
fn parse_arg_list(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
let mut args = Vec::new();
let mut rest = multispace0(i)?.0;
if rest.starts_with(')') {
return Ok((rest, args));
}
let (r, first) = self.parse_comparison(rest)?;
args.push(first);
rest = r;
loop {
rest = multispace0(rest)?.0;
if let Some(after_comma) = rest.strip_prefix(',') {
let (r, arg) = self.parse_comparison(after_comma)?;
args.push(arg);
rest = r;
} else {
break;
}
}
Ok((rest, args))
}
fn parse_postfix(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (rest, expr) = self.parse_primary(i)?;
let rest_ws = multispace0(rest)?.0;
if let Some(after) = rest_ws.strip_prefix('%') {
return Ok((after, Expr::UnaryOp {
op: UnaryOp::Percent,
operand: Box::new(expr),
span: self.span(i, after),
}));
}
Ok((rest, expr))
}
fn parse_unary(&self, i: &'a str) -> IResult<&'a str, Expr> {
let i_ws = multispace0(i)?.0;
if let Some(after_minus) = i_ws.strip_prefix('-') {
let (rest, operand) = self.parse_unary(after_minus)?;
return Ok((rest, Expr::UnaryOp {
op: UnaryOp::Neg,
operand: Box::new(operand),
span: self.span(i_ws, rest),
}));
}
self.parse_postfix(i)
}
fn parse_power(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (rest, left) = self.parse_unary(i)?;
let rest_ws = multispace0(rest)?.0;
if let Some(after_op) = rest_ws.strip_prefix('^') {
let (rest2, right) = self.parse_power(after_op)?;
return Ok((rest2, Expr::BinaryOp {
op: BinaryOp::Pow,
left: Box::new(left),
right: Box::new(right),
span: self.span(i, rest2),
}));
}
Ok((rest, left))
}
fn parse_multiplicative(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (mut rest, mut left) = self.parse_power(i)?;
loop {
let ws = multispace0(rest)?.0;
let op = ws.strip_prefix('*').map(|after| (BinaryOp::Mul, after))
.or_else(|| ws.strip_prefix('/').map(|after| (BinaryOp::Div, after)));
match op {
None => break,
Some((op, after)) => {
let (r, right) = self.parse_power(after)?;
left = Expr::BinaryOp {
op,
span: self.span(i, r),
left: Box::new(left),
right: Box::new(right),
};
rest = r;
}
}
}
Ok((rest, left))
}
fn parse_additive(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (mut rest, mut left) = self.parse_multiplicative(i)?;
loop {
let ws = multispace0(rest)?.0;
let op = ws.strip_prefix('+').map(|after| (BinaryOp::Add, after))
.or_else(|| ws.strip_prefix('-').map(|after| (BinaryOp::Sub, after)));
match op {
None => break,
Some((op, after)) => {
let (r, right) = self.parse_multiplicative(after)?;
left = Expr::BinaryOp {
op,
span: self.span(i, r),
left: Box::new(left),
right: Box::new(right),
};
rest = r;
}
}
}
Ok((rest, left))
}
fn parse_concat(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (mut rest, mut left) = self.parse_additive(i)?;
loop {
let ws = multispace0(rest)?.0;
if let Some(after) = ws.strip_prefix('&') {
let (r, right) = self.parse_additive(after)?;
left = Expr::BinaryOp {
op: BinaryOp::Concat,
span: self.span(i, r),
left: Box::new(left),
right: Box::new(right),
};
rest = r;
} else {
break;
}
}
Ok((rest, left))
}
fn parse_comparison(&self, i: &'a str) -> IResult<&'a str, Expr> {
let (rest, left) = self.parse_concat(i)?;
let ws = multispace0(rest)?.0;
let op_result: Option<(BinaryOp, &'a str)> = if let Some(after) = ws.strip_prefix("<>") {
Some((BinaryOp::Ne, after))
} else if let Some(after) = ws.strip_prefix("<=") {
Some((BinaryOp::Le, after))
} else if let Some(after) = ws.strip_prefix(">=") {
Some((BinaryOp::Ge, after))
} else if let Some(after) = ws.strip_prefix('<') {
Some((BinaryOp::Lt, after))
} else if let Some(after) = ws.strip_prefix('>') {
Some((BinaryOp::Gt, after))
} else if let Some(after) = ws.strip_prefix('=') {
Some((BinaryOp::Eq, after))
} else {
None
};
if let Some((op, after)) = op_result {
let (r, right) = self.parse_concat(after)?;
return Ok((r, Expr::BinaryOp {
op,
span: self.span(i, r),
left: Box::new(left),
right: Box::new(right),
}));
}
Ok((rest, left))
}
}
pub fn parse(formula: &str) -> Result<Expr, ParseError> {
let input = formula.strip_prefix('=').unwrap_or(formula).trim();
let p = Parser::new(formula);
match p.parse_comparison(input) {
Ok((rest, expr)) => {
let rest = rest.trim();
if rest.is_empty() {
Ok(expr)
} else {
Err(ParseError {
message: format!("Unexpected input '{}'", rest),
position: offset(formula, rest),
})
}
}
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
message: "Parse error".into(),
position: offset(formula, e.input),
}),
Err(nom::Err::Incomplete(_)) => Err(ParseError {
message: "Incomplete input".into(),
position: formula.len(),
}),
}
}
pub fn validate(formula: &str) -> Result<(), ParseError> {
parse(formula).map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
#[test]
fn parse_number_literal() {
let expr = parse("=42").unwrap();
assert!(matches!(expr, Expr::Number(n, _) if n == 42.0));
}
#[test]
fn parse_binary_add() {
let expr = parse("=1+2").unwrap();
assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Add, .. }));
}
#[test]
fn parse_precedence() {
let expr = parse("=2+3*4").unwrap();
match expr {
Expr::BinaryOp { op: BinaryOp::Add, right, .. } => {
assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
}
_ => panic!("Expected Add at top"),
}
}
#[test]
fn parse_function_call() {
let expr = parse("=SUM(1,2,3)").unwrap();
match expr {
Expr::FunctionCall { name, args, .. } => {
assert_eq!(name, "SUM");
assert_eq!(args.len(), 3);
}
_ => panic!("Expected FunctionCall"),
}
}
#[test]
fn parse_percent() {
let expr = parse("=50%").unwrap();
assert!(matches!(expr, Expr::UnaryOp { op: UnaryOp::Percent, .. }));
}
#[test]
fn parse_string_literal() {
let expr = parse("=\"hello\"").unwrap();
assert!(matches!(expr, Expr::Text(ref s, _) if s == "hello"));
}
#[test]
fn parse_concat_op() {
let expr = parse("=\"a\"&\"b\"").unwrap();
assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Concat, .. }));
}
#[test]
fn validate_incomplete_fails() {
let err = validate("=SUM(1,").unwrap_err();
assert!(!err.message.is_empty());
}
#[test]
fn parse_nested() {
assert!(parse("=ROUND(SUM(1,2)*1.1, 1)").is_ok());
}
#[test]
fn parse_boolean() {
let expr = parse("=TRUE").unwrap();
assert!(matches!(expr, Expr::Bool(true, _)));
}
#[test]
fn parse_variable() {
let expr = parse("=myVar").unwrap();
assert!(matches!(expr, Expr::Variable(ref n, _) if n == "myVar"));
}
#[test]
fn parse_power_right_assoc() {
let expr = parse("=2^3^2").unwrap();
match expr {
Expr::BinaryOp { op: BinaryOp::Pow, right, .. } => {
assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Pow, .. }));
}
_ => panic!("Expected Pow at top"),
}
}
}