use nom::IResult;
use super::error::{
ParseError, Span, cold_parse_error_unexpected_eof, cold_parse_error_unexpected_token,
};
use super::lexer::{parse_char, parse_constant, parse_identifier, ws};
use crate::dag::builder::DagBuilder;
use crate::dag::node::DagNodeId;
use crate::dag::symbol::SymbolKind;
pub const MAX_PAREN_DEPTH: u16 = 200;
#[derive(Debug, Clone)]
pub struct PrecedenceTable {
entries: std::collections::HashMap<String, (u8, bool)>,
unary: std::collections::HashMap<String, SymbolKind>,
}
impl PrecedenceTable {
#[must_use]
pub fn default_table() -> Self {
let mut t = Self {
entries: std::collections::HashMap::new(),
unary: std::collections::HashMap::new(),
};
t.entries.insert("+".into(), (1, false));
t.entries.insert("-".into(), (1, false));
t.entries.insert("*".into(), (2, false));
t.entries.insert("/".into(), (2, false));
t.entries.insert("%".into(), (2, false));
t.entries.insert("^".into(), (3, true));
t
}
#[must_use]
pub fn empty() -> Self {
Self {
entries: std::collections::HashMap::new(),
unary: std::collections::HashMap::new(),
}
}
pub fn register_op(&mut self, op: impl Into<String>, precedence: u8, right_associative: bool) {
self.entries
.insert(op.into(), (precedence, right_associative));
}
pub fn register(&mut self, op: char, precedence: u8, right_associative: bool) {
self.register_op(op.to_string(), precedence, right_associative);
}
#[must_use]
pub fn precedence(&self, op: char) -> Option<u8> {
self.entries.get(&op.to_string()).map(|&(prec, _)| prec)
}
#[must_use]
pub fn precedence_str(&self, op: &str) -> Option<u8> {
self.entries.get(op).map(|&(prec, _)| prec)
}
#[must_use]
pub fn is_right_associative(&self, op: char) -> bool {
self.entries.get(&op.to_string()).is_some_and(|&(_, ra)| ra)
}
#[must_use]
pub fn is_right_associative_str(&self, op: &str) -> bool {
self.entries.get(op).is_some_and(|&(_, ra)| ra)
}
#[must_use]
pub fn named_ops(&self) -> impl Iterator<Item = &str> {
self.entries
.keys()
.filter(|k| k.len() > 1)
.map(String::as_str)
}
pub fn register_unary_op(&mut self, prefix: impl Into<String>, kind: SymbolKind) {
self.unary.insert(prefix.into(), kind);
}
pub fn unary_ops(&self) -> impl Iterator<Item = (&str, &SymbolKind)> {
self.unary.iter().map(|(k, v)| (k.as_str(), v))
}
}
const fn op_precedence(op: char) -> Option<u8> {
match op {
'+' | '-' => Some(1),
'*' | '/' | '%' => Some(2),
'^' => Some(3),
_ => None,
}
}
const fn op_right_associative(op: char) -> bool {
op == '^'
}
#[doc(hidden)]
#[cold]
#[track_caller]
#[inline(never)]
fn too_deep(input: &str) -> nom::Err<nom::error::Error<&str>> {
nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::TooLarge,
))
}
fn parse_atom<'a>(
input: &'a str,
builder: &mut DagBuilder,
depth: u16,
) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
if let Ok((rem, _)) = ws(parse_char('('))(input) {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let (rem, expr) = parse_expr_climbing(rem, builder, 0, depth + 1)?;
let (rem, _) = ws(parse_char(')'))(rem)?;
return Ok((rem, expr));
}
if let Ok((rem, _)) = ws(parse_char('-'))(input) {
let (rem, atom) = parse_expr_climbing(rem, builder, 3, depth)?;
let neg = builder.neg(atom);
return Ok((rem, neg));
}
if let Ok((rem, val)) = ws(parse_constant)(input) {
let node_id = builder.constant(val);
return Ok((rem, node_id));
}
if let Ok((rem, name)) = ws(parse_identifier)(input) {
if let Ok((mut cur, _)) = ws(parse_char('('))(rem) {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let mut args: Vec<DagNodeId> = Vec::new();
if let Ok((after_close, _)) = ws(parse_char(')'))(cur) {
cur = after_close;
} else {
loop {
let (r, arg) = parse_expr_climbing(cur, builder, 0, depth + 1)?;
args.push(arg);
cur = r;
if let Ok((r2, _)) = ws(parse_char(','))(cur) {
cur = r2;
} else if let Ok((r2, _)) = ws(parse_char(')'))(cur) {
cur = r2;
break;
} else {
return Err(nom::Err::Error(nom::error::Error::new(
cur,
nom::error::ErrorKind::Tag, )));
}
}
}
let fn_id = builder.intern_function(name);
let node_id = builder.function_call(fn_id, &args);
return Ok((cur, node_id));
}
let node_id = builder.variable(name);
return Ok((rem, node_id));
}
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char, )))
}
fn parse_expr_climbing<'a>(
input: &'a str,
builder: &mut DagBuilder,
min_prec: u8,
depth: u16,
) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let (mut rem, mut lhs) = parse_atom(input, builder, depth)?;
loop {
let next_input = rem;
let mut chars = next_input.trim_start().chars();
let Some(op_char) = chars.next() else {
break;
};
let Some(op_prec) = op_precedence(op_char) else {
break;
};
if op_prec < min_prec {
break;
}
let (rem_after_op, _) = ws(parse_char(op_char))(rem)?;
rem = rem_after_op;
let next_min_prec = if op_right_associative(op_char) {
op_prec
} else {
op_prec + 1
};
let next_depth = if op_right_associative(op_char) {
depth.saturating_add(1)
} else {
depth
};
let (rem_after_rhs, rhs) = parse_expr_climbing(rem, builder, next_min_prec, next_depth)?;
rem = rem_after_rhs;
lhs = match op_char {
'+' => builder.add(lhs, rhs),
'-' => builder.sub(lhs, rhs),
'*' => builder.mul(lhs, rhs),
'/' => builder.div(lhs, rhs),
'%' => builder.modulo(lhs, rhs),
'^' => builder.pow(lhs, rhs),
_ => return Err(too_deep(input)),
};
}
Ok((rem, lhs))
}
fn parse_atom_with_table<'a>(
input: &'a str,
builder: &mut DagBuilder,
depth: u16,
table: &PrecedenceTable,
) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
if let Ok((rem, _)) = ws(parse_char('('))(input) {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let (rem, expr) = parse_expr_climbing_with_table(rem, builder, 0, depth + 1, table)?;
let (rem, _) = ws(parse_char(')'))(rem)?;
return Ok((rem, expr));
}
if let Ok((rem, _)) = ws(parse_char('-'))(input) {
let (rem, atom) = parse_expr_climbing_with_table(rem, builder, 3, depth, table)?;
let neg = builder.neg(atom);
return Ok((rem, neg));
}
{
let trimmed = input.trim_start();
let mut candidates: Vec<(&str, &SymbolKind)> = table.unary_ops().collect();
candidates.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
for (prefix, kind) in candidates {
if let Some(after) = trimmed.strip_prefix(prefix) {
let ok = prefix
.chars()
.next()
.is_some_and(|c| !c.is_alphanumeric() && c != '_')
|| after.is_empty()
|| !after
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
if ok {
let (rem, atom) =
parse_expr_climbing_with_table(after, builder, 3, depth, table)?;
use crate::dag::metadata::NodeFlags;
let node_id = builder.operator(*kind, &[atom], NodeFlags::EMPTY);
return Ok((rem, node_id));
}
}
}
}
if let Ok((rem, val)) = ws(parse_constant)(input) {
let node_id = builder.constant(val);
return Ok((rem, node_id));
}
if let Ok((rem, name)) = ws(parse_identifier)(input) {
if let Ok((mut cur, _)) = ws(parse_char('('))(rem) {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let mut args: Vec<DagNodeId> = Vec::new();
if let Ok((after_close, _)) = ws(parse_char(')'))(cur) {
cur = after_close;
} else {
loop {
let (r, arg) =
parse_expr_climbing_with_table(cur, builder, 0, depth + 1, table)?;
args.push(arg);
cur = r;
if let Ok((r2, _)) = ws(parse_char(','))(cur) {
cur = r2;
} else if let Ok((r2, _)) = ws(parse_char(')'))(cur) {
cur = r2;
break;
} else {
return Err(nom::Err::Error(nom::error::Error::new(
cur,
nom::error::ErrorKind::Tag,
)));
}
}
}
let fn_id = builder.intern_function(name);
let node_id = builder.function_call(fn_id, &args);
return Ok((cur, node_id));
}
let node_id = builder.variable(name);
return Ok((rem, node_id));
}
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)))
}
fn parse_expr_climbing_with_table<'a>(
input: &'a str,
builder: &mut DagBuilder,
min_prec: u8,
depth: u16,
table: &PrecedenceTable,
) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
if depth >= MAX_PAREN_DEPTH {
return Err(too_deep(input));
}
let (mut rem, mut lhs) = parse_atom_with_table(input, builder, depth, table)?;
loop {
let trimmed = rem.trim_start();
let mut named_match: Option<(&str, u8, bool, &str)> = None; {
let mut candidates: Vec<&str> = table.named_ops().collect();
candidates.sort_by(|a, b| b.len().cmp(&a.len()));
for named_op in candidates {
if let Some(after) = trimmed.strip_prefix(named_op) {
let is_word_boundary = after.is_empty()
|| !after
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
if is_word_boundary && let Some(prec) = table.precedence_str(named_op) {
let ra = table.is_right_associative_str(named_op);
named_match = Some((named_op, prec, ra, after.trim_start()));
break;
}
}
}
}
if let Some((op_name, op_prec, ra, rem_after_op)) = named_match {
if op_prec < min_prec {
break;
}
rem = rem_after_op;
let next_min_prec = if ra { op_prec } else { op_prec + 1 };
let next_depth = if ra { depth.saturating_add(1) } else { depth };
let (rem_after_rhs, rhs) =
parse_expr_climbing_with_table(rem, builder, next_min_prec, next_depth, table)?;
rem = rem_after_rhs;
let fn_id = builder.intern_function(op_name);
lhs = builder.function_call(fn_id, &[lhs, rhs]);
continue;
}
let mut chars = trimmed.chars();
let Some(op_char) = chars.next() else {
break;
};
let Some(op_prec) = table.precedence(op_char) else {
break;
};
if op_prec < min_prec {
break;
}
let (rem_after_op, _) = ws(parse_char(op_char))(rem)?;
rem = rem_after_op;
let ra = table.is_right_associative(op_char);
let next_min_prec = if ra { op_prec } else { op_prec + 1 };
let next_depth = if ra { depth.saturating_add(1) } else { depth };
let (rem_after_rhs, rhs) =
parse_expr_climbing_with_table(rem, builder, next_min_prec, next_depth, table)?;
rem = rem_after_rhs;
lhs = match op_char {
'+' => builder.add(lhs, rhs),
'-' => builder.sub(lhs, rhs),
'*' => builder.mul(lhs, rhs),
'/' => builder.div(lhs, rhs),
'%' => builder.modulo(lhs, rhs),
'^' => builder.pow(lhs, rhs),
_ => return Err(too_deep(input)),
};
}
Ok((rem, lhs))
}
pub fn parse_with_table(
input: &str,
builder: &mut DagBuilder,
table: &PrecedenceTable,
) -> Result<DagNodeId, super::error::ParseError> {
match parse_expr_climbing_with_table(input, builder, 0, 0, table) {
Ok((remaining, id)) => {
let trimmed = remaining.trim_start();
if !trimmed.is_empty() {
let offset = offset_in(input, trimmed).unwrap_or(input.len());
return Err(super::error::ParseError {
message: "Unexpected trailing tokens".to_owned(),
span: super::error::Span::from_offset(input, offset, trimmed.len()),
});
}
Ok(id)
}
Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
let offset = offset_in(input, e.input).unwrap_or(input.len());
let len = e.input.len().min(input.len().saturating_sub(offset));
let span = super::error::Span::from_offset(input, offset, len);
match e.code {
nom::error::ErrorKind::TooLarge => Err(super::error::ParseError {
message: format!("Parenthesis depth exceeded {MAX_PAREN_DEPTH}"),
span,
}),
nom::error::ErrorKind::Char => {
if e.input.trim_start().is_empty() {
Err(cold_parse_error_unexpected_eof(span))
} else {
let bad = e.input.trim_start().chars().next().unwrap_or('?');
Err(cold_parse_error_unexpected_token(span, bad))
}
}
nom::error::ErrorKind::Tag => Err(super::error::ParseError {
message: "Expected ',' or ')' to close function argument list".to_owned(),
span,
}),
nom::error::ErrorKind::Eof => Err(super::error::ParseError {
message: "Unexpected end of input; expression is incomplete".to_owned(),
span,
}),
_ => Err(super::error::ParseError {
message: format!("Syntax error near {:?}", &e.input[..e.input.len().min(8)]),
span,
}),
}
}
Err(nom::Err::Incomplete(_)) => Err(super::error::ParseError {
message: "Incomplete input".to_owned(),
span: super::error::Span::from_offset(input, input.len(), 0),
}),
}
}
fn offset_in(whole: &str, slice: &str) -> Option<usize> {
let whole_ptr = whole.as_ptr() as usize;
let slice_ptr = slice.as_ptr() as usize;
let whole_end = whole_ptr.checked_add(whole.len())?;
if slice_ptr < whole_ptr || slice_ptr > whole_end {
return None;
}
Some(slice_ptr - whole_ptr)
}
pub fn parse_expression(input: &str, builder: &mut DagBuilder) -> Result<DagNodeId, ParseError> {
match parse_expr_climbing(input, builder, 0, 0) {
Ok((remaining, id)) => {
let trimmed = remaining.trim_start();
if !trimmed.is_empty() {
let offset = offset_in(input, trimmed).unwrap_or(input.len());
return Err(ParseError {
message: "Unexpected trailing tokens".to_owned(),
span: Span::from_offset(input, offset, trimmed.len()),
});
}
Ok(id)
}
Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
let offset = offset_in(input, e.input).unwrap_or(input.len());
let len = e.input.len().min(input.len().saturating_sub(offset));
let span = Span::from_offset(input, offset, len);
match e.code {
nom::error::ErrorKind::TooLarge => Err(ParseError {
message: format!("Parenthesis depth exceeded {MAX_PAREN_DEPTH}"),
span,
}),
nom::error::ErrorKind::Char => {
if e.input.trim_start().is_empty() {
Err(cold_parse_error_unexpected_eof(span))
} else {
let bad = e.input.trim_start().chars().next().unwrap_or('?');
Err(cold_parse_error_unexpected_token(span, bad))
}
}
nom::error::ErrorKind::Tag => Err(ParseError {
message: "Expected ',' or ')' to close function argument list".to_owned(),
span,
}),
nom::error::ErrorKind::Eof => Err(ParseError {
message: "Unexpected end of input; expression is incomplete".to_owned(),
span,
}),
_ => Err(ParseError {
message: format!("Syntax error near {:?}", &e.input[..e.input.len().min(8)]),
span,
}),
}
}
Err(nom::Err::Incomplete(_)) => Err(ParseError {
message: "Incomplete input".to_owned(),
span: Span::from_offset(input, input.len(), 0),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::symbol::SymbolKind;
#[test]
fn test_parse_expression_precedence() {
let mut builder = DagBuilder::new();
let id = parse_expression("x + y * z", &mut builder).expect("ok");
let node = builder.arena().get(id).expect("root");
assert_eq!(node.children.len(), 2);
assert_eq!(
node.kind,
SymbolKind::Operator(crate::dag::symbol::OpKind::Add)
);
let id2 = parse_expression("(x + y) * z", &mut builder).expect("ok");
let node2 = builder.arena().get(id2).expect("root");
assert_eq!(
node2.kind,
SymbolKind::Operator(crate::dag::symbol::OpKind::Mul)
);
}
#[test]
fn test_parse_exponentiation_right_associative() {
let mut builder = DagBuilder::new();
let id = parse_expression("x ^ y ^ z", &mut builder).expect("ok");
let node = builder.arena().get(id).expect("root");
assert_eq!(
node.kind,
SymbolKind::Operator(crate::dag::symbol::OpKind::Pow)
);
}
#[test]
fn paren_depth_overflow_is_a_clean_error() {
let n: usize = 400;
let mut input = String::with_capacity(n * 2 + 1);
for _ in 0..n {
input.push('(');
}
input.push('1');
for _ in 0..n {
input.push(')');
}
let mut b = DagBuilder::new();
let err = parse_expression(&input, &mut b).expect_err("must error");
assert!(err.message.contains("depth"));
}
#[test]
fn function_call_parses_correctly() {
let mut b = DagBuilder::new();
let id = parse_expression("sin(x)", &mut b).expect("ok");
let node = b.arena().get(id).expect("root");
assert!(matches!(
node.kind,
crate::dag::symbol::SymbolKind::Function(_)
));
assert_eq!(node.children.len(), 1);
}
#[test]
fn function_call_with_multiple_args() {
let mut b = DagBuilder::new();
let id = parse_expression("pow(x, y)", &mut b).expect("ok");
let node = b.arena().get(id).expect("root");
assert!(matches!(
node.kind,
crate::dag::symbol::SymbolKind::Function(_)
));
assert_eq!(node.children.len(), 2);
}
#[test]
fn function_call_nested_in_expression() {
let mut b = DagBuilder::new();
let id = parse_expression("sin(x) + cos(y)", &mut b).expect("ok");
let node = b.arena().get(id).expect("root");
assert!(matches!(
node.kind,
crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Add)
));
}
#[test]
fn unary_minus_has_lower_precedence_than_power() {
let mut b = DagBuilder::new();
let id = parse_expression("-x^y", &mut b).expect("ok");
let node = b.arena().get(id).expect("root");
assert_eq!(
node.kind,
crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Neg),
"-x^y must parse as -(x^y), root should be Neg"
);
let child_id = node.children.as_slice()[0];
let child = b.arena().get(child_id).expect("child");
assert_eq!(
child.kind,
crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Pow)
);
}
#[test]
fn span_carries_line_col_on_error() {
let src = "x +\n+ ";
let mut b = DagBuilder::new();
let err = parse_expression(src, &mut b).expect_err("trailing junk");
assert!(err.span.line >= 1);
assert!(err.span.col >= 1);
}
}