Skip to main content

ganit_core/parser/
mod.rs

1pub mod ast;
2pub mod tokens;
3
4pub use ast::Expr;
5use ast::{BinaryOp, Span, UnaryOp};
6use crate::types::ParseError;
7use nom::{IResult, character::complete::multispace0};
8use tokens::{bool_literal, identifier, number_literal, offset, string_literal};
9
10struct Parser<'a> {
11    full: &'a str,
12}
13
14impl<'a> Parser<'a> {
15    fn new(full: &'a str) -> Self {
16        Self { full }
17    }
18
19    fn span(&self, before: &str, after: &str) -> Span {
20        let start = offset(self.full, before);
21        let end = offset(self.full, after);
22        Span::new(start, end - start)
23    }
24
25    // ── primary ────────────────────────────────────────────────────────────
26
27    fn parse_primary(&self, i: &'a str) -> IResult<&'a str, Expr> {
28        let i = multispace0(i)?.0;
29
30        // Number literal (must come before identifier to catch e.g. "1e3")
31        if let Ok((rest, n)) = number_literal(i) {
32            return Ok((rest, Expr::Number(n, self.span(i, rest))));
33        }
34
35        // String literal
36        if let Ok((rest, text)) = string_literal(i) {
37            return Ok((rest, Expr::Text(text, self.span(i, rest))));
38        }
39
40        // Array literal: {expr, expr, ...}
41        if let Some(inner) = i.strip_prefix('{') {
42            let (rest, elems) = self.parse_array_elements(inner)?;
43            let rest = multispace0(rest)?.0;
44            if let Some(after) = rest.strip_prefix('}') {
45                return Ok((after, Expr::Array(elems, self.span(i, after))));
46            }
47            return Err(nom::Err::Error(nom::error::Error::new(
48                rest,
49                nom::error::ErrorKind::Char,
50            )));
51        }
52
53        // Parenthesised expression
54        if let Some(inner) = i.strip_prefix('(') {
55            let (rest, expr) = self.parse_comparison(inner)?;
56            let rest = multispace0(rest)?.0;
57            if let Some(after) = rest.strip_prefix(')') {
58                return Ok((after, expr));
59            }
60            return Err(nom::Err::Error(nom::error::Error::new(
61                rest,
62                nom::error::ErrorKind::Char,
63            )));
64        }
65
66        // Boolean (before identifier — uses word-boundary check in bool_literal)
67        if let Ok((rest, b)) = bool_literal(i) {
68            return Ok((rest, Expr::Bool(b, self.span(i, rest))));
69        }
70
71        // Identifier: variable or function call
72        if let Ok((rest, name)) = identifier(i) {
73            let rest_ws = multispace0(rest)?.0;
74            if let Some(args_input) = rest_ws.strip_prefix('(') {
75                // Function call
76                let (rest2, args) = self.parse_arg_list(args_input)?;
77                let rest2 = multispace0(rest2)?.0;
78                if let Some(after) = rest2.strip_prefix(')') {
79                    return Ok((after, Expr::FunctionCall {
80                        name: name.to_uppercase(),
81                        args,
82                        span: self.span(i, after),
83                    }));
84                }
85                return Err(nom::Err::Error(nom::error::Error::new(
86                    rest2,
87                    nom::error::ErrorKind::Char,
88                )));
89            }
90            return Ok((rest, Expr::Variable(name.to_string(), self.span(i, rest))));
91        }
92
93        Err(nom::Err::Error(nom::error::Error::new(i, nom::error::ErrorKind::Alt)))
94    }
95
96    fn parse_arg_list(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
97        let mut args = Vec::new();
98        let mut rest = multispace0(i)?.0;
99
100        if rest.starts_with(')') {
101            return Ok((rest, args));
102        }
103
104        let (r, first) = self.parse_comparison(rest)?;
105        args.push(first);
106        rest = r;
107
108        loop {
109            rest = multispace0(rest)?.0;
110            if let Some(after_comma) = rest.strip_prefix(',') {
111                let (r, arg) = self.parse_comparison(after_comma)?;
112                args.push(arg);
113                rest = r;
114            } else {
115                break;
116            }
117        }
118
119        Ok((rest, args))
120    }
121
122    fn parse_array_elements(&self, i: &'a str) -> IResult<&'a str, Vec<Expr>> {
123        let mut elems = Vec::new();
124        let mut rest = multispace0(i)?.0;
125        if rest.starts_with('}') {
126            return Ok((rest, elems)); // empty array {}
127        }
128        let (r, first) = self.parse_comparison(rest)?;
129        elems.push(first);
130        rest = r;
131        loop {
132            rest = multispace0(rest)?.0;
133            if let Some(after_comma) = rest.strip_prefix(',') {
134                let (r, elem) = self.parse_comparison(after_comma)?;
135                elems.push(elem);
136                rest = r;
137            } else {
138                break;
139            }
140        }
141        Ok((rest, elems))
142    }
143
144    // ── postfix % ─────────────────────────────────────────────────────────
145
146    fn parse_postfix(&self, i: &'a str) -> IResult<&'a str, Expr> {
147        let (rest, expr) = self.parse_primary(i)?;
148        let rest_ws = multispace0(rest)?.0;
149        if let Some(after) = rest_ws.strip_prefix('%') {
150            return Ok((after, Expr::UnaryOp {
151                op: UnaryOp::Percent,
152                operand: Box::new(expr),
153                span: self.span(i, after),
154            }));
155        }
156        Ok((rest, expr))
157    }
158
159    // ── unary minus ───────────────────────────────────────────────────────
160
161    fn parse_unary(&self, i: &'a str) -> IResult<&'a str, Expr> {
162        let i_ws = multispace0(i)?.0;
163        if let Some(after_minus) = i_ws.strip_prefix('-') {
164            let (rest, operand) = self.parse_unary(after_minus)?;
165            return Ok((rest, Expr::UnaryOp {
166                op: UnaryOp::Neg,
167                operand: Box::new(operand),
168                span: self.span(i_ws, rest),
169            }));
170        }
171        self.parse_postfix(i)
172    }
173
174    // ── power ^ (right-associative) ───────────────────────────────────────
175
176    fn parse_power(&self, i: &'a str) -> IResult<&'a str, Expr> {
177        let (rest, left) = self.parse_unary(i)?;
178        let rest_ws = multispace0(rest)?.0;
179        if let Some(after_op) = rest_ws.strip_prefix('^') {
180            let (rest2, right) = self.parse_power(after_op)?;
181            return Ok((rest2, Expr::BinaryOp {
182                op: BinaryOp::Pow,
183                left: Box::new(left),
184                right: Box::new(right),
185                span: self.span(i, rest2),
186            }));
187        }
188        Ok((rest, left))
189    }
190
191    // ── multiplicative * / ────────────────────────────────────────────────
192
193    fn parse_multiplicative(&self, i: &'a str) -> IResult<&'a str, Expr> {
194        let (mut rest, mut left) = self.parse_power(i)?;
195        loop {
196            let ws = multispace0(rest)?.0;
197            let op = ws.strip_prefix('*').map(|after| (BinaryOp::Mul, after))
198                .or_else(|| ws.strip_prefix('/').map(|after| (BinaryOp::Div, after)));
199            match op {
200                None => break,
201                Some((op, after)) => {
202                    let (r, right) = self.parse_power(after)?;
203                    left = Expr::BinaryOp {
204                        op,
205                        span: self.span(i, r),
206                        left: Box::new(left),
207                        right: Box::new(right),
208                    };
209                    rest = r;
210                }
211            }
212        }
213        Ok((rest, left))
214    }
215
216    // ── additive + - ──────────────────────────────────────────────────────
217
218    fn parse_additive(&self, i: &'a str) -> IResult<&'a str, Expr> {
219        let (mut rest, mut left) = self.parse_multiplicative(i)?;
220        loop {
221            let ws = multispace0(rest)?.0;
222            let op = ws.strip_prefix('+').map(|after| (BinaryOp::Add, after))
223                .or_else(|| ws.strip_prefix('-').map(|after| (BinaryOp::Sub, after)));
224            match op {
225                None => break,
226                Some((op, after)) => {
227                    let (r, right) = self.parse_multiplicative(after)?;
228                    left = Expr::BinaryOp {
229                        op,
230                        span: self.span(i, r),
231                        left: Box::new(left),
232                        right: Box::new(right),
233                    };
234                    rest = r;
235                }
236            }
237        }
238        Ok((rest, left))
239    }
240
241    // ── concat & ─────────────────────────────────────────────────────────
242
243    fn parse_concat(&self, i: &'a str) -> IResult<&'a str, Expr> {
244        let (mut rest, mut left) = self.parse_additive(i)?;
245        loop {
246            let ws = multispace0(rest)?.0;
247            if let Some(after) = ws.strip_prefix('&') {
248                let (r, right) = self.parse_additive(after)?;
249                left = Expr::BinaryOp {
250                    op: BinaryOp::Concat,
251                    span: self.span(i, r),
252                    left: Box::new(left),
253                    right: Box::new(right),
254                };
255                rest = r;
256            } else {
257                break;
258            }
259        }
260        Ok((rest, left))
261    }
262
263    // ── comparison = <> < > <= >= ─────────────────────────────────────────
264
265    fn parse_comparison(&self, i: &'a str) -> IResult<&'a str, Expr> {
266        let (rest, left) = self.parse_concat(i)?;
267        let ws = multispace0(rest)?.0;
268
269        // Longest match first
270        let op_result: Option<(BinaryOp, &'a str)> = if let Some(after) = ws.strip_prefix("<>") {
271            Some((BinaryOp::Ne, after))
272        } else if let Some(after) = ws.strip_prefix("<=") {
273            Some((BinaryOp::Le, after))
274        } else if let Some(after) = ws.strip_prefix(">=") {
275            Some((BinaryOp::Ge, after))
276        } else if let Some(after) = ws.strip_prefix('<') {
277            Some((BinaryOp::Lt, after))
278        } else if let Some(after) = ws.strip_prefix('>') {
279            Some((BinaryOp::Gt, after))
280        } else if let Some(after) = ws.strip_prefix('=') {
281            Some((BinaryOp::Eq, after))
282        } else {
283            None
284        };
285
286        if let Some((op, after)) = op_result {
287            let (r, right) = self.parse_concat(after)?;
288            return Ok((r, Expr::BinaryOp {
289                op,
290                span: self.span(i, r),
291                left: Box::new(left),
292                right: Box::new(right),
293            }));
294        }
295
296        Ok((rest, left))
297    }
298}
299
300// ── public API ──────────────────────────────────────────────────────────────
301
302/// Parse a formula string into an expression tree.
303///
304/// The formula must start with `=`. Returns a [`ParseError`] if the input
305/// is not a valid formula.
306pub fn parse(formula: &str) -> Result<Expr, ParseError> {
307    let input = formula.strip_prefix('=').unwrap_or(formula).trim();
308    let p = Parser::new(formula);
309    match p.parse_comparison(input) {
310        Ok((rest, expr)) => {
311            let rest = rest.trim();
312            if rest.is_empty() {
313                Ok(expr)
314            } else {
315                Err(ParseError {
316                    message: format!("Unexpected input '{}'", rest),
317                    position: offset(formula, rest),
318                })
319            }
320        }
321        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(ParseError {
322            message: "Parse error".into(),
323            position: offset(formula, e.input),
324        }),
325        Err(nom::Err::Incomplete(_)) => Err(ParseError {
326            message: "Incomplete input".into(),
327            position: formula.len(),
328        }),
329    }
330}
331
332/// Validate that a formula string is syntactically correct without returning the AST.
333pub fn validate(formula: &str) -> Result<(), ParseError> {
334    parse(formula).map(|_| ())
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::parser::ast::{BinaryOp, Expr, UnaryOp};
341
342    #[test]
343    fn parse_number_literal() {
344        let expr = parse("=42").unwrap();
345        assert!(matches!(expr, Expr::Number(n, _) if n == 42.0));
346    }
347
348    #[test]
349    fn parse_binary_add() {
350        let expr = parse("=1+2").unwrap();
351        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Add, .. }));
352    }
353
354    #[test]
355    fn parse_precedence() {
356        // 2+3*4 should parse as 2+(3*4)
357        let expr = parse("=2+3*4").unwrap();
358        match expr {
359            Expr::BinaryOp { op: BinaryOp::Add, right, .. } => {
360                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Mul, .. }));
361            }
362            _ => panic!("Expected Add at top"),
363        }
364    }
365
366    #[test]
367    fn parse_function_call() {
368        let expr = parse("=SUM(1,2,3)").unwrap();
369        match expr {
370            Expr::FunctionCall { name, args, .. } => {
371                assert_eq!(name, "SUM");
372                assert_eq!(args.len(), 3);
373            }
374            _ => panic!("Expected FunctionCall"),
375        }
376    }
377
378    #[test]
379    fn parse_percent() {
380        let expr = parse("=50%").unwrap();
381        assert!(matches!(expr, Expr::UnaryOp { op: UnaryOp::Percent, .. }));
382    }
383
384    #[test]
385    fn parse_string_literal() {
386        let expr = parse("=\"hello\"").unwrap();
387        assert!(matches!(expr, Expr::Text(ref s, _) if s == "hello"));
388    }
389
390    #[test]
391    fn parse_concat_op() {
392        let expr = parse("=\"a\"&\"b\"").unwrap();
393        assert!(matches!(expr, Expr::BinaryOp { op: BinaryOp::Concat, .. }));
394    }
395
396    #[test]
397    fn validate_incomplete_fails() {
398        let err = validate("=SUM(1,").unwrap_err();
399        assert!(!err.message.is_empty());
400    }
401
402    #[test]
403    fn parse_nested() {
404        assert!(parse("=ROUND(SUM(1,2)*1.1, 1)").is_ok());
405    }
406
407    #[test]
408    fn parse_boolean() {
409        let expr = parse("=TRUE").unwrap();
410        assert!(matches!(expr, Expr::Bool(true, _)));
411    }
412
413    #[test]
414    fn parse_variable() {
415        let expr = parse("=myVar").unwrap();
416        assert!(matches!(expr, Expr::Variable(ref n, _) if n == "myVar"));
417    }
418
419    #[test]
420    fn parse_array_literal_numbers() {
421        let expr = parse("={1,2,3}").unwrap();
422        match expr {
423            Expr::Array(elems, _) => assert_eq!(elems.len(), 3),
424            _ => panic!("Expected Array"),
425        }
426    }
427
428    #[test]
429    fn parse_array_literal_mixed() {
430        let expr = parse("={1,\"hello\",TRUE}").unwrap();
431        assert!(matches!(expr, Expr::Array(_, _)));
432    }
433
434    #[test]
435    fn parse_array_literal_empty() {
436        let expr = parse("={}").unwrap();
437        assert!(matches!(expr, Expr::Array(ref e, _) if e.is_empty()));
438    }
439
440    #[test]
441    fn parse_array_in_function_call() {
442        let expr = parse("=SUM({1,2,3})").unwrap();
443        match expr {
444            Expr::FunctionCall { name, args, .. } => {
445                assert_eq!(name, "SUM");
446                assert_eq!(args.len(), 1);
447                assert!(matches!(args[0], Expr::Array(_, _)));
448            }
449            _ => panic!("Expected FunctionCall"),
450        }
451    }
452
453    #[test]
454    fn parse_power_right_assoc() {
455        // 2^3^2 = 2^(3^2) = 2^9 = 512 (right-associative)
456        let expr = parse("=2^3^2").unwrap();
457        match expr {
458            Expr::BinaryOp { op: BinaryOp::Pow, right, .. } => {
459                assert!(matches!(*right, Expr::BinaryOp { op: BinaryOp::Pow, .. }));
460            }
461            _ => panic!("Expected Pow at top"),
462        }
463    }
464}