Skip to main content

oxigdal_gpu/band_math/
parser.rs

1//! Lexer + Pratt-style precedence-climbing parser for band-math strings.
2//!
3//! Output is a [`BandExpression`] AST that can be evaluated directly via
4//! [`BandExpression::evaluate`](crate::algebra::BandExpression::evaluate) or
5//! lowered to WGSL via [`crate::band_math::band_expression_to_wgsl`].
6
7use crate::algebra::BandExpression;
8use std::fmt;
9
10/// Errors produced while lexing/parsing a band-math expression.
11#[derive(Debug, Clone, PartialEq)]
12pub enum BandMathError {
13    /// Encountered a character or token that wasn't expected at this position.
14    UnexpectedToken {
15        /// Byte offset into the original source string.
16        pos: usize,
17        /// Display text of the offending fragment.
18        found: String,
19    },
20    /// Identifier used like a function (`name(...)`) is not a known builtin.
21    UnknownFunction(String),
22    /// `Bn` band reference is outside the allowed inclusive range `1..=999`.
23    ///
24    /// The wrapped value is the raw integer that was parsed, or `0` for `B0`.
25    InvalidBandIndex(u32),
26    /// Function call had the wrong number of arguments.
27    WrongArgCount {
28        /// Function name as it appeared in the source.
29        function: String,
30        /// Number of arguments the function expects.
31        expected: usize,
32        /// Number of arguments actually supplied.
33        actual: usize,
34    },
35    /// Detected literal `/ 0` or `/ 0.0` at parse time.
36    DivByConstantZero,
37    /// Parser stopped before consuming the entire token stream.
38    TrailingTokens(usize),
39    /// Input string was empty or contained only whitespace.
40    EmptyExpression,
41    /// `(` without a matching `)` (or vice-versa).
42    UnmatchedParen,
43    /// Number literal failed to parse as `f32`.
44    InvalidNumber {
45        /// Byte offset into the original source string.
46        pos: usize,
47        /// Raw text that failed to parse.
48        text: String,
49    },
50}
51
52impl fmt::Display for BandMathError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            BandMathError::UnexpectedToken { pos, found } => {
56                write!(f, "unexpected token `{}` at position {}", found, pos)
57            }
58            BandMathError::UnknownFunction(name) => write!(f, "unknown function `{}`", name),
59            BandMathError::InvalidBandIndex(n) => {
60                write!(f, "invalid band index B{} (allowed range: 1..=999)", n)
61            }
62            BandMathError::WrongArgCount {
63                function,
64                expected,
65                actual,
66            } => write!(
67                f,
68                "function `{}` expects {} argument(s), got {}",
69                function, expected, actual
70            ),
71            BandMathError::DivByConstantZero => write!(f, "division by literal zero"),
72            BandMathError::TrailingTokens(pos) => {
73                write!(f, "trailing tokens after expression (position {})", pos)
74            }
75            BandMathError::EmptyExpression => write!(f, "empty expression"),
76            BandMathError::UnmatchedParen => write!(f, "unmatched parenthesis"),
77            BandMathError::InvalidNumber { pos, text } => {
78                write!(f, "invalid number literal `{}` at position {}", text, pos)
79            }
80        }
81    }
82}
83
84impl std::error::Error for BandMathError {}
85
86/// Token kinds produced by [`Lexer`].
87#[derive(Debug, Clone, PartialEq)]
88enum Token {
89    /// A floating-point literal (already validated to parse as `f64`).
90    Number(f64),
91    /// `Bn` — a 1-based band reference; stored as the 0-based index for the AST.
92    Band(usize),
93    /// One of `+ - * / ^`.
94    Op(char),
95    /// `(`
96    LParen,
97    /// `)`
98    RParen,
99    /// `,`
100    Comma,
101    /// Function-name identifier immediately followed by `(`.
102    Func(String),
103}
104
105/// Hand-rolled lexer that walks the source bytes left-to-right.
106struct Lexer<'a> {
107    src: &'a [u8],
108    pos: usize,
109}
110
111impl<'a> Lexer<'a> {
112    fn new(src: &'a str) -> Self {
113        Self {
114            src: src.as_bytes(),
115            pos: 0,
116        }
117    }
118
119    fn tokenize(mut self) -> Result<Vec<(Token, usize)>, BandMathError> {
120        let mut out: Vec<(Token, usize)> = Vec::new();
121        while self.pos < self.src.len() {
122            let b = self.src[self.pos];
123            if b.is_ascii_whitespace() {
124                self.pos += 1;
125                continue;
126            }
127            let start = self.pos;
128            if b.is_ascii_digit() || b == b'.' {
129                let (tok, new_pos) = self.lex_number(start)?;
130                out.push((tok, start));
131                self.pos = new_pos;
132            } else if b == b'B' && self.peek_is_digit(self.pos + 1) {
133                let (tok, new_pos) = self.lex_band(start)?;
134                out.push((tok, start));
135                self.pos = new_pos;
136            } else if b.is_ascii_alphabetic() || b == b'_' {
137                let (tok, new_pos) = self.lex_identifier(start)?;
138                out.push((tok, start));
139                self.pos = new_pos;
140            } else {
141                match b {
142                    b'+' | b'-' | b'*' | b'/' | b'^' => {
143                        out.push((Token::Op(b as char), start));
144                        self.pos += 1;
145                    }
146                    b'(' => {
147                        out.push((Token::LParen, start));
148                        self.pos += 1;
149                    }
150                    b')' => {
151                        out.push((Token::RParen, start));
152                        self.pos += 1;
153                    }
154                    b',' => {
155                        out.push((Token::Comma, start));
156                        self.pos += 1;
157                    }
158                    _ => {
159                        return Err(BandMathError::UnexpectedToken {
160                            pos: start,
161                            found: (b as char).to_string(),
162                        });
163                    }
164                }
165            }
166        }
167        Ok(out)
168    }
169
170    fn peek_is_digit(&self, idx: usize) -> bool {
171        idx < self.src.len() && self.src[idx].is_ascii_digit()
172    }
173
174    fn lex_number(&self, start: usize) -> Result<(Token, usize), BandMathError> {
175        let mut i = start;
176        let mut saw_dot = false;
177        let mut saw_digit = false;
178        while i < self.src.len() {
179            let c = self.src[i];
180            if c.is_ascii_digit() {
181                saw_digit = true;
182                i += 1;
183            } else if c == b'.' && !saw_dot {
184                saw_dot = true;
185                i += 1;
186            } else {
187                break;
188            }
189        }
190        // Optional exponent: e/E [+-]? digits+
191        if i < self.src.len() && (self.src[i] == b'e' || self.src[i] == b'E') {
192            i += 1;
193            if i < self.src.len() && (self.src[i] == b'+' || self.src[i] == b'-') {
194                i += 1;
195            }
196            let exp_start = i;
197            while i < self.src.len() && self.src[i].is_ascii_digit() {
198                i += 1;
199            }
200            if i == exp_start {
201                let text = std::str::from_utf8(&self.src[start..i])
202                    .unwrap_or("")
203                    .to_string();
204                return Err(BandMathError::InvalidNumber { pos: start, text });
205            }
206        }
207        if !saw_digit {
208            // Just a `.` with no digits — invalid.
209            let text = std::str::from_utf8(&self.src[start..i])
210                .unwrap_or("")
211                .to_string();
212            return Err(BandMathError::InvalidNumber { pos: start, text });
213        }
214        let text = std::str::from_utf8(&self.src[start..i]).unwrap_or("");
215        let n: f64 = text.parse().map_err(|_| BandMathError::InvalidNumber {
216            pos: start,
217            text: text.to_string(),
218        })?;
219        Ok((Token::Number(n), i))
220    }
221
222    fn lex_band(&self, start: usize) -> Result<(Token, usize), BandMathError> {
223        // start points at 'B'; subsequent chars are digits.
224        let mut i = start + 1;
225        while i < self.src.len() && self.src[i].is_ascii_digit() {
226            i += 1;
227        }
228        let digits = std::str::from_utf8(&self.src[start + 1..i]).unwrap_or("");
229        let n: u32 = digits
230            .parse()
231            .map_err(|_| BandMathError::InvalidBandIndex(0))?;
232        if n == 0 || n > 999 {
233            return Err(BandMathError::InvalidBandIndex(n));
234        }
235        // Internal AST index is 0-based.
236        Ok((Token::Band((n - 1) as usize), i))
237    }
238
239    fn lex_identifier(&self, start: usize) -> Result<(Token, usize), BandMathError> {
240        let mut i = start;
241        while i < self.src.len() {
242            let c = self.src[i];
243            if c.is_ascii_alphanumeric() || c == b'_' {
244                i += 1;
245            } else {
246                break;
247            }
248        }
249        let name = std::str::from_utf8(&self.src[start..i]).unwrap_or("");
250        // Skip whitespace to see if `(` follows.
251        let mut j = i;
252        while j < self.src.len() && self.src[j].is_ascii_whitespace() {
253            j += 1;
254        }
255        if j < self.src.len() && self.src[j] == b'(' {
256            Ok((Token::Func(name.to_string()), i))
257        } else {
258            Err(BandMathError::UnexpectedToken {
259                pos: start,
260                found: name.to_string(),
261            })
262        }
263    }
264}
265
266/// Pratt-style precedence-climbing parser.
267struct Parser {
268    tokens: Vec<(Token, usize)>,
269    pos: usize,
270}
271
272impl Parser {
273    fn peek(&self) -> Option<&(Token, usize)> {
274        self.tokens.get(self.pos)
275    }
276
277    fn advance(&mut self) -> Option<(Token, usize)> {
278        let t = self.tokens.get(self.pos).cloned();
279        if t.is_some() {
280            self.pos += 1;
281        }
282        t
283    }
284
285    fn current_pos(&self) -> usize {
286        self.tokens
287            .get(self.pos)
288            .map(|(_, p)| *p)
289            .unwrap_or_else(|| self.tokens.last().map(|(_, p)| *p).unwrap_or(0))
290    }
291
292    fn parse_expr(&mut self) -> Result<BandExpression, BandMathError> {
293        let mut lhs = self.parse_term()?;
294        while let Some((Token::Op(op), _)) = self.peek().cloned() {
295            if op != '+' && op != '-' {
296                break;
297            }
298            self.advance();
299            let rhs = self.parse_term()?;
300            lhs = match op {
301                '+' => BandExpression::Add(Box::new(lhs), Box::new(rhs)),
302                '-' => BandExpression::Sub(Box::new(lhs), Box::new(rhs)),
303                _ => unreachable!("operator dispatch guarded above"),
304            };
305        }
306        Ok(lhs)
307    }
308
309    fn parse_term(&mut self) -> Result<BandExpression, BandMathError> {
310        let mut lhs = self.parse_factor()?;
311        while let Some((Token::Op(op), _)) = self.peek().cloned() {
312            if op != '*' && op != '/' {
313                break;
314            }
315            self.advance();
316            let rhs = self.parse_factor()?;
317            lhs = match op {
318                '*' => BandExpression::Mul(Box::new(lhs), Box::new(rhs)),
319                '/' => {
320                    // Reject literal divide-by-zero at parse time when the
321                    // right-hand side is a single `Constant(0.0)`.
322                    if let BandExpression::Constant(c) = &rhs {
323                        if c.abs() < 1e-20 {
324                            return Err(BandMathError::DivByConstantZero);
325                        }
326                    }
327                    BandExpression::Div(Box::new(lhs), Box::new(rhs))
328                }
329                _ => unreachable!("operator dispatch guarded above"),
330            };
331        }
332        Ok(lhs)
333    }
334
335    fn parse_factor(&mut self) -> Result<BandExpression, BandMathError> {
336        self.parse_power()
337    }
338
339    fn parse_power(&mut self) -> Result<BandExpression, BandMathError> {
340        let lhs = self.parse_unary()?;
341        if let Some((Token::Op('^'), _)) = self.peek().cloned() {
342            self.advance();
343            // Right-associative: recurse into parse_power again so that
344            // `a ^ b ^ c` parses as `a ^ (b ^ c)`.
345            let rhs = self.parse_power()?;
346            Ok(BandExpression::Pow(Box::new(lhs), Box::new(rhs)))
347        } else {
348            Ok(lhs)
349        }
350    }
351
352    fn parse_unary(&mut self) -> Result<BandExpression, BandMathError> {
353        if let Some((Token::Op('-'), _)) = self.peek().cloned() {
354            self.advance();
355            let inner = self.parse_unary()?;
356            return Ok(BandExpression::Neg(Box::new(inner)));
357        }
358        if let Some((Token::Op('+'), _)) = self.peek().cloned() {
359            // Unary plus is a no-op.
360            self.advance();
361            return self.parse_unary();
362        }
363        self.parse_primary()
364    }
365
366    fn parse_primary(&mut self) -> Result<BandExpression, BandMathError> {
367        let (tok, pos) = self.advance().ok_or(BandMathError::EmptyExpression)?;
368        match tok {
369            Token::Number(n) => Ok(BandExpression::Constant(n as f32)),
370            Token::Band(idx) => Ok(BandExpression::Band(idx)),
371            Token::Func(name) => {
372                // Expect `(`.
373                match self.advance() {
374                    Some((Token::LParen, _)) => {}
375                    Some((other, p)) => {
376                        return Err(BandMathError::UnexpectedToken {
377                            pos: p,
378                            found: format!("{:?}", other),
379                        });
380                    }
381                    None => return Err(BandMathError::UnmatchedParen),
382                }
383                let args = self.parse_args()?;
384                // Expect `)`.
385                match self.advance() {
386                    Some((Token::RParen, _)) => {}
387                    Some((_, p)) => {
388                        return Err(BandMathError::UnexpectedToken {
389                            pos: p,
390                            found: ")".to_string(),
391                        });
392                    }
393                    None => return Err(BandMathError::UnmatchedParen),
394                }
395                self.dispatch_function(&name, args)
396            }
397            Token::LParen => {
398                let inner = self.parse_expr()?;
399                match self.advance() {
400                    Some((Token::RParen, _)) => Ok(inner),
401                    _ => Err(BandMathError::UnmatchedParen),
402                }
403            }
404            other => Err(BandMathError::UnexpectedToken {
405                pos,
406                found: format!("{:?}", other),
407            }),
408        }
409    }
410
411    fn parse_args(&mut self) -> Result<Vec<BandExpression>, BandMathError> {
412        let mut args: Vec<BandExpression> = Vec::new();
413        // Empty argument list `f()`.
414        if let Some((Token::RParen, _)) = self.peek() {
415            return Ok(args);
416        }
417        args.push(self.parse_expr()?);
418        while let Some((Token::Comma, _)) = self.peek() {
419            self.advance();
420            args.push(self.parse_expr()?);
421        }
422        Ok(args)
423    }
424
425    fn dispatch_function(
426        &self,
427        name: &str,
428        mut args: Vec<BandExpression>,
429    ) -> Result<BandExpression, BandMathError> {
430        let lower = name.to_ascii_lowercase();
431        match lower.as_str() {
432            "log" | "ln" => {
433                self.check_arity(name, 1, &args)?;
434                Ok(BandExpression::Log(Box::new(args.remove(0))))
435            }
436            "exp" => {
437                self.check_arity(name, 1, &args)?;
438                Ok(BandExpression::Exp(Box::new(args.remove(0))))
439            }
440            "sqrt" => {
441                self.check_arity(name, 1, &args)?;
442                Ok(BandExpression::Sqrt(Box::new(args.remove(0))))
443            }
444            "abs" => {
445                self.check_arity(name, 1, &args)?;
446                Ok(BandExpression::Abs(Box::new(args.remove(0))))
447            }
448            "min" => {
449                self.check_arity(name, 2, &args)?;
450                let b = args.remove(1);
451                let a = args.remove(0);
452                Ok(BandExpression::Min(Box::new(a), Box::new(b)))
453            }
454            "max" => {
455                self.check_arity(name, 2, &args)?;
456                let b = args.remove(1);
457                let a = args.remove(0);
458                Ok(BandExpression::Max(Box::new(a), Box::new(b)))
459            }
460            "pow" => {
461                self.check_arity(name, 2, &args)?;
462                let b = args.remove(1);
463                let a = args.remove(0);
464                Ok(BandExpression::Pow(Box::new(a), Box::new(b)))
465            }
466            "clamp" => {
467                self.check_arity(name, 3, &args)?;
468                let hi = args.remove(2);
469                let lo = args.remove(1);
470                let value = args.remove(0);
471                Ok(BandExpression::Clamp {
472                    value: Box::new(value),
473                    lo: Box::new(lo),
474                    hi: Box::new(hi),
475                })
476            }
477            _ => Err(BandMathError::UnknownFunction(name.to_string())),
478        }
479    }
480
481    fn check_arity(
482        &self,
483        name: &str,
484        expected: usize,
485        args: &[BandExpression],
486    ) -> Result<(), BandMathError> {
487        if args.len() == expected {
488            Ok(())
489        } else {
490            Err(BandMathError::WrongArgCount {
491                function: name.to_string(),
492                expected,
493                actual: args.len(),
494            })
495        }
496    }
497}
498
499/// Parse `s` into a [`BandExpression`].
500///
501/// # Errors
502///
503/// Returns [`BandMathError`] on any lex/parse failure.
504pub fn parse_band_expression(s: &str) -> Result<BandExpression, BandMathError> {
505    let tokens = Lexer::new(s).tokenize()?;
506    if tokens.is_empty() {
507        return Err(BandMathError::EmptyExpression);
508    }
509    let mut p = Parser { tokens, pos: 0 };
510    let expr = p.parse_expr()?;
511    if p.pos < p.tokens.len() {
512        return Err(BandMathError::TrailingTokens(p.current_pos()));
513    }
514    Ok(expr)
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn test_lex_number_simple() {
523        let toks = Lexer::new("1.5 + 2").tokenize().expect("tokenize");
524        assert_eq!(toks.len(), 3);
525        assert_eq!(toks[0].0, Token::Number(1.5));
526        assert_eq!(toks[1].0, Token::Op('+'));
527        assert_eq!(toks[2].0, Token::Number(2.0));
528    }
529
530    #[test]
531    fn test_lex_band_reference() {
532        let toks = Lexer::new("B1 - B42").tokenize().expect("tokenize");
533        assert_eq!(toks[0].0, Token::Band(0));
534        assert_eq!(toks[2].0, Token::Band(41));
535    }
536
537    #[test]
538    fn test_lex_unknown_char_errors() {
539        let err = Lexer::new("B1 @ B2").tokenize().expect_err("must fail");
540        assert!(
541            matches!(err, BandMathError::UnexpectedToken { .. }),
542            "wrong variant: {:?}",
543            err
544        );
545    }
546
547    #[test]
548    fn test_parse_addition() {
549        let e = parse_band_expression("B1 + B2").expect("parse");
550        assert!(
551            matches!(e, BandExpression::Add(_, _)),
552            "expected Add, got {:?}",
553            e
554        );
555    }
556}