Skip to main content

panproto_expr_parser/
parser.rs

1//! Chumsky parser producing `panproto_expr::Expr` from the token stream.
2//!
3//! Uses Pratt parsing for operator precedence and recursive descent for
4//! the rest. Layout tokens (`Indent`/`Dedent`/`Newline`) from the lexer
5//! are consumed directly as delimiters for layout-sensitive blocks.
6
7use std::sync::Arc;
8
9use chumsky::input::{Input as _, Stream, ValueInput};
10use chumsky::pratt::{infix, left, prefix, right};
11use chumsky::prelude::*;
12use chumsky::span::SimpleSpan;
13
14use panproto_expr::{BuiltinOp, Expr, Literal, Pattern};
15
16use crate::token::Token;
17
18/// A parse error.
19pub type ParseError = Rich<'static, Token, SimpleSpan>;
20
21/// Parse a token stream into an `Expr`.
22///
23/// The input should come from [`crate::tokenize`].
24///
25/// # Errors
26///
27/// Returns parse errors with source spans on failure.
28pub fn parse(tokens: &[crate::Spanned]) -> Result<Expr, Vec<ParseError>> {
29    let mapped: Vec<(Token, SimpleSpan)> = tokens
30        .iter()
31        .filter(|s| s.token != Token::Eof)
32        .map(|s| (s.token.clone(), SimpleSpan::new(s.span.start, s.span.end)))
33        .collect();
34    let eoi = tokens.last().map_or_else(
35        || SimpleSpan::new(0, 0),
36        |s| SimpleSpan::new(s.span.start, s.span.end),
37    );
38    let stream = Stream::from_iter(mapped).map(eoi, |(tok, span)| (tok, span));
39    expr_parser().parse(stream).into_result().map_err(|errs| {
40        errs.into_iter()
41            .map(chumsky::error::Rich::into_owned)
42            .collect()
43    })
44}
45
46// ── Token matchers ──────────────────────────────────────────────────
47
48/// Match an identifier and return its name.
49fn ident<'t, 'src: 't, I>()
50-> impl Parser<'t, I, Arc<str>, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
51where
52    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
53{
54    select! { Token::Ident(s) => Arc::from(s.as_str()) }.labelled("identifier")
55}
56
57/// Match an upper-case identifier.
58fn upper_ident<'t, 'src: 't, I>()
59-> impl Parser<'t, I, Arc<str>, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
60where
61    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
62{
63    select! { Token::UpperIdent(s) => Arc::from(s.as_str()) }.labelled("constructor")
64}
65
66// ── Layout blocks ───────────────────────────────────────────────────
67
68/// Parse a layout block: either `{ item ; item ; ... }` or
69/// `INDENT item NEWLINE item ... DEDENT`.
70fn layout_block<'t, 'src: 't, I, T: 't>(
71    item: impl Parser<'t, I, T, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone,
72) -> impl Parser<'t, I, Vec<T>, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
73where
74    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
75{
76    let explicit = item
77        .clone()
78        .separated_by(just(Token::Newline).or(just(Token::Comma)))
79        .allow_trailing()
80        .collect::<Vec<_>>()
81        .delimited_by(just(Token::LBrace), just(Token::RBrace));
82
83    let implicit = item
84        .separated_by(just(Token::Newline))
85        .allow_trailing()
86        .collect::<Vec<_>>()
87        .delimited_by(just(Token::Indent), just(Token::Dedent));
88
89    explicit.or(implicit)
90}
91
92// ── Pattern parser ──────────────────────────────────────────────────
93
94/// Parse a pattern.
95fn pattern_parser<'t, 'src: 't, I>()
96-> impl Parser<'t, I, Pattern, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
97where
98    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
99{
100    recursive(|pat| {
101        let wildcard = select! { Token::Ident(s) if s == "_" => Pattern::Wildcard };
102
103        let var = ident().map(Pattern::Var);
104
105        let literal_pat = literal_parser().map(Pattern::Lit);
106
107        let paren = pat
108            .clone()
109            .delimited_by(just(Token::LParen), just(Token::RParen));
110
111        let list_pat = pat
112            .clone()
113            .separated_by(just(Token::Comma))
114            .collect::<Vec<_>>()
115            .delimited_by(just(Token::LBracket), just(Token::RBracket))
116            .map(Pattern::List);
117
118        let field_pat = ident()
119            .then(just(Token::Eq).ignore_then(pat.clone()).or_not())
120            .map(|(name, maybe_pat): (Arc<str>, Option<Pattern>)| {
121                let p = maybe_pat.unwrap_or_else(|| Pattern::Var(name.clone()));
122                (name, p)
123            });
124
125        let record_pat = field_pat
126            .separated_by(just(Token::Comma))
127            .collect::<Vec<_>>()
128            .delimited_by(just(Token::LBrace), just(Token::RBrace))
129            .map(Pattern::Record);
130
131        let constructor = upper_ident()
132            .then(pat.clone().repeated().collect::<Vec<_>>())
133            .map(|(name, args): (Arc<str>, Vec<Pattern>)| Pattern::Constructor(name, args));
134
135        choice((
136            wildcard,
137            literal_pat,
138            paren,
139            list_pat,
140            record_pat,
141            constructor,
142            var,
143        ))
144    })
145}
146
147// ── Literal parser ──────────────────────────────────────────────────
148
149/// Parse a literal value.
150fn literal_parser<'t, 'src: 't, I>()
151-> impl Parser<'t, I, Literal, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
152where
153    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
154{
155    select! {
156        Token::Int(n) => Literal::Int(n),
157        Token::Float(f) => Literal::Float(f),
158        Token::Str(s) => Literal::Str(s),
159        Token::True => Literal::Bool(true),
160        Token::False => Literal::Bool(false),
161        Token::Nothing => Literal::Null,
162    }
163    .labelled("literal")
164}
165
166// ── Builtin name → op mapping ───────────────────────────────────────
167
168/// Resolve a lowercase identifier to a builtin op, if any.
169fn resolve_builtin(name: &str) -> Option<BuiltinOp> {
170    BuiltinOp::from_name(name)
171}
172
173// ── Expression parser ───────────────────────────────────────────────
174
175/// Top-level expression parser.
176///
177/// This is a single `chumsky::recursive` combinator defining the entire
178/// expression grammar: literals, variables, list literals/comprehensions,
179/// function application, the pratt operator table, lambdas, let, if/case,
180/// do-notation, and where-clauses. Extracting individual sub-parsers
181/// would require forwarding the recursive `expr` parser by reference
182/// with a verbose `impl Parser<…> + Clone` bound at each call site, which
183/// would hurt rather than help readability. The grammar is kept inline
184/// and the length lint is silenced locally.
185#[allow(clippy::too_many_lines)]
186fn expr_parser<'t, 'src: 't, I>()
187-> impl Parser<'t, I, Expr, extra::Err<Rich<'t, Token, SimpleSpan>>> + Clone
188where
189    I: ValueInput<'t, Token = Token, Span = SimpleSpan>,
190{
191    recursive(|expr| {
192        let pattern = pattern_parser();
193
194        // ── Atoms ───────────────────────────────────────────
195
196        let lit = literal_parser().map(Expr::Lit);
197
198        let var_or_builtin = ident().map(Expr::Var);
199
200        let constructor = upper_ident().map(Expr::Var);
201
202        let paren_expr = expr
203            .clone()
204            .delimited_by(just(Token::LParen), just(Token::RParen));
205
206        // List literal or comprehension
207        let list_expr = {
208            let plain_list = expr
209                .clone()
210                .separated_by(just(Token::Comma))
211                .collect::<Vec<_>>()
212                .map(Expr::List);
213
214            // List comprehension: [e | x <- xs, pred]
215            let comprehension = expr
216                .clone()
217                .then_ignore(just(Token::Pipe))
218                .then(
219                    ident()
220                        .then_ignore(just(Token::LeftArrow))
221                        .then(expr.clone())
222                        .map(|(n, e): (Arc<str>, Expr)| Qual::Generator(n, e))
223                        .or(expr.clone().map(Qual::Guard))
224                        .separated_by(just(Token::Comma))
225                        .at_least(1)
226                        .collect::<Vec<Qual>>(),
227                )
228                .map(|(body, quals): (Expr, Vec<Qual>)| desugar_comprehension(body, &quals));
229
230            // Range: [1..10]. Lowers to the `range` builtin, which
231            // constructs the list and charges its length against the
232            // evaluator's list budget. An open-ended `[1..]` is rejected:
233            // the language has no lazy lists, so there is nothing correct
234            // to lower it to, and the alternative of quietly yielding the
235            // one-element list `[1]` is a wrong answer rather than a
236            // missing feature.
237            let range = expr
238                .clone()
239                .then_ignore(just(Token::DotDot))
240                .then(expr.clone().or_not())
241                .validate(|(start, end): (Expr, Option<Expr>), extra, emitter| {
242                    if let Some(stop) = end {
243                        return Expr::Builtin(BuiltinOp::Range, vec![start, stop]);
244                    }
245                    emitter.emit(Rich::custom(
246                        extra.span(),
247                        "open-ended range `[a..]` is not supported: the expression \
248                         language has no lazy lists. Give an upper bound, as in `[a..b]`.",
249                    ));
250                    // Emitting rather than failing keeps this branch the winning
251                    // alternative, so the message above survives instead of being
252                    // masked by a backtrack into the plain-list parser. The value
253                    // is never evaluated: parsing fails on the emitted error.
254                    Expr::List(vec![start])
255                });
256
257            choice((comprehension, range, plain_list))
258                .delimited_by(just(Token::LBracket), just(Token::RBracket))
259        };
260
261        // Record literal
262        let record_expr = {
263            let field_bind = ident()
264                .then(just(Token::Eq).ignore_then(expr.clone()).or_not())
265                .map(|(name, val): (Arc<str>, Option<Expr>)| {
266                    let v = val.unwrap_or_else(|| Expr::Var(name.clone()));
267                    (name, v)
268                });
269
270            field_bind
271                .separated_by(just(Token::Comma))
272                .allow_trailing()
273                .collect::<Vec<_>>()
274                .delimited_by(just(Token::LBrace), just(Token::RBrace))
275                .map(Expr::Record)
276        };
277
278        let atom = choice((
279            lit,
280            paren_expr,
281            list_expr,
282            record_expr,
283            constructor,
284            var_or_builtin,
285        ));
286
287        // ── Postfix: field access (.field) and edge traversal (->edge) ──
288
289        let postfix_chain = atom.foldl(
290            choice((
291                just(Token::Dot).ignore_then(ident()).map(PostfixOp::Field),
292                just(Token::Arrow).ignore_then(ident()).map(PostfixOp::Edge),
293            ))
294            .repeated(),
295            |expr, postfix| match postfix {
296                PostfixOp::Field(name) => Expr::Field(Box::new(expr), name),
297                PostfixOp::Edge(edge) => Expr::Builtin(
298                    BuiltinOp::Edge,
299                    vec![expr, Expr::Lit(Literal::Str(edge.to_string()))],
300                ),
301            },
302        );
303
304        // ── Application (juxtaposition) ─────────────────────
305
306        let app = postfix_chain
307            .clone()
308            .foldl(postfix_chain.repeated(), resolve_application);
309
310        // ── Pratt parser for infix/prefix operators ─────────
311
312        let pratt = app.pratt((
313            // Precedence 1: pipe (&)
314            infix(left(1), just(Token::Ampersand), |l, _, r, _| {
315                Expr::App(Box::new(r), Box::new(l))
316            }),
317            // Precedence 3: logical or
318            infix(left(3), just(Token::OrOr), |l, _, r, _| {
319                Expr::Builtin(BuiltinOp::Or, vec![l, r])
320            }),
321            // Precedence 4: logical and
322            infix(left(4), just(Token::AndAnd), |l, _, r, _| {
323                Expr::Builtin(BuiltinOp::And, vec![l, r])
324            }),
325            // Precedence 5: comparison
326            infix(right(5), just(Token::EqEq), |l, _, r, _| {
327                Expr::Builtin(BuiltinOp::Eq, vec![l, r])
328            }),
329            infix(right(5), just(Token::Neq), |l, _, r, _| {
330                Expr::Builtin(BuiltinOp::Neq, vec![l, r])
331            }),
332            infix(right(5), just(Token::Lt), |l, _, r, _| {
333                Expr::Builtin(BuiltinOp::Lt, vec![l, r])
334            }),
335            infix(right(5), just(Token::Lte), |l, _, r, _| {
336                Expr::Builtin(BuiltinOp::Lte, vec![l, r])
337            }),
338            infix(right(5), just(Token::Gt), |l, _, r, _| {
339                Expr::Builtin(BuiltinOp::Gt, vec![l, r])
340            }),
341            infix(right(5), just(Token::Gte), |l, _, r, _| {
342                Expr::Builtin(BuiltinOp::Gte, vec![l, r])
343            }),
344            // Precedence 6: string concat
345            infix(right(6), just(Token::PlusPlus), |l, _, r, _| {
346                Expr::Builtin(BuiltinOp::Concat, vec![l, r])
347            }),
348            // Precedence 7: addition/subtraction
349            infix(left(7), just(Token::Plus), |l, _, r, _| {
350                Expr::Builtin(BuiltinOp::Add, vec![l, r])
351            }),
352            infix(left(7), just(Token::Minus), |l, _, r, _| {
353                Expr::Builtin(BuiltinOp::Sub, vec![l, r])
354            }),
355            // Precedence 8: multiplication/division
356            infix(left(8), just(Token::Star), |l, _, r, _| {
357                Expr::Builtin(BuiltinOp::Mul, vec![l, r])
358            }),
359            infix(left(8), just(Token::Slash), |l, _, r, _| {
360                Expr::Builtin(BuiltinOp::Div, vec![l, r])
361            }),
362            infix(left(8), just(Token::Percent), |l, _, r, _| {
363                Expr::Builtin(BuiltinOp::Mod, vec![l, r])
364            }),
365            infix(left(8), just(Token::ModKw), |l, _, r, _| {
366                Expr::Builtin(BuiltinOp::Mod, vec![l, r])
367            }),
368            infix(left(8), just(Token::DivKw), |l, _, r, _| {
369                Expr::Builtin(BuiltinOp::Div, vec![l, r])
370            }),
371            // Precedence 9: unary prefix
372            prefix(9, just(Token::Minus), |_, rhs, _| {
373                Expr::Builtin(BuiltinOp::Neg, vec![rhs])
374            }),
375            prefix(9, just(Token::Not), |_, rhs, _| {
376                Expr::Builtin(BuiltinOp::Not, vec![rhs])
377            }),
378        ));
379
380        // ── Compound expressions ────────────────────────────
381
382        // Lambda: \x y -> body
383        let lambda = just(Token::Backslash)
384            .ignore_then(
385                pattern
386                    .clone()
387                    .repeated()
388                    .at_least(1)
389                    .collect::<Vec<Pattern>>(),
390            )
391            .then_ignore(just(Token::Arrow))
392            .then(expr.clone())
393            .map(|(params, body): (Vec<Pattern>, Expr)| desugar_lambda(&params, body));
394
395        // Let binding
396        let let_bind = ident()
397            .then(pattern.clone().repeated().collect::<Vec<Pattern>>())
398            .then_ignore(just(Token::Eq))
399            .then(expr.clone())
400            .map(|((name, params), val): ((Arc<str>, Vec<Pattern>), Expr)| {
401                if params.is_empty() {
402                    (name, val)
403                } else {
404                    (name, desugar_lambda(&params, val))
405                }
406            });
407
408        let let_expr = just(Token::Let)
409            .ignore_then(layout_block(let_bind.clone()).or(let_bind.clone().map(|b| vec![b])))
410            .then_ignore(just(Token::In))
411            .then(expr.clone())
412            .map(|(binds, body)| desugar_let_binds(binds, body));
413
414        // If-then-else
415        let if_expr = just(Token::If)
416            .ignore_then(expr.clone())
417            .then_ignore(just(Token::Then))
418            .then(expr.clone())
419            .then_ignore(just(Token::Else))
420            .then(expr.clone())
421            .map(|((cond, then_branch), else_branch)| Expr::Match {
422                scrutinee: Box::new(cond),
423                arms: vec![
424                    (Pattern::Lit(Literal::Bool(true)), then_branch),
425                    (Pattern::Wildcard, else_branch),
426                ],
427            });
428
429        // Case-of
430        let case_arm = pattern
431            .clone()
432            .then_ignore(just(Token::Arrow))
433            .then(expr.clone());
434
435        let case_expr = just(Token::Case)
436            .ignore_then(expr.clone())
437            .then_ignore(just(Token::Of))
438            .then(layout_block(case_arm))
439            .map(|(scrutinee, arms)| Expr::Match {
440                scrutinee: Box::new(scrutinee),
441                arms,
442            });
443
444        // Do-notation
445        let do_stmt = choice((
446            ident()
447                .then_ignore(just(Token::LeftArrow))
448                .then(expr.clone())
449                .map(|(name, e): (Arc<str>, Expr)| DoStmt::Bind(name, e)),
450            just(Token::Let)
451                .ignore_then(let_bind.clone())
452                .map(|(name, val)| DoStmt::Let(name, val)),
453            expr.clone().map(DoStmt::Expr),
454        ));
455
456        let do_expr = just(Token::Do)
457            .ignore_then(layout_block(do_stmt))
458            .map(desugar_do);
459
460        // ── Combine all expression forms ────────────────────
461
462        let full_expr = choice((do_expr, let_expr, if_expr, case_expr, lambda, pratt));
463
464        // Where clause as postfix
465        let where_bind = ident()
466            .then(pattern.repeated().collect::<Vec<Pattern>>())
467            .then_ignore(just(Token::Eq))
468            .then(expr.clone())
469            .map(|((name, params), val): ((Arc<str>, Vec<Pattern>), Expr)| {
470                if params.is_empty() {
471                    (name, val)
472                } else {
473                    (name, desugar_lambda(&params, val))
474                }
475            });
476
477        let where_clause = just(Token::Where)
478            .ignore_then(layout_block(where_bind.clone()).or(where_bind.map(|b| vec![b])));
479
480        full_expr
481            .then(where_clause.or_not())
482            .map(|(body, where_binds)| match where_binds {
483                Some(binds) => desugar_let_binds(binds, body),
484                None => body,
485            })
486    })
487}
488
489// ── Helper types ────────────────────────────────────────────────────
490
491/// Postfix operation.
492#[derive(Debug, Clone)]
493enum PostfixOp {
494    /// `.field`
495    Field(Arc<str>),
496    /// `->edge`
497    Edge(Arc<str>),
498}
499
500/// List comprehension qualifier.
501#[derive(Debug, Clone)]
502enum Qual {
503    /// `x <- xs`
504    Generator(Arc<str>, Expr),
505    /// Predicate.
506    Guard(Expr),
507}
508
509/// Do-notation statement.
510#[derive(Debug, Clone)]
511enum DoStmt {
512    /// `x <- e`
513    Bind(Arc<str>, Expr),
514    /// `let x = e`
515    Let(Arc<str>, Expr),
516    /// Bare expression.
517    Expr(Expr),
518}
519
520// ── Desugaring helpers ──────────────────────────────────────────────
521
522/// Desugar `\p1 p2 ... -> body` into nested lambdas.
523fn desugar_lambda(params: &[Pattern], body: Expr) -> Expr {
524    params.iter().rev().fold(body, |acc, pat| match pat {
525        Pattern::Var(name) => Expr::Lam(name.clone(), Box::new(acc)),
526        Pattern::Wildcard => Expr::Lam(Arc::from("_"), Box::new(acc)),
527        other => {
528            let fresh: Arc<str> = Arc::from("_arg");
529            Expr::Lam(
530                fresh.clone(),
531                Box::new(Expr::Match {
532                    scrutinee: Box::new(Expr::Var(fresh)),
533                    arms: vec![(other.clone(), acc)],
534                }),
535            )
536        }
537    })
538}
539
540/// Desugar `let a = e1; b = e2 in body` into nested `Let`.
541fn desugar_let_binds(binds: Vec<(Arc<str>, Expr)>, body: Expr) -> Expr {
542    binds
543        .into_iter()
544        .rev()
545        .fold(body, |acc, (name, val)| Expr::Let {
546            name,
547            value: Box::new(val),
548            body: Box::new(acc),
549        })
550}
551
552/// Desugar list comprehension `[e | quals]` into `flatMap`/guard.
553fn desugar_comprehension(body: Expr, quals: &[Qual]) -> Expr {
554    quals
555        .iter()
556        .rev()
557        .fold(Expr::List(vec![body]), |acc, qual| match qual {
558            Qual::Generator(name, source) => Expr::Builtin(
559                BuiltinOp::FlatMap,
560                vec![source.clone(), Expr::Lam(name.clone(), Box::new(acc))],
561            ),
562            Qual::Guard(pred) => Expr::Match {
563                scrutinee: Box::new(pred.clone()),
564                arms: vec![
565                    (Pattern::Lit(Literal::Bool(true)), acc),
566                    (Pattern::Wildcard, Expr::List(vec![])),
567                ],
568            },
569        })
570}
571
572/// Desugar do-notation into nested `flatMap`/`let`.
573fn desugar_do(stmts: Vec<DoStmt>) -> Expr {
574    if stmts.is_empty() {
575        return Expr::List(vec![]);
576    }
577    let mut iter = stmts.into_iter().rev();
578    // Safety: we checked `is_empty()` above, so `next()` always returns `Some`.
579    let Some(last) = iter.next() else {
580        return Expr::List(vec![]);
581    };
582    let init = match last {
583        DoStmt::Expr(e) | DoStmt::Bind(_, e) => e,
584        DoStmt::Let(name, val) => Expr::Let {
585            name,
586            value: Box::new(val),
587            body: Box::new(Expr::List(vec![])),
588        },
589    };
590    iter.fold(init, |acc, stmt| match stmt {
591        DoStmt::Bind(name, source) => Expr::Builtin(
592            BuiltinOp::FlatMap,
593            vec![source, Expr::Lam(name, Box::new(acc))],
594        ),
595        DoStmt::Let(name, val) => Expr::Let {
596            name,
597            value: Box::new(val),
598            body: Box::new(acc),
599        },
600        DoStmt::Expr(e) => Expr::Builtin(
601            BuiltinOp::FlatMap,
602            vec![e, Expr::Lam(Arc::from("_"), Box::new(acc))],
603        ),
604    })
605}
606
607/// Permute a higher-order list builtin's arguments from surface order
608/// into evaluator order.
609///
610/// The surface syntax follows the usual functional convention of naming
611/// the function first (`map f xs`, `fold f z xs`), while [`Expr::Builtin`]
612/// takes the list first and the function last. The two orders are
613/// deliberately distinct: `Expr` is serialized into stored lens
614/// documents, so its argument order is the compatibility-bearing one and
615/// the surface syntax lowers into it.
616///
617/// Applied only once the builtin is saturated, since a partial
618/// application has no complete order to permute. Builtins outside this
619/// set take their arguments in the same order at both layers and pass
620/// through untouched.
621fn lower_list_builtin_args(op: BuiltinOp, args: Vec<Expr>) -> Vec<Expr> {
622    op.surface_args_to_expr_args(args)
623}
624
625/// Resolve function application, detecting builtin names.
626fn resolve_application(func: Expr, arg: Expr) -> Expr {
627    match &func {
628        Expr::Var(name) => {
629            if let Some(op) = resolve_builtin(name) {
630                let args = lower_list_builtin_args(op, vec![arg]);
631                Expr::Builtin(op, args)
632            } else {
633                Expr::App(Box::new(func), Box::new(arg))
634            }
635        }
636        Expr::Builtin(op, args) if args.len() < op.arity() => {
637            let mut new_args = args.clone();
638            new_args.push(arg);
639            Expr::Builtin(*op, lower_list_builtin_args(*op, new_args))
640        }
641        _ => Expr::App(Box::new(func), Box::new(arg)),
642    }
643}
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use crate::tokenize;
649
650    fn parse_ok(input: &str) -> Expr {
651        let tokens = tokenize(input).unwrap_or_else(|e| panic!("lex failed: {e}"));
652        parse(&tokens).unwrap_or_else(|e| panic!("parse failed: {e:?}"))
653    }
654
655    #[test]
656    fn parse_literal_int() {
657        assert_eq!(parse_ok("42"), Expr::Lit(Literal::Int(42)));
658    }
659
660    #[test]
661    fn parse_literal_string() {
662        assert_eq!(
663            parse_ok(r#""hello""#),
664            Expr::Lit(Literal::Str("hello".into()))
665        );
666    }
667
668    #[test]
669    fn parse_literal_bool() {
670        assert_eq!(parse_ok("True"), Expr::Lit(Literal::Bool(true)));
671        assert_eq!(parse_ok("False"), Expr::Lit(Literal::Bool(false)));
672    }
673
674    #[test]
675    fn parse_nothing() {
676        assert_eq!(parse_ok("Nothing"), Expr::Lit(Literal::Null));
677    }
678
679    #[test]
680    fn parse_variable() {
681        assert_eq!(parse_ok("x"), Expr::Var(Arc::from("x")));
682    }
683
684    #[test]
685    fn parse_arithmetic() {
686        assert_eq!(
687            parse_ok("1 + 2"),
688            Expr::Builtin(
689                BuiltinOp::Add,
690                vec![Expr::Lit(Literal::Int(1)), Expr::Lit(Literal::Int(2))]
691            )
692        );
693    }
694
695    #[test]
696    fn parse_precedence() {
697        assert_eq!(
698            parse_ok("1 + 2 * 3"),
699            Expr::Builtin(
700                BuiltinOp::Add,
701                vec![
702                    Expr::Lit(Literal::Int(1)),
703                    Expr::Builtin(
704                        BuiltinOp::Mul,
705                        vec![Expr::Lit(Literal::Int(2)), Expr::Lit(Literal::Int(3))]
706                    ),
707                ]
708            )
709        );
710    }
711
712    #[test]
713    fn parse_comparison() {
714        assert_eq!(
715            parse_ok("x == 1"),
716            Expr::Builtin(
717                BuiltinOp::Eq,
718                vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))]
719            )
720        );
721    }
722
723    #[test]
724    fn parse_logical() {
725        assert_eq!(
726            parse_ok("a && b || c"),
727            Expr::Builtin(
728                BuiltinOp::Or,
729                vec![
730                    Expr::Builtin(
731                        BuiltinOp::And,
732                        vec![Expr::Var(Arc::from("a")), Expr::Var(Arc::from("b"))]
733                    ),
734                    Expr::Var(Arc::from("c")),
735                ]
736            )
737        );
738    }
739
740    #[test]
741    fn parse_negation() {
742        assert_eq!(
743            parse_ok("-x"),
744            Expr::Builtin(BuiltinOp::Neg, vec![Expr::Var(Arc::from("x"))])
745        );
746    }
747
748    #[test]
749    fn parse_not() {
750        assert_eq!(
751            parse_ok("not True"),
752            Expr::Builtin(BuiltinOp::Not, vec![Expr::Lit(Literal::Bool(true))])
753        );
754    }
755
756    #[test]
757    fn parse_field_access() {
758        assert_eq!(
759            parse_ok("x.name"),
760            Expr::Field(Box::new(Expr::Var(Arc::from("x"))), Arc::from("name"))
761        );
762    }
763
764    #[test]
765    fn parse_edge_traversal() {
766        assert_eq!(
767            parse_ok("doc -> layers"),
768            Expr::Builtin(
769                BuiltinOp::Edge,
770                vec![
771                    Expr::Var(Arc::from("doc")),
772                    Expr::Lit(Literal::Str("layers".into())),
773                ]
774            )
775        );
776    }
777
778    #[test]
779    fn parse_lambda() {
780        assert_eq!(
781            parse_ok("\\x -> x + 1"),
782            Expr::Lam(
783                Arc::from("x"),
784                Box::new(Expr::Builtin(
785                    BuiltinOp::Add,
786                    vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))]
787                ))
788            )
789        );
790    }
791
792    #[test]
793    fn parse_multi_param_lambda() {
794        let e = parse_ok("\\x y -> x + y");
795        match &e {
796            Expr::Lam(x, inner) => {
797                assert_eq!(&**x, "x");
798                assert!(matches!(&**inner, Expr::Lam(y, _) if &**y == "y"));
799            }
800            _ => panic!("expected nested Lam, got {e:?}"),
801        }
802    }
803
804    #[test]
805    fn parse_let_in() {
806        assert_eq!(
807            parse_ok("let x = 1 in x + 1"),
808            Expr::Let {
809                name: Arc::from("x"),
810                value: Box::new(Expr::Lit(Literal::Int(1))),
811                body: Box::new(Expr::Builtin(
812                    BuiltinOp::Add,
813                    vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))]
814                )),
815            }
816        );
817    }
818
819    #[test]
820    fn parse_if_then_else() {
821        let e = parse_ok("if True then 1 else 0");
822        assert!(matches!(e, Expr::Match { .. }));
823    }
824
825    #[test]
826    fn parse_case_of() {
827        let e = parse_ok("case x of\n  True -> 1\n  False -> 0");
828        match e {
829            Expr::Match { arms, .. } => assert_eq!(arms.len(), 2),
830            _ => panic!("expected Match"),
831        }
832    }
833
834    #[test]
835    fn parse_list() {
836        assert_eq!(
837            parse_ok("[1, 2, 3]"),
838            Expr::List(vec![
839                Expr::Lit(Literal::Int(1)),
840                Expr::Lit(Literal::Int(2)),
841                Expr::Lit(Literal::Int(3)),
842            ])
843        );
844    }
845
846    #[test]
847    fn parse_empty_list() {
848        assert_eq!(parse_ok("[]"), Expr::List(vec![]));
849    }
850
851    #[test]
852    fn parse_record() {
853        assert_eq!(
854            parse_ok("{ name = x, age = 30 }"),
855            Expr::Record(vec![
856                (Arc::from("name"), Expr::Var(Arc::from("x"))),
857                (Arc::from("age"), Expr::Lit(Literal::Int(30))),
858            ])
859        );
860    }
861
862    #[test]
863    fn parse_record_punning() {
864        assert_eq!(
865            parse_ok("{ name, age }"),
866            Expr::Record(vec![
867                (Arc::from("name"), Expr::Var(Arc::from("name"))),
868                (Arc::from("age"), Expr::Var(Arc::from("age"))),
869            ])
870        );
871    }
872
873    #[test]
874    #[allow(clippy::expect_used)]
875    fn ranges_lower_to_the_range_builtin_and_evaluate() {
876        let config = panproto_expr::EvalConfig::default();
877        let eval =
878            |src: &str| panproto_expr::eval(&parse_ok(src), &panproto_expr::Env::new(), &config);
879
880        assert_eq!(
881            parse_ok("[1..3]"),
882            Expr::Builtin(
883                BuiltinOp::Range,
884                vec![Expr::Lit(Literal::Int(1)), Expr::Lit(Literal::Int(3)),]
885            )
886        );
887        assert_eq!(
888            eval("[1..3]").expect("range should evaluate"),
889            panproto_expr::Literal::List(vec![
890                panproto_expr::Literal::Int(1),
891                panproto_expr::Literal::Int(2),
892                panproto_expr::Literal::Int(3),
893            ]),
894            "both bounds are inclusive"
895        );
896        assert_eq!(
897            eval("[0..0]").expect("singleton range should evaluate"),
898            panproto_expr::Literal::List(vec![panproto_expr::Literal::Int(0)])
899        );
900        assert_eq!(
901            eval("[3..1]").expect("descending range should evaluate"),
902            panproto_expr::Literal::List(vec![]),
903            "a descending range is empty, not an error"
904        );
905        // `range a b` names the same builtin as the bracket syntax.
906        assert_eq!(parse_ok("range 1 3"), parse_ok("[1..3]"));
907    }
908
909    #[test]
910    #[allow(clippy::expect_used)]
911    fn ranges_compose_with_the_list_builtins() {
912        let config = panproto_expr::EvalConfig::default();
913        let eval =
914            |src: &str| panproto_expr::eval(&parse_ok(src), &panproto_expr::Env::new(), &config);
915
916        assert_eq!(
917            eval("map (\\x -> x * x) [1..4]").expect("map over a range should evaluate"),
918            panproto_expr::Literal::List(vec![
919                panproto_expr::Literal::Int(1),
920                panproto_expr::Literal::Int(4),
921                panproto_expr::Literal::Int(9),
922                panproto_expr::Literal::Int(16),
923            ])
924        );
925        assert_eq!(
926            eval("fold (\\a -> \\b -> a + b) 0 [1..100]").expect("fold over a range"),
927            panproto_expr::Literal::Int(5050)
928        );
929    }
930
931    #[test]
932    fn an_oversized_range_is_rejected_before_it_allocates() {
933        // Range is the one builtin that turns a constant-size expression
934        // into an arbitrarily long list, so its length is checked against
935        // the list budget rather than discovered after allocating.
936        let config = panproto_expr::EvalConfig::default();
937        let result = panproto_expr::eval(
938            &parse_ok("[0..99999999]"),
939            &panproto_expr::Env::new(),
940            &config,
941        );
942        assert!(
943            matches!(result, Err(panproto_expr::ExprError::ListLengthExceeded(_))),
944            "expected ListLengthExceeded, got {result:?}"
945        );
946    }
947
948    #[test]
949    #[allow(clippy::expect_used)]
950    fn an_open_ended_range_is_rejected() {
951        // There are no lazy lists to lower `[1..]` to. It previously
952        // parsed to the one-element list `[1]`, which is a wrong answer
953        // rather than a missing feature.
954        let tokens = tokenize("[1..]").unwrap_or_else(|e| panic!("lex failed: {e}"));
955        let errs = parse(&tokens).expect_err("an open-ended range must not parse");
956        let rendered = errs
957            .iter()
958            .map(std::string::ToString::to_string)
959            .collect::<Vec<_>>()
960            .join("; ");
961        assert!(
962            rendered.contains("open-ended range"),
963            "the error should name the construct, got: {rendered}"
964        );
965    }
966
967    #[test]
968    fn parse_builtin_application() {
969        // Surface order is `map f xs`; the stored form is list-first,
970        // which is the order `eval_map` reads.
971        assert_eq!(
972            parse_ok("map f xs"),
973            Expr::Builtin(
974                BuiltinOp::Map,
975                vec![Expr::Var(Arc::from("xs")), Expr::Var(Arc::from("f"))]
976            )
977        );
978    }
979
980    #[test]
981    fn parse_fold_lowers_list_first() {
982        assert_eq!(
983            parse_ok("fold f z xs"),
984            Expr::Builtin(
985                BuiltinOp::Fold,
986                vec![
987                    Expr::Var(Arc::from("xs")),
988                    Expr::Var(Arc::from("z")),
989                    Expr::Var(Arc::from("f")),
990                ]
991            )
992        );
993    }
994
995    #[test]
996    #[allow(clippy::expect_used)]
997    fn parsed_list_builtins_evaluate() {
998        // The lowering exists so that surface-authored list expressions
999        // actually run: before it, every `map` / `filter` / `fold` failed
1000        // with `expected list, got function`.
1001        let env = panproto_expr::Env::new().extend(
1002            Arc::from("xs"),
1003            panproto_expr::Literal::List(vec![
1004                panproto_expr::Literal::Int(1),
1005                panproto_expr::Literal::Int(2),
1006                panproto_expr::Literal::Int(3),
1007            ]),
1008        );
1009        let config = panproto_expr::EvalConfig::default();
1010        let eval = |src: &str| panproto_expr::eval(&parse_ok(src), &env, &config);
1011
1012        assert_eq!(
1013            eval("map (\\x -> x * 2) xs").expect("map should evaluate"),
1014            panproto_expr::Literal::List(vec![
1015                panproto_expr::Literal::Int(2),
1016                panproto_expr::Literal::Int(4),
1017                panproto_expr::Literal::Int(6),
1018            ])
1019        );
1020        assert_eq!(
1021            eval("filter (\\x -> x > 1) xs").expect("filter should evaluate"),
1022            panproto_expr::Literal::List(vec![
1023                panproto_expr::Literal::Int(2),
1024                panproto_expr::Literal::Int(3),
1025            ])
1026        );
1027        assert_eq!(
1028            eval("fold (\\a -> \\b -> a + b) 0 xs").expect("fold should evaluate"),
1029            panproto_expr::Literal::Int(6)
1030        );
1031    }
1032
1033    #[test]
1034    fn parse_string_concat() {
1035        assert_eq!(
1036            parse_ok(r#""hello" ++ " world""#),
1037            Expr::Builtin(
1038                BuiltinOp::Concat,
1039                vec![
1040                    Expr::Lit(Literal::Str("hello".into())),
1041                    Expr::Lit(Literal::Str(" world".into())),
1042                ]
1043            )
1044        );
1045    }
1046
1047    #[test]
1048    fn parse_pipe() {
1049        assert_eq!(
1050            parse_ok("x & f"),
1051            Expr::App(
1052                Box::new(Expr::Var(Arc::from("f"))),
1053                Box::new(Expr::Var(Arc::from("x"))),
1054            )
1055        );
1056    }
1057
1058    #[test]
1059    fn parse_chained_field_access() {
1060        assert_eq!(
1061            parse_ok("x.a.b"),
1062            Expr::Field(
1063                Box::new(Expr::Field(
1064                    Box::new(Expr::Var(Arc::from("x"))),
1065                    Arc::from("a"),
1066                )),
1067                Arc::from("b"),
1068            )
1069        );
1070    }
1071
1072    #[test]
1073    fn parse_comprehension() {
1074        let e = parse_ok("[ x + 1 | x <- xs ]");
1075        assert!(matches!(e, Expr::Builtin(BuiltinOp::FlatMap, _)));
1076    }
1077}