Skip to main content

rssn_advanced/parser/
expr.rs

1//! Precedence-climbing expression parser.
2//!
3//! Per `parser_review §2` / `§3`:
4//!
5//! * Recursion is depth-capped at [`MAX_PAREN_DEPTH`] so `(((...)))`
6//!   inputs cannot blow the OS stack.
7//! * Errors carry a `Span` with line/column information, computed
8//!   against the original source buffer (not the remaining suffix).
9
10use 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
20/// Maximum allowed depth of parenthesis / operator recursion.
21///
22/// Each parse level requires two stack frames (`parse_expr_climbing` +
23/// `parse_atom`). In debug builds these frames are large enough that
24/// 1024 levels can overflow the default 8 MB thread stack. 200 keeps
25/// the recursive depth ≈ 400 frames, which is safe on every target.
26pub const MAX_PAREN_DEPTH: u16 = 200;
27
28/// A runtime-extensible operator precedence table for the expression parser.
29///
30/// The built-in table handles `+`, `-`, `*`, `/`, `%`, and `^`. Additional
31/// infix operators with custom precedence levels can be registered at runtime
32/// without modifying the parser source.
33///
34/// Named operators (multi-character strings such as `"and"`, `"or"`, `"mod"`,
35/// `"xor"`) are supported alongside single-character operators. Named operators
36/// are matched as identifiers during infix parsing — after the left operand is
37/// parsed, the parser checks whether the next token matches any registered
38/// named operator before falling back to single-character matching.
39///
40/// Higher precedence numbers bind more tightly (e.g. `*` before `+`).
41/// Right-associative operators (currently only `^`) are marked separately.
42///
43/// The `parse_with_table` function uses a `PrecedenceTable` instead of the
44/// hardcoded `op_precedence` / `op_right_associative` functions.
45#[derive(Debug, Clone)]
46pub struct PrecedenceTable {
47    /// Maps operator string → (precedence, `is_right_associative`).
48    /// Single-character operators are stored as single-char strings.
49    entries: std::collections::HashMap<String, (u8, bool)>,
50    /// Maps prefix unary operator string → DAG `SymbolKind` for the node to build.
51    unary: std::collections::HashMap<String, SymbolKind>,
52}
53
54impl PrecedenceTable {
55    /// Creates the default table matching the built-in parser behaviour.
56    #[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    /// Creates an empty table (no operators registered).
72    #[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    /// Registers a new infix operator by name (single- or multi-character).
81    ///
82    /// - `op`: any string key; single chars such as `'+'` are converted to a
83    ///   one-character string. Multi-char keys like `"and"` or `"mod"` are
84    ///   matched as identifiers during infix parsing.
85    /// - `precedence`: binding strength; higher binds tighter.
86    /// - `right_associative`: `true` for right-to-left evaluation (like `^`).
87    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    /// Registers a single-character infix operator.
93    ///
94    /// Convenience alias for [`register_op`](Self::register_op) with a `char`
95    /// argument; preserves backward compatibility with the previous API.
96    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    /// Returns the precedence of a single-char operator, or `None` if not registered.
101    #[must_use]
102    pub fn precedence(&self, op: char) -> Option<u8> {
103        self.entries.get(&op.to_string()).map(|&(prec, _)| prec)
104    }
105
106    /// Returns the precedence of any operator (single- or multi-char).
107    #[must_use]
108    pub fn precedence_str(&self, op: &str) -> Option<u8> {
109        self.entries.get(op).map(|&(prec, _)| prec)
110    }
111
112    /// Returns `true` if a single-char operator is right-associative.
113    #[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    /// Returns `true` if any operator (single- or multi-char) is right-associative.
119    #[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    /// Returns all registered multi-character operator names (length > 1).
125    ///
126    /// Used by the infix parser to attempt named-operator matching before
127    /// falling back to single-character operators.
128    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    /// Registers a prefix unary operator (e.g. `"!"`, `"~"`, `"not"`).
136    ///
137    /// When the parser encounters this prefix in atom position, it consumes
138    /// it, recursively parses the operand, and wraps it in a DAG node whose
139    /// `SymbolKind` is `kind`. The prefix is matched literally from the
140    /// current position (after whitespace).
141    pub fn register_unary_op(&mut self, prefix: impl Into<String>, kind: SymbolKind) {
142        self.unary.insert(prefix.into(), kind);
143    }
144
145    /// Iterator over all registered prefix unary operators.
146    ///
147    /// Yields `(prefix, kind)` pairs. Use this to inspect what custom
148    /// unary operators are active without modifying the table.
149    pub fn unary_ops(&self) -> impl Iterator<Item = (&str, &SymbolKind)> {
150        self.unary.iter().map(|(k, v)| (k.as_str(), v))
151    }
152}
153
154/// Returns the precedence of an operator. Higher number means higher precedence.
155const fn op_precedence(op: char) -> Option<u8> {
156    match op {
157        '+' | '-' => Some(1),
158        '*' | '/' | '%' => Some(2),
159        '^' => Some(3),
160        _ => None,
161    }
162}
163
164/// Returns true if the operator is right-associative (e.g., `^`).
165const fn op_right_associative(op: char) -> bool {
166    op == '^'
167}
168
169/// Internal recursion-capped error sentinel. We return it via an
170/// `ErrorKind::TooLarge` so `nom`'s Err path knows to propagate it.
171#[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    // 1. Parenthesized expression — bounded recursion.
188    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    // 2. Unary minus.
198    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    // 3. Numeric constant.
205    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    // 4. Function call `identifier '(' args ')'` or plain variable.
211    if let Ok((rem, name)) = ws(parse_identifier)(input) {
212        // Peek for '(' to distinguish function call from variable.
213        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            // Handle zero-arg call: `f()`.
219            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, // "expected ',' or ')'"
235                        )));
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        // Plain variable.
244        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, // "unexpected character"
251    )))
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    // Guard against deep right-associative chains (e.g. `x^y^z^...`)
261    // which recurse here for each `^` — independent of paren depth.
262    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        // Consume the operator.
284        let (rem_after_op, _) = ws(parse_char(op_char))(rem)?;
285        rem = rem_after_op;
286
287        // For right-associative operators increment depth to cap chains
288        // like `x^y^z^...` which otherwise bypass the paren depth check.
289        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        // Combine lhs and rhs using the builder (every constructor
304        // hash-conses through DedupMap, so the precedence climber
305        // can't accidentally produce duplicates).
306        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
320// ---------------------------------------------------------------------------
321// Table-driven parsing (public API)
322// ---------------------------------------------------------------------------
323
324fn 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    // 1. Parenthesized expression — bounded recursion.
331    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    // 2. Unary minus.
341    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    // 2b. User-registered prefix unary operators (e.g. "!", "~", "not").
348    // Sort by prefix length descending so longer prefixes take priority.
349    {
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                // For single-char prefixes accept any position; for
356                // multi-char word prefixes ensure a word boundary.
357                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    // 3. Numeric constant.
378    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    // 4. Function call `identifier '(' args ')'` or plain variable.
384    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        // Try named operators (multi-char, matched as identifiers) first.
442        // We sort by length descending to prefer longer matches ("and" over "a").
443        let mut named_match: Option<(&str, u8, bool, &str)> = None; // (op_name, prec, ra, rem_after)
444        {
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                    // Ensure the match is a complete identifier token (not a prefix of a longer word).
450                    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            // Named operators beyond the built-ins become FunctionCall nodes with two children.
475            let fn_id = builder.intern_function(op_name);
476            lhs = builder.function_call(fn_id, &[lhs, rhs]);
477            continue;
478        }
479
480        // Single-character operator path.
481        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
519/// Parses `input` using a caller-supplied [`PrecedenceTable`].
520///
521/// This is the runtime-extensible counterpart to [`parse_expression`].  Where
522/// [`parse_expression`] always uses the six built-in operators (`+`, `-`, `*`,
523/// `/`, `%`, `^`), this function consults `table` for every infix token it
524/// encounters.  Custom operators registered via [`PrecedenceTable::register`]
525/// are fully supported.
526///
527/// # Errors
528///
529/// Returns a [`ParseError`] on syntax errors, unexpected trailing tokens, or
530/// paren-depth overflow — identical semantics to [`parse_expression`].
531pub 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
586/// Byte offset of `slice` within `whole`, or `None` if `slice` isn't
587/// a substring view of `whole` (different allocation).
588fn 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
598/// Parses a mathematical string expression into the global DAG.
599///
600/// # Errors
601///
602/// Returns a [`ParseError`] (with line/column [`Span`]) on syntax
603/// errors, unexpected trailing tokens, or paren-depth overflow.
604pub 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                    // `nom_char(c)` emits Char on mismatch. When input is
628                    // exhausted it means a required character (e.g. `)`) was
629                    // never found; when input has a character it is unexpected.
630                    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        // Use a depth well above MAX_PAREN_DEPTH (200) but small enough
697        // that even in debug builds we never actually recurse that deep
698        // (the error fires at depth 200, long before 400).
699        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        // Standard math: `-x^y` = `-(x^y)`, not `(-x)^y`.
751        // With x=2, y=3: -(2^3) = -8, not (-2)^3 = -8 (same here but different for even exponents).
752        // Use y=2 to distinguish: -(2^2) = -4, but (-2)^2 = +4.
753        let mut b = DagBuilder::new();
754        let id = parse_expression("-x^y", &mut b).expect("ok");
755        // Root should be Neg, not Pow.
756        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        // The child of Neg must be Pow.
763        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        // The error region begins past the first newline.
777        assert!(err.span.line >= 1);
778        assert!(err.span.col >= 1);
779    }
780}