Skip to main content

oxigdal_algorithms/dsl/
parser.rs

1//! Parser for Raster Algebra DSL
2//!
3//! This module uses Pest to parse DSL text into an AST.
4
5// Parser derive macro generates items that don't have documentation
6#![allow(missing_docs)]
7
8use super::ast::{BinaryOp, Expr, Program, Statement, Type, UnaryOp};
9use crate::error::{AlgorithmError, Result};
10use crate::expr_depth::{check_source_nesting_depth, guard_depth};
11use pest::Parser;
12use pest_derive::Parser;
13
14#[cfg(not(feature = "std"))]
15use alloc::{boxed::Box, string::String, vec::Vec};
16
17#[derive(Parser)]
18#[grammar = "dsl/grammar.pest"]
19struct RasterParser;
20
21/// Parses a DSL program from text
22///
23/// # Errors
24///
25/// Returns [`AlgorithmError::NestingTooDeep`] when `input` nests more deeply
26/// than [`crate::MAX_EXPRESSION_DEPTH`]. The check runs before the generated
27/// parser is invoked, because Pest builds its own recursive descent that no
28/// depth counter can be threaded through; without it, deeply nested input
29/// overflows the thread stack and aborts the process.
30pub fn parse_program(input: &str) -> Result<Program> {
31    check_source_nesting_depth(input, "dsl")?;
32
33    let pairs = RasterParser::parse(Rule::program, input).map_err(|e| {
34        AlgorithmError::InvalidParameter {
35            parameter: "dsl",
36            message: format!("Parse error: {e}"),
37        }
38    })?;
39
40    let mut statements = Vec::new();
41
42    for pair in pairs {
43        match pair.as_rule() {
44            Rule::program => {
45                for inner in pair.into_inner() {
46                    if inner.as_rule() == Rule::statement {
47                        statements.push(parse_statement(inner, 0)?);
48                    }
49                }
50            }
51            Rule::EOI => {}
52            _ => {
53                return Err(AlgorithmError::InvalidParameter {
54                    parameter: "dsl",
55                    message: format!("Unexpected rule: {:?}", pair.as_rule()),
56                });
57            }
58        }
59    }
60
61    Ok(Program { statements })
62}
63
64/// Parses a single expression from text
65///
66/// # Errors
67///
68/// Returns [`AlgorithmError::NestingTooDeep`] when `input` nests more deeply
69/// than [`crate::MAX_EXPRESSION_DEPTH`]. See [`parse_program`] for why the
70/// check has to happen before the generated parser runs.
71pub fn parse_expression(input: &str) -> Result<Expr> {
72    check_source_nesting_depth(input, "dsl")?;
73
74    let mut full_input = String::from(input);
75    if !full_input.ends_with(';') {
76        full_input.push(';');
77    }
78
79    let pairs = RasterParser::parse(Rule::program, &full_input).map_err(|e| {
80        AlgorithmError::InvalidParameter {
81            parameter: "dsl",
82            message: format!("Parse error: {e}"),
83        }
84    })?;
85
86    for pair in pairs {
87        if pair.as_rule() == Rule::program {
88            for inner in pair.into_inner() {
89                if inner.as_rule() == Rule::statement {
90                    // Get the inner rule from statement (expr_stmt, variable_decl, etc.)
91                    let stmt_inner = inner.into_inner().next().ok_or_else(|| {
92                        AlgorithmError::InvalidParameter {
93                            parameter: "dsl",
94                            message: "Empty statement".to_string(),
95                        }
96                    })?;
97
98                    // If it's an expr_stmt, extract the expression
99                    if stmt_inner.as_rule() == Rule::expr_stmt {
100                        return parse_expr_stmt(stmt_inner, 0);
101                    }
102                }
103            }
104        }
105    }
106
107    Err(AlgorithmError::InvalidParameter {
108        parameter: "dsl",
109        message: "No expression found".to_string(),
110    })
111}
112
113fn parse_statement(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Statement> {
114    guard_depth(depth, "dsl")?;
115
116    let inner = pair
117        .into_inner()
118        .next()
119        .ok_or_else(|| AlgorithmError::InvalidParameter {
120            parameter: "dsl",
121            message: "Empty statement".to_string(),
122        })?;
123
124    match inner.as_rule() {
125        Rule::variable_decl => {
126            let mut parts = inner.into_inner();
127            let name = parts
128                .next()
129                .ok_or_else(|| AlgorithmError::InvalidParameter {
130                    parameter: "dsl",
131                    message: "Missing variable name".to_string(),
132                })?
133                .as_str()
134                .to_string();
135
136            let value = parts
137                .next()
138                .ok_or_else(|| AlgorithmError::InvalidParameter {
139                    parameter: "dsl",
140                    message: "Missing variable value".to_string(),
141                })?;
142
143            Ok(Statement::VariableDecl {
144                name,
145                value: Box::new(parse_expr(value, depth)?),
146            })
147        }
148        Rule::function_decl => {
149            let mut parts = inner.into_inner();
150            let name = parts
151                .next()
152                .ok_or_else(|| AlgorithmError::InvalidParameter {
153                    parameter: "dsl",
154                    message: "Missing function name".to_string(),
155                })?
156                .as_str()
157                .to_string();
158
159            let mut params = Vec::new();
160            let mut body_pair = None;
161
162            for part in parts {
163                match part.as_rule() {
164                    Rule::param_list => {
165                        for param in part.into_inner() {
166                            params.push(param.as_str().to_string());
167                        }
168                    }
169                    Rule::expression => {
170                        body_pair = Some(part);
171                    }
172                    _ => {}
173                }
174            }
175
176            let body = body_pair.ok_or_else(|| AlgorithmError::InvalidParameter {
177                parameter: "dsl",
178                message: "Missing function body".to_string(),
179            })?;
180
181            Ok(Statement::FunctionDecl {
182                name,
183                params,
184                body: Box::new(parse_expr(body, depth)?),
185            })
186        }
187        Rule::return_stmt => {
188            let expr =
189                inner
190                    .into_inner()
191                    .next()
192                    .ok_or_else(|| AlgorithmError::InvalidParameter {
193                        parameter: "dsl",
194                        message: "Missing return expression".to_string(),
195                    })?;
196
197            Ok(Statement::Return(Box::new(parse_expr(expr, depth)?)))
198        }
199        Rule::expr_stmt => parse_expr_stmt(inner, depth).map(|e| Statement::Expr(Box::new(e))),
200        _ => Err(AlgorithmError::InvalidParameter {
201            parameter: "dsl",
202            message: format!("Unexpected statement: {:?}", inner.as_rule()),
203        }),
204    }
205}
206
207fn parse_expr_stmt(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
208    guard_depth(depth, "dsl")?;
209
210    let expr = pair
211        .into_inner()
212        .next()
213        .ok_or_else(|| AlgorithmError::InvalidParameter {
214            parameter: "dsl",
215            message: "Empty expression statement".to_string(),
216        })?;
217
218    parse_expr(expr, depth)
219}
220
221/// Builds an [`Expr`] from a Pest pair.
222///
223/// `depth` counts *source* nesting levels, matching what
224/// [`crate::expr_depth::source_nesting_depth`] estimates: it is incremented
225/// only where the grammar re-enters `expression` (bracketing, call arguments,
226/// conditional branches, blocks, and unary sign chains), not for each step down
227/// the precedence ladder, which stays at a fixed source level.
228fn parse_expr(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
229    guard_depth(depth, "dsl")?;
230
231    match pair.as_rule() {
232        Rule::expression => parse_expr(
233            pair.into_inner()
234                .next()
235                .ok_or_else(|| AlgorithmError::InvalidParameter {
236                    parameter: "dsl",
237                    message: "Empty expression".to_string(),
238                })?,
239            depth,
240        ),
241        Rule::logical_or => parse_binary_op(pair, BinaryOp::Or, depth),
242        Rule::logical_and => parse_binary_op(pair, BinaryOp::And, depth),
243        Rule::logical_not => {
244            let mut inner = pair.into_inner();
245            let mut not_count = 0;
246
247            // Count NOT operators
248            while let Some(next) = inner.peek() {
249                if matches!(next.as_rule(), Rule::not_op) {
250                    not_count += 1;
251                    inner.next();
252                } else {
253                    break;
254                }
255            }
256
257            let mut expr = parse_expr(
258                inner
259                    .next()
260                    .ok_or_else(|| AlgorithmError::InvalidParameter {
261                        parameter: "dsl",
262                        message: "Missing expression after not".to_string(),
263                    })?,
264                depth,
265            )?;
266
267            // Apply NOT operators
268            for _ in 0..not_count {
269                expr = Expr::Unary {
270                    op: UnaryOp::Not,
271                    expr: Box::new(expr),
272                    ty: Type::Unknown,
273                };
274            }
275
276            Ok(expr)
277        }
278        Rule::comparison => parse_comparison(pair, depth),
279        Rule::additive => parse_additive(pair, depth),
280        Rule::multiplicative => parse_multiplicative(pair, depth),
281        Rule::power => parse_power(pair, depth),
282        Rule::unary => parse_unary(pair, depth),
283        Rule::primary => parse_primary(pair, depth),
284        _ => Err(AlgorithmError::InvalidParameter {
285            parameter: "dsl",
286            message: format!("Unexpected expression rule: {:?}", pair.as_rule()),
287        }),
288    }
289}
290
291fn parse_binary_op(
292    pair: pest::iterators::Pair<Rule>,
293    default_op: BinaryOp,
294    depth: usize,
295) -> Result<Expr> {
296    guard_depth(depth, "dsl")?;
297
298    let mut inner = pair.into_inner();
299    let mut left = parse_expr(
300        inner
301            .next()
302            .ok_or_else(|| AlgorithmError::InvalidParameter {
303                parameter: "dsl",
304                message: "Missing left operand".to_string(),
305            })?,
306        depth,
307    )?;
308
309    while let Some(next) = inner.next() {
310        let op = match next.as_rule() {
311            Rule::or_op => BinaryOp::Or,
312            Rule::and_op => BinaryOp::And,
313            _ => {
314                let right = parse_expr(next, depth)?;
315                left = Expr::Binary {
316                    left: Box::new(left),
317                    op: default_op,
318                    right: Box::new(right),
319                    ty: Type::Unknown,
320                };
321                continue;
322            }
323        };
324
325        let right = parse_expr(
326            inner
327                .next()
328                .ok_or_else(|| AlgorithmError::InvalidParameter {
329                    parameter: "dsl",
330                    message: "Missing right operand".to_string(),
331                })?,
332            depth,
333        )?;
334
335        left = Expr::Binary {
336            left: Box::new(left),
337            op,
338            right: Box::new(right),
339            ty: Type::Unknown,
340        };
341    }
342
343    Ok(left)
344}
345
346fn parse_comparison(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
347    guard_depth(depth, "dsl")?;
348
349    let mut inner = pair.into_inner();
350    let left = parse_expr(
351        inner
352            .next()
353            .ok_or_else(|| AlgorithmError::InvalidParameter {
354                parameter: "dsl",
355                message: "Missing left operand".to_string(),
356            })?,
357        depth,
358    )?;
359
360    if let Some(op_pair) = inner.next() {
361        let op = match op_pair.as_rule() {
362            Rule::eq_op => BinaryOp::Equal,
363            Rule::ne_op => BinaryOp::NotEqual,
364            Rule::lt_op => BinaryOp::Less,
365            Rule::le_op => BinaryOp::LessEqual,
366            Rule::gt_op => BinaryOp::Greater,
367            Rule::ge_op => BinaryOp::GreaterEqual,
368            _ => {
369                return Err(AlgorithmError::InvalidParameter {
370                    parameter: "dsl",
371                    message: format!("Unknown comparison operator: {:?}", op_pair.as_rule()),
372                });
373            }
374        };
375
376        let right = parse_expr(
377            inner
378                .next()
379                .ok_or_else(|| AlgorithmError::InvalidParameter {
380                    parameter: "dsl",
381                    message: "Missing right operand".to_string(),
382                })?,
383            depth,
384        )?;
385
386        Ok(Expr::Binary {
387            left: Box::new(left),
388            op,
389            right: Box::new(right),
390            ty: Type::Unknown,
391        })
392    } else {
393        Ok(left)
394    }
395}
396
397fn parse_additive(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
398    guard_depth(depth, "dsl")?;
399
400    let mut inner = pair.into_inner();
401    let mut left = parse_expr(
402        inner
403            .next()
404            .ok_or_else(|| AlgorithmError::InvalidParameter {
405                parameter: "dsl",
406                message: "Missing left operand".to_string(),
407            })?,
408        depth,
409    )?;
410
411    while let Some(op_pair) = inner.next() {
412        let op = match op_pair.as_rule() {
413            Rule::add_op => BinaryOp::Add,
414            Rule::sub_op => BinaryOp::Subtract,
415            _ => {
416                let right = parse_expr(op_pair, depth)?;
417                left = Expr::Binary {
418                    left: Box::new(left),
419                    op: BinaryOp::Add,
420                    right: Box::new(right),
421                    ty: Type::Unknown,
422                };
423                continue;
424            }
425        };
426
427        let right = parse_expr(
428            inner
429                .next()
430                .ok_or_else(|| AlgorithmError::InvalidParameter {
431                    parameter: "dsl",
432                    message: "Missing right operand".to_string(),
433                })?,
434            depth,
435        )?;
436
437        left = Expr::Binary {
438            left: Box::new(left),
439            op,
440            right: Box::new(right),
441            ty: Type::Unknown,
442        };
443    }
444
445    Ok(left)
446}
447
448fn parse_multiplicative(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
449    guard_depth(depth, "dsl")?;
450
451    let mut inner = pair.into_inner();
452    let mut left = parse_expr(
453        inner
454            .next()
455            .ok_or_else(|| AlgorithmError::InvalidParameter {
456                parameter: "dsl",
457                message: "Missing left operand".to_string(),
458            })?,
459        depth,
460    )?;
461
462    while let Some(op_pair) = inner.next() {
463        let op = match op_pair.as_rule() {
464            Rule::mul_op => BinaryOp::Multiply,
465            Rule::div_op => BinaryOp::Divide,
466            Rule::mod_op => BinaryOp::Modulo,
467            _ => {
468                let right = parse_expr(op_pair, depth)?;
469                left = Expr::Binary {
470                    left: Box::new(left),
471                    op: BinaryOp::Multiply,
472                    right: Box::new(right),
473                    ty: Type::Unknown,
474                };
475                continue;
476            }
477        };
478
479        let right = parse_expr(
480            inner
481                .next()
482                .ok_or_else(|| AlgorithmError::InvalidParameter {
483                    parameter: "dsl",
484                    message: "Missing right operand".to_string(),
485                })?,
486            depth,
487        )?;
488
489        left = Expr::Binary {
490            left: Box::new(left),
491            op,
492            right: Box::new(right),
493            ty: Type::Unknown,
494        };
495    }
496
497    Ok(left)
498}
499
500fn parse_power(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
501    guard_depth(depth, "dsl")?;
502
503    let mut inner = pair.into_inner();
504    let mut left = parse_expr(
505        inner
506            .next()
507            .ok_or_else(|| AlgorithmError::InvalidParameter {
508                parameter: "dsl",
509                message: "Missing left operand".to_string(),
510            })?,
511        depth,
512    )?;
513
514    while let Some(op_pair) = inner.next() {
515        if matches!(op_pair.as_rule(), Rule::pow_op) {
516            let right = parse_expr(
517                inner
518                    .next()
519                    .ok_or_else(|| AlgorithmError::InvalidParameter {
520                        parameter: "dsl",
521                        message: "Missing right operand".to_string(),
522                    })?,
523                depth,
524            )?;
525
526            left = Expr::Binary {
527                left: Box::new(left),
528                op: BinaryOp::Power,
529                right: Box::new(right),
530                ty: Type::Unknown,
531            };
532        } else {
533            let right = parse_expr(op_pair, depth)?;
534            left = Expr::Binary {
535                left: Box::new(left),
536                op: BinaryOp::Power,
537                right: Box::new(right),
538                ty: Type::Unknown,
539            };
540        }
541    }
542
543    Ok(left)
544}
545
546fn parse_unary(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
547    guard_depth(depth, "dsl")?;
548
549    let mut inner = pair.into_inner();
550    let first = inner
551        .next()
552        .ok_or_else(|| AlgorithmError::InvalidParameter {
553            parameter: "dsl",
554            message: "Empty unary expression".to_string(),
555        })?;
556
557    match first.as_rule() {
558        Rule::sub_op => {
559            // Next element will be another unary expression
560            let expr = inner
561                .next()
562                .ok_or_else(|| AlgorithmError::InvalidParameter {
563                    parameter: "dsl",
564                    message: "Missing expression after -".to_string(),
565                })?;
566
567            Ok(Expr::Unary {
568                op: UnaryOp::Negate,
569                expr: Box::new(parse_expr(expr, depth + 1)?),
570                ty: Type::Unknown,
571            })
572        }
573        Rule::add_op => {
574            // Next element will be another unary expression
575            let expr = inner
576                .next()
577                .ok_or_else(|| AlgorithmError::InvalidParameter {
578                    parameter: "dsl",
579                    message: "Missing expression after +".to_string(),
580                })?;
581
582            Ok(Expr::Unary {
583                op: UnaryOp::Plus,
584                expr: Box::new(parse_expr(expr, depth + 1)?),
585                ty: Type::Unknown,
586            })
587        }
588        _ => parse_expr(first, depth),
589    }
590}
591
592fn parse_primary(pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<Expr> {
593    guard_depth(depth, "dsl")?;
594
595    let inner = pair
596        .into_inner()
597        .next()
598        .ok_or_else(|| AlgorithmError::InvalidParameter {
599            parameter: "dsl",
600            message: "Empty primary expression".to_string(),
601        })?;
602
603    match inner.as_rule() {
604        Rule::number => {
605            let num =
606                inner
607                    .as_str()
608                    .parse::<f64>()
609                    .map_err(|_| AlgorithmError::InvalidParameter {
610                        parameter: "dsl",
611                        message: format!("Invalid number: {}", inner.as_str()),
612                    })?;
613            Ok(Expr::Number(num))
614        }
615        Rule::band_ref => {
616            let band_str = inner.as_str();
617            let band_num =
618                band_str[1..]
619                    .parse::<usize>()
620                    .map_err(|_| AlgorithmError::InvalidParameter {
621                        parameter: "dsl",
622                        message: format!("Invalid band reference: {band_str}"),
623                    })?;
624            Ok(Expr::Band(band_num))
625        }
626        Rule::function_call => {
627            let mut parts = inner.into_inner();
628            let name = parts
629                .next()
630                .ok_or_else(|| AlgorithmError::InvalidParameter {
631                    parameter: "dsl",
632                    message: "Missing function name".to_string(),
633                })?
634                .as_str()
635                .to_string();
636
637            let mut args = Vec::new();
638            if let Some(arg_list) = parts.next() {
639                for arg in arg_list.into_inner() {
640                    args.push(parse_expr(arg, depth + 1)?);
641                }
642            }
643
644            Ok(Expr::Call {
645                name,
646                args,
647                ty: Type::Unknown,
648            })
649        }
650        Rule::variable_ref => Ok(Expr::Variable(inner.as_str().to_string())),
651        Rule::conditional => {
652            let mut parts = inner.into_inner();
653            let condition = parse_expr(
654                parts
655                    .next()
656                    .ok_or_else(|| AlgorithmError::InvalidParameter {
657                        parameter: "dsl",
658                        message: "Missing condition".to_string(),
659                    })?,
660                depth + 1,
661            )?;
662
663            let then_expr = parse_expr(
664                parts
665                    .next()
666                    .ok_or_else(|| AlgorithmError::InvalidParameter {
667                        parameter: "dsl",
668                        message: "Missing then expression".to_string(),
669                    })?,
670                depth + 1,
671            )?;
672
673            let else_expr = parse_expr(
674                parts
675                    .next()
676                    .ok_or_else(|| AlgorithmError::InvalidParameter {
677                        parameter: "dsl",
678                        message: "Missing else expression".to_string(),
679                    })?,
680                depth + 1,
681            )?;
682
683            Ok(Expr::Conditional {
684                condition: Box::new(condition),
685                then_expr: Box::new(then_expr),
686                else_expr: Box::new(else_expr),
687                ty: Type::Unknown,
688            })
689        }
690        Rule::block => {
691            let mut statements = Vec::new();
692            let mut result = None;
693
694            for part in inner.into_inner() {
695                match part.as_rule() {
696                    Rule::statement => statements.push(parse_statement(part, depth + 1)?),
697                    Rule::expression => result = Some(Box::new(parse_expr(part, depth + 1)?)),
698                    _ => {}
699                }
700            }
701
702            Ok(Expr::Block {
703                statements,
704                result,
705                ty: Type::Unknown,
706            })
707        }
708        Rule::expression => parse_expr(inner, depth + 1),
709        _ => Err(AlgorithmError::InvalidParameter {
710            parameter: "dsl",
711            message: format!("Unexpected primary: {:?}", inner.as_rule()),
712        }),
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn test_parse_number() {
722        let expr = parse_expression("42.5").expect("Should parse");
723        assert!(matches!(expr, Expr::Number(n) if (n - 42.5).abs() < 1e-10));
724    }
725
726    #[test]
727    fn test_parse_band() {
728        let expr = parse_expression("B1").expect("Should parse");
729        assert!(matches!(expr, Expr::Band(1)));
730    }
731
732    #[test]
733    fn test_parse_arithmetic() {
734        let result = parse_expression("1 + 2 * 3");
735        assert!(result.is_ok());
736    }
737
738    #[test]
739    fn test_parse_ndvi() {
740        let result = parse_expression("(B1 - B2) / (B1 + B2)");
741        assert!(result.is_ok());
742    }
743
744    #[test]
745    fn test_parse_conditional() {
746        let result = parse_expression("if B1 > 0.5 then 1 else 0");
747        if let Err(e) = &result {
748            eprintln!("Parse error: {:?}", e);
749        }
750        assert!(result.is_ok(), "Parse failed: {:?}", result);
751    }
752
753    #[test]
754    fn test_parse_function_call() {
755        let result = parse_expression("sqrt(B1 * B1 + B2 * B2)");
756        assert!(result.is_ok());
757    }
758
759    #[test]
760    fn test_parse_program() {
761        let program = r#"
762            let ndvi = (B8 - B4) / (B8 + B4);
763            let result = if ndvi > 0.5 then 1 else 0;
764        "#;
765        let result = parse_program(program);
766        assert!(result.is_ok());
767    }
768
769    // ===================================================================
770    // Nesting-depth limit
771    //
772    // Every case below used to abort the process with a stack overflow
773    // (SIGABRT), which is unrecoverable — so these are regression tests for
774    // a denial-of-service bug, not merely for an error message.
775    // ===================================================================
776
777    use crate::expr_depth::MAX_EXPRESSION_DEPTH;
778
779    fn nested_parens(depth: usize) -> String {
780        let mut expr = String::from("B1");
781        for _ in 0..depth {
782            expr = format!("({expr} + 1)");
783        }
784        expr
785    }
786
787    #[test]
788    fn test_parse_accepts_expression_at_the_depth_limit() {
789        let expr = nested_parens(MAX_EXPRESSION_DEPTH);
790        assert!(
791            parse_expression(&expr).is_ok(),
792            "an expression exactly at the limit must still parse"
793        );
794    }
795
796    #[test]
797    fn test_parse_rejects_one_level_past_the_depth_limit() {
798        let expr = nested_parens(MAX_EXPRESSION_DEPTH + 1);
799        let err = parse_expression(&expr).expect_err("one level past the limit must be rejected");
800        assert!(
801            matches!(err, AlgorithmError::NestingTooDeep { max, .. } if max == MAX_EXPRESSION_DEPTH),
802            "expected NestingTooDeep, got {err:?}"
803        );
804    }
805
806    #[test]
807    fn test_parse_rejects_deeply_nested_parentheses_without_overflowing() {
808        let err =
809            parse_expression(&nested_parens(1000)).expect_err("deep nesting must be rejected");
810        assert!(matches!(err, AlgorithmError::NestingTooDeep { .. }));
811        // The limit has to be discoverable from the message.
812        assert!(err.to_string().contains(&MAX_EXPRESSION_DEPTH.to_string()));
813    }
814
815    #[test]
816    fn test_parse_rejects_deep_conditional_chain_without_overflowing() {
817        // `conditional` recurses through its `else` branch, so this nests just
818        // as deeply as parentheses do while containing no brackets at all.
819        let mut expr = String::new();
820        for _ in 0..1000 {
821            expr.push_str("if B1 > 0 then 1 else ");
822        }
823        expr.push('0');
824
825        let err = parse_expression(&expr).expect_err("deep if-chain must be rejected");
826        assert!(matches!(err, AlgorithmError::NestingTooDeep { .. }));
827    }
828
829    #[test]
830    fn test_parse_rejects_deep_unary_chain_without_overflowing() {
831        // `unary = (sub_op | add_op) ~ unary` recurses once per sign.
832        let mut expr = "-".repeat(5000);
833        expr.push_str("B1");
834
835        let err = parse_expression(&expr).expect_err("deep unary chain must be rejected");
836        assert!(matches!(err, AlgorithmError::NestingTooDeep { .. }));
837    }
838
839    #[test]
840    fn test_parse_program_rejects_deep_nesting_without_overflowing() {
841        let program = format!("let x = {};", nested_parens(1000));
842        let err = parse_program(&program).expect_err("deep nesting must be rejected");
843        assert!(matches!(err, AlgorithmError::NestingTooDeep { .. }));
844    }
845
846    #[test]
847    fn test_depth_limit_does_not_reject_realistic_expressions() {
848        // Guard against the pre-scan being over-eager: none of these are deep.
849        let cases = [
850            "(B1 - B2) / (B1 + B2)",
851            "sqrt(B1 * B1 + B2 * B2)",
852            "if B1 > 0.6 then 1.0 else if B1 > 0.3 then 0.5 else 0.0",
853            "2.5 * ((B8 - B4) / (B8 + 6 * B4 - 7.5 * B2 + 1))",
854            "clamp(pow(abs(B1 - B2), 2), 0, 1)",
855            "-B1 + -B2",
856            "1e-3 * B1 + 2.5e+4",
857        ];
858        for case in cases {
859            assert!(parse_expression(case).is_ok(), "should parse: {case}");
860        }
861
862        // A long *flat* sum is depth 0 and must not be mistaken for nesting.
863        let flat = (0..500)
864            .map(|i| format!("B{}", i % 8 + 1))
865            .collect::<Vec<_>>()
866            .join(" + ");
867        assert!(parse_expression(&flat).is_ok(), "flat sums must parse");
868
869        // Many sibling statements must not accumulate depth against each other.
870        let program = "let a = if B1 > 0 then 1 else 0;\n".repeat(200);
871        assert!(parse_program(&program).is_ok(), "sibling statements");
872    }
873}