Skip to main content

async_geotiff/render/
expression.rs

1//! Arithmetic band-math expression: parse `(b5-b4)/(b5+b4)` → RPN, eval per-pixel.
2//! Grammar is arithmetic-only: + - * /, unary minus, parens, float literals, `b<N>`.
3//! Numbers are plain decimals (`2.5`, `0.5`); scientific notation (`1e3`) is not supported.
4
5use crate::error::{AgError, AgResult};
6
7const MAX_EXPRESSION_BYTES: usize = 4096;
8const MAX_EXPRESSION_TOKENS: usize = 256;
9
10/// RPN token. `Band(i)` is 0-indexed (source `b1` → `Band(0)`).
11#[derive(Debug, Clone, PartialEq)]
12pub enum Token {
13    /// A numeric constant.
14    Num(f64),
15    /// A zero-based source band index.
16    Band(usize),
17    /// Add the two preceding operands.
18    Add,
19    /// Subtract the right operand from the left operand.
20    Sub,
21    /// Multiply the two preceding operands.
22    Mul,
23    /// Divide the left operand by the right operand.
24    Div,
25    /// Negate the preceding operand.
26    Neg,
27}
28
29/// Parse arithmetic and one-based band references (`b1`, `b2`, …) into RPN.
30///
31/// Supports parentheses, unary minus and `+`, `-`, `*`, `/`. Invalid syntax,
32/// excessive nesting, more than 4096 input bytes or 256 lexical tokens returns
33/// [`AgError::Query`]. Band availability is checked separately by the caller.
34pub fn parse_expression(input: &str) -> AgResult<Vec<Token>> {
35    if input.len() > MAX_EXPRESSION_BYTES {
36        return Err(AgError::query("expression", "expression is too long"));
37    }
38    let toks = lex(input)?;
39    if toks.len() > MAX_EXPRESSION_TOKENS {
40        return Err(AgError::query(
41            "expression",
42            "expression has too many tokens",
43        ));
44    }
45    let mut p = Parser {
46        toks: &toks,
47        pos: 0,
48        out: Vec::new(),
49        depth: 0,
50    };
51    p.expr()?;
52    if p.pos != p.toks.len() {
53        return Err(AgError::query("expression", "unexpected trailing input"));
54    }
55    if p.out.is_empty() {
56        return Err(AgError::query("expression", "empty expression"));
57    }
58    Ok(p.out)
59}
60
61/// Highest 0-indexed band referenced, or None if no band refs.
62pub fn max_band_ref(rpn: &[Token]) -> Option<usize> {
63    rpn.iter()
64        .filter_map(|t| match t {
65            Token::Band(i) => Some(*i),
66            _ => None,
67        })
68        .max()
69}
70
71/// Like [`eval_rpn`] but reuses a caller-provided scratch stack to avoid a
72/// per-call heap allocation in hot per-pixel loops. `stack` is cleared on entry.
73pub(crate) fn eval_rpn_with(rpn: &[Token], bands: &[f64], stack: &mut Vec<f64>) -> f64 {
74    stack.clear();
75    for t in rpn {
76        match t {
77            Token::Num(n) => stack.push(*n),
78            Token::Band(i) => stack.push(bands.get(*i).copied().unwrap_or(f64::NAN)),
79            Token::Neg => {
80                let a = stack.pop().unwrap_or(f64::NAN);
81                stack.push(-a);
82            }
83            op => {
84                let b = stack.pop().unwrap_or(f64::NAN);
85                let a = stack.pop().unwrap_or(f64::NAN);
86                stack.push(match op {
87                    Token::Add => a + b,
88                    Token::Sub => a - b,
89                    Token::Mul => a * b,
90                    Token::Div => a / b,
91                    _ => f64::NAN,
92                });
93            }
94        }
95    }
96    stack.pop().unwrap_or(f64::NAN)
97}
98
99/// Evaluate RPN given per-pixel band values (`bands[i]` = value of source band i+1).
100/// Caller must validate `max_band_ref < bands.len()` once before the pixel loop;
101/// an out-of-range band defensively yields NaN (never panics).
102pub fn eval_rpn(rpn: &[Token], bands: &[f64]) -> f64 {
103    let mut stack = Vec::with_capacity(8);
104    eval_rpn_with(rpn, bands, &mut stack)
105}
106
107#[derive(Debug, Clone, PartialEq)]
108enum Lexeme {
109    Num(f64),
110    Band(usize),
111    Plus,
112    Minus,
113    Star,
114    Slash,
115    LParen,
116    RParen,
117}
118
119fn lex(s: &str) -> AgResult<Vec<Lexeme>> {
120    let b = s.as_bytes();
121    let mut i = 0;
122    let mut out = Vec::new();
123    while i < b.len() {
124        let c = b[i] as char;
125        match c {
126            ' ' | '\t' | '\n' | '\r' => i += 1,
127            '+' => {
128                out.push(Lexeme::Plus);
129                i += 1;
130            }
131            '-' => {
132                out.push(Lexeme::Minus);
133                i += 1;
134            }
135            '*' => {
136                out.push(Lexeme::Star);
137                i += 1;
138            }
139            '/' => {
140                out.push(Lexeme::Slash);
141                i += 1;
142            }
143            '(' => {
144                out.push(Lexeme::LParen);
145                i += 1;
146            }
147            ')' => {
148                out.push(Lexeme::RParen);
149                i += 1;
150            }
151            'b' | 'B' => {
152                let start = i + 1;
153                let mut j = start;
154                while j < b.len() && b[j].is_ascii_digit() {
155                    j += 1;
156                }
157                if j == start {
158                    return Err(AgError::query("expression", "band ref 'b' needs a number"));
159                }
160                let n: usize = s[start..j]
161                    .parse()
162                    .map_err(|_| AgError::query("expression", "invalid band number"))?;
163                if n == 0 {
164                    return Err(AgError::query(
165                        "expression",
166                        "bands are 1-indexed (b1 is first)",
167                    ));
168                }
169                out.push(Lexeme::Band(n - 1));
170                i = j;
171            }
172            c if c.is_ascii_digit() || c == '.' => {
173                let start = i;
174                let mut j = i;
175                while j < b.len() && (b[j].is_ascii_digit() || b[j] == b'.') {
176                    j += 1;
177                }
178                let n: f64 = s[start..j]
179                    .parse()
180                    .map_err(|_| AgError::query("expression", "invalid number"))?;
181                if !n.is_finite() {
182                    return Err(AgError::query("expression", "number must be finite"));
183                }
184                out.push(Lexeme::Num(n));
185                i = j;
186            }
187            other => {
188                return Err(AgError::query(
189                    "expression",
190                    format!("unexpected character {other:?}"),
191                ))
192            }
193        }
194    }
195    Ok(out)
196}
197
198struct Parser<'a> {
199    toks: &'a [Lexeme],
200    pos: usize,
201    out: Vec<Token>,
202    depth: usize,
203}
204
205impl<'a> Parser<'a> {
206    fn peek(&self) -> Option<&Lexeme> {
207        self.toks.get(self.pos)
208    }
209    fn expr(&mut self) -> AgResult<()> {
210        self.term()?;
211        while let Some(op) = self.peek() {
212            let t = match op {
213                Lexeme::Plus => Token::Add,
214                Lexeme::Minus => Token::Sub,
215                _ => break,
216            };
217            self.pos += 1;
218            self.term()?;
219            self.out.push(t);
220        }
221        Ok(())
222    }
223    fn term(&mut self) -> AgResult<()> {
224        self.factor()?;
225        while let Some(op) = self.peek() {
226            let t = match op {
227                Lexeme::Star => Token::Mul,
228                Lexeme::Slash => Token::Div,
229                _ => break,
230            };
231            self.pos += 1;
232            self.factor()?;
233            self.out.push(t);
234        }
235        Ok(())
236    }
237    fn factor(&mut self) -> AgResult<()> {
238        self.depth += 1;
239        if self.depth > 128 {
240            return Err(AgError::query("expression", "expression nesting too deep"));
241        }
242        let r = self.factor_inner();
243        self.depth -= 1;
244        r
245    }
246    fn factor_inner(&mut self) -> AgResult<()> {
247        match self.peek() {
248            Some(Lexeme::Num(n)) => {
249                let n = *n;
250                self.out.push(Token::Num(n));
251                self.pos += 1;
252                Ok(())
253            }
254            Some(Lexeme::Band(i)) => {
255                let i = *i;
256                self.out.push(Token::Band(i));
257                self.pos += 1;
258                Ok(())
259            }
260            Some(Lexeme::Minus) => {
261                self.pos += 1;
262                self.factor()?;
263                self.out.push(Token::Neg);
264                Ok(())
265            }
266            Some(Lexeme::LParen) => {
267                self.pos += 1;
268                self.expr()?;
269                match self.peek() {
270                    Some(Lexeme::RParen) => {
271                        self.pos += 1;
272                        Ok(())
273                    }
274                    _ => Err(AgError::query("expression", "unbalanced parentheses")),
275                }
276            }
277            _ => Err(AgError::query(
278                "expression",
279                "expected a number, band, or '('",
280            )),
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    fn rpn(s: &str) -> Vec<Token> {
290        parse_expression(s).unwrap()
291    }
292
293    #[test]
294    fn ndvi_value() {
295        let t = rpn("(b5-b4)/(b5+b4)");
296        let mut bands = vec![0.0; 5];
297        bands[4] = 0.5;
298        bands[3] = 0.1;
299        let v = eval_rpn(&t, &bands);
300        assert!((v - 0.6666667).abs() < 1e-5, "ndvi={v}");
301    }
302    #[test]
303    fn precedence_and_parens() {
304        assert_eq!(eval_rpn(&rpn("1+2*3"), &[]), 7.0);
305        assert_eq!(eval_rpn(&rpn("(1+2)*3"), &[]), 9.0);
306    }
307    #[test]
308    fn unary_minus() {
309        assert_eq!(eval_rpn(&rpn("-b1+3"), &[2.0]), 1.0);
310    }
311    #[test]
312    fn div_by_zero_is_nonfinite() {
313        let v = eval_rpn(&rpn("b1/b2"), &[1.0, 0.0]);
314        assert!(!v.is_finite(), "expected inf/nan, got {v}");
315    }
316    #[test]
317    fn max_band_ref_reports_highest() {
318        assert_eq!(max_band_ref(&rpn("(b5-b4)/(b5+b4)")), Some(4));
319        assert_eq!(max_band_ref(&rpn("1+2")), None);
320    }
321    #[test]
322    fn rejects_empty() {
323        assert!(matches!(
324            parse_expression("   "),
325            Err(AgError::Query { .. })
326        ));
327    }
328
329    #[test]
330    fn rejects_overflowing_numeric_literal() {
331        let literal = "9".repeat(400);
332        assert!(matches!(
333            parse_expression(&literal),
334            Err(AgError::Query { name, .. }) if name == "expression"
335        ));
336    }
337    #[test]
338    fn rejects_band_zero() {
339        assert!(matches!(
340            parse_expression("b0+1"),
341            Err(AgError::Query { .. })
342        ));
343    }
344    #[test]
345    fn rejects_bare_b() {
346        assert!(matches!(
347            parse_expression("b+1"),
348            Err(AgError::Query { .. })
349        ));
350    }
351    #[test]
352    fn rejects_trailing_garbage() {
353        assert!(matches!(
354            parse_expression("b1+1 xyz"),
355            Err(AgError::Query { .. })
356        ));
357        assert!(matches!(
358            parse_expression("b1)"),
359            Err(AgError::Query { .. })
360        ));
361    }
362    #[test]
363    fn rejects_unbalanced_parens() {
364        assert!(matches!(
365            parse_expression("(b1+1"),
366            Err(AgError::Query { .. })
367        ));
368    }
369
370    #[test]
371    fn left_associative() {
372        assert_eq!(eval_rpn(&rpn("1-2-3"), &[]), -4.0);
373        assert_eq!(eval_rpn(&rpn("8/2/2"), &[]), 2.0);
374    }
375
376    #[test]
377    fn unary_after_binary() {
378        assert_eq!(eval_rpn(&rpn("2*-3"), &[]), -6.0);
379        assert_eq!(eval_rpn(&rpn("3 - -2"), &[]), 5.0);
380    }
381
382    #[test]
383    fn rejects_empty_parens_and_leading_op() {
384        assert!(parse_expression("()").is_err());
385        assert!(parse_expression("*b1").is_err());
386        assert!(parse_expression("b1 b2").is_err());
387    }
388
389    #[test]
390    fn generic_ndvi_expression_evaluates() {
391        let tokens = rpn("(b5-b4)/(b5+b4)");
392        let mut bands = vec![0.0; 5];
393        bands[4] = 0.5;
394        bands[3] = 0.1;
395
396        assert!((eval_rpn(&tokens, &bands) - 0.6666667).abs() < 1e-5);
397    }
398
399    #[test]
400    fn rejects_deeply_nested() {
401        let deep = format!("{}b1{}", "(".repeat(500), ")".repeat(500));
402        assert!(matches!(
403            parse_expression(&deep),
404            Err(AgError::Query { .. })
405        ));
406    }
407
408    #[test]
409    fn rejects_excessively_long_expression() {
410        let long = ["b1"; 300].join("+");
411        assert!(matches!(
412            parse_expression(&long),
413            Err(AgError::Query { .. })
414        ));
415    }
416}