Skip to main content

panproto_expr_parser/
parser.rs

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