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_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::pipe_expr     => parse_pipeline(pair),
77        Rule::coalesce_expr => parse_coalesce(pair),
78        Rule::or_expr       => parse_or(pair),
79        Rule::and_expr      => parse_and(pair),
80        Rule::not_expr      => parse_not(pair),
81        Rule::kind_expr     => parse_kind(pair),
82        Rule::cmp_expr      => parse_cmp(pair),
83        Rule::add_expr      => parse_add(pair),
84        Rule::mul_expr      => parse_mul(pair),
85        Rule::cast_expr     => parse_cast(pair),
86        Rule::unary_expr    => parse_unary(pair),
87        Rule::postfix_expr  => parse_postfix_expr(pair),
88        Rule::primary       => parse_primary(pair),
89        r => panic!("unexpected rule in parse_expr: {:?}", r),
90    }
91}
92
93// ── Pipeline (pipe + bind) ────────────────────────────────────────────────────
94
95fn parse_pipeline(pair: Pair<Rule>) -> Expr {
96    let mut inner = pair.into_inner();
97    let base = parse_expr(inner.next().unwrap()); // coalesce_expr
98    let mut steps: Vec<PipeStep> = Vec::new();
99    for step_pair in inner {
100        // each is a pipe_step
101        let inner_step = step_pair.into_inner().next().unwrap();
102        match inner_step.as_rule() {
103            Rule::pipe_forward => {
104                let inner_pair = inner_step.into_inner().next().unwrap();
105                let expr = if inner_pair.as_rule() == Rule::pipe_method_call {
106                    let mut mi = inner_pair.into_inner();
107                    let name = mi.next().unwrap().as_str().to_string();
108                    let args = mi.next().map(parse_arg_list).unwrap_or_default();
109                    Expr::Chain(Box::new(Expr::Current), vec![Step::Method(name, args)])
110                } else {
111                    parse_expr(inner_pair)
112                };
113                steps.push(PipeStep::Forward(expr));
114            }
115            Rule::pipe_bind => {
116                let target = parse_bind_target(inner_step.into_inner().next().unwrap());
117                steps.push(PipeStep::Bind(target));
118            }
119            r => panic!("unexpected pipe_step inner: {:?}", r),
120        }
121    }
122    if steps.is_empty() { base } else { Expr::Pipeline { base: Box::new(base), steps } }
123}
124
125fn parse_bind_target(pair: Pair<Rule>) -> BindTarget {
126    // pair.as_rule() == Rule::bind_target
127    let inner = pair.into_inner().next().unwrap();
128    match inner.as_rule() {
129        Rule::ident => BindTarget::Name(inner.as_str().to_string()),
130        Rule::bind_obj => {
131            let mut fields = Vec::new();
132            let mut rest = None;
133            for p in inner.into_inner() {
134                match p.as_rule() {
135                    Rule::ident => fields.push(p.as_str().to_string()),
136                    Rule::bind_rest => {
137                        rest = Some(
138                            p.into_inner()
139                             .find(|x| x.as_rule() == Rule::ident)
140                             .unwrap()
141                             .as_str()
142                             .to_string()
143                        );
144                    }
145                    _ => {}
146                }
147            }
148            BindTarget::Obj { fields, rest }
149        }
150        Rule::bind_arr => {
151            let fields: Vec<String> = inner.into_inner()
152                .filter(|p| p.as_rule() == Rule::ident)
153                .map(|p| p.as_str().to_string())
154                .collect();
155            BindTarget::Arr(fields)
156        }
157        r => panic!("unexpected bind_target inner: {:?}", r),
158    }
159}
160
161// ── Coalesce ──────────────────────────────────────────────────────────────────
162
163fn parse_coalesce(pair: Pair<Rule>) -> Expr {
164    let mut inner = pair.into_inner();
165    let first = parse_expr(inner.next().unwrap());
166    inner.fold(first, |acc, rhs| {
167        Expr::Coalesce(Box::new(acc), Box::new(parse_expr(rhs)))
168    })
169}
170
171// ── Logical ──────────────────────────────────────────────────────────────────
172
173fn parse_or(pair: Pair<Rule>) -> Expr {
174    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
175    let first = parse_expr(inner.next().unwrap());
176    inner.fold(first, |acc, rhs| {
177        Expr::BinOp(Box::new(acc), BinOp::Or, Box::new(parse_expr(rhs)))
178    })
179}
180
181fn parse_and(pair: Pair<Rule>) -> Expr {
182    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
183    let first = parse_expr(inner.next().unwrap());
184    inner.fold(first, |acc, rhs| {
185        Expr::BinOp(Box::new(acc), BinOp::And, Box::new(parse_expr(rhs)))
186    })
187}
188
189fn parse_not(pair: Pair<Rule>) -> Expr {
190    let mut inner = pair.into_inner();
191    let first = inner.next().unwrap();
192    if first.as_rule() == Rule::kw_not {
193        let operand = inner.next().unwrap();
194        Expr::Not(Box::new(parse_expr(operand)))
195    } else {
196        parse_expr(first)
197    }
198}
199
200// ── Kind check ────────────────────────────────────────────────────────────────
201
202fn parse_kind(pair: Pair<Rule>) -> Expr {
203    let mut inner = pair.into_inner();
204    let cmp = parse_expr(inner.next().unwrap());
205    match inner.next() {
206        None => cmp,
207        Some(p) if matches!(p.as_rule(), Rule::kw_kind | Rule::kw_is) => {
208            let next = inner.next().unwrap();
209            let (negate, kind_type_str) = if next.as_rule() == Rule::kw_not {
210                (true, inner.next().unwrap().as_str())
211            } else {
212                (false, next.as_str())
213            };
214            let ty = match kind_type_str {
215                "null"   => KindType::Null,
216                "bool"   => KindType::Bool,
217                "number" => KindType::Number,
218                "string" => KindType::Str,
219                "array"  => KindType::Array,
220                "object" => KindType::Object,
221                other    => panic!("unknown kind type: {}", other),
222            };
223            Expr::Kind { expr: Box::new(cmp), ty, negate }
224        }
225        _ => cmp,
226    }
227}
228
229// ── Comparison ───────────────────────────────────────────────────────────────
230
231fn parse_cmp(pair: Pair<Rule>) -> Expr {
232    let mut inner = pair.into_inner();
233    let lhs = parse_expr(inner.next().unwrap());
234    if let Some(op_pair) = inner.next() {
235        let op = match op_pair.as_str() {
236            "==" => BinOp::Eq,  "!=" => BinOp::Neq,
237            "<"  => BinOp::Lt,  "<=" => BinOp::Lte,
238            ">"  => BinOp::Gt,  ">=" => BinOp::Gte,
239            "~=" => BinOp::Fuzzy,
240            o    => panic!("unknown cmp op: {}", o),
241        };
242        let rhs = parse_expr(inner.next().unwrap());
243        Expr::BinOp(Box::new(lhs), op, Box::new(rhs))
244    } else {
245        lhs
246    }
247}
248
249// ── Additive / multiplicative ─────────────────────────────────────────────────
250
251fn parse_add(pair: Pair<Rule>) -> Expr {
252    parse_left_assoc(pair, |s| match s {
253        "+" => Some(BinOp::Add), "-" => Some(BinOp::Sub), _ => None,
254    })
255}
256
257fn parse_mul(pair: Pair<Rule>) -> Expr {
258    parse_left_assoc(pair, |s| match s {
259        "*" => Some(BinOp::Mul), "/" => Some(BinOp::Div), "%" => Some(BinOp::Mod), _ => None,
260    })
261}
262
263fn parse_left_assoc<F>(pair: Pair<Rule>, op_fn: F) -> Expr
264where
265    F: Fn(&str) -> Option<BinOp>,
266{
267    let mut inner = pair.into_inner().peekable();
268    let first = parse_expr(inner.next().unwrap());
269    let mut acc = first;
270    while inner.peek().is_some() {
271        let op_pair = inner.next().unwrap();
272        let op = op_fn(op_pair.as_str()).unwrap();
273        let rhs = parse_expr(inner.next().unwrap());
274        acc = Expr::BinOp(Box::new(acc), op, Box::new(rhs));
275    }
276    acc
277}
278
279// ── Cast ──────────────────────────────────────────────────────────────────────
280
281fn parse_cast(pair: Pair<Rule>) -> Expr {
282    // cast_expr = { unary_expr ~ (kw_as ~ kind_type)* }
283    let mut inner = pair.into_inner().peekable();
284    let mut acc = parse_expr(inner.next().unwrap());
285    while inner.peek().is_some() {
286        // Consume kw_as then kind_type
287        let kw = inner.next().unwrap();
288        debug_assert_eq!(kw.as_rule(), Rule::kw_as);
289        let ty_pair = inner.next().unwrap();
290        let ty = match ty_pair.as_str() {
291            "int"    => CastType::Int,
292            "float"  => CastType::Float,
293            "number" => CastType::Number,
294            "string" => CastType::Str,
295            "bool"   => CastType::Bool,
296            "array"  => CastType::Array,
297            "object" => CastType::Object,
298            "null"   => CastType::Null,
299            o        => panic!("unknown cast type: {}", o),
300        };
301        acc = Expr::Cast { expr: Box::new(acc), ty };
302    }
303    acc
304}
305
306// ── Unary ─────────────────────────────────────────────────────────────────────
307
308fn parse_unary(pair: Pair<Rule>) -> Expr {
309    let mut inner = pair.into_inner();
310    let first = inner.next().unwrap();
311    match first.as_rule() {
312        Rule::unary_neg => {
313            let operand = parse_expr(inner.next().unwrap());
314            Expr::UnaryNeg(Box::new(operand))
315        }
316        _ => parse_expr(first),
317    }
318}
319
320// ── Postfix chain ─────────────────────────────────────────────────────────────
321
322fn parse_postfix_expr(pair: Pair<Rule>) -> Expr {
323    let mut inner = pair.into_inner();
324    let base = parse_primary(inner.next().unwrap());
325    let steps: Vec<Step> = inner.flat_map(parse_postfix_step).collect();
326    base.maybe_chain(steps)
327}
328
329fn parse_postfix_step(pair: Pair<Rule>) -> Vec<Step> {
330    let inner_pair = pair.into_inner().next().unwrap();
331    match inner_pair.as_rule() {
332        Rule::field_access => {
333            let name = inner_pair.into_inner().next().unwrap().as_str().to_string();
334            vec![Step::Field(name)]
335        }
336        Rule::opt_field => {
337            let name = inner_pair.into_inner().next().unwrap().as_str().to_string();
338            vec![Step::OptField(name)]
339        }
340        Rule::descendant => {
341            let mut di = inner_pair.into_inner();
342            match di.next() {
343                Some(p) => vec![Step::Descendant(p.as_str().to_string())],
344                None    => vec![Step::DescendAll],
345            }
346        }
347        Rule::inline_filter => {
348            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
349            vec![Step::InlineFilter(Box::new(expr))]
350        }
351        Rule::quantifier => {
352            let s = inner_pair.as_str();
353            if s.starts_with('!') { vec![Step::Quantifier(QuantifierKind::One)] }
354            else                  { vec![Step::Quantifier(QuantifierKind::First)] }
355        }
356        Rule::method_call => {
357            let mut mi = inner_pair.into_inner();
358            let name = mi.next().unwrap().as_str().to_string();
359            let args = mi.next().map(parse_arg_list).unwrap_or_default();
360            vec![Step::Method(name, args)]
361        }
362        Rule::opt_method => {
363            let mut mi = inner_pair.into_inner();
364            let name = mi.next().unwrap().as_str().to_string();
365            let args = mi.next().map(parse_arg_list).unwrap_or_default();
366            vec![Step::OptMethod(name, args)]
367        }
368        Rule::index_access => {
369            let bi = inner_pair.into_inner().next().unwrap();
370            vec![parse_bracket(bi)]
371        }
372        Rule::dyn_field => {
373            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
374            vec![Step::DynIndex(Box::new(expr))]
375        }
376        Rule::map_into_shape => {
377            // `[*] => body` or `[* if g] => body`
378            // Desugar: with guard  → .filter(g).map(body)
379            //          no guard    → .map(body)
380            let mut guard: Option<Expr> = None;
381            let mut body:  Option<Expr> = None;
382            let mut saw_if = false;
383            for p in inner_pair.into_inner() {
384                match p.as_rule() {
385                    Rule::kw_if => saw_if = true,
386                    Rule::expr  => {
387                        if saw_if && guard.is_none() { guard = Some(parse_expr(p)); }
388                        else                         { body  = Some(parse_expr(p)); }
389                    }
390                    _ => {}
391                }
392            }
393            let body = body.expect("map_into_shape requires body");
394            let mut steps = Vec::new();
395            if let Some(g) = guard {
396                steps.push(Step::Method("filter".into(), vec![Arg::Pos(g)]));
397            }
398            steps.push(Step::Method("map".into(), vec![Arg::Pos(body)]));
399            steps
400        }
401        r => panic!("unexpected postfix rule: {:?}", r),
402    }
403}
404
405fn parse_bracket(pair: Pair<Rule>) -> Step {
406    let inner = pair.into_inner().next().unwrap();
407    match inner.as_rule() {
408        Rule::idx_only   => Step::Index(inner.as_str().parse().unwrap()),
409        Rule::slice_full => {
410            let mut i = inner.into_inner();
411            let a = i.next().unwrap().as_str().parse().ok();
412            let b = i.next().unwrap().as_str().parse().ok();
413            Step::Slice(a, b)
414        }
415        Rule::slice_from => {
416            let a = inner.into_inner().next().unwrap().as_str().parse().ok();
417            Step::Slice(a, None)
418        }
419        Rule::slice_to => {
420            let b = inner.into_inner().next().unwrap().as_str().parse().ok();
421            Step::Slice(None, b)
422        }
423        Rule::expr => Step::DynIndex(Box::new(parse_expr(inner))),
424        r => panic!("unexpected bracket rule: {:?}", r),
425    }
426}
427
428// ── Primary ───────────────────────────────────────────────────────────────────
429
430fn parse_primary(pair: Pair<Rule>) -> Expr {
431    let inner = if pair.as_rule() == Rule::primary {
432        pair.into_inner().next().unwrap()
433    } else {
434        pair
435    };
436    match inner.as_rule() {
437        Rule::literal       => parse_literal(inner),
438        Rule::root          => Expr::Root,
439        Rule::current       => Expr::Current,
440        Rule::ident         => Expr::Ident(inner.as_str().to_string()),
441        Rule::let_expr      => parse_let(inner),
442        Rule::lambda_expr   => parse_lambda(inner),
443        Rule::arrow_lambda  => parse_arrow_lambda(inner),
444        Rule::list_comp     => parse_list_comp(inner),
445        Rule::dict_comp     => parse_dict_comp(inner),
446        Rule::set_comp      => parse_set_comp(inner),
447        Rule::gen_comp      => parse_gen_comp(inner),
448        Rule::obj_construct => parse_obj(inner),
449        Rule::arr_construct => parse_arr(inner),
450        Rule::global_call   => parse_global_call(inner),
451        Rule::expr          => parse_expr(inner),
452        Rule::patch_block   => parse_patch(inner),
453        Rule::kw_delete     => Expr::DeleteMark,
454        r => panic!("unexpected primary rule: {:?}", r),
455    }
456}
457
458// ── Literals ──────────────────────────────────────────────────────────────────
459
460fn parse_literal(pair: Pair<Rule>) -> Expr {
461    let inner = pair.into_inner().next().unwrap();
462    match inner.as_rule() {
463        Rule::lit_null    => Expr::Null,
464        Rule::lit_true    => Expr::Bool(true),
465        Rule::lit_false   => Expr::Bool(false),
466        Rule::lit_int     => Expr::Int(inner.as_str().parse().unwrap()),
467        Rule::lit_float   => Expr::Float(inner.as_str().parse().unwrap()),
468        Rule::lit_fstring => {
469            let raw = inner.as_str();
470            let content = &raw[2..raw.len() - 1]; // strip f" and "
471            let parts = parse_fstring_content(content);
472            Expr::FString(parts)
473        }
474        Rule::lit_str => {
475            let s = inner.into_inner().next().unwrap();
476            let raw = s.as_str();
477            // Strip surrounding quotes (now atomic rules, so as_str includes them)
478            Expr::Str(raw[1..raw.len()-1].to_string())
479        }
480        r => panic!("unexpected literal rule: {:?}", r),
481    }
482}
483
484// ── F-string parser ───────────────────────────────────────────────────────────
485
486fn parse_fstring_content(raw: &str) -> Vec<FStringPart> {
487    let mut parts = Vec::new();
488    let mut lit = String::new();
489    let mut chars = raw.chars().peekable();
490
491    while let Some(c) = chars.next() {
492        match c {
493            '{' => {
494                if chars.peek() == Some(&'{') {
495                    chars.next();
496                    lit.push('{');
497                } else {
498                    if !lit.is_empty() {
499                        parts.push(FStringPart::Lit(std::mem::take(&mut lit)));
500                    }
501                    let mut inner = String::new();
502                    let mut depth = 1usize;
503                    for c2 in chars.by_ref() {
504                        match c2 {
505                            '{' => { depth += 1; inner.push(c2); }
506                            '}' => {
507                                depth -= 1;
508                                if depth == 0 { break; }
509                                inner.push(c2);
510                            }
511                            _ => inner.push(c2),
512                        }
513                    }
514                    let (expr_str, fmt) = split_fstring_interp(&inner);
515                    let expr = parse(expr_str.trim())
516                        .unwrap_or_else(|e| panic!("f-string parse error in {{{}}}: {}", inner, e));
517                    parts.push(FStringPart::Interp { expr, fmt });
518                }
519            }
520            '}' if chars.peek() == Some(&'}') => {
521                chars.next();
522                lit.push('}');
523            }
524            _ => lit.push(c),
525        }
526    }
527    if !lit.is_empty() {
528        parts.push(FStringPart::Lit(lit));
529    }
530    parts
531}
532
533fn split_fstring_interp(inner: &str) -> (&str, Option<FmtSpec>) {
534    // Find top-level `|` or `:` (not inside parens/brackets/braces)
535    let mut depth = 0usize;
536    let mut pipe_pos: Option<usize> = None;
537    let mut colon_pos: Option<usize> = None;
538    for (i, c) in inner.char_indices() {
539        match c {
540            '(' | '[' | '{' => depth += 1,
541            ')' | ']' | '}' => { if depth > 0 { depth -= 1; } }
542            '|' if depth == 0 && pipe_pos.is_none() => pipe_pos = Some(i),
543            ':' if depth == 0 && colon_pos.is_none() => colon_pos = Some(i),
544            _ => {}
545        }
546    }
547    if let Some(p) = pipe_pos {
548        return (&inner[..p], Some(FmtSpec::Pipe(inner[p + 1..].trim().to_string())));
549    }
550    if let Some(c) = colon_pos {
551        return (&inner[..c], Some(FmtSpec::Spec(inner[c + 1..].to_string())));
552    }
553    (inner, None)
554}
555
556// ── Let ───────────────────────────────────────────────────────────────────────
557
558fn parse_let(pair: Pair<Rule>) -> Expr {
559    // let_expr = { kw_let ~ let_binding ~ ("," ~ let_binding)* ~ kw_in ~ expr }
560    // Desugar multi-binding into nested Let: `let a=x, b=y in body` → Let a x (Let b y body)
561    let inner: Vec<Pair<Rule>> = pair.into_inner()
562        .filter(|p| !matches!(p.as_rule(), Rule::kw_let | Rule::kw_in))
563        .collect();
564    let (bindings, body_pair) = inner.split_at(inner.len() - 1);
565    let body = parse_expr(body_pair[0].clone());
566    bindings.iter().rev().fold(body, |acc, b| {
567        let mut bi = b.clone().into_inner();
568        let name = bi.next().unwrap().as_str().to_string();
569        let init = parse_expr(bi.next().unwrap());
570        Expr::Let { name, init: Box::new(init), body: Box::new(acc) }
571    })
572}
573
574// ── Lambda ────────────────────────────────────────────────────────────────────
575
576fn parse_lambda(pair: Pair<Rule>) -> Expr {
577    let mut inner = pair.into_inner()
578        .filter(|p| p.as_rule() != Rule::kw_lambda);
579    let params_pair = inner.next().unwrap();
580    let params: Vec<String> = params_pair
581        .into_inner()
582        .filter(|p| p.as_rule() == Rule::ident)
583        .map(|p| p.as_str().to_string())
584        .collect();
585    let body = parse_expr(inner.next().unwrap());
586    Expr::Lambda { params, body: Box::new(body) }
587}
588
589/// Arrow lambda: `x => body` or `(x, y) => body`.  Lowered to same
590/// `Expr::Lambda` node — the `=>` form is pure surface sugar.
591fn parse_arrow_lambda(pair: Pair<Rule>) -> Expr {
592    let mut inner = pair.into_inner();
593    let params_pair = inner.next().unwrap();
594    let params: Vec<String> = params_pair
595        .into_inner()
596        .filter(|p| p.as_rule() == Rule::ident)
597        .map(|p| p.as_str().to_string())
598        .collect();
599    let body = parse_expr(inner.next().unwrap());
600    Expr::Lambda { params, body: Box::new(body) }
601}
602
603// ── Comprehensions ────────────────────────────────────────────────────────────
604
605fn comp_inner_filter(pair: Pair<Rule>) -> impl Iterator<Item = Pair<Rule>> {
606    pair.into_inner()
607        .filter(|p| !matches!(p.as_rule(), Rule::kw_for | Rule::kw_in | Rule::kw_if))
608}
609
610fn parse_comp_vars(pair: Pair<Rule>) -> Vec<String> {
611    pair.into_inner()
612        .filter(|p| p.as_rule() == Rule::ident)
613        .map(|p| p.as_str().to_string())
614        .collect()
615}
616
617fn parse_list_comp(pair: Pair<Rule>) -> Expr {
618    let mut inner = comp_inner_filter(pair);
619    let expr = parse_expr(inner.next().unwrap());
620    let vars = parse_comp_vars(inner.next().unwrap());
621    let iter = parse_expr(inner.next().unwrap());
622    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
623    Expr::ListComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
624}
625
626fn parse_dict_comp(pair: Pair<Rule>) -> Expr {
627    let mut inner = comp_inner_filter(pair);
628    let key  = parse_expr(inner.next().unwrap());
629    let val  = parse_expr(inner.next().unwrap());
630    let vars = parse_comp_vars(inner.next().unwrap());
631    let iter = parse_expr(inner.next().unwrap());
632    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
633    Expr::DictComp { key: Box::new(key), val: Box::new(val), vars, iter: Box::new(iter), cond }
634}
635
636fn parse_set_comp(pair: Pair<Rule>) -> Expr {
637    let mut inner = comp_inner_filter(pair);
638    let expr = parse_expr(inner.next().unwrap());
639    let vars = parse_comp_vars(inner.next().unwrap());
640    let iter = parse_expr(inner.next().unwrap());
641    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
642    Expr::SetComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
643}
644
645fn parse_gen_comp(pair: Pair<Rule>) -> Expr {
646    let mut inner = comp_inner_filter(pair);
647    let expr = parse_expr(inner.next().unwrap());
648    let vars = parse_comp_vars(inner.next().unwrap());
649    let iter = parse_expr(inner.next().unwrap());
650    let cond = inner.next().map(|p| Box::new(parse_expr(p)));
651    Expr::GenComp { expr: Box::new(expr), vars, iter: Box::new(iter), cond }
652}
653
654// ── Object / array construction ────────────────────────────────────────────────
655
656fn parse_obj(pair: Pair<Rule>) -> Expr {
657    let fields = pair.into_inner()
658        .filter(|p| p.as_rule() == Rule::obj_field)
659        .map(parse_obj_field)
660        .collect();
661    Expr::Object(fields)
662}
663
664fn parse_obj_field(pair: Pair<Rule>) -> ObjField {
665    let inner = pair.into_inner().next().unwrap();
666    match inner.as_rule() {
667        Rule::obj_field_dyn => {
668            let mut i = inner.into_inner();
669            let key = parse_expr(i.next().unwrap());
670            let val = parse_expr(i.next().unwrap());
671            ObjField::Dynamic { key, val }
672        }
673        Rule::obj_field_opt_v => {
674            let mut i = inner.into_inner();
675            let key = obj_key_str(i.next().unwrap());
676            let val = parse_expr(i.next().unwrap());
677            ObjField::Kv { key, val, optional: true, cond: None }
678        }
679        Rule::obj_field_opt => {
680            let key = obj_key_str(inner.into_inner().next().unwrap());
681            ObjField::Kv { key: key.clone(), val: Expr::Ident(key), optional: true, cond: None }
682        }
683        Rule::obj_field_spread => {
684            let expr = parse_expr(inner.into_inner().next().unwrap());
685            ObjField::Spread(expr)
686        }
687        Rule::obj_field_spread_deep => {
688            let expr = parse_expr(inner.into_inner().next().unwrap());
689            ObjField::SpreadDeep(expr)
690        }
691        Rule::obj_field_kv => {
692            let mut cond: Option<Expr> = None;
693            let mut key: Option<String> = None;
694            let mut val: Option<Expr> = None;
695            let mut saw_when = false;
696            for p in inner.into_inner() {
697                match p.as_rule() {
698                    Rule::kw_when => saw_when = true,
699                    Rule::obj_key_expr => key = Some(obj_key_str(p)),
700                    Rule::expr => {
701                        if saw_when { cond = Some(parse_expr(p)); }
702                        else        { val  = Some(parse_expr(p)); }
703                    }
704                    _ => {}
705                }
706            }
707            ObjField::Kv {
708                key:      key.expect("obj_field_kv missing key"),
709                val:      val.expect("obj_field_kv missing val"),
710                optional: false,
711                cond,
712            }
713        }
714        Rule::obj_field_short => {
715            let name = inner.into_inner().next().unwrap().as_str().to_string();
716            ObjField::Short(name)
717        }
718        r => panic!("unexpected obj_field rule: {:?}", r),
719    }
720}
721
722fn obj_key_str(pair: Pair<Rule>) -> String {
723    let inner = pair.into_inner().next().unwrap();
724    match inner.as_rule() {
725        Rule::ident => inner.as_str().to_string(),
726        Rule::lit_str => {
727            let s = inner.into_inner().next().unwrap();
728            let raw = s.as_str();
729            raw[1..raw.len()-1].to_string()
730        }
731        r => panic!("unexpected obj_key_expr rule: {:?}", r),
732    }
733}
734
735fn parse_arr(pair: Pair<Rule>) -> Expr {
736    let elems = pair.into_inner()
737        .filter(|p| p.as_rule() == Rule::arr_elem)
738        .map(|elem| {
739            let inner = elem.into_inner().next().unwrap();
740            match inner.as_rule() {
741                Rule::arr_spread => {
742                    let expr = parse_expr(inner.into_inner().next().unwrap());
743                    ArrayElem::Spread(expr)
744                }
745                _ => ArrayElem::Expr(parse_expr(inner)),
746            }
747        })
748        .collect();
749    Expr::Array(elems)
750}
751
752// ── Global call ───────────────────────────────────────────────────────────────
753
754fn parse_global_call(pair: Pair<Rule>) -> Expr {
755    let mut inner = pair.into_inner();
756    let name = inner.next().unwrap().as_str().to_string();
757    let args = inner.next().map(parse_arg_list).unwrap_or_default();
758    Expr::GlobalCall { name, args }
759}
760
761// ── Patch block ───────────────────────────────────────────────────────────────
762
763fn parse_patch(pair: Pair<Rule>) -> Expr {
764    let mut root: Option<Expr> = None;
765    let mut ops: Vec<PatchOp> = Vec::new();
766    for p in pair.into_inner() {
767        match p.as_rule() {
768            Rule::kw_patch   => {}
769            Rule::patch_field => ops.push(parse_patch_field(p)),
770            _ => {
771                // This branch handles the root expression (coalesce_expr and
772                // any of its descendants that parse_expr accepts).
773                if root.is_none() { root = Some(parse_expr(p)); }
774            }
775        }
776    }
777    Expr::Patch {
778        root: Box::new(root.expect("patch requires root expression")),
779        ops,
780    }
781}
782
783fn parse_patch_field(pair: Pair<Rule>) -> PatchOp {
784    let mut path: Vec<PathStep> = Vec::new();
785    let mut val:  Option<Expr> = None;
786    let mut cond: Option<Expr> = None;
787    let mut saw_when = false;
788    for p in pair.into_inner() {
789        match p.as_rule() {
790            Rule::patch_key => path = parse_patch_key(p),
791            Rule::kw_when   => saw_when = true,
792            Rule::expr => {
793                if saw_when { cond = Some(parse_expr(p)); }
794                else        { val  = Some(parse_expr(p)); }
795            }
796            _ => {}
797        }
798    }
799    PatchOp {
800        path,
801        val:  val.expect("patch_field missing val"),
802        cond,
803    }
804}
805
806fn parse_patch_key(pair: Pair<Rule>) -> Vec<PathStep> {
807    let mut steps: Vec<PathStep> = Vec::new();
808    let mut first = true;
809    for p in pair.into_inner() {
810        match p.as_rule() {
811            Rule::ident if first => {
812                steps.push(PathStep::Field(p.as_str().to_string()));
813                first = false;
814            }
815            Rule::patch_step => steps.push(parse_patch_step(p)),
816            _ => {}
817        }
818    }
819    steps
820}
821
822fn parse_patch_step(pair: Pair<Rule>) -> PathStep {
823    let inner = pair.into_inner().next().unwrap();
824    match inner.as_rule() {
825        Rule::pp_dot_field => {
826            let name = inner.into_inner().next().unwrap().as_str().to_string();
827            PathStep::Field(name)
828        }
829        Rule::pp_index => {
830            let idx: i64 = inner.into_inner().next().unwrap().as_str().parse().unwrap();
831            PathStep::Index(idx)
832        }
833        Rule::pp_wild => PathStep::Wildcard,
834        Rule::pp_wild_filter => {
835            // `[* if expr]`
836            let mut e: Option<Expr> = None;
837            for p in inner.into_inner() {
838                if p.as_rule() == Rule::expr { e = Some(parse_expr(p)); }
839            }
840            PathStep::WildcardFilter(Box::new(e.expect("pp_wild_filter missing expr")))
841        }
842        Rule::pp_descendant => {
843            let name = inner.into_inner().next().unwrap().as_str().to_string();
844            PathStep::Descendant(name)
845        }
846        r => panic!("unexpected patch_step rule: {:?}", r),
847    }
848}
849
850// ── Arguments ─────────────────────────────────────────────────────────────────
851
852fn parse_arg_list(pair: Pair<Rule>) -> Vec<Arg> {
853    pair.into_inner()
854        .filter(|p| p.as_rule() == Rule::arg)
855        .map(parse_arg)
856        .collect()
857}
858
859fn parse_arg(pair: Pair<Rule>) -> Arg {
860    let inner = pair.into_inner().next().unwrap();
861    match inner.as_rule() {
862        Rule::named_arg => {
863            let mut i = inner.into_inner();
864            let name = i.next().unwrap().as_str().to_string();
865            let val  = parse_expr(i.next().unwrap());
866            Arg::Named(name, val)
867        }
868        Rule::pos_arg => {
869            Arg::Pos(parse_expr(inner.into_inner().next().unwrap()))
870        }
871        r => panic!("unexpected arg rule: {:?}", r),
872    }
873}