1use nom::IResult;
11
12use super::error::{
13 ParseError, Span, cold_parse_error_unexpected_eof, cold_parse_error_unexpected_token,
14};
15use super::lexer::{parse_char, parse_constant, parse_identifier, ws};
16use crate::dag::builder::DagBuilder;
17use crate::dag::node::DagNodeId;
18use crate::dag::symbol::SymbolKind;
19
20pub const MAX_PAREN_DEPTH: u16 = 200;
27
28#[derive(Debug, Clone)]
46pub struct PrecedenceTable {
47 entries: std::collections::HashMap<String, (u8, bool)>,
50 unary: std::collections::HashMap<String, SymbolKind>,
52}
53
54impl PrecedenceTable {
55 #[must_use]
57 pub fn default_table() -> Self {
58 let mut t = Self {
59 entries: std::collections::HashMap::new(),
60 unary: std::collections::HashMap::new(),
61 };
62 t.entries.insert("+".into(), (1, false));
63 t.entries.insert("-".into(), (1, false));
64 t.entries.insert("*".into(), (2, false));
65 t.entries.insert("/".into(), (2, false));
66 t.entries.insert("%".into(), (2, false));
67 t.entries.insert("^".into(), (3, true));
68 t
69 }
70
71 #[must_use]
73 pub fn empty() -> Self {
74 Self {
75 entries: std::collections::HashMap::new(),
76 unary: std::collections::HashMap::new(),
77 }
78 }
79
80 pub fn register_op(&mut self, op: impl Into<String>, precedence: u8, right_associative: bool) {
88 self.entries
89 .insert(op.into(), (precedence, right_associative));
90 }
91
92 pub fn register(&mut self, op: char, precedence: u8, right_associative: bool) {
97 self.register_op(op.to_string(), precedence, right_associative);
98 }
99
100 #[must_use]
102 pub fn precedence(&self, op: char) -> Option<u8> {
103 self.entries.get(&op.to_string()).map(|&(prec, _)| prec)
104 }
105
106 #[must_use]
108 pub fn precedence_str(&self, op: &str) -> Option<u8> {
109 self.entries.get(op).map(|&(prec, _)| prec)
110 }
111
112 #[must_use]
114 pub fn is_right_associative(&self, op: char) -> bool {
115 self.entries.get(&op.to_string()).is_some_and(|&(_, ra)| ra)
116 }
117
118 #[must_use]
120 pub fn is_right_associative_str(&self, op: &str) -> bool {
121 self.entries.get(op).is_some_and(|&(_, ra)| ra)
122 }
123
124 pub fn named_ops(&self) -> impl Iterator<Item = &str> {
129 self.entries
130 .keys()
131 .filter(|k| k.len() > 1)
132 .map(String::as_str)
133 }
134
135 pub fn register_unary_op(&mut self, prefix: impl Into<String>, kind: SymbolKind) {
142 self.unary.insert(prefix.into(), kind);
143 }
144
145 pub fn unary_ops(&self) -> impl Iterator<Item = (&str, &SymbolKind)> {
150 self.unary.iter().map(|(k, v)| (k.as_str(), v))
151 }
152}
153
154const fn op_precedence(op: char) -> Option<u8> {
156 match op {
157 '+' | '-' => Some(1),
158 '*' | '/' | '%' => Some(2),
159 '^' => Some(3),
160 _ => None,
161 }
162}
163
164const fn op_right_associative(op: char) -> bool {
166 op == '^'
167}
168
169#[doc(hidden)]
172#[cold]
173#[track_caller]
174#[inline(never)]
175fn too_deep(input: &str) -> nom::Err<nom::error::Error<&str>> {
176 nom::Err::Failure(nom::error::Error::new(
177 input,
178 nom::error::ErrorKind::TooLarge,
179 ))
180}
181
182fn parse_atom<'a>(
183 input: &'a str,
184 builder: &mut DagBuilder,
185 depth: u16,
186) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
187 if let Ok((rem, _)) = ws(parse_char('('))(input) {
189 if depth >= MAX_PAREN_DEPTH {
190 return Err(too_deep(input));
191 }
192 let (rem, expr) = parse_expr_climbing(rem, builder, 0, depth + 1)?;
193 let (rem, _) = ws(parse_char(')'))(rem)?;
194 return Ok((rem, expr));
195 }
196
197 if let Ok((rem, _)) = ws(parse_char('-'))(input) {
199 let (rem, atom) = parse_expr_climbing(rem, builder, 3, depth)?;
200 let neg = builder.neg(atom);
201 return Ok((rem, neg));
202 }
203
204 if let Ok((rem, val)) = ws(parse_constant)(input) {
206 let node_id = builder.constant(val);
207 return Ok((rem, node_id));
208 }
209
210 if let Ok((rem, name)) = ws(parse_identifier)(input) {
212 if let Ok((mut cur, _)) = ws(parse_char('('))(rem) {
214 if depth >= MAX_PAREN_DEPTH {
215 return Err(too_deep(input));
216 }
217 let mut args: Vec<DagNodeId> = Vec::new();
218 if let Ok((after_close, _)) = ws(parse_char(')'))(cur) {
220 cur = after_close;
221 } else {
222 loop {
223 let (r, arg) = parse_expr_climbing(cur, builder, 0, depth + 1)?;
224 args.push(arg);
225 cur = r;
226 if let Ok((r2, _)) = ws(parse_char(','))(cur) {
227 cur = r2;
228 } else if let Ok((r2, _)) = ws(parse_char(')'))(cur) {
229 cur = r2;
230 break;
231 } else {
232 return Err(nom::Err::Error(nom::error::Error::new(
233 cur,
234 nom::error::ErrorKind::Tag, )));
236 }
237 }
238 }
239 let fn_id = builder.intern_function(name);
240 let node_id = builder.function_call(fn_id, &args);
241 return Ok((cur, node_id));
242 }
243 let node_id = builder.variable(name);
245 return Ok((rem, node_id));
246 }
247
248 Err(nom::Err::Error(nom::error::Error::new(
249 input,
250 nom::error::ErrorKind::Char, )))
252}
253
254fn parse_expr_climbing<'a>(
255 input: &'a str,
256 builder: &mut DagBuilder,
257 min_prec: u8,
258 depth: u16,
259) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
260 if depth >= MAX_PAREN_DEPTH {
263 return Err(too_deep(input));
264 }
265
266 let (mut rem, mut lhs) = parse_atom(input, builder, depth)?;
267
268 loop {
269 let next_input = rem;
270 let mut chars = next_input.trim_start().chars();
271 let Some(op_char) = chars.next() else {
272 break;
273 };
274
275 let Some(op_prec) = op_precedence(op_char) else {
276 break;
277 };
278
279 if op_prec < min_prec {
280 break;
281 }
282
283 let (rem_after_op, _) = ws(parse_char(op_char))(rem)?;
285 rem = rem_after_op;
286
287 let next_min_prec = if op_right_associative(op_char) {
290 op_prec
291 } else {
292 op_prec + 1
293 };
294 let next_depth = if op_right_associative(op_char) {
295 depth.saturating_add(1)
296 } else {
297 depth
298 };
299
300 let (rem_after_rhs, rhs) = parse_expr_climbing(rem, builder, next_min_prec, next_depth)?;
301 rem = rem_after_rhs;
302
303 lhs = match op_char {
307 '+' => builder.add(lhs, rhs),
308 '-' => builder.sub(lhs, rhs),
309 '*' => builder.mul(lhs, rhs),
310 '/' => builder.div(lhs, rhs),
311 '%' => builder.modulo(lhs, rhs),
312 '^' => builder.pow(lhs, rhs),
313 _ => return Err(too_deep(input)),
314 };
315 }
316
317 Ok((rem, lhs))
318}
319
320fn parse_atom_with_table<'a>(
325 input: &'a str,
326 builder: &mut DagBuilder,
327 depth: u16,
328 table: &PrecedenceTable,
329) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
330 if let Ok((rem, _)) = ws(parse_char('('))(input) {
332 if depth >= MAX_PAREN_DEPTH {
333 return Err(too_deep(input));
334 }
335 let (rem, expr) = parse_expr_climbing_with_table(rem, builder, 0, depth + 1, table)?;
336 let (rem, _) = ws(parse_char(')'))(rem)?;
337 return Ok((rem, expr));
338 }
339
340 if let Ok((rem, _)) = ws(parse_char('-'))(input) {
342 let (rem, atom) = parse_expr_climbing_with_table(rem, builder, 3, depth, table)?;
343 let neg = builder.neg(atom);
344 return Ok((rem, neg));
345 }
346
347 {
350 let trimmed = input.trim_start();
351 let mut candidates: Vec<(&str, &SymbolKind)> = table.unary_ops().collect();
352 candidates.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
353 for (prefix, kind) in candidates {
354 if let Some(after) = trimmed.strip_prefix(prefix) {
355 let ok = prefix
358 .chars()
359 .next()
360 .is_some_and(|c| !c.is_alphanumeric() && c != '_')
361 || after.is_empty()
362 || !after
363 .chars()
364 .next()
365 .is_some_and(|c| c.is_alphanumeric() || c == '_');
366 if ok {
367 let (rem, atom) =
368 parse_expr_climbing_with_table(after, builder, 3, depth, table)?;
369 use crate::dag::metadata::NodeFlags;
370 let node_id = builder.operator(*kind, &[atom], NodeFlags::EMPTY);
371 return Ok((rem, node_id));
372 }
373 }
374 }
375 }
376
377 if let Ok((rem, val)) = ws(parse_constant)(input) {
379 let node_id = builder.constant(val);
380 return Ok((rem, node_id));
381 }
382
383 if let Ok((rem, name)) = ws(parse_identifier)(input) {
385 if let Ok((mut cur, _)) = ws(parse_char('('))(rem) {
386 if depth >= MAX_PAREN_DEPTH {
387 return Err(too_deep(input));
388 }
389 let mut args: Vec<DagNodeId> = Vec::new();
390 if let Ok((after_close, _)) = ws(parse_char(')'))(cur) {
391 cur = after_close;
392 } else {
393 loop {
394 let (r, arg) =
395 parse_expr_climbing_with_table(cur, builder, 0, depth + 1, table)?;
396 args.push(arg);
397 cur = r;
398 if let Ok((r2, _)) = ws(parse_char(','))(cur) {
399 cur = r2;
400 } else if let Ok((r2, _)) = ws(parse_char(')'))(cur) {
401 cur = r2;
402 break;
403 } else {
404 return Err(nom::Err::Error(nom::error::Error::new(
405 cur,
406 nom::error::ErrorKind::Tag,
407 )));
408 }
409 }
410 }
411 let fn_id = builder.intern_function(name);
412 let node_id = builder.function_call(fn_id, &args);
413 return Ok((cur, node_id));
414 }
415 let node_id = builder.variable(name);
416 return Ok((rem, node_id));
417 }
418
419 Err(nom::Err::Error(nom::error::Error::new(
420 input,
421 nom::error::ErrorKind::Char,
422 )))
423}
424
425fn parse_expr_climbing_with_table<'a>(
426 input: &'a str,
427 builder: &mut DagBuilder,
428 min_prec: u8,
429 depth: u16,
430 table: &PrecedenceTable,
431) -> IResult<&'a str, DagNodeId, nom::error::Error<&'a str>> {
432 if depth >= MAX_PAREN_DEPTH {
433 return Err(too_deep(input));
434 }
435
436 let (mut rem, mut lhs) = parse_atom_with_table(input, builder, depth, table)?;
437
438 loop {
439 let trimmed = rem.trim_start();
440
441 let mut named_match: Option<(&str, u8, bool, &str)> = None; {
445 let mut candidates: Vec<&str> = table.named_ops().collect();
446 candidates.sort_by_key(|b| std::cmp::Reverse(b.len()));
447 for named_op in candidates {
448 if let Some(after) = trimmed.strip_prefix(named_op) {
449 let is_word_boundary = after.is_empty()
451 || !after
452 .chars()
453 .next()
454 .is_some_and(|c| c.is_alphanumeric() || c == '_');
455 if is_word_boundary && let Some(prec) = table.precedence_str(named_op) {
456 let ra = table.is_right_associative_str(named_op);
457 named_match = Some((named_op, prec, ra, after.trim_start()));
458 break;
459 }
460 }
461 }
462 }
463
464 if let Some((op_name, op_prec, ra, rem_after_op)) = named_match {
465 if op_prec < min_prec {
466 break;
467 }
468 rem = rem_after_op;
469 let next_min_prec = if ra { op_prec } else { op_prec + 1 };
470 let next_depth = if ra { depth.saturating_add(1) } else { depth };
471 let (rem_after_rhs, rhs) =
472 parse_expr_climbing_with_table(rem, builder, next_min_prec, next_depth, table)?;
473 rem = rem_after_rhs;
474 let fn_id = builder.intern_function(op_name);
476 lhs = builder.function_call(fn_id, &[lhs, rhs]);
477 continue;
478 }
479
480 let mut chars = trimmed.chars();
482 let Some(op_char) = chars.next() else {
483 break;
484 };
485
486 let Some(op_prec) = table.precedence(op_char) else {
487 break;
488 };
489
490 if op_prec < min_prec {
491 break;
492 }
493
494 let (rem_after_op, _) = ws(parse_char(op_char))(rem)?;
495 rem = rem_after_op;
496
497 let ra = table.is_right_associative(op_char);
498 let next_min_prec = if ra { op_prec } else { op_prec + 1 };
499 let next_depth = if ra { depth.saturating_add(1) } else { depth };
500
501 let (rem_after_rhs, rhs) =
502 parse_expr_climbing_with_table(rem, builder, next_min_prec, next_depth, table)?;
503 rem = rem_after_rhs;
504
505 lhs = match op_char {
506 '+' => builder.add(lhs, rhs),
507 '-' => builder.sub(lhs, rhs),
508 '*' => builder.mul(lhs, rhs),
509 '/' => builder.div(lhs, rhs),
510 '%' => builder.modulo(lhs, rhs),
511 '^' => builder.pow(lhs, rhs),
512 _ => return Err(too_deep(input)),
513 };
514 }
515
516 Ok((rem, lhs))
517}
518
519pub fn parse_with_table(
532 input: &str,
533 builder: &mut DagBuilder,
534 table: &PrecedenceTable,
535) -> Result<DagNodeId, super::error::ParseError> {
536 match parse_expr_climbing_with_table(input, builder, 0, 0, table) {
537 Ok((remaining, id)) => {
538 let trimmed = remaining.trim_start();
539 if !trimmed.is_empty() {
540 let offset = offset_in(input, trimmed).unwrap_or(input.len());
541 return Err(super::error::ParseError {
542 message: "Unexpected trailing tokens".to_owned(),
543 span: super::error::Span::from_offset(input, offset, trimmed.len()),
544 });
545 }
546 Ok(id)
547 }
548 Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
549 let offset = offset_in(input, e.input).unwrap_or(input.len());
550 let len = e.input.len().min(input.len().saturating_sub(offset));
551 let span = super::error::Span::from_offset(input, offset, len);
552 match e.code {
553 nom::error::ErrorKind::TooLarge => Err(super::error::ParseError {
554 message: format!("Parenthesis depth exceeded {MAX_PAREN_DEPTH}"),
555 span,
556 }),
557 nom::error::ErrorKind::Char => {
558 if e.input.trim_start().is_empty() {
559 Err(cold_parse_error_unexpected_eof(span))
560 } else {
561 let bad = e.input.trim_start().chars().next().unwrap_or('?');
562 Err(cold_parse_error_unexpected_token(span, bad))
563 }
564 }
565 nom::error::ErrorKind::Tag => Err(super::error::ParseError {
566 message: "Expected ',' or ')' to close function argument list".to_owned(),
567 span,
568 }),
569 nom::error::ErrorKind::Eof => Err(super::error::ParseError {
570 message: "Unexpected end of input; expression is incomplete".to_owned(),
571 span,
572 }),
573 _ => Err(super::error::ParseError {
574 message: format!("Syntax error near {:?}", &e.input[..e.input.len().min(8)]),
575 span,
576 }),
577 }
578 }
579 Err(nom::Err::Incomplete(_)) => Err(super::error::ParseError {
580 message: "Incomplete input".to_owned(),
581 span: super::error::Span::from_offset(input, input.len(), 0),
582 }),
583 }
584}
585
586fn offset_in(whole: &str, slice: &str) -> Option<usize> {
589 let whole_ptr = whole.as_ptr() as usize;
590 let slice_ptr = slice.as_ptr() as usize;
591 let whole_end = whole_ptr.checked_add(whole.len())?;
592 if slice_ptr < whole_ptr || slice_ptr > whole_end {
593 return None;
594 }
595 Some(slice_ptr - whole_ptr)
596}
597
598pub fn parse_expression(input: &str, builder: &mut DagBuilder) -> Result<DagNodeId, ParseError> {
605 match parse_expr_climbing(input, builder, 0, 0) {
606 Ok((remaining, id)) => {
607 let trimmed = remaining.trim_start();
608 if !trimmed.is_empty() {
609 let offset = offset_in(input, trimmed).unwrap_or(input.len());
610 return Err(ParseError {
611 message: "Unexpected trailing tokens".to_owned(),
612 span: Span::from_offset(input, offset, trimmed.len()),
613 });
614 }
615 Ok(id)
616 }
617 Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
618 let offset = offset_in(input, e.input).unwrap_or(input.len());
619 let len = e.input.len().min(input.len().saturating_sub(offset));
620 let span = Span::from_offset(input, offset, len);
621 match e.code {
622 nom::error::ErrorKind::TooLarge => Err(ParseError {
623 message: format!("Parenthesis depth exceeded {MAX_PAREN_DEPTH}"),
624 span,
625 }),
626 nom::error::ErrorKind::Char => {
627 if e.input.trim_start().is_empty() {
631 Err(cold_parse_error_unexpected_eof(span))
632 } else {
633 let bad = e.input.trim_start().chars().next().unwrap_or('?');
634 Err(cold_parse_error_unexpected_token(span, bad))
635 }
636 }
637 nom::error::ErrorKind::Tag => Err(ParseError {
638 message: "Expected ',' or ')' to close function argument list".to_owned(),
639 span,
640 }),
641 nom::error::ErrorKind::Eof => Err(ParseError {
642 message: "Unexpected end of input; expression is incomplete".to_owned(),
643 span,
644 }),
645 _ => Err(ParseError {
646 message: format!("Syntax error near {:?}", &e.input[..e.input.len().min(8)]),
647 span,
648 }),
649 }
650 }
651 Err(nom::Err::Incomplete(_)) => Err(ParseError {
652 message: "Incomplete input".to_owned(),
653 span: Span::from_offset(input, input.len(), 0),
654 }),
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::dag::symbol::SymbolKind;
662
663 #[test]
664 fn test_parse_expression_precedence() {
665 let mut builder = DagBuilder::new();
666
667 let id = parse_expression("x + y * z", &mut builder).expect("ok");
668 let node = builder.arena().get(id).expect("root");
669 assert_eq!(node.children.len(), 2);
670 assert_eq!(
671 node.kind,
672 SymbolKind::Operator(crate::dag::symbol::OpKind::Add)
673 );
674
675 let id2 = parse_expression("(x + y) * z", &mut builder).expect("ok");
676 let node2 = builder.arena().get(id2).expect("root");
677 assert_eq!(
678 node2.kind,
679 SymbolKind::Operator(crate::dag::symbol::OpKind::Mul)
680 );
681 }
682
683 #[test]
684 fn test_parse_exponentiation_right_associative() {
685 let mut builder = DagBuilder::new();
686 let id = parse_expression("x ^ y ^ z", &mut builder).expect("ok");
687 let node = builder.arena().get(id).expect("root");
688 assert_eq!(
689 node.kind,
690 SymbolKind::Operator(crate::dag::symbol::OpKind::Pow)
691 );
692 }
693
694 #[test]
695 fn paren_depth_overflow_is_a_clean_error() {
696 let n: usize = 400;
700 let mut input = String::with_capacity(n * 2 + 1);
701 for _ in 0..n {
702 input.push('(');
703 }
704 input.push('1');
705 for _ in 0..n {
706 input.push(')');
707 }
708 let mut b = DagBuilder::new();
709 let err = parse_expression(&input, &mut b).expect_err("must error");
710 assert!(err.message.contains("depth"));
711 }
712
713 #[test]
714 fn function_call_parses_correctly() {
715 let mut b = DagBuilder::new();
716 let id = parse_expression("sin(x)", &mut b).expect("ok");
717 let node = b.arena().get(id).expect("root");
718 assert!(matches!(
719 node.kind,
720 crate::dag::symbol::SymbolKind::Function(_)
721 ));
722 assert_eq!(node.children.len(), 1);
723 }
724
725 #[test]
726 fn function_call_with_multiple_args() {
727 let mut b = DagBuilder::new();
728 let id = parse_expression("pow(x, y)", &mut b).expect("ok");
729 let node = b.arena().get(id).expect("root");
730 assert!(matches!(
731 node.kind,
732 crate::dag::symbol::SymbolKind::Function(_)
733 ));
734 assert_eq!(node.children.len(), 2);
735 }
736
737 #[test]
738 fn function_call_nested_in_expression() {
739 let mut b = DagBuilder::new();
740 let id = parse_expression("sin(x) + cos(y)", &mut b).expect("ok");
741 let node = b.arena().get(id).expect("root");
742 assert!(matches!(
743 node.kind,
744 crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Add)
745 ));
746 }
747
748 #[test]
749 fn unary_minus_has_lower_precedence_than_power() {
750 let mut b = DagBuilder::new();
754 let id = parse_expression("-x^y", &mut b).expect("ok");
755 let node = b.arena().get(id).expect("root");
757 assert_eq!(
758 node.kind,
759 crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Neg),
760 "-x^y must parse as -(x^y), root should be Neg"
761 );
762 let child_id = node.children.as_slice()[0];
764 let child = b.arena().get(child_id).expect("child");
765 assert_eq!(
766 child.kind,
767 crate::dag::symbol::SymbolKind::Operator(crate::dag::symbol::OpKind::Pow)
768 );
769 }
770
771 #[test]
772 fn span_carries_line_col_on_error() {
773 let src = "x +\n+ ";
774 let mut b = DagBuilder::new();
775 let err = parse_expression(src, &mut b).expect_err("trailing junk");
776 assert!(err.span.line >= 1);
778 assert!(err.span.col >= 1);
779 }
780}