Skip to main content

simple_expressions/
parser.rs

1use crate::types::error::{Error, Result};
2use crate::types::expression::{BinaryOp, Expr, UnaryOp};
3use crate::types::primitive::Primitive;
4use pest::error::{Error as PestError, ErrorVariant, InputLocation, LineColLocation};
5use pest::iterators::Pair;
6use pest::pratt_parser::{Assoc, Op, PrattParser};
7use pest::{Parser, Position, Span};
8
9#[derive(pest_derive::Parser)]
10#[grammar = "expr.pest"]
11struct InnerParser;
12
13/// Where the fragment being parsed sits inside the text the user actually wrote.
14///
15/// An interpolated expression is parsed from a slice starting mid-string, so pest
16/// reports positions relative to that slice. Carrying the whole input alongside
17/// the slice's byte offset lets errors be re-anchored onto what the user typed.
18#[derive(Clone, Copy)]
19pub(crate) struct Origin<'a> {
20    input: &'a str,
21    offset: usize,
22}
23
24impl<'a> Origin<'a> {
25    /// The fragment is the whole input.
26    pub(crate) fn whole(input: &'a str) -> Self {
27        Self { input, offset: 0 }
28    }
29
30    /// The fragment begins at byte `offset` within `input`.
31    pub(crate) fn fragment(input: &'a str, offset: usize) -> Self {
32        Self { input, offset }
33    }
34
35    /// The fragment itself -- what gets handed to pest.
36    fn text(&self) -> &'a str {
37        &self.input[self.offset..]
38    }
39
40    /// Re-anchor a pest parse failure onto the whole input.
41    fn convert(&self, err: PestError<Rule>) -> Error {
42        match err.location {
43            InputLocation::Pos(p) => self.build(err.variant, p, None),
44            InputLocation::Span((s, e)) => self.build(err.variant, s, Some(e)),
45        }
46    }
47
48    /// A parse failure we detect ourselves rather than one pest reports, covering
49    /// the whole of `pair`.
50    fn at_pair(&self, pair: &Pair<Rule>, message: String) -> Error {
51        let span = pair.as_span();
52        self.build(ErrorVariant::CustomError { message }, span.start(), Some(span.end()))
53    }
54
55    /// As [`Origin::at_pair`], for a single position within the fragment.
56    fn at(&self, start: usize, message: String) -> Error {
57        self.build(ErrorVariant::CustomError { message }, start, None)
58    }
59
60    /// Positions arrive relative to the fragment; shift them and rebuild the error
61    /// through pest against the whole input. Shifting the reported line and column
62    /// directly would not work -- the fragment's line 1 is not the input's line 1,
63    /// and the source line pest renders in its caret diagram is the fragment's
64    /// rather than the user's. Re-running pest's own machinery fixes all three at
65    /// once, and keeps the diagram consistent with the numbers beside it.
66    fn build(&self, variant: ErrorVariant<Rule>, start: usize, end: Option<usize>) -> Error {
67        // A String, not the Rule: the grammar stays an implementation detail, so
68        // renaming a rule is not a breaking change for callers.
69        let message = variant.message().into_owned();
70        let offset = self.offset + start;
71
72        let rebased = match end {
73            Some(end) => Span::new(self.input, offset, self.offset + end).map(|span| PestError::new_from_span(variant, span)),
74            None => Position::new(self.input, offset).map(|pos| PestError::new_from_pos(variant, pos)),
75        };
76
77        match rebased {
78            Some(err) => {
79                let (line, column) = match err.line_col {
80                    LineColLocation::Pos(pos) => pos,
81                    LineColLocation::Span(start, _) => start,
82                };
83                Error::ParseError {
84                    line,
85                    column,
86                    offset,
87                    message,
88                    rendered: err.to_string(),
89                }
90            }
91            // Unreachable for offsets pest handed us, but guessing a position would
92            // be worse than computing the one thing we can still be sure of.
93            None => {
94                let (line, column) = line_col(self.input, offset);
95                Error::ParseError {
96                    line,
97                    column,
98                    offset,
99                    message: message.clone(),
100                    rendered: message,
101                }
102            }
103        }
104    }
105}
106
107/// 1-based line and character column of `offset` within `text`.
108///
109/// Counts `\n` only, which matches both pest and the grammar's `NEWLINE`.
110fn line_col(text: &str, offset: usize) -> (usize, usize) {
111    let offset = offset.min(text.len());
112    let before = &text[..offset];
113    let line_start = before.rfind('\n').map_or(0, |i| i + 1);
114    (before.matches('\n').count() + 1, before[line_start..].chars().count() + 1)
115}
116
117pub fn parse_expression(input: &str) -> Result<Expr> {
118    parse_internal(Origin::whole(input), Rule::program).map(|r| r.0)
119}
120
121/// Returns the expression and how far into [`Origin::text`] the parse consumed.
122pub(crate) fn parse_internal(origin: Origin, rule: Rule) -> Result<(Expr, usize)> {
123    let mut pairs = InnerParser::parse(rule, origin.text()).map_err(|e| origin.convert(e))?;
124    let pair = pairs.next().expect("program always produces one pair");
125
126    debug_assert_eq!(pair.as_rule(), rule);
127    let end_pos = pair.as_span().end_pos().pos();
128    let expr_pair = pair.into_inner().next().expect("program contains expr");
129    let expr = parse_expr(expr_pair, origin)?;
130    Ok((expr, end_pos))
131}
132
133fn pratt() -> PrattParser<Rule> {
134    PrattParser::new()
135        .op(Op::infix(Rule::op_or, Assoc::Left))
136        .op(Op::infix(Rule::op_and, Assoc::Left))
137        .op(Op::infix(Rule::op_eq, Assoc::Left))
138        .op(Op::infix(Rule::op_cmp, Assoc::Left))
139        .op(Op::infix(Rule::op_add, Assoc::Left))
140        .op(Op::infix(Rule::op_mul, Assoc::Left))
141        .op(Op::infix(Rule::op_pow, Assoc::Right))
142}
143
144fn parse_expr(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
145    match pair.as_rule() {
146        Rule::expr => {
147            let pairs = pair.into_inner();
148            pratt()
149                .map_primary(|p: Pair<Rule>| parse_unary(p, origin))
150                .map_infix(|lhs: Result<Expr>, op: Pair<Rule>, rhs: Result<Expr>| {
151                    let left = lhs?;
152                    let right = rhs?;
153                    let mut l = left;
154                    let mut r = right;
155                    let bop = match op.as_rule() {
156                        Rule::op_or => BinaryOp::Or,
157                        Rule::op_and => BinaryOp::And,
158                        Rule::op_eq => {
159                            let s = op.as_str();
160                            if s.contains("==") { BinaryOp::Eq } else { BinaryOp::Ne }
161                        }
162                        Rule::op_cmp => {
163                            let s = op.as_str();
164                            if s.contains("<=") {
165                                // a <= b  ==>  b >= a
166                                std::mem::swap(&mut l, &mut r);
167                                BinaryOp::Ge
168                            } else if s.contains(">=") {
169                                BinaryOp::Ge
170                            } else if s.contains('<') {
171                                BinaryOp::Lt
172                            } else {
173                                BinaryOp::Gt
174                            }
175                        }
176                        Rule::op_add => {
177                            if op.as_str().contains('-') {
178                                BinaryOp::Sub
179                            } else {
180                                BinaryOp::Add
181                            }
182                        }
183                        Rule::op_mul => {
184                            let s = op.as_str();
185                            if s.contains('*') {
186                                BinaryOp::Mul
187                            } else if s.contains('/') {
188                                BinaryOp::Div
189                            } else {
190                                BinaryOp::Mod
191                            }
192                        }
193                        Rule::op_pow => BinaryOp::Pow,
194                        r => {
195                            return Err(Error::InternalParserError(format!("unexpected infix op: {:?}", r)));
196                        }
197                    };
198                    Ok(Expr::Binary {
199                        left: Box::new(l),
200                        op: bop,
201                        right: Box::new(r),
202                    })
203                })
204                .parse(pairs)
205        }
206        _ => Err(Error::InternalParserError(format!("expected expr, got: {:?}", pair))),
207    }
208}
209
210fn parse_unary(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
211    match pair.as_rule() {
212        Rule::unary => {
213            let mut ops: Vec<UnaryOp> = Vec::new();
214            let mut inner = pair.into_inner();
215            // Collect zero or more unary_op then the postfix expression
216            while let Some(next) = inner.peek() {
217                if !matches!(next.as_rule(), Rule::unary_op) {
218                    break;
219                }
220                let op_pair = inner.next().unwrap();
221                let op_inner = op_pair.into_inner().next().unwrap();
222                let op = match op_inner.as_rule() {
223                    Rule::not_op => UnaryOp::Not,
224                    Rule::neg_op => UnaryOp::Neg,
225                    r => {
226                        return Err(Error::InternalParserError(format!("unexpected unary op: {:?}", r)));
227                    }
228                };
229                ops.push(op);
230            }
231            let post = inner.next().expect("unary must end with postfix");
232            let mut expr = parse_postfix(post, origin)?;
233            for op in ops.into_iter().rev() {
234                expr = Expr::Unary { op, expr: Box::new(expr) };
235            }
236            Ok(expr)
237        }
238        _ => parse_postfix(pair, origin),
239    }
240}
241
242fn parse_postfix(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
243    match pair.as_rule() {
244        Rule::postfix => {
245            let mut inner = pair.into_inner();
246            let first = inner.next().expect("postfix starts with primary");
247            let mut expr = parse_primary(first, origin)?;
248            for next in inner {
249                match next.as_rule() {
250                    Rule::call => {
251                        let args = parse_call_args(next, origin)?;
252                        expr = Expr::Call { callee: Box::new(expr), args };
253                    }
254                    Rule::index => {
255                        let idx_pair = next.into_inner().next().expect("index inner expr");
256                        let index_expr = parse_expr(idx_pair, origin)?;
257                        expr = Expr::Index {
258                            object: Box::new(expr),
259                            index: Box::new(index_expr),
260                        };
261                    }
262                    Rule::property => {
263                        let name = next.into_inner().next().expect("property ident").as_str().to_string();
264                        expr = Expr::Member { object: Box::new(expr), field: name };
265                    }
266                    r => {
267                        return Err(Error::InternalParserError(format!("unexpected postfix op: {:?}", r)));
268                    }
269                }
270            }
271            Ok(expr)
272        }
273        _ => parse_primary(pair, origin),
274    }
275}
276
277fn parse_call_args(pair: Pair<Rule>, origin: Origin) -> Result<Vec<Expr>> {
278    debug_assert_eq!(pair.as_rule(), Rule::call);
279    let mut args = Vec::new();
280    for p in pair.into_inner() {
281        // call contains expr separated by commas -> grammar emits only expr pairs inside
282        if matches!(p.as_rule(), Rule::expr) {
283            args.push(parse_expr(p, origin)?);
284        }
285    }
286    Ok(args)
287}
288
289fn parse_primary(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
290    match pair.as_rule() {
291        Rule::primary => parse_primary(pair.into_inner().next().unwrap(), origin),
292        Rule::parens => parse_expr(pair.into_inner().next().unwrap(), origin),
293        Rule::ident => Ok(Expr::Var(pair.as_str().to_string())),
294        Rule::number => parse_number(pair, origin),
295        Rule::boolean => {
296            let inner = pair.into_inner().next().unwrap();
297            let val = matches!(inner.as_rule(), Rule::true_kw);
298            Ok(Expr::Literal(Primitive::Bool(val)))
299        }
300        Rule::string => {
301            let s = unescape_string(&pair, origin)?;
302            Ok(Expr::Literal(Primitive::Str(s)))
303        }
304        Rule::list => parse_list(pair, origin),
305        Rule::dict => parse_dict(pair, origin),
306        r => Err(Error::InternalParserError(format!("unexpected primary op: {:?}", r))),
307    }
308}
309
310fn parse_number(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
311    let inner = pair.into_inner().next().unwrap();
312    match inner.as_rule() {
313        Rule::int => {
314            let s = inner.as_str();
315            let v: i64 = s.parse().map_err(|_| origin.at_pair(&inner, format!("integer literal out of range for a 64-bit signed integer: {}", s)))?;
316            Ok(Expr::Literal(Primitive::Int(v)))
317        }
318        Rule::float => {
319            let s = inner.as_str();
320            let v: f64 = s.parse().map_err(|_| origin.at_pair(&inner, format!("invalid float literal: {}", s)))?;
321            Ok(Expr::Literal(Primitive::Float(v)))
322        }
323        r => Err(Error::InternalParserError(format!("unexpected number: {:?}", r))),
324    }
325}
326
327fn parse_list(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
328    let mut elems = Vec::new();
329    for p in pair.into_inner() {
330        if let Rule::expr = p.as_rule() {
331            elems.push(parse_expr(p, origin)?);
332        }
333    }
334    Ok(Expr::ListLiteral(elems))
335}
336
337fn parse_dict(pair: Pair<Rule>, origin: Origin) -> Result<Expr> {
338    let mut items = Vec::new();
339    for p in pair.into_inner() {
340        if let Rule::pair = p.as_rule() {
341            let mut it = p.into_inner();
342            let key_pair = it.next().expect("pair key expr");
343            let key = parse_expr(key_pair, origin)?;
344            let value_pair = it.next().expect("pair value expr");
345            let value = parse_expr(value_pair, origin)?;
346            items.push((key, value));
347        }
348    }
349    Ok(Expr::DictLiteral(items))
350}
351
352fn unescape_string(pair: &Pair<Rule>, origin: Origin) -> Result<String> {
353    // The grammar guarantees matching single-byte quotes around the contents.
354    let src = pair.as_str();
355    let escape_char = src.chars().next().unwrap();
356    let inner = &src[1..src.len() - 1];
357    // Offsets from `char_indices` are into `inner`, so shift past the open quote to
358    // point at the backslash in the fragment.
359    let inner_start = pair.as_span().start() + 1;
360
361    let mut out = String::with_capacity(inner.len());
362    let mut chars = inner.char_indices();
363    while let Some((i, c)) = chars.next() {
364        if c == '\\' {
365            match chars.next() {
366                Some((_, 'n')) => out.push('\n'),
367                Some((_, '\\')) => out.push('\\'),
368                Some((_, next)) if next == escape_char => out.push(escape_char),
369                next => {
370                    let message = match next {
371                        Some((_, next)) => format!("unknown escape sequence: \\{}", next),
372                        None => "string literal ends in a trailing backslash".to_string(),
373                    };
374                    return Err(origin.at(inner_start + i, message));
375                }
376            }
377        } else {
378            out.push(c);
379        }
380    }
381    Ok(out)
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_interpolated_expr() {
390        let input = "123}x";
391        let (expr, idx) = parse_internal(Origin::whole(input), Rule::delimited_expr).unwrap();
392        assert_eq!(expr, Expr::Literal(Primitive::Int(123)));
393        assert_eq!(idx, 4);
394    }
395
396    /// `(line, column, offset, message)` of the parse error for `input`.
397    fn parse_failure(input: &str) -> (usize, usize, usize, String) {
398        match parse_expression(input).unwrap_err() {
399            Error::ParseError { line, column, offset, message, .. } => (line, column, offset, message),
400            other => panic!("expected a parse error, got: {:?}", other),
401        }
402    }
403
404    #[test]
405    fn parse_errors_carry_a_position() {
406        let (line, column, offset, message) = parse_failure("1 + + ");
407        assert_eq!((line, column, offset), (1, 5, 4));
408        assert_eq!(message, "expected unary");
409    }
410
411    /// The type renders its own prefix; the payload must not repeat it.
412    #[test]
413    fn parse_error_message_is_not_doubly_prefixed() {
414        let rendered = parse_expression("1 + + ").unwrap_err().to_string();
415        assert_eq!(rendered.matches("parse error").count(), 1);
416    }
417
418    /// The caret diagram is the useful part of a pest error, so it survives.
419    #[test]
420    fn parse_errors_keep_the_caret_diagram() {
421        let Error::ParseError { rendered, .. } = parse_expression("1 + + ").unwrap_err() else {
422            panic!("expected a parse error");
423        };
424        assert!(rendered.contains("1 | 1 + + "), "{}", rendered);
425        assert!(rendered.contains('^'), "{}", rendered);
426    }
427
428    #[test]
429    fn positions_count_lines_and_are_columns_within_them() {
430        let (line, column, offset, _) = parse_failure("1 +\n2 * * 3");
431        assert_eq!((line, column, offset), (2, 5, 8));
432    }
433
434    /// The grammar's `NEWLINE` is deliberately narrower than pest's builtin: it
435    /// omits the lone `\r`, which pest's position reporting does not count as a
436    /// line break. Accepting one would put the reported line out of step with the
437    /// line the parser is actually on.
438    #[test]
439    fn a_lone_carriage_return_is_not_whitespace() {
440        let (line, column, offset, _) = parse_failure("1 +\r2");
441        assert_eq!((line, column, offset), (1, 4, 3));
442
443        // And so does not end a comment, which runs to the end of the input.
444        assert!(parse_expression("1 + // c\r2").is_err());
445        assert!(parse_expression("1 + // c\r\n2").is_ok());
446        assert!(parse_expression("1 + // c\n2").is_ok());
447    }
448
449    /// Narrowing `NEWLINE` must not change what a string literal holds. A raw line
450    /// break inside one is preserved either way: `string` is not an atomic rule, so
451    /// before the change `WHITESPACE` skipped it and now `string_char` matches it,
452    /// and `unescape_string` reads the raw span regardless.
453    #[test]
454    fn raw_line_breaks_in_string_literals_are_preserved() {
455        for (input, expected) in [("'a\rb'", "a\rb"), ("'a\nb'", "a\nb")] {
456            let expr = parse_expression(input).unwrap_or_else(|e| panic!("{:?}: {}", input, e));
457            assert_eq!(expr, Expr::Literal(Primitive::Str(expected.to_string())), "input: {:?}", input);
458        }
459    }
460
461    /// Both accepted terminators have to count, and both have to leave the reported
462    /// offset slicing the caller's own input.
463    #[test]
464    fn newline_style_does_not_move_reported_positions() {
465        for input in ["'a'\n+ 'b' + 99999999999999999999", "'a'\r\n+ 'b' + 99999999999999999999"] {
466            let (line, column, offset, _) = parse_failure(input);
467            assert_eq!((line, column), (2, 9), "input: {:?}", input);
468            assert_eq!(&input[offset..], "99999999999999999999", "input: {:?}", input);
469        }
470    }
471
472    /// The diagram has to agree with the numbers reported beside it.
473    #[test]
474    fn rendered_diagram_splits_on_newlines() {
475        let Error::ParseError { rendered, .. } = parse_expression("1 +\r\n2 * * 3").unwrap_err() else {
476            panic!("expected a parse error");
477        };
478        assert!(rendered.contains("2 | 2 * * 3"), "{}", rendered);
479    }
480
481    /// A literal too large for an `i64` is a user error, not an internal one.
482    #[test]
483    fn integer_overflow_points_at_the_literal() {
484        let (line, column, _, message) = parse_failure("1 + 99999999999999999999");
485        assert_eq!((line, column), (1, 5));
486        assert!(message.starts_with("integer literal out of range"), "{}", message);
487    }
488
489    /// `primary` tries `boolean` before `ident`, so the boolean literals need a
490    /// trailing word boundary or every identifier starting with one of them is a
491    /// parse error.
492    #[test]
493    fn identifiers_may_start_with_a_boolean_literal() {
494        for name in ["trueish", "true_", "true1", "falsey", "false_value", "false2"] {
495            let expr = parse_expression(name).unwrap_or_else(|e| panic!("{:?}: {}", name, e));
496            assert_eq!(expr, Expr::Var(name.to_string()), "input: {:?}", name);
497        }
498
499        // and the literals themselves still parse as literals
500        assert_eq!(parse_expression("true").unwrap(), Expr::Literal(Primitive::Bool(true)));
501        assert_eq!(parse_expression("false").unwrap(), Expr::Literal(Primitive::Bool(false)));
502        assert_eq!(parse_expression("(true)").unwrap(), Expr::Literal(Primitive::Bool(true)));
503
504        // a boundary that is not alphanumeric still ends the literal
505        assert_eq!(
506            parse_expression("true.length").unwrap(),
507            Expr::Member {
508                object: Box::new(Expr::Literal(Primitive::Bool(true))),
509                field: "length".to_string(),
510            }
511        );
512    }
513
514    #[test]
515    fn unknown_escape_points_at_the_backslash() {
516        let (line, column, offset, message) = parse_failure(r"'a\tb'");
517        assert_eq!((line, column, offset), (1, 3, 2));
518        assert_eq!(message, r"unknown escape sequence: \t");
519    }
520}