Skip to main content

jetro_core/
parser.rs

1//! PEG parser for Jetro v2 source text.
2//!
3//! The grammar lives in [`grammar.pest`]; this module walks the pest
4//! parse tree and builds an [`Expr`] AST.  Operator precedence and
5//! associativity are encoded in the grammar — the walker here is a
6//! near-mechanical tree fold with no precedence decisions of its own.
7//!
8//! # Error handling
9//!
10//! Pest reports errors with line/column spans; we wrap them into a
11//! [`ParseError`] that implements `Display` and `Error`.  The wrapper
12//! exists because callers shouldn't need to depend on pest types.
13//!
14//! # Parser state
15//!
16//! The original v1 parser threaded a `RefCell`-shared counter for
17//! gensym (unique ident generation).  v2 does that differently — we
18//! emit fresh temp names in the compiler, not the parser, so the
19//! parser is a pure function of its input.
20
21use pest::iterators::Pair;
22use pest::Parser as PestParser;
23use pest_derive::Parser;
24use std::fmt;
25
26use super::ast::*;
27
28#[derive(Parser)]
29#[grammar = "grammar.pest"]
30pub struct V2Parser;
31
32// ── Error ─────────────────────────────────────────────────────────────────────
33
34#[derive(Debug)]
35pub struct ParseError(pub String);
36
37impl fmt::Display for ParseError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "parse error: {}", self.0)
40    }
41}
42
43impl std::error::Error for ParseError {}
44
45impl From<pest::error::Error<Rule>> for ParseError {
46    fn from(e: pest::error::Error<Rule>) -> Self {
47        ParseError(e.to_string())
48    }
49}
50
51// ── Entry point ───────────────────────────────────────────────────────────────
52
53pub fn parse(input: &str) -> Result<Expr, ParseError> {
54    let mut pairs = V2Parser::parse(Rule::program, input)?;
55    let program = pairs.next().unwrap();
56    let expr_pair = program.into_inner().next().unwrap();
57    Ok(parse_expr(expr_pair))
58}
59
60// ── Keyword helpers ───────────────────────────────────────────────────────────
61
62fn is_kw(rule: Rule) -> bool {
63    matches!(
64        rule,
65        Rule::kw_and | Rule::kw_or | Rule::kw_not | Rule::kw_for
66            | Rule::kw_in | Rule::kw_if | Rule::kw_else | Rule::kw_let | Rule::kw_lambda | Rule::kw_kind
67            | Rule::kw_is | Rule::kw_as
68    )
69}
70
71// ── Expr dispatch ─────────────────────────────────────────────────────────────
72
73fn parse_expr(pair: Pair<Rule>) -> Expr {
74    match pair.as_rule() {
75        Rule::expr          => parse_expr(pair.into_inner().next().unwrap()),
76        Rule::cond_expr     => parse_cond(pair),
77        Rule::pipe_expr     => parse_pipeline(pair),
78        Rule::coalesce_expr => parse_coalesce(pair),
79        Rule::or_expr       => parse_or(pair),
80        Rule::and_expr      => parse_and(pair),
81        Rule::not_expr      => parse_not(pair),
82        Rule::kind_expr     => parse_kind(pair),
83        Rule::cmp_expr      => parse_cmp(pair),
84        Rule::add_expr      => parse_add(pair),
85        Rule::mul_expr      => parse_mul(pair),
86        Rule::cast_expr     => parse_cast(pair),
87        Rule::unary_expr    => parse_unary(pair),
88        Rule::postfix_expr  => parse_postfix_expr(pair),
89        Rule::primary       => parse_primary(pair),
90        r => panic!("unexpected rule in parse_expr: {:?}", r),
91    }
92}
93
94// ── Conditional (Python-style ternary) ────────────────────────────────────────
95// `then_ if cond else else_` — right-associative. Parser receives:
96//   cond_expr := pipe_expr (kw_if pipe_expr kw_else cond_expr)?
97// So inner pairs are either [then_] or [then_, kw_if, cond, kw_else, else_].
98// When the `if` tail is absent we pass through to keep the single pipe_expr.
99
100fn parse_cond(pair: Pair<Rule>) -> Expr {
101    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
102    let then_ = parse_expr(inner.next().unwrap());
103    let cond = match inner.next() {
104        Some(p) => parse_expr(p),
105        None    => return then_,
106    };
107    let else_ = parse_expr(inner.next().unwrap());
108    Expr::IfElse {
109        cond:  Box::new(cond),
110        then_: Box::new(then_),
111        else_: Box::new(else_),
112    }
113}
114
115// ── Pipeline (pipe + bind) ────────────────────────────────────────────────────
116
117fn parse_pipeline(pair: Pair<Rule>) -> Expr {
118    let mut inner = pair.into_inner();
119    let base = parse_expr(inner.next().unwrap()); // coalesce_expr
120    let mut steps: Vec<PipeStep> = Vec::new();
121    for step_pair in inner {
122        // each is a pipe_step
123        let inner_step = step_pair.into_inner().next().unwrap();
124        match inner_step.as_rule() {
125            Rule::pipe_forward => {
126                let inner_pair = inner_step.into_inner().next().unwrap();
127                let expr = if inner_pair.as_rule() == Rule::pipe_method_call {
128                    let mut mi = inner_pair.into_inner();
129                    let name = mi.next().unwrap().as_str().to_string();
130                    let args = mi.next().map(parse_arg_list).unwrap_or_default();
131                    Expr::Chain(Box::new(Expr::Current), vec![Step::Method(name, args)])
132                } else {
133                    parse_expr(inner_pair)
134                };
135                steps.push(PipeStep::Forward(expr));
136            }
137            Rule::pipe_bind => {
138                let target = parse_bind_target(inner_step.into_inner().next().unwrap());
139                steps.push(PipeStep::Bind(target));
140            }
141            r => panic!("unexpected pipe_step inner: {:?}", r),
142        }
143    }
144    if steps.is_empty() { base } else { Expr::Pipeline { base: Box::new(base), steps } }
145}
146
147fn parse_bind_target(pair: Pair<Rule>) -> BindTarget {
148    // pair.as_rule() == Rule::bind_target
149    let inner = pair.into_inner().next().unwrap();
150    match inner.as_rule() {
151        Rule::ident => BindTarget::Name(inner.as_str().to_string()),
152        Rule::bind_obj => {
153            let mut fields = Vec::new();
154            let mut rest = None;
155            for p in inner.into_inner() {
156                match p.as_rule() {
157                    Rule::ident => fields.push(p.as_str().to_string()),
158                    Rule::bind_rest => {
159                        rest = Some(
160                            p.into_inner()
161                             .find(|x| x.as_rule() == Rule::ident)
162                             .unwrap()
163                             .as_str()
164                             .to_string()
165                        );
166                    }
167                    _ => {}
168                }
169            }
170            BindTarget::Obj { fields, rest }
171        }
172        Rule::bind_arr => {
173            let fields: Vec<String> = inner.into_inner()
174                .filter(|p| p.as_rule() == Rule::ident)
175                .map(|p| p.as_str().to_string())
176                .collect();
177            BindTarget::Arr(fields)
178        }
179        r => panic!("unexpected bind_target inner: {:?}", r),
180    }
181}
182
183// ── Coalesce ──────────────────────────────────────────────────────────────────
184
185fn parse_coalesce(pair: Pair<Rule>) -> Expr {
186    let mut inner = pair.into_inner();
187    let first = parse_expr(inner.next().unwrap());
188    inner.fold(first, |acc, rhs| {
189        Expr::Coalesce(Box::new(acc), Box::new(parse_expr(rhs)))
190    })
191}
192
193// ── Logical ──────────────────────────────────────────────────────────────────
194
195fn parse_or(pair: Pair<Rule>) -> Expr {
196    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
197    let first = parse_expr(inner.next().unwrap());
198    inner.fold(first, |acc, rhs| {
199        Expr::BinOp(Box::new(acc), BinOp::Or, Box::new(parse_expr(rhs)))
200    })
201}
202
203fn parse_and(pair: Pair<Rule>) -> Expr {
204    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
205    let first = parse_expr(inner.next().unwrap());
206    inner.fold(first, |acc, rhs| {
207        Expr::BinOp(Box::new(acc), BinOp::And, Box::new(parse_expr(rhs)))
208    })
209}
210
211fn parse_not(pair: Pair<Rule>) -> Expr {
212    let mut inner = pair.into_inner();
213    let first = inner.next().unwrap();
214    if first.as_rule() == Rule::kw_not {
215        let operand = inner.next().unwrap();
216        Expr::Not(Box::new(parse_expr(operand)))
217    } else {
218        parse_expr(first)
219    }
220}
221
222// ── Kind check ────────────────────────────────────────────────────────────────
223
224fn parse_kind(pair: Pair<Rule>) -> Expr {
225    let mut inner = pair.into_inner();
226    let cmp = parse_expr(inner.next().unwrap());
227    match inner.next() {
228        None => cmp,
229        Some(p) if matches!(p.as_rule(), Rule::kw_kind | Rule::kw_is) => {
230            let next = inner.next().unwrap();
231            let (negate, kind_type_str) = if next.as_rule() == Rule::kw_not {
232                (true, inner.next().unwrap().as_str())
233            } else {
234                (false, next.as_str())
235            };
236            let ty = match kind_type_str {
237                "null"   => KindType::Null,
238                "bool"   => KindType::Bool,
239                "number" => KindType::Number,
240                "string" => KindType::Str,
241                "array"  => KindType::Array,
242                "object" => KindType::Object,
243                other    => panic!("unknown kind type: {}", other),
244            };
245            Expr::Kind { expr: Box::new(cmp), ty, negate }
246        }
247        _ => cmp,
248    }
249}
250
251// ── Comparison ───────────────────────────────────────────────────────────────
252
253fn parse_cmp(pair: Pair<Rule>) -> Expr {
254    let mut inner = pair.into_inner();
255    let lhs = parse_expr(inner.next().unwrap());
256    if let Some(op_pair) = inner.next() {
257        let op = match op_pair.as_str() {
258            "==" => BinOp::Eq,  "!=" => BinOp::Neq,
259            "<"  => BinOp::Lt,  "<=" => BinOp::Lte,
260            ">"  => BinOp::Gt,  ">=" => BinOp::Gte,
261            "~=" => BinOp::Fuzzy,
262            o    => panic!("unknown cmp op: {}", o),
263        };
264        let rhs = parse_expr(inner.next().unwrap());
265        Expr::BinOp(Box::new(lhs), op, Box::new(rhs))
266    } else {
267        lhs
268    }
269}
270
271// ── Additive / multiplicative ─────────────────────────────────────────────────
272
273fn parse_add(pair: Pair<Rule>) -> Expr {
274    parse_left_assoc(pair, |s| match s {
275        "+" => Some(BinOp::Add), "-" => Some(BinOp::Sub), _ => None,
276    })
277}
278
279fn parse_mul(pair: Pair<Rule>) -> Expr {
280    parse_left_assoc(pair, |s| match s {
281        "*" => Some(BinOp::Mul), "/" => Some(BinOp::Div), "%" => Some(BinOp::Mod), _ => None,
282    })
283}
284
285fn parse_left_assoc<F>(pair: Pair<Rule>, op_fn: F) -> Expr
286where
287    F: Fn(&str) -> Option<BinOp>,
288{
289    let mut inner = pair.into_inner().peekable();
290    let first = parse_expr(inner.next().unwrap());
291    let mut acc = first;
292    while inner.peek().is_some() {
293        let op_pair = inner.next().unwrap();
294        let op = op_fn(op_pair.as_str()).unwrap();
295        let rhs = parse_expr(inner.next().unwrap());
296        acc = Expr::BinOp(Box::new(acc), op, Box::new(rhs));
297    }
298    acc
299}
300
301// ── Cast ──────────────────────────────────────────────────────────────────────
302
303fn parse_cast(pair: Pair<Rule>) -> Expr {
304    // cast_expr = { unary_expr ~ (kw_as ~ kind_type)* }
305    let mut inner = pair.into_inner().peekable();
306    let mut acc = parse_expr(inner.next().unwrap());
307    while inner.peek().is_some() {
308        // Consume kw_as then kind_type
309        let kw = inner.next().unwrap();
310        debug_assert_eq!(kw.as_rule(), Rule::kw_as);
311        let ty_pair = inner.next().unwrap();
312        let ty = match ty_pair.as_str() {
313            "int"    => CastType::Int,
314            "float"  => CastType::Float,
315            "number" => CastType::Number,
316            "string" => CastType::Str,
317            "bool"   => CastType::Bool,
318            "array"  => CastType::Array,
319            "object" => CastType::Object,
320            "null"   => CastType::Null,
321            o        => panic!("unknown cast type: {}", o),
322        };
323        acc = Expr::Cast { expr: Box::new(acc), ty };
324    }
325    acc
326}
327
328// ── Unary ─────────────────────────────────────────────────────────────────────
329
330fn parse_unary(pair: Pair<Rule>) -> Expr {
331    let mut inner = pair.into_inner();
332    let first = inner.next().unwrap();
333    match first.as_rule() {
334        Rule::unary_neg => {
335            let operand = parse_expr(inner.next().unwrap());
336            Expr::UnaryNeg(Box::new(operand))
337        }
338        _ => parse_expr(first),
339    }
340}
341
342// ── Postfix chain ─────────────────────────────────────────────────────────────
343
344fn parse_postfix_expr(pair: Pair<Rule>) -> Expr {
345    let mut inner = pair.into_inner();
346    let base = parse_primary(inner.next().unwrap());
347    let raw_steps: Vec<Step> = inner.flat_map(parse_postfix_step).collect();
348    // Postfix `?` is emitted by the grammar as `Step::Quantifier(First)`
349    // (historical). We collapse it into null-propagation only:
350    //   Field(k)     + `?` → OptField(k)          (null-safe field)
351    //   Method(n, a) + `?` → OptMethod(n, a)      (null-safe method call)
352    //   Descendant   + `?` → Descendant           (drop `?`, keep array)
353    //   Index/Slice  + `?` → step unchanged       (drop `?`)
354    //
355    // Postfix `?` never takes first-of-array. Use `.first()` explicitly
356    // when you want the first element (e.g. `$..services?.first()`).
357    //
358    // `!` (Quantifier::One) keeps its exact-one-element meaning everywhere.
359    let mut steps: Vec<Step> = Vec::with_capacity(raw_steps.len());
360    for s in raw_steps {
361        match s {
362            Step::Quantifier(QuantifierKind::First) => {
363                match steps.last() {
364                    Some(Step::Field(_)) => {
365                        if let Some(Step::Field(k)) = steps.pop() {
366                            steps.push(Step::OptField(k));
367                        }
368                    }
369                    Some(Step::Method(_, _)) => {
370                        if let Some(Step::Method(n, a)) = steps.pop() {
371                            steps.push(Step::OptMethod(n, a));
372                        }
373                    }
374                    _ => {
375                        // Descendant / Index / Slice / DynIndex / etc:
376                        // drop the `?` — value passes through unchanged.
377                    }
378                }
379            }
380            other => steps.push(other),
381        }
382    }
383    if let Some(rewritten) = classify_chain_write(&base, &steps) {
384        return rewritten;
385    }
386    base.maybe_chain(steps)
387}
388
389// ── Chain-style terminal writes ──────────────────────────────────────────────
390//
391// When the expression is `$.<traversal>.<terminal>(args)` where `<terminal>`
392// is one of `set / modify / delete / unset / replace`, rewrite it as an
393// `Expr::Patch`.  This lets users write inline updates without the full
394// `patch $ { ... }` block.  Collisions with existing method names are
395// avoided: non-root chains (e.g. `@.set(...)`) are *not* rewritten — they
396// keep their method-call semantics.
397fn classify_chain_write(base: &Expr, steps: &[Step]) -> Option<Expr> {
398    if !matches!(base, Expr::Root) { return None; }
399    let last = steps.last()?;
400    let (name, args) = match last { Step::Method(n, a) => (n.as_str(), a), _ => return None };
401    if !is_terminal_write(name) { return None; }
402
403    let prefix = &steps[..steps.len() - 1];
404    let path = match steps_to_path(prefix) {
405        Ok(p)  => p,
406        Err(_) => return None,  // not a valid traversal — leave as method call
407    };
408
409    let op = build_write_op(name, args, path)?;
410    Some(Expr::Patch { root: Box::new(Expr::Root), ops: vec![op] })
411}
412
413fn is_terminal_write(name: &str) -> bool {
414    // `.replace` deliberately omitted — it would clash with the 2-arg
415    // string `.replace(needle, with)` builtin.  Use `.set(v)` for full
416    // value replacement.
417    matches!(name, "set" | "modify" | "delete" | "unset" | "merge" | "deep_merge" | "deepMerge")
418}
419
420fn steps_to_path(steps: &[Step]) -> Result<Vec<PathStep>, String> {
421    let mut out = Vec::with_capacity(steps.len());
422    for s in steps {
423        match s {
424            Step::Field(f)        => out.push(PathStep::Field(f.clone())),
425            Step::Index(i)        => out.push(PathStep::Index(*i)),
426            Step::OptField(f)     => out.push(PathStep::Field(f.clone())),
427            Step::Descendant(f)   => out.push(PathStep::Descendant(f.clone())),
428            Step::DynIndex(e)     => {
429                // Defer resolution to apply time — PathStep carries the
430                // boxed expression, evaluated against the root doc then.
431                out.push(PathStep::DynIndex((**e).clone()));
432            }
433            _ => return Err("chain-write: unsupported step in path".into()),
434        }
435    }
436    Ok(out)
437}
438
439fn build_write_op(name: &str, args: &[Arg], path: Vec<PathStep>) -> Option<PatchOp> {
440    match name {
441        "set" => {
442            let v = arg_expr(args.first()?).clone();
443            Some(PatchOp { path, val: v, cond: None })
444        }
445        // `.modify(expr)` — expr sees `@` bound to the current value at the path
446        // (patch semantics already bind `@` at the leaf).  Lambda form
447        // `.modify(lambda x: ...)` rewrites to `let x = @ in <body>` so the
448        // bound `@` flows into the param name.
449        "modify" => {
450            let v = match arg_expr(args.first()?).clone() {
451                Expr::Lambda { params, body } => {
452                    if let Some(p) = params.into_iter().next() {
453                        Expr::Let { name: p, init: Box::new(Expr::Current), body }
454                    } else {
455                        *body
456                    }
457                }
458                other => other,
459            };
460            Some(PatchOp { path, val: v, cond: None })
461        }
462        "delete" => {
463            if !args.is_empty() { return None; }
464            Some(PatchOp { path, val: Expr::DeleteMark, cond: None })
465        }
466        // `.merge(obj)` / `.deep_merge(obj)` — desugar to `.modify(@.merge(arg))`
467        // so the patch leaf evaluates against the bound `@`.
468        "merge" | "deep_merge" | "deepMerge" => {
469            let arg = arg_expr(args.first()?).clone();
470            let method = if name == "merge" { "merge".to_string() } else { "deep_merge".to_string() };
471            let v = Expr::Chain(
472                Box::new(Expr::Current),
473                vec![Step::Method(method, vec![Arg::Pos(arg)])],
474            );
475            Some(PatchOp { path, val: v, cond: None })
476        }
477        // `.unset(key)` — append the key onto the path and delete it.
478        "unset" => {
479            let key = match arg_expr(args.first()?) {
480                Expr::Str(s)     => s.clone(),
481                Expr::Ident(s)   => s.clone(),
482                _                => return None,
483            };
484            let mut p = path;
485            p.push(PathStep::Field(key));
486            Some(PatchOp { path: p, val: Expr::DeleteMark, cond: None })
487        }
488        _ => None,
489    }
490}
491
492fn arg_expr(a: &Arg) -> &Expr {
493    match a { Arg::Pos(e) | Arg::Named(_, e) => e }
494}
495
496fn parse_postfix_step(pair: Pair<Rule>) -> Vec<Step> {
497    let inner_pair = pair.into_inner().next().unwrap();
498    match inner_pair.as_rule() {
499        Rule::field_access => {
500            let name = inner_pair.into_inner().next().unwrap().as_str().to_string();
501            vec![Step::Field(name)]
502        }
503        Rule::descendant => {
504            let mut di = inner_pair.into_inner();
505            match di.next() {
506                Some(p) => vec![Step::Descendant(p.as_str().to_string())],
507                None    => vec![Step::DescendAll],
508            }
509        }
510        Rule::deep_method => {
511            // `..name(args)` — desugar to `.deep_name(args)` for find/shape/like.
512            let mut mi = inner_pair.into_inner();
513            let name = mi.next().unwrap().as_str().to_string();
514            let args = mi.next().map(parse_arg_list).unwrap_or_default();
515            let mapped = match name.as_str() {
516                "find"  | "find_all" | "findAll" => "deep_find".to_string(),
517                "shape"                          => "deep_shape".to_string(),
518                "like"                           => "deep_like".to_string(),
519                other                            => format!("deep_{}", other),
520            };
521            vec![Step::Method(mapped, args)]
522        }
523        Rule::inline_filter => {
524            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
525            vec![Step::InlineFilter(Box::new(expr))]
526        }
527        Rule::quantifier => {
528            let s = inner_pair.as_str();
529            if s.starts_with('!') { vec![Step::Quantifier(QuantifierKind::One)] }
530            else                  { vec![Step::Quantifier(QuantifierKind::First)] }
531        }
532        Rule::method_call => {
533            let mut mi = inner_pair.into_inner();
534            let name = mi.next().unwrap().as_str().to_string();
535            let args = mi.next().map(parse_arg_list).unwrap_or_default();
536            vec![Step::Method(name, args)]
537        }
538        Rule::index_access => {
539            let bi = inner_pair.into_inner().next().unwrap();
540            vec![parse_bracket(bi)]
541        }
542        Rule::dyn_field => {
543            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
544            vec![Step::DynIndex(Box::new(expr))]
545        }
546        Rule::map_into_shape => {
547            // `[*] => body` or `[* if g] => body`
548            // Desugar: with guard  → .filter(g).map(body)
549            //          no guard    → .map(body)
550            let mut guard: Option<Expr> = None;
551            let mut body:  Option<Expr> = None;
552            let mut saw_if = false;
553            for p in inner_pair.into_inner() {
554                match p.as_rule() {
555                    Rule::kw_if => saw_if = true,
556                    Rule::expr  => {
557                        if saw_if && guard.is_none() { guard = Some(parse_expr(p)); }
558                        else                         { body  = Some(parse_expr(p)); }
559                    }
560                    _ => {}
561                }
562            }
563            let body = body.expect("map_into_shape requires body");
564            let mut steps = Vec::new();
565            if let Some(g) = guard {
566                steps.push(Step::Method("filter".into(), vec![Arg::Pos(g)]));
567            }
568            steps.push(Step::Method("map".into(), vec![Arg::Pos(body)]));
569            steps
570        }
571        r => panic!("unexpected postfix rule: {:?}", r),
572    }
573}
574
575fn parse_bracket(pair: Pair<Rule>) -> Step {
576    let inner = pair.into_inner().next().unwrap();
577    match inner.as_rule() {
578        Rule::idx_only   => Step::Index(inner.as_str().parse().unwrap()),
579        Rule::slice_full => {
580            let mut i = inner.into_inner();
581            let a = i.next().unwrap().as_str().parse().ok();
582            let b = i.next().unwrap().as_str().parse().ok();
583            Step::Slice(a, b)
584        }
585        Rule::slice_from => {
586            let a = inner.into_inner().next().unwrap().as_str().parse().ok();
587            Step::Slice(a, None)
588        }
589        Rule::slice_to => {
590            let b = inner.into_inner().next().unwrap().as_str().parse().ok();
591            Step::Slice(None, b)
592        }
593        Rule::expr => Step::DynIndex(Box::new(parse_expr(inner))),
594        r => panic!("unexpected bracket rule: {:?}", r),
595    }
596}
597
598// ── Primary ───────────────────────────────────────────────────────────────────
599
600fn parse_primary(pair: Pair<Rule>) -> Expr {
601    let inner = if pair.as_rule() == Rule::primary {
602        pair.into_inner().next().unwrap()
603    } else {
604        pair
605    };
606    match inner.as_rule() {
607        Rule::literal       => parse_literal(inner),
608        Rule::root          => Expr::Root,
609        Rule::current       => Expr::Current,
610        Rule::ident         => Expr::Ident(inner.as_str().to_string()),
611        Rule::let_expr      => parse_let(inner),
612        Rule::lambda_expr   => parse_lambda(inner),
613        Rule::arrow_lambda  => parse_arrow_lambda(inner),
614        Rule::list_comp     => parse_list_comp(inner),
615        Rule::dict_comp     => parse_dict_comp(inner),
616        Rule::set_comp      => parse_set_comp(inner),
617        Rule::gen_comp      => parse_gen_comp(inner),
618        Rule::obj_construct => parse_obj(inner),
619        Rule::arr_construct => parse_arr(inner),
620        Rule::global_call   => parse_global_call(inner),
621        Rule::expr          => parse_expr(inner),
622        Rule::patch_block   => parse_patch(inner),
623        Rule::kw_delete     => Expr::DeleteMark,
624        r => panic!("unexpected primary rule: {:?}", r),
625    }
626}
627
628// ── Literals ──────────────────────────────────────────────────────────────────
629
630fn parse_literal(pair: Pair<Rule>) -> Expr {
631    let inner = pair.into_inner().next().unwrap();
632    match inner.as_rule() {
633        Rule::lit_null    => Expr::Null,
634        Rule::lit_true    => Expr::Bool(true),
635        Rule::lit_false   => Expr::Bool(false),
636        Rule::lit_int     => Expr::Int(inner.as_str().parse().unwrap()),
637        Rule::lit_float   => Expr::Float(inner.as_str().parse().unwrap()),
638        Rule::lit_fstring => {
639            let raw = inner.as_str();
640            let content = &raw[2..raw.len() - 1]; // strip f" and "
641            let parts = parse_fstring_content(content);
642            Expr::FString(parts)
643        }
644        Rule::lit_str => {
645            let s = inner.into_inner().next().unwrap();
646            let raw = s.as_str();
647            // Strip surrounding quotes (now atomic rules, so as_str includes them)
648            Expr::Str(raw[1..raw.len()-1].to_string())
649        }
650        r => panic!("unexpected literal rule: {:?}", r),
651    }
652}
653
654// ── F-string parser ───────────────────────────────────────────────────────────
655
656fn parse_fstring_content(raw: &str) -> Vec<FStringPart> {
657    let mut parts = Vec::new();
658    let mut lit = String::new();
659    let mut chars = raw.chars().peekable();
660
661    while let Some(c) = chars.next() {
662        match c {
663            '{' => {
664                if chars.peek() == Some(&'{') {
665                    chars.next();
666                    lit.push('{');
667                } else {
668                    if !lit.is_empty() {
669                        parts.push(FStringPart::Lit(std::mem::take(&mut lit)));
670                    }
671                    let mut inner = String::new();
672                    let mut depth = 1usize;
673                    for c2 in chars.by_ref() {
674                        match c2 {
675                            '{' => { depth += 1; inner.push(c2); }
676                            '}' => {
677                                depth -= 1;
678                                if depth == 0 { break; }
679                                inner.push(c2);
680                            }
681                            _ => inner.push(c2),
682                        }
683                    }
684                    let (expr_str, fmt) = split_fstring_interp(&inner);
685                    let expr = parse(expr_str.trim())
686                        .unwrap_or_else(|e| panic!("f-string parse error in {{{}}}: {}", inner, e));
687                    parts.push(FStringPart::Interp { expr, fmt });
688                }
689            }
690            '}' if chars.peek() == Some(&'}') => {
691                chars.next();
692                lit.push('}');
693            }
694            _ => lit.push(c),
695        }
696    }
697    if !lit.is_empty() {
698        parts.push(FStringPart::Lit(lit));
699    }
700    parts
701}
702
703fn split_fstring_interp(inner: &str) -> (&str, Option<FmtSpec>) {
704    // Find top-level `|` or `:` (not inside parens/brackets/braces)
705    let mut depth = 0usize;
706    let mut pipe_pos: Option<usize> = None;
707    let mut colon_pos: Option<usize> = None;
708    for (i, c) in inner.char_indices() {
709        match c {
710            '(' | '[' | '{' => depth += 1,
711            ')' | ']' | '}' => { if depth > 0 { depth -= 1; } }
712            '|' if depth == 0 && pipe_pos.is_none() => pipe_pos = Some(i),
713            ':' if depth == 0 && colon_pos.is_none() => colon_pos = Some(i),
714            _ => {}
715        }
716    }
717    if let Some(p) = pipe_pos {
718        return (&inner[..p], Some(FmtSpec::Pipe(inner[p + 1..].trim().to_string())));
719    }
720    if let Some(c) = colon_pos {
721        return (&inner[..c], Some(FmtSpec::Spec(inner[c + 1..].to_string())));
722    }
723    (inner, None)
724}
725
726// ── Let ───────────────────────────────────────────────────────────────────────
727
728fn parse_let(pair: Pair<Rule>) -> Expr {
729    // let_expr = { kw_let ~ let_binding ~ ("," ~ let_binding)* ~ kw_in ~ expr }
730    // Desugar multi-binding into nested Let: `let a=x, b=y in body` → Let a x (Let b y body)
731    let inner: Vec<Pair<Rule>> = pair.into_inner()
732        .filter(|p| !matches!(p.as_rule(), Rule::kw_let | Rule::kw_in))
733        .collect();
734    let (bindings, body_pair) = inner.split_at(inner.len() - 1);
735    let body = parse_expr(body_pair[0].clone());
736    bindings.iter().rev().fold(body, |acc, b| {
737        let mut bi = b.clone().into_inner();
738        let name = bi.next().unwrap().as_str().to_string();
739        let init = parse_expr(bi.next().unwrap());
740        Expr::Let { name, init: Box::new(init), body: Box::new(acc) }
741    })
742}
743
744// ── Lambda ────────────────────────────────────────────────────────────────────
745
746fn parse_lambda(pair: Pair<Rule>) -> Expr {
747    let mut inner = pair.into_inner()
748        .filter(|p| p.as_rule() != Rule::kw_lambda);
749    let params_pair = inner.next().unwrap();
750    let params: Vec<String> = params_pair
751        .into_inner()
752        .filter(|p| p.as_rule() == Rule::ident)
753        .map(|p| p.as_str().to_string())
754        .collect();
755    let body = parse_expr(inner.next().unwrap());
756    Expr::Lambda { params, body: Box::new(body) }
757}
758
759/// Arrow lambda: `x => body` or `(x, y) => body`.  Lowered to same
760/// `Expr::Lambda` node — the `=>` form is pure surface sugar.
761fn parse_arrow_lambda(pair: Pair<Rule>) -> Expr {
762    let mut inner = pair.into_inner();
763    let params_pair = inner.next().unwrap();
764    let params: Vec<String> = params_pair
765        .into_inner()
766        .filter(|p| p.as_rule() == Rule::ident)
767        .map(|p| p.as_str().to_string())
768        .collect();
769    let body = parse_expr(inner.next().unwrap());
770    Expr::Lambda { params, body: Box::new(body) }
771}
772
773// ── Comprehensions ────────────────────────────────────────────────────────────
774
775fn comp_inner_filter(pair: Pair<Rule>) -> impl Iterator<Item = Pair<Rule>> {
776    pair.into_inner()
777        .filter(|p| !matches!(p.as_rule(), Rule::kw_for | Rule::kw_in | Rule::kw_if))
778}
779
780fn parse_comp_vars(pair: Pair<Rule>) -> Vec<String> {
781    pair.into_inner()
782        .filter(|p| p.as_rule() == Rule::ident)
783        .map(|p| p.as_str().to_string())
784        .collect()
785}
786
787fn parse_list_comp(pair: Pair<Rule>) -> Expr {
788    let mut inner = comp_inner_filter(pair);
789    let expr = parse_expr(inner.next().unwrap());
790    let vars = parse_comp_vars(inner.next().unwrap());
791    let iter = parse_expr(inner.next().unwrap());
792    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
793    Expr::ListComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
794}
795
796fn parse_dict_comp(pair: Pair<Rule>) -> Expr {
797    let mut inner = comp_inner_filter(pair);
798    let key  = parse_expr(inner.next().unwrap());
799    let val  = parse_expr(inner.next().unwrap());
800    let vars = parse_comp_vars(inner.next().unwrap());
801    let iter = parse_expr(inner.next().unwrap());
802    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
803    Expr::DictComp { key: Box::new(key), val: Box::new(val), vars, iter: Box::new(iter), cond }
804}
805
806fn parse_set_comp(pair: Pair<Rule>) -> Expr {
807    let mut inner = comp_inner_filter(pair);
808    let expr = parse_expr(inner.next().unwrap());
809    let vars = parse_comp_vars(inner.next().unwrap());
810    let iter = parse_expr(inner.next().unwrap());
811    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
812    Expr::SetComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
813}
814
815fn parse_gen_comp(pair: Pair<Rule>) -> Expr {
816    let mut inner = comp_inner_filter(pair);
817    let expr = parse_expr(inner.next().unwrap());
818    let vars = parse_comp_vars(inner.next().unwrap());
819    let iter = parse_expr(inner.next().unwrap());
820    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
821    Expr::GenComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
822}
823
824// ── Object / array construction ────────────────────────────────────────────────
825
826fn parse_obj(pair: Pair<Rule>) -> Expr {
827    let fields = pair.into_inner()
828        .filter(|p| p.as_rule() == Rule::obj_field)
829        .map(parse_obj_field)
830        .collect();
831    Expr::Object(fields)
832}
833
834fn parse_obj_field(pair: Pair<Rule>) -> ObjField {
835    let inner = pair.into_inner().next().unwrap();
836    match inner.as_rule() {
837        Rule::obj_field_dyn => {
838            let mut i = inner.into_inner();
839            let key = parse_expr(i.next().unwrap());
840            let val = parse_expr(i.next().unwrap());
841            ObjField::Dynamic { key, val }
842        }
843        Rule::obj_field_opt_v => {
844            let mut i = inner.into_inner();
845            let key = obj_key_str(i.next().unwrap());
846            let val = parse_expr(i.next().unwrap());
847            ObjField::Kv { key, val, optional: true, cond: None }
848        }
849        Rule::obj_field_opt => {
850            let key = obj_key_str(inner.into_inner().next().unwrap());
851            ObjField::Kv { key: key.clone(), val: Expr::Ident(key), optional: true, cond: None }
852        }
853        Rule::obj_field_spread => {
854            let expr = parse_expr(inner.into_inner().next().unwrap());
855            ObjField::Spread(expr)
856        }
857        Rule::obj_field_spread_deep => {
858            let expr = parse_expr(inner.into_inner().next().unwrap());
859            ObjField::SpreadDeep(expr)
860        }
861        Rule::obj_field_kv => {
862            let mut cond: Option<Expr> = None;
863            let mut key: Option<String> = None;
864            let mut val: Option<Expr> = None;
865            let mut saw_when = false;
866            for p in inner.into_inner() {
867                match p.as_rule() {
868                    Rule::kw_when => saw_when = true,
869                    Rule::obj_key_expr => key = Some(obj_key_str(p)),
870                    Rule::expr => {
871                        if saw_when { cond = Some(parse_expr(p)); }
872                        else        { val  = Some(parse_expr(p)); }
873                    }
874                    _ => {}
875                }
876            }
877            ObjField::Kv {
878                key:      key.expect("obj_field_kv missing key"),
879                val:      val.expect("obj_field_kv missing val"),
880                optional: false,
881                cond,
882            }
883        }
884        Rule::obj_field_short => {
885            let name = inner.into_inner().next().unwrap().as_str().to_string();
886            ObjField::Short(name)
887        }
888        r => panic!("unexpected obj_field rule: {:?}", r),
889    }
890}
891
892fn obj_key_str(pair: Pair<Rule>) -> String {
893    let inner = pair.into_inner().next().unwrap();
894    match inner.as_rule() {
895        Rule::ident => inner.as_str().to_string(),
896        Rule::lit_str => {
897            let s = inner.into_inner().next().unwrap();
898            let raw = s.as_str();
899            raw[1..raw.len()-1].to_string()
900        }
901        r => panic!("unexpected obj_key_expr rule: {:?}", r),
902    }
903}
904
905fn parse_arr(pair: Pair<Rule>) -> Expr {
906    let elems = pair.into_inner()
907        .filter(|p| p.as_rule() == Rule::arr_elem)
908        .map(|elem| {
909            let inner = elem.into_inner().next().unwrap();
910            match inner.as_rule() {
911                Rule::arr_spread => {
912                    let expr = parse_expr(inner.into_inner().next().unwrap());
913                    ArrayElem::Spread(expr)
914                }
915                _ => ArrayElem::Expr(parse_expr(inner)),
916            }
917        })
918        .collect();
919    Expr::Array(elems)
920}
921
922// ── Global call ───────────────────────────────────────────────────────────────
923
924fn parse_global_call(pair: Pair<Rule>) -> Expr {
925    let mut inner = pair.into_inner();
926    let name = inner.next().unwrap().as_str().to_string();
927    let args = inner.next().map(parse_arg_list).unwrap_or_default();
928    Expr::GlobalCall { name, args }
929}
930
931// ── Patch block ───────────────────────────────────────────────────────────────
932
933fn parse_patch(pair: Pair<Rule>) -> Expr {
934    let mut root: Option<Expr> = None;
935    let mut ops: Vec<PatchOp> = Vec::new();
936    for p in pair.into_inner() {
937        match p.as_rule() {
938            Rule::kw_patch   => {}
939            Rule::patch_field => ops.push(parse_patch_field(p)),
940            _ => {
941                // This branch handles the root expression (coalesce_expr and
942                // any of its descendants that parse_expr accepts).
943                if root.is_none() { root = Some(parse_expr(p)); }
944            }
945        }
946    }
947    Expr::Patch {
948        root: Box::new(root.expect("patch requires root expression")),
949        ops,
950    }
951}
952
953fn parse_patch_field(pair: Pair<Rule>) -> PatchOp {
954    let mut path: Vec<PathStep> = Vec::new();
955    let mut val:  Option<Expr> = None;
956    let mut cond: Option<Expr> = None;
957    let mut saw_when = false;
958    for p in pair.into_inner() {
959        match p.as_rule() {
960            Rule::patch_key => path = parse_patch_key(p),
961            Rule::kw_when   => saw_when = true,
962            Rule::expr => {
963                if saw_when { cond = Some(parse_expr(p)); }
964                else        { val  = Some(parse_expr(p)); }
965            }
966            _ => {}
967        }
968    }
969    PatchOp {
970        path,
971        val:  val.expect("patch_field missing val"),
972        cond,
973    }
974}
975
976fn parse_patch_key(pair: Pair<Rule>) -> Vec<PathStep> {
977    let mut steps: Vec<PathStep> = Vec::new();
978    let mut first = true;
979    for p in pair.into_inner() {
980        match p.as_rule() {
981            Rule::ident if first => {
982                steps.push(PathStep::Field(p.as_str().to_string()));
983                first = false;
984            }
985            Rule::patch_step => steps.push(parse_patch_step(p)),
986            _ => {}
987        }
988    }
989    steps
990}
991
992fn parse_patch_step(pair: Pair<Rule>) -> PathStep {
993    let inner = pair.into_inner().next().unwrap();
994    match inner.as_rule() {
995        Rule::pp_dot_field => {
996            let name = inner.into_inner().next().unwrap().as_str().to_string();
997            PathStep::Field(name)
998        }
999        Rule::pp_index => {
1000            let idx: i64 = inner.into_inner().next().unwrap().as_str().parse().unwrap();
1001            PathStep::Index(idx)
1002        }
1003        Rule::pp_wild => PathStep::Wildcard,
1004        Rule::pp_wild_filter => {
1005            // `[* if expr]`
1006            let mut e: Option<Expr> = None;
1007            for p in inner.into_inner() {
1008                if p.as_rule() == Rule::expr { e = Some(parse_expr(p)); }
1009            }
1010            PathStep::WildcardFilter(Box::new(e.expect("pp_wild_filter missing expr")))
1011        }
1012        Rule::pp_descendant => {
1013            let name = inner.into_inner().next().unwrap().as_str().to_string();
1014            PathStep::Descendant(name)
1015        }
1016        r => panic!("unexpected patch_step rule: {:?}", r),
1017    }
1018}
1019
1020// ── Arguments ─────────────────────────────────────────────────────────────────
1021
1022fn parse_arg_list(pair: Pair<Rule>) -> Vec<Arg> {
1023    pair.into_inner()
1024        .filter(|p| p.as_rule() == Rule::arg)
1025        .map(parse_arg)
1026        .collect()
1027}
1028
1029fn parse_arg(pair: Pair<Rule>) -> Arg {
1030    let inner = pair.into_inner().next().unwrap();
1031    match inner.as_rule() {
1032        Rule::named_arg => {
1033            let mut i = inner.into_inner();
1034            let name = i.next().unwrap().as_str().to_string();
1035            let val  = parse_expr(i.next().unwrap());
1036            Arg::Named(name, val)
1037        }
1038        Rule::pos_arg => {
1039            Arg::Pos(parse_expr(inner.into_inner().next().unwrap()))
1040        }
1041        r => panic!("unexpected arg rule: {:?}", r),
1042    }
1043}