//! PEG parser for the Jetro query language.
//!
//! `grammar.pest` defines the grammar; `pest_derive` generates `V2Parser`.
//! `parse()` drives the parser and walks the parse tree into an `Expr` AST.
//! `classify_chain_write` post-processes rooted chain expressions that end in
//! `.set` / `.modify` / `.delete` / `.unset` into update AST nodes so the
//! evaluator never needs to special-case the write surface at runtime.

use pest::iterators::Pair;
use pest::Parser as PestParser;
use pest_derive::Parser;
use std::{fmt, sync::Arc};

use super::ast::*;
use crate::data::value::Val;
use super::write_terminal::{
    build_patch_op, is_chain_write_terminal, steps_to_path as write_steps_to_path,
};

const INVALID_HAS_ARRAY_RHS: &str = "__jetro_invalid_has_array_rhs";

/// Pest-derived parser for the v2 grammar. The grammar file is embedded at
/// compile time via the `#[grammar]` attribute and is not loaded at runtime.
#[derive(Parser)]
#[grammar = "grammar.pest"]
pub struct V2Parser;

/// Returned by `parse` when the input does not conform to the grammar or when
/// a semantic constraint (e.g. unknown cast type) is violated.
#[derive(Debug)]
pub struct ParseError(pub String);

impl fmt::Display for ParseError {
    /// Format the error as a human-readable message including the source snippet.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "parse error: {}", self.0)
    }
}

impl std::error::Error for ParseError {}

impl From<pest::error::Error<Rule>> for ParseError {
    /// Convert a pest parse error into a `ParseError`, preserving the full
    /// location and message pest provides.
    fn from(e: pest::error::Error<Rule>) -> Self {
        ParseError(e.to_string())
    }
}

/// Parse a Jetro query string into an `Expr` AST. This is the primary public
/// entry point; all other `parse_*` functions are internal helpers.
///
/// In addition to grammar-level parsing, this entry point runs post-parse
/// semantic validation passes (currently or-pattern linearity for `match`
/// expressions) so callers see structural errors before the expression
/// reaches the compiler.
pub fn parse(input: &str) -> Result<Expr, ParseError> {
    let mut pairs = V2Parser::parse(Rule::program, input)?;
    let program = pairs.next().unwrap();
    let expr_pair = program.into_inner().next().unwrap();
    let expr = parse_expr(expr_pair);
    validate_has_array_rhs(&expr)?;
    validate_pattern_linearity(&expr)?;
    if strict_match_lint_enabled() {
        validate_match_exhaustiveness(&expr)?;
    }
    // First-class lambda macro expansion: `let f = (x => …) in …` inlines
    // `f` at every method-arg position so downstream compile sees the
    // lambda directly. Pure AST rewrite; no runtime closure value.
    let expr = crate::compile::lambda_lower::inline_let_bound_lambdas(expr);
    Ok(expr)
}

fn validate_has_array_rhs(expr: &Expr) -> Result<(), ParseError> {
    fn check(expr: &Expr) -> Result<(), ParseError> {
        match expr {
            Expr::Chain(base, steps) => {
                check(base)?;
                for step in steps {
                    match step {
                        Step::Method(name, _) if name == INVALID_HAS_ARRAY_RHS => {
                            return Err(ParseError(
                                "has [...] requires scalar literal elements".to_string(),
                            ));
                        }
                        Step::DynIndex(expr) | Step::InlineFilter(expr) => check(expr)?,
                        Step::Method(_, args) | Step::OptMethod(_, args) => {
                            for arg in args {
                                match arg {
                                    Arg::Pos(expr) | Arg::Named(_, expr) => check(expr)?,
                                }
                            }
                        }
                        Step::DeepMatch { arms, .. } => {
                            for arm in arms {
                                if let Some(guard) = &arm.guard {
                                    check(guard)?;
                                }
                                check(&arm.body)?;
                            }
                        }
                        _ => {}
                    }
                }
            }
            Expr::BinOp(lhs, _, rhs) | Expr::Coalesce(lhs, rhs) => {
                check(lhs)?;
                check(rhs)?;
            }
            Expr::UnaryNeg(inner) | Expr::Not(inner) => check(inner)?,
            Expr::Kind { expr, .. } => check(expr)?,
            Expr::Array(elems) => {
                for elem in elems {
                    match elem {
                        ArrayElem::Expr(expr) | ArrayElem::Spread(expr) => check(expr)?,
                    }
                }
            }
            Expr::Object(fields) => {
                for field in fields {
                    match field {
                        ObjField::Kv { val, cond, .. } => {
                            check(val)?;
                            if let Some(cond) = cond {
                                check(cond)?;
                            }
                        }
                        ObjField::Dynamic { key, val } => {
                            check(key)?;
                            check(val)?;
                        }
                        ObjField::Short(_) => {}
                        ObjField::Spread(expr) | ObjField::SpreadDeep(expr) => check(expr)?,
                    }
                }
            }
            Expr::Pipeline { base, steps } => {
                check(base)?;
                for step in steps {
                    match step {
                        PipeStep::Forward(expr) => check(expr)?,
                        PipeStep::Bind(_) => {}
                    }
                }
            }
            Expr::ListComp { expr, iter, cond, .. }
            | Expr::GenComp { expr, iter, cond, .. }
            | Expr::SetComp { expr, iter, cond, .. } => {
                check(expr)?;
                check(iter)?;
                if let Some(cond) = cond {
                    check(cond)?;
                }
            }
            Expr::DictComp {
                key,
                val,
                iter,
                cond,
                ..
            } => {
                check(key)?;
                check(val)?;
                check(iter)?;
                if let Some(cond) = cond {
                    check(cond)?;
                }
            }
            Expr::Lambda { body, .. } => check(body)?,
            Expr::Let { init, body, .. } => {
                check(init)?;
                check(body)?;
            }
            Expr::IfElse { cond, then_, else_ } => {
                check(cond)?;
                check(then_)?;
                check(else_)?;
            }
            Expr::Try { body, default } => {
                check(body)?;
                check(default)?;
            }
            Expr::GlobalCall { args, .. } => {
                for arg in args {
                    match arg {
                        Arg::Pos(expr) | Arg::Named(_, expr) => check(expr)?,
                    }
                }
            }
            Expr::Match { scrutinee, arms } => {
                check(scrutinee)?;
                for arm in arms {
                    if let Some(guard) = &arm.guard {
                        check(guard)?;
                    }
                    check(&arm.body)?;
                }
            }
            Expr::Cast { expr, .. } => check(expr)?,
            Expr::Patch { root, ops } => {
                check(root)?;
                for op in ops {
                    check_patch_op(op)?;
                }
            }
            Expr::UpdateBatch { root, ops, .. } => {
                check(root)?;
                for op in ops {
                    check_patch_op(op)?;
                }
            }
            Expr::DeleteMark => {}
            Expr::FString(parts) => {
                for part in parts {
                    if let FStringPart::Interp { expr, .. } = part {
                        check(expr)?;
                    }
                }
            }
            Expr::Null
            | Expr::Bool(_)
            | Expr::Int(_)
            | Expr::Float(_)
            | Expr::Str(_)
            | Expr::Root
            | Expr::Current
            | Expr::Ident(_) => {}
        }
        Ok(())
    }
    fn check_patch_op(op: &PatchOp) -> Result<(), ParseError> {
        check(&op.val)?;
        if let Some(cond) = &op.cond {
            check(cond)?;
        }
        Ok(())
    }
    check(expr)
}

/// Read the `JETRO_STRICT_MATCH` environment variable on every parse.
/// Toggling the flag at runtime is rare in practice, and reading it per
/// call lets tooling and tests flip the lint without restarting the
/// process.
fn strict_match_lint_enabled() -> bool {
    std::env::var_os("JETRO_STRICT_MATCH").is_some()
}

/// Recognise irrefutable patterns — those that match every value
/// regardless of shape and therefore qualify as a catch-all arm in the
/// exhaustiveness analysis. Beyond the trivial `Wild` and bare `Bind`
/// cases, this also accepts `Or` patterns whose alternative list
/// contains an irrefutable alt (since at least one alt fires for every
/// value) and `Kind`-bound patterns whose kind covers all runtime
/// types (currently never, but the structure is in place for future
/// extension to a `_: any` form).
fn pat_is_irrefutable(pat: &Pat) -> bool {
    match pat {
        Pat::Wild | Pat::Bind(_) => true,
        Pat::Or(alts) => alts.iter().any(pat_is_irrefutable),
        _ => false,
    }
}

/// Walk the `Expr` tree and reject any `match` whose arms cannot prove
/// exhaustiveness — that is, when no arm has an unguarded catch-all
/// (`Pat::Wild`, a bare `Pat::Bind`, or an `Or`-pattern containing one).
/// Triggered only when the `JETRO_STRICT_MATCH` environment variable
/// is set; the default behaviour is to surface non-exhaustive matches
/// as a runtime `EvalError` when no arm fires.
fn validate_match_exhaustiveness(expr: &Expr) -> Result<(), ParseError> {
    fn check(e: &Expr) -> Result<(), ParseError> {
        match e {
            Expr::Match { scrutinee, arms } => {
                check(scrutinee)?;
                let exhaustive = arms
                    .iter()
                    .any(|a| a.guard.is_none() && pat_is_irrefutable(&a.pat));
                if !exhaustive {
                    return Err(ParseError(
                        "non-exhaustive match: no unguarded catch-all arm \
                         (`_`, a bare bind, or an or-pattern containing one)"
                            .to_string(),
                    ));
                }
                for arm in arms {
                    if let Some(g) = arm.guard.as_ref() {
                        check(g)?;
                    }
                    check(&arm.body)?;
                }
                Ok(())
            }
            Expr::Chain(base, _) => check(base),
            Expr::BinOp(l, _, r) | Expr::Coalesce(l, r) => {
                check(l)?;
                check(r)
            }
            Expr::UnaryNeg(e) | Expr::Not(e) | Expr::Cast { expr: e, .. } => check(e),
            Expr::Kind { expr, .. } => check(expr),
            _ => Ok(()),
        }
    }
    check(expr)
}

/// Walk the `Expr` tree and validate that every `match` arm's pattern
/// satisfies the linearity rule for or-patterns: each alternative inside
/// an `Or` must bind exactly the same set of variable names. This rules
/// out arms whose body cannot consistently reference a captured value
/// (e.g. `{a: x} | {b: y} -> x` would be ambiguous).
fn validate_pattern_linearity(expr: &Expr) -> Result<(), ParseError> {
    use std::collections::BTreeSet;

    fn collect_binds(pat: &Pat, out: &mut BTreeSet<String>) {
        match pat {
            Pat::Wild | Pat::Lit(_) | Pat::Range { .. } => {}
            Pat::Bind(name) => {
                out.insert(name.clone());
            }
            Pat::Kind { name, .. } => {
                if let Some(n) = name.as_deref() {
                    out.insert(n.to_string());
                }
            }
            Pat::Or(alts) => {
                for alt in alts {
                    collect_binds(alt, out);
                }
            }
            Pat::Obj { fields, rest } => {
                for (_, sub) in fields {
                    collect_binds(sub, out);
                }
                if let Some(Some(name)) = rest {
                    out.insert(name.clone());
                }
            }
            Pat::Arr { elems, rest } => {
                for sub in elems {
                    collect_binds(sub, out);
                }
                if let Some(Some(name)) = rest {
                    out.insert(name.clone());
                }
            }
        }
    }

    fn walk_pat(pat: &Pat) -> Result<(), ParseError> {
        if let Pat::Or(alts) = pat {
            let mut first: Option<BTreeSet<String>> = None;
            for alt in alts {
                let mut names = BTreeSet::new();
                collect_binds(alt, &mut names);
                match &first {
                    None => first = Some(names),
                    Some(existing) if existing == &names => {}
                    Some(existing) => {
                        return Err(ParseError(format!(
                            "or-pattern arms must bind the same variables; \
                             got {{{}}} vs {{{}}}",
                            existing.iter().cloned().collect::<Vec<_>>().join(", "),
                            names.iter().cloned().collect::<Vec<_>>().join(", "),
                        )));
                    }
                }
            }
        }
        match pat {
            Pat::Wild | Pat::Lit(_) | Pat::Bind(_) | Pat::Kind { .. } | Pat::Range { .. } => {}
            Pat::Or(alts) => {
                for alt in alts {
                    walk_pat(alt)?;
                }
            }
            Pat::Obj { fields, .. } => {
                for (_, sub) in fields {
                    walk_pat(sub)?;
                }
            }
            Pat::Arr { elems, .. } => {
                for sub in elems {
                    walk_pat(sub)?;
                }
            }
        }
        Ok(())
    }

    fn walk(e: &Expr) -> Result<(), ParseError> {
        match e {
            Expr::Match { scrutinee, arms } => {
                walk(scrutinee)?;
                for arm in arms {
                    walk_pat(&arm.pat)?;
                    if let Some(g) = arm.guard.as_ref() {
                        walk(g)?;
                    }
                    walk(&arm.body)?;
                }
            }
            Expr::Chain(base, steps) => {
                walk(base)?;
                for step in steps {
                    if let Step::DeepMatch { arms, .. } = step {
                        for arm in arms {
                            walk_pat(&arm.pat)?;
                            if let Some(g) = arm.guard.as_ref() {
                                walk(g)?;
                            }
                            walk(&arm.body)?;
                        }
                    }
                }
            }
            Expr::BinOp(l, _, r) | Expr::Coalesce(l, r) => {
                walk(l)?;
                walk(r)?;
            }
            Expr::UnaryNeg(e) | Expr::Not(e) | Expr::Cast { expr: e, .. } => walk(e)?,
            Expr::Kind { expr, .. } => walk(expr)?,
            Expr::Object(_)
            | Expr::Array(_)
            | Expr::Pipeline { .. }
            | Expr::ListComp { .. }
            | Expr::DictComp { .. }
            | Expr::SetComp { .. }
            | Expr::GenComp { .. }
            | Expr::Lambda { .. }
            | Expr::Let { .. }
            | Expr::IfElse { .. }
            | Expr::Try { .. }
            | Expr::GlobalCall { .. }
            | Expr::Patch { .. }
            | Expr::UpdateBatch { .. }
            | Expr::FString(_)
            | Expr::Null
            | Expr::Bool(_)
            | Expr::Int(_)
            | Expr::Float(_)
            | Expr::Str(_)
            | Expr::Root
            | Expr::Current
            | Expr::Ident(_)
            | Expr::DeleteMark => {}
        }
        Ok(())
    }

    walk(expr)
}

/// Return `true` when `rule` is a keyword terminal (`and`, `or`, `not`, …).
/// Used to skip keyword tokens that appear as decoration in binary/unary rules.
fn is_kw(rule: Rule) -> bool {
    matches!(
        rule,
        Rule::kw_and
            | Rule::kw_or
            | Rule::kw_not
            | Rule::kw_for
            | Rule::kw_in
            | Rule::kw_if
            | Rule::kw_else
            | Rule::kw_let
            | Rule::kw_lambda
            | Rule::kw_kind
            | Rule::kw_is
            | Rule::kw_as
            | Rule::kw_try
            | Rule::kw_when
            | Rule::kw_match
            | Rule::kw_with
    )
}

/// Dispatch on `pair.as_rule()` and delegate to the appropriate specialised
/// `parse_*` function, covering the full expression precedence hierarchy.
fn parse_expr(pair: Pair<Rule>) -> Expr {
    match pair.as_rule() {
        Rule::expr => parse_expr(pair.into_inner().next().unwrap()),
        Rule::cond_expr => parse_cond(pair),
        Rule::pipe_expr => parse_pipeline(pair),
        Rule::coalesce_expr => parse_coalesce(pair),
        Rule::or_expr => parse_or(pair),
        Rule::and_expr => parse_and(pair),
        Rule::not_expr => parse_not(pair),
        Rule::kind_expr => parse_kind(pair),
        Rule::contains_expr => parse_contains(pair),
        Rule::cmp_expr => parse_cmp(pair),
        Rule::add_expr => parse_add(pair),
        Rule::mul_expr => parse_mul(pair),
        Rule::cast_expr => parse_cast(pair),
        Rule::unary_expr => parse_unary(pair),
        Rule::postfix_expr => parse_postfix_expr(pair),
        Rule::primary => parse_primary(pair),
        r => panic!("unexpected rule in parse_expr: {:?}", r),
    }
}

/// Parse a conditional expression (`if … then … else …`) or a `try … else …`
/// expression. When the pair contains only one sub-expression, it is returned
/// directly without wrapping.
fn parse_cond(pair: Pair<Rule>) -> Expr {
    // Grammar: cond_expr = { try_expr | (pipe_expr ("if" pipe_expr "else" pipe_expr)?) }
    // After filtering keywords the order is: then, cond, else.
    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
    let head = inner.next().unwrap();
    if head.as_rule() == Rule::try_expr {
        return parse_try(head);
    }
    let then_ = parse_expr(head);
    let cond = match inner.next() {
        Some(p) => parse_expr(p),
        None => return then_,
    };
    let else_ = parse_expr(inner.next().unwrap());
    Expr::IfElse {
        cond: Box::new(cond),
        then_: Box::new(then_),
        else_: Box::new(else_),
    }
}

/// Parse a `try <expr> else <default>` expression into `Expr::Try`,
/// evaluating `body` and falling back to `default` on any evaluation error.
fn parse_try(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
    // The body is wrapped in a try_body rule; unwrap it.
    let body_pair = inner.next().unwrap();
    let body = {
        // try_body contains a single expr child
        let mut bi = body_pair.into_inner();
        parse_expr(bi.next().unwrap())
    };
    let default = parse_expr(inner.next().unwrap());
    Expr::Try {
        body: Box::new(body),
        default: Box::new(default),
    }
}

/// Parse a pipeline expression `base | step1 | step2 …` into `Expr::Pipeline`.
/// Each step is either a forward expression or a `-> pattern` bind target.
/// Returns `base` directly when there are no pipeline steps.
fn parse_pipeline(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let base = parse_expr(inner.next().unwrap()); // first child is the base expr
    let mut steps: Vec<PipeStep> = Vec::new();
    for step_pair in inner {
        // Each pipe_step has exactly one inner rule: pipe_forward or pipe_bind.
        let inner_step = step_pair.into_inner().next().unwrap();
        match inner_step.as_rule() {
            Rule::pipe_forward => {
                let inner_pair = inner_step.into_inner().next().unwrap();
                let expr = if inner_pair.as_rule() == Rule::pipe_method_call {
                    let mut mi = inner_pair.into_inner();
                    let name = mi.next().unwrap().as_str().to_string();
                    let args = mi.next().map(parse_arg_list).unwrap_or_default();
                    Expr::Chain(Box::new(Expr::Current), vec![Step::Method(name, args)])
                } else {
                    parse_expr(inner_pair)
                };
                steps.push(PipeStep::Forward(expr));
            }
            Rule::pipe_bind => {
                let target = parse_bind_target(inner_step.into_inner().next().unwrap());
                steps.push(PipeStep::Bind(target));
            }
            r => panic!("unexpected pipe_step inner: {:?}", r),
        }
    }
    if steps.is_empty() {
        base
    } else {
        Expr::Pipeline {
            base: Box::new(base),
            steps,
        }
    }
}

/// Parse a bind target for a pipe bind step (`-> name`, `-> {a, b, ..rest}`,
/// or `-> [a, b]`), returning the corresponding `BindTarget` variant.
fn parse_bind_target(pair: Pair<Rule>) -> BindTarget {
    // pair is bind_target; its single inner child determines the variant.
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::ident => BindTarget::Name(inner.as_str().to_string()),
        Rule::bind_obj => {
            let mut fields = Vec::new();
            let mut rest = None;
            for p in inner.into_inner() {
                match p.as_rule() {
                    Rule::ident => fields.push(p.as_str().to_string()),
                    Rule::bind_rest => {
                        rest = Some(
                            p.into_inner()
                                .find(|x| x.as_rule() == Rule::ident)
                                .unwrap()
                                .as_str()
                                .to_string(),
                        );
                    }
                    _ => {}
                }
            }
            BindTarget::Obj { fields, rest }
        }
        Rule::bind_arr => {
            let fields: Vec<String> = inner
                .into_inner()
                .filter(|p| p.as_rule() == Rule::ident)
                .map(|p| p.as_str().to_string())
                .collect();
            BindTarget::Arr(fields)
        }
        r => panic!("unexpected bind_target inner: {:?}", r),
    }
}

/// Parse a coalesce expression `a ?? b ?? c` left-associatively into nested
/// `Expr::Coalesce` nodes, returning the first non-null result at runtime.
fn parse_coalesce(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let first = parse_expr(inner.next().unwrap());
    inner.fold(first, |acc, rhs| {
        Expr::Coalesce(Box::new(acc), Box::new(parse_expr(rhs)))
    })
}

/// Parse an `or` expression `a or b or c` left-associatively, filtering keyword
/// tokens, into nested `Expr::BinOp(_, BinOp::Or, _)` nodes.
fn parse_or(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
    let first = parse_expr(inner.next().unwrap());
    inner.fold(first, |acc, rhs| {
        Expr::BinOp(Box::new(acc), BinOp::Or, Box::new(parse_expr(rhs)))
    })
}

/// Parse an `and` expression left-associatively, filtering keyword tokens,
/// into nested `Expr::BinOp(_, BinOp::And, _)` nodes.
fn parse_and(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
    let first = parse_expr(inner.next().unwrap());
    inner.fold(first, |acc, rhs| {
        Expr::BinOp(Box::new(acc), BinOp::And, Box::new(parse_expr(rhs)))
    })
}

/// Parse a `not` expression; wraps the operand in `Expr::Not` when the first
/// token is the `not` keyword, otherwise delegates to the operand directly.
fn parse_not(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let first = inner.next().unwrap();
    if first.as_rule() == Rule::kw_not {
        let operand = inner.next().unwrap();
        Expr::Not(Box::new(parse_expr(operand)))
    } else {
        parse_expr(first)
    }
}

/// Parse a `kind is <type>` or `kind is not <type>` type-check expression,
/// returning the bare operand when no kind clause is present.
fn parse_kind(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let cmp = parse_expr(inner.next().unwrap());
    match inner.next() {
        None => cmp,
        Some(p) if matches!(p.as_rule(), Rule::kw_kind | Rule::kw_is) => {
            let next = inner.next().unwrap();
            let (negate, kind_type_str) = if next.as_rule() == Rule::kw_not {
                (true, inner.next().unwrap().as_str())
            } else {
                (false, next.as_str())
            };
            let ty = match kind_type_str {
                "null" => KindType::Null,
                "bool" => KindType::Bool,
                "number" => KindType::Number,
                "string" => KindType::Str,
                "array" => KindType::Array,
                "object" => KindType::Object,
                other => panic!("unknown kind type: {}", other),
            };
            Expr::Kind {
                expr: Box::new(cmp),
                ty,
                negate,
            }
        }
        _ => cmp,
    }
}

/// Parse a `contains` / `in` membership test, desugaring it into a call to the
/// `.includes(rhs)` method on the left-hand side. Returns `lhs` when no
/// operator is present.
fn parse_contains(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let lhs = parse_expr(inner.next().unwrap());
    match inner.next() {
        None => lhs,
        Some(_op_pair) => {
            let rhs = parse_expr(inner.next().unwrap());
            let method = match has_array_literal_arg(&rhs) {
                Some(arg) => Step::Method("has_all".to_string(), vec![Arg::Pos(arg)]),
                None if matches!(rhs, Expr::Array(_)) => {
                    Step::Method(INVALID_HAS_ARRAY_RHS.to_string(), Vec::new())
                }
                None => Step::Method("includes".to_string(), vec![Arg::Pos(rhs)]),
            };
            Expr::Chain(
                Box::new(lhs),
                vec![method],
            )
        }
    }
}

fn has_array_literal_arg(expr: &Expr) -> Option<Expr> {
    let Expr::Array(elems) = expr else {
        return None;
    };
    let mut out = Vec::with_capacity(elems.len());
    for elem in elems {
        let ArrayElem::Expr(expr) = elem else {
            return None;
        };
        out.push(scalar_literal_to_val(expr)?);
    }
    Some(val_to_expr(Val::Arr(Arc::new(out))))
}

fn scalar_literal_to_val(expr: &Expr) -> Option<Val> {
    match expr {
        Expr::Null => Some(Val::Null),
        Expr::Bool(b) => Some(Val::Bool(*b)),
        Expr::Int(n) => Some(Val::Int(*n)),
        Expr::Float(f) => Some(Val::Float(*f)),
        Expr::Str(s) => Some(Val::Str(Arc::from(s.as_str()))),
        _ => None,
    }
}

fn val_to_expr(value: Val) -> Expr {
    match value {
        Val::Null => Expr::Null,
        Val::Bool(b) => Expr::Bool(b),
        Val::Int(n) => Expr::Int(n),
        Val::Float(f) => Expr::Float(f),
        Val::Str(s) => Expr::Str(s.to_string()),
        Val::Arr(items) => Expr::Array(
            items
                .iter()
                .cloned()
                .map(|item| ArrayElem::Expr(val_to_expr(item)))
                .collect(),
        ),
        other => panic!("unexpected has literal value: {other:?}"),
    }
}

/// Parse a comparison expression `lhs op rhs` (`==`, `!=`, `<`, `<=`, `>`,
/// `>=`, `~=`) into `Expr::BinOp`. Returns the bare `lhs` when no operator
/// is present.
fn parse_cmp(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let lhs = parse_expr(inner.next().unwrap());
    if let Some(op_pair) = inner.next() {
        let op = match op_pair.as_str() {
            "==" => BinOp::Eq,
            "!=" => BinOp::Neq,
            "<" => BinOp::Lt,
            "<=" => BinOp::Lte,
            ">" => BinOp::Gt,
            ">=" => BinOp::Gte,
            "~=" => BinOp::Fuzzy,
            o => panic!("unknown cmp op: {}", o),
        };
        let rhs = parse_expr(inner.next().unwrap());
        Expr::BinOp(Box::new(lhs), op, Box::new(rhs))
    } else {
        lhs
    }
}

/// Parse additive expressions (`+`, `-`) left-associatively using
/// `parse_left_assoc`.
fn parse_add(pair: Pair<Rule>) -> Expr {
    parse_left_assoc(pair, |s| match s {
        "+" => Some(BinOp::Add),
        "-" => Some(BinOp::Sub),
        _ => None,
    })
}

/// Parse multiplicative expressions (`*`, `/`, `%`) left-associatively using
/// `parse_left_assoc`.
fn parse_mul(pair: Pair<Rule>) -> Expr {
    parse_left_assoc(pair, |s| match s {
        "*" => Some(BinOp::Mul),
        "/" => Some(BinOp::Div),
        "%" => Some(BinOp::Mod),
        _ => None,
    })
}

/// Generic left-associative binary expression builder. Iterates alternating
/// `expr op expr` children; `op_fn` maps operator text to `BinOp`.
fn parse_left_assoc<F>(pair: Pair<Rule>, op_fn: F) -> Expr
where
    F: Fn(&str) -> Option<BinOp>,
{
    let mut inner = pair.into_inner().peekable();
    let first = parse_expr(inner.next().unwrap());
    let mut acc = first;
    while inner.peek().is_some() {
        let op_pair = inner.next().unwrap();
        let op = op_fn(op_pair.as_str()).unwrap();
        let rhs = parse_expr(inner.next().unwrap());
        acc = Expr::BinOp(Box::new(acc), op, Box::new(rhs));
    }
    acc
}

/// Parse a chain of `as <type>` cast suffixes, building nested `Expr::Cast`
/// nodes left-to-right. Returns the bare operand when no cast is present.
fn parse_cast(pair: Pair<Rule>) -> Expr {
    // cast_expr = { unary_expr ~ (kw_as ~ cast_type)* }
    let mut inner = pair.into_inner().peekable();
    let mut acc = parse_expr(inner.next().unwrap());
    while inner.peek().is_some() {
        // consume the `as` keyword token
        let kw = inner.next().unwrap();
        debug_assert_eq!(kw.as_rule(), Rule::kw_as);
        let ty_pair = inner.next().unwrap();
        let ty = match ty_pair.as_str() {
            "int" => CastType::Int,
            "float" => CastType::Float,
            "number" => CastType::Number,
            "string" => CastType::Str,
            "bool" => CastType::Bool,
            "array" => CastType::Array,
            "object" => CastType::Object,
            "null" => CastType::Null,
            o => panic!("unknown cast type: {}", o),
        };
        acc = Expr::Cast {
            expr: Box::new(acc),
            ty,
        };
    }
    acc
}

/// Parse a unary negation expression (`-expr`); delegates to `parse_expr` for
/// any rule that is not a `unary_neg` marker.
fn parse_unary(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let first = inner.next().unwrap();
    match first.as_rule() {
        Rule::unary_neg => {
            let operand = parse_expr(inner.next().unwrap());
            Expr::UnaryNeg(Box::new(operand))
        }
        _ => parse_expr(first),
    }
}

/// Parse a postfix expression: a primary value followed by zero or more
/// postfix steps (field access, method calls, index, slice, `?` optional).
/// Coalesces adjacent `?` quantifiers into `OptField`/`OptMethod` steps and
/// delegates to `classify_chain_write` for write rewrites.
fn parse_postfix_expr(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let base = parse_primary(inner.next().unwrap());
    let raw_steps: Vec<Step> = inner.flat_map(parse_postfix_step).collect();

    // Merge `?` quantifiers into the preceding step by converting Field → OptField
    // and Method → OptMethod; bare quantifiers with no suitable predecessor are kept.
    let mut steps: Vec<Step> = Vec::with_capacity(raw_steps.len());
    for s in raw_steps {
        match s {
            Step::Quantifier(QuantifierKind::First) => {
                match steps.last() {
                    Some(Step::Field(_)) => {
                        if let Some(Step::Field(k)) = steps.pop() {
                            steps.push(Step::OptField(k));
                        }
                    }
                    Some(Step::Method(_, _)) => {
                        if let Some(Step::Method(n, a)) = steps.pop() {
                            steps.push(Step::OptMethod(n, a));
                        }
                    }
                    _ => {
                        // No suitable predecessor; discard the quantifier to
                        // avoid silently changing semantics.
                    }
                }
            }
            other => steps.push(other),
        }
    }
    if let Some(rewritten) = classify_chain_write(&base, &steps) {
        return rewritten;
    }

    // Wildcard expansion. Mid-chain `[*]` (e.g. `$.items[*].x`) means
    // "iterate the receiver and apply the rest of the chain per element".
    // Rewrite the chain so subsequent steps are pushed inside a `.map(@ +
    // rest)` lambda. End-of-chain `[*]` is dropped (identity over array).
    //
    // Done before write classification so chain-writes like
    // `$.xs[*].field.set(v)` are seen as `$.xs.map(@.field.set(v))` (which
    // the planner can broadcast through `patch_fusion`).
    let steps = expand_wildcards(steps);
    base.maybe_chain(steps)
}

/// Rewrite mid-chain `Step::Wildcard` into an equivalent `.map(@ + rest)`
/// method call. Trailing wildcards (with no further steps) collapse to
/// nothing: `$.xs[*]` is equivalent to `$.xs`.
fn expand_wildcards(steps: Vec<Step>) -> Vec<Step> {
    // Fast path: no wildcards present.
    if !steps.iter().any(|s| matches!(s, Step::Wildcard)) {
        return steps;
    }
    let mut out: Vec<Step> = Vec::with_capacity(steps.len());
    let mut iter = steps.into_iter().peekable();
    while let Some(step) = iter.next() {
        if matches!(step, Step::Wildcard) {
            // Collect remaining steps. Recursively expand any further
            // wildcards in the rest so `xs[*].ys[*].x` is supported.
            let rest: Vec<Step> = iter.by_ref().collect();
            if rest.is_empty() {
                // Trailing `[*]` is identity over the array.
                break;
            }
            let rest = expand_wildcards(rest);
            // Build `Lambda(@ -> Chain(@, rest))` as the map argument.
            let body = Expr::Chain(Box::new(Expr::Current), rest);
            let lam = Expr::Lambda {
                params: vec!["@".to_string()],
                body: Box::new(body),
            };
            out.push(Step::Method("map".to_string(), vec![Arg::Pos(lam)]));
            return out;
        }
        out.push(step);
    }
    out
}

/// Detect rooted write terminals (`$.path.set(v)` etc.) and rewrite them into
/// `Expr::Patch` nodes. Only fires when `base` is `Expr::Root` and the last
/// step is a recognised terminal write method. Returns `None` for all other
/// expressions, leaving them unchanged.
fn classify_chain_write(base: &Expr, steps: &[Step]) -> Option<Expr> {
    if !matches!(base, Expr::Root) {
        return None;
    }
    let last = steps.last()?;
    let (name, args) = match last {
        Step::Method(n, a) => (n.as_str(), a),
        _ => return None,
    };
    if !is_chain_write_terminal(name) {
        return None;
    }

    let prefix = &steps[..steps.len() - 1];
    let path = write_steps_to_path(prefix, true)?;

    if name == "update" {
        let ops = build_update_ops(args)?;
        return Some(Expr::UpdateBatch {
            root: Box::new(Expr::Root),
            selector: path,
            ops,
        });
    }

    let op = build_patch_op(name, args, Vec::new())?;
    Some(Expr::UpdateBatch {
        root: Box::new(Expr::Root),
        selector: path,
        ops: vec![op],
    })
}

fn build_update_ops(args: &[Arg]) -> Option<Vec<PatchOp>> {
    if args.len() != 1 {
        return None;
    }
    let Expr::Object(fields) = arg_expr(args.first()?) else {
        return None;
    };
    let focus = crate::plan::update::UPDATE_FOCUS_BINDING.to_string();
    let mut ops = Vec::new();
    for field in fields {
        let ObjField::Kv {
            key,
            val,
            optional: false,
            cond,
        } = field
        else {
            return None;
        };
        ops.push(PatchOp {
            path: parse_update_path_key(key)?,
            val: qualify_update_expr(val.clone(), &focus, &[]),
            cond: cond
                .as_ref()
                .map(|expr| qualify_update_expr(expr.clone(), &focus, &[])),
        });
    }
    Some(ops)
}

/// Parse object keys inside `.update({ ... })`.
///
/// Plain keys (`tags`) update fields below the selected focus. Quoted path
/// keys (`"books[*].tags"`) allow root-level batched updates without widening
/// the object-key grammar for every object literal.
fn parse_update_path_key(key: &str) -> Option<Vec<PathStep>> {
    let mut idx = 0usize;
    let bytes = key.as_bytes();
    let first = parse_update_ident(key, &mut idx)?;
    let mut out = vec![PathStep::Field(first)];
    while idx < bytes.len() {
        match bytes[idx] {
            b'.' => {
                idx += 1;
                out.push(PathStep::Field(parse_update_ident(key, &mut idx)?));
            }
            b'[' => {
                let close = find_matching_bracket(key, idx)?;
                let inner = key[idx + 1..close].trim();
                if inner == "*" {
                    out.push(PathStep::Wildcard);
                } else if let Some(rest) = inner.strip_prefix("* if") {
                    let expr = parse_embedded_expr(rest.trim())?;
                    out.push(PathStep::WildcardFilter(Box::new(expr)));
                } else if let Ok(n) = inner.parse::<i64>() {
                    out.push(PathStep::Index(n));
                } else {
                    let expr = parse_embedded_expr(inner)?;
                    out.push(PathStep::DynIndex(expr));
                }
                idx = close + 1;
            }
            _ => return None,
        }
    }
    Some(out)
}

fn parse_update_ident(src: &str, idx: &mut usize) -> Option<String> {
    let start = *idx;
    for (off, ch) in src[start..].char_indices() {
        if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
            if off == 0 {
                return None;
            }
            *idx = start + off;
            return Some(src[start..start + off].to_string());
        }
    }
    if start == src.len() {
        return None;
    }
    *idx = src.len();
    Some(src[start..].to_string())
}

fn find_matching_bracket(src: &str, open: usize) -> Option<usize> {
    let mut depth = 0usize;
    for (idx, ch) in src[open..].char_indices() {
        match ch {
            '[' => depth += 1,
            ']' => {
                depth = depth.checked_sub(1)?;
                if depth == 0 {
                    return Some(open + idx);
                }
            }
            _ => {}
        }
    }
    None
}

fn parse_embedded_expr(src: &str) -> Option<Expr> {
    let mut pairs = V2Parser::parse(Rule::expr, src).ok()?;
    Some(parse_expr(pairs.next()?))
}

fn qualify_update_expr(expr: Expr, focus: &str, bound: &[String]) -> Expr {
    match expr {
        Expr::Ident(name) if !bound.iter().any(|b| b == &name) => Expr::Chain(
            Box::new(Expr::Ident(focus.to_string())),
            vec![Step::Field(name)],
        ),
        Expr::Chain(base, steps) => {
            Expr::Chain(Box::new(qualify_update_expr(*base, focus, bound)), steps)
        }
        Expr::BinOp(lhs, op, rhs) => Expr::BinOp(
            Box::new(qualify_update_expr(*lhs, focus, bound)),
            op,
            Box::new(qualify_update_expr(*rhs, focus, bound)),
        ),
        Expr::UnaryNeg(inner) => {
            Expr::UnaryNeg(Box::new(qualify_update_expr(*inner, focus, bound)))
        }
        Expr::Not(inner) => Expr::Not(Box::new(qualify_update_expr(*inner, focus, bound))),
        Expr::Kind { expr, ty, negate } => Expr::Kind {
            expr: Box::new(qualify_update_expr(*expr, focus, bound)),
            ty,
            negate,
        },
        Expr::Coalesce(lhs, rhs) => Expr::Coalesce(
            Box::new(qualify_update_expr(*lhs, focus, bound)),
            Box::new(qualify_update_expr(*rhs, focus, bound)),
        ),
        Expr::Object(fields) => Expr::Object(
            fields
                .into_iter()
                .map(|field| qualify_update_obj_field(field, focus, bound))
                .collect(),
        ),
        Expr::Array(items) => Expr::Array(
            items
                .into_iter()
                .map(|item| match item {
                    ArrayElem::Expr(expr) => {
                        ArrayElem::Expr(qualify_update_expr(expr, focus, bound))
                    }
                    ArrayElem::Spread(expr) => {
                        ArrayElem::Spread(qualify_update_expr(expr, focus, bound))
                    }
                })
                .collect(),
        ),
        Expr::IfElse { cond, then_, else_ } => Expr::IfElse {
            cond: Box::new(qualify_update_expr(*cond, focus, bound)),
            then_: Box::new(qualify_update_expr(*then_, focus, bound)),
            else_: Box::new(qualify_update_expr(*else_, focus, bound)),
        },
        Expr::Try { body, default } => Expr::Try {
            body: Box::new(qualify_update_expr(*body, focus, bound)),
            default: Box::new(qualify_update_expr(*default, focus, bound)),
        },
        Expr::Cast { expr, ty } => Expr::Cast {
            expr: Box::new(qualify_update_expr(*expr, focus, bound)),
            ty,
        },
        Expr::FString(parts) => Expr::FString(
            parts
                .into_iter()
                .map(|part| match part {
                    FStringPart::Lit(s) => FStringPart::Lit(s),
                    FStringPart::Interp { expr, fmt } => FStringPart::Interp {
                        expr: qualify_update_expr(expr, focus, bound),
                        fmt,
                    },
                })
                .collect(),
        ),
        Expr::GlobalCall { name, args } => Expr::GlobalCall {
            name,
            args: args
                .into_iter()
                .map(|arg| qualify_update_arg(arg, focus, bound))
                .collect(),
        },
        Expr::Pipeline { base, steps } => Expr::Pipeline {
            base: Box::new(qualify_update_expr(*base, focus, bound)),
            steps: qualify_update_pipe_steps(steps, focus, bound),
        },
        Expr::ListComp {
            expr,
            vars,
            iter,
            cond,
        } => {
            let iter = qualify_update_expr(*iter, focus, bound);
            let nested = extend_bound(bound, &vars);
            Expr::ListComp {
                expr: Box::new(qualify_update_expr(*expr, focus, &nested)),
                vars,
                iter: Box::new(iter),
                cond: cond.map(|expr| Box::new(qualify_update_expr(*expr, focus, &nested))),
            }
        }
        Expr::DictComp {
            key,
            val,
            vars,
            iter,
            cond,
        } => {
            let iter = qualify_update_expr(*iter, focus, bound);
            let nested = extend_bound(bound, &vars);
            Expr::DictComp {
                key: Box::new(qualify_update_expr(*key, focus, &nested)),
                val: Box::new(qualify_update_expr(*val, focus, &nested)),
                vars,
                iter: Box::new(iter),
                cond: cond.map(|expr| Box::new(qualify_update_expr(*expr, focus, &nested))),
            }
        }
        Expr::SetComp {
            expr,
            vars,
            iter,
            cond,
        } => {
            let iter = qualify_update_expr(*iter, focus, bound);
            let nested = extend_bound(bound, &vars);
            Expr::SetComp {
                expr: Box::new(qualify_update_expr(*expr, focus, &nested)),
                vars,
                iter: Box::new(iter),
                cond: cond.map(|expr| Box::new(qualify_update_expr(*expr, focus, &nested))),
            }
        }
        Expr::GenComp {
            expr,
            vars,
            iter,
            cond,
        } => {
            let iter = qualify_update_expr(*iter, focus, bound);
            let nested = extend_bound(bound, &vars);
            Expr::GenComp {
                expr: Box::new(qualify_update_expr(*expr, focus, &nested)),
                vars,
                iter: Box::new(iter),
                cond: cond.map(|expr| Box::new(qualify_update_expr(*expr, focus, &nested))),
            }
        }
        Expr::Lambda { params, body } => {
            let nested = extend_bound(bound, &params);
            Expr::Lambda {
                params,
                body: Box::new(qualify_update_expr(*body, focus, &nested)),
            }
        }
        Expr::Let { name, init, body } => {
            let init = qualify_update_expr(*init, focus, bound);
            let mut nested = bound.to_vec();
            nested.push(name.clone());
            Expr::Let {
                name,
                init: Box::new(init),
                body: Box::new(qualify_update_expr(*body, focus, &nested)),
            }
        }
        Expr::Patch { root, ops } => Expr::Patch {
            root: Box::new(qualify_update_expr(*root, focus, bound)),
            ops: ops
                .into_iter()
                .map(|op| qualify_update_patch_op(op, focus, bound))
                .collect(),
        },
        Expr::Match { scrutinee, arms } => Expr::Match {
            scrutinee: Box::new(qualify_update_expr(*scrutinee, focus, bound)),
            arms: arms
                .into_iter()
                .map(|arm| qualify_update_match_arm(arm, focus, bound))
                .collect(),
        },
        other => other,
    }
}

fn qualify_update_obj_field(field: ObjField, focus: &str, bound: &[String]) -> ObjField {
    match field {
        ObjField::Kv {
            key,
            val,
            optional,
            cond,
        } => ObjField::Kv {
            key,
            val: qualify_update_expr(val, focus, bound),
            optional,
            cond: cond.map(|expr| qualify_update_expr(expr, focus, bound)),
        },
        ObjField::Dynamic { key, val } => ObjField::Dynamic {
            key: qualify_update_expr(key, focus, bound),
            val: qualify_update_expr(val, focus, bound),
        },
        ObjField::Spread(expr) => ObjField::Spread(qualify_update_expr(expr, focus, bound)),
        ObjField::SpreadDeep(expr) => ObjField::SpreadDeep(qualify_update_expr(expr, focus, bound)),
        other => other,
    }
}

fn qualify_update_arg(arg: Arg, focus: &str, bound: &[String]) -> Arg {
    match arg {
        Arg::Pos(expr) => Arg::Pos(qualify_update_expr(expr, focus, bound)),
        Arg::Named(name, expr) => Arg::Named(name, qualify_update_expr(expr, focus, bound)),
    }
}

fn qualify_update_pipe_steps(steps: Vec<PipeStep>, focus: &str, bound: &[String]) -> Vec<PipeStep> {
    let mut scoped = bound.to_vec();
    steps
        .into_iter()
        .map(|step| match step {
            PipeStep::Forward(expr) => PipeStep::Forward(qualify_update_expr(expr, focus, &scoped)),
            PipeStep::Bind(target) => {
                collect_bind_target_names(&target, &mut scoped);
                PipeStep::Bind(target)
            }
        })
        .collect()
}

fn qualify_update_patch_op(op: PatchOp, focus: &str, bound: &[String]) -> PatchOp {
    PatchOp {
        path: op
            .path
            .into_iter()
            .map(|step| qualify_update_path_step(step, focus, bound))
            .collect(),
        val: qualify_update_expr(op.val, focus, bound),
        cond: op.cond.map(|expr| qualify_update_expr(expr, focus, bound)),
    }
}

fn qualify_update_path_step(step: PathStep, focus: &str, bound: &[String]) -> PathStep {
    match step {
        PathStep::DynIndex(expr) => PathStep::DynIndex(qualify_update_expr(expr, focus, bound)),
        PathStep::WildcardFilter(expr) => {
            PathStep::WildcardFilter(Box::new(qualify_update_expr(*expr, focus, bound)))
        }
        other => other,
    }
}

fn qualify_update_match_arm(arm: MatchArm, focus: &str, bound: &[String]) -> MatchArm {
    let mut nested = bound.to_vec();
    collect_pat_names(&arm.pat, &mut nested);
    MatchArm {
        pat: arm.pat,
        guard: arm
            .guard
            .map(|expr| qualify_update_expr(expr, focus, &nested)),
        body: qualify_update_expr(arm.body, focus, &nested),
    }
}

fn extend_bound(bound: &[String], names: &[String]) -> Vec<String> {
    let mut nested = bound.to_vec();
    nested.extend(names.iter().cloned());
    nested
}

fn collect_bind_target_names(target: &BindTarget, out: &mut Vec<String>) {
    match target {
        BindTarget::Name(name) => out.push(name.clone()),
        BindTarget::Obj { fields, rest } => {
            out.extend(fields.iter().cloned());
            if let Some(name) = rest {
                out.push(name.clone());
            }
        }
        BindTarget::Arr(names) => out.extend(names.iter().cloned()),
    }
}

fn collect_pat_names(pat: &Pat, out: &mut Vec<String>) {
    match pat {
        Pat::Bind(name) => out.push(name.clone()),
        Pat::Or(alts) => {
            for alt in alts {
                collect_pat_names(alt, out);
            }
        }
        Pat::Obj { fields, rest } => {
            for (_, field_pat) in fields {
                collect_pat_names(field_pat, out);
            }
            if let Some(Some(name)) = rest {
                out.push(name.clone());
            }
        }
        Pat::Arr { elems, rest } => {
            for elem in elems {
                collect_pat_names(elem, out);
            }
            if let Some(Some(name)) = rest {
                out.push(name.clone());
            }
        }
        Pat::Kind {
            name: Some(name), ..
        } => out.push(name.clone()),
        Pat::Wild | Pat::Lit(_) | Pat::Kind { name: None, .. } | Pat::Range { .. } => {}
    }
}

/// Extract the expression from a positional or named `Arg`, unwrapping the
/// outer `Arg` wrapper.
fn arg_expr(a: &Arg) -> &Expr {
    match a {
        Arg::Pos(e) | Arg::Named(_, e) => e,
    }
}

/// Parse a single postfix step from a `postfix_step` rule pair, returning a
/// `Vec<Step>` because `map_into_shape` can expand to two steps.
fn parse_postfix_step(pair: Pair<Rule>) -> Vec<Step> {
    let inner_pair = pair.into_inner().next().unwrap();
    match inner_pair.as_rule() {
        Rule::field_access => {
            let name = inner_pair.into_inner().next().unwrap().as_str().to_string();
            vec![Step::Field(name)]
        }
        Rule::descendant => {
            let mut di = inner_pair.into_inner();
            match di.next() {
                Some(p) => vec![Step::Descendant(p.as_str().to_string())],
                None => vec![Step::DescendAll],
            }
        }
        Rule::deep_method => {
            // `$..find(pred)` etc. are parsed here and mapped to `deep_*` method names.
            let mut mi = inner_pair.into_inner();
            let name = mi.next().unwrap().as_str().to_string();
            let args = mi.next().map(parse_arg_list).unwrap_or_default();
            let mapped = match name.as_str() {
                "find" | "find_all" | "findAll" => "deep_find".to_string(),
                "shape" => "deep_shape".to_string(),
                "like" => "deep_like".to_string(),
                other => format!("deep_{}", other),
            };
            vec![Step::Method(mapped, args)]
        }
        Rule::deep_match => {
            // `$..match { arms }` (collect all) and `$..match! { arms }`
            // (early-stop on first truthy match) share the parsing path.
            // The `deep_match_op` token spelling distinguishes the two.
            let mut arms: Vec<MatchArm> = Vec::new();
            let mut early_stop = false;
            for child in inner_pair.into_inner().filter(|p| !is_kw(p.as_rule())) {
                match child.as_rule() {
                    Rule::deep_match_op => {
                        early_stop = child.as_str().ends_with('!');
                    }
                    Rule::match_arm => {
                        let mut ai = child.into_inner().filter(|p| !is_kw(p.as_rule()));
                        let pat = parse_pat(ai.next().expect("match arm pattern"));
                        let rest: Vec<_> = ai.collect();
                        let (guard, body) = match rest.len() {
                            1 => (None, parse_expr(rest.into_iter().next().unwrap())),
                            2 => {
                                let mut it = rest.into_iter();
                                let g = parse_expr(it.next().unwrap());
                                let b = parse_expr(it.next().unwrap());
                                (Some(g), b)
                            }
                            _ => panic!(
                                "deep_match arm: expected 1 or 2 trailing exprs (guard?, body)"
                            ),
                        };
                        arms.push(MatchArm { pat, guard, body });
                    }
                    _ => {}
                }
            }
            vec![Step::DeepMatch { arms, early_stop }]
        }
        Rule::inline_filter => {
            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
            vec![Step::InlineFilter(Box::new(expr))]
        }
        Rule::quantifier => {
            let s = inner_pair.as_str();
            if s.starts_with('!') {
                vec![Step::Quantifier(QuantifierKind::One)]
            } else {
                vec![Step::Quantifier(QuantifierKind::First)]
            }
        }
        Rule::method_call => {
            let mut mi = inner_pair.into_inner();
            let name = mi.next().unwrap().as_str().to_string();
            let args = mi.next().map(parse_arg_list).unwrap_or_default();
            vec![Step::Method(name, args)]
        }
        Rule::index_access => {
            let bi = inner_pair.into_inner().next().unwrap();
            vec![parse_bracket(bi)]
        }
        Rule::dyn_field => {
            let expr = parse_expr(inner_pair.into_inner().next().unwrap());
            vec![Step::DynIndex(Box::new(expr))]
        }
        Rule::map_into_shape => {
            // `[if pred] { body }` desugars to an optional `.filter(pred)` step
            // followed by a `.map(body)` step.
            let mut guard: Option<Expr> = None;
            let mut body: Option<Expr> = None;
            let mut saw_if = false;
            for p in inner_pair.into_inner() {
                match p.as_rule() {
                    Rule::kw_if => saw_if = true,
                    Rule::expr => {
                        if saw_if && guard.is_none() {
                            guard = Some(parse_expr(p));
                        } else {
                            body = Some(parse_expr(p));
                        }
                    }
                    _ => {}
                }
            }
            let body = body.expect("map_into_shape requires body");
            let mut steps = Vec::new();
            if let Some(g) = guard {
                steps.push(Step::Method("filter".into(), vec![Arg::Pos(g)]));
            }
            steps.push(Step::Method("map".into(), vec![Arg::Pos(body)]));
            steps
        }
        r => panic!("unexpected postfix rule: {:?}", r),
    }
}

/// Parse a bracket access expression into the appropriate `Step`. Forms:
///
/// - `[n]` → `Step::Index(n)`
/// - `[a:b]`, `[a:]`, `[:b]` → `Step::Slice(_, _, None)` (step defaults to 1)
/// - `[a:b:s]`, `[::s]`, `[a::s]`, `[:b:s]` → `Step::Slice(_, _, Some(s))`
/// - `[*]` → `Step::Wildcard`
/// - `[* if expr]` → `Step::InlineFilter(expr)`
/// - `[expr]` → `Step::DynIndex(expr)`
fn parse_bracket(pair: Pair<Rule>) -> Step {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::idx_only => Step::Index(inner.as_str().parse().unwrap()),
        Rule::wildcard => Step::Wildcard,
        Rule::wildcard_filter => {
            let expr = inner
                .into_inner()
                .find(|p| p.as_rule() == Rule::expr)
                .map(parse_expr)
                .expect("wildcard filter requires predicate");
            Step::InlineFilter(Box::new(expr))
        }
        Rule::slice_full => {
            let mut i = inner.into_inner();
            let a = i.next().unwrap().as_str().parse().ok();
            let b = i.next().unwrap().as_str().parse().ok();
            Step::Slice(a, b, None)
        }
        Rule::slice_from => {
            let a = inner.into_inner().next().unwrap().as_str().parse().ok();
            Step::Slice(a, None, None)
        }
        Rule::slice_to => {
            let b = inner.into_inner().next().unwrap().as_str().parse().ok();
            Step::Slice(None, b, None)
        }
        Rule::slice_step => {
            // The grammar produces 0-3 `idx_val` children; missing positions
            // are absent in the inner iterator. Walk text-position aware via
            // a 3-slot accumulator parsed from the literal source.
            let s = inner.as_str();
            // Split on ':' in the raw literal — preserves positional missing-ness.
            let mut parts = s.split(':');
            let a = parts.next().and_then(|p| p.parse().ok());
            let b = parts.next().and_then(|p| p.parse().ok());
            let st = parts.next().and_then(|p| p.parse().ok());
            Step::Slice(a, b, st)
        }
        Rule::expr => Step::DynIndex(Box::new(parse_expr(inner))),
        r => panic!("unexpected bracket rule: {:?}", r),
    }
}

/// Parse a primary expression: a literal, `$`, `@`, identifier, `let`, lambda,
/// comprehension, object/array constructor, global call, or patch block.
fn parse_primary(pair: Pair<Rule>) -> Expr {
    let inner = if pair.as_rule() == Rule::primary {
        pair.into_inner().next().unwrap()
    } else {
        pair
    };
    match inner.as_rule() {
        Rule::literal => parse_literal(inner),
        Rule::root => Expr::Root,
        Rule::current => Expr::Current,
        Rule::bare_leading_field => {
            // `.field` is sugar for `@.field` — desugar at parse time so
            // method-arg shapes like `filter(.active)` become indistinguishable
            // from the explicit `filter(@.active)` form downstream.
            let name = inner
                .into_inner()
                .next()
                .expect("bare_leading_field has a field_name child")
                .as_str()
                .to_string();
            Expr::Chain(
                Box::new(Expr::Current),
                vec![Step::Field(name)],
            )
        }
        Rule::ident => Expr::Ident(inner.as_str().to_string()),
        Rule::let_expr => parse_let(inner),
        Rule::lambda_expr => parse_lambda(inner),
        Rule::arrow_lambda => parse_arrow_lambda(inner),
        Rule::list_comp => parse_list_comp(inner),
        Rule::dict_comp => parse_dict_comp(inner),
        Rule::set_comp => parse_set_comp(inner),
        Rule::gen_comp => parse_gen_comp(inner),
        Rule::obj_construct => parse_obj(inner),
        Rule::arr_construct => parse_arr(inner),
        Rule::global_call => parse_global_call(inner),
        Rule::expr => parse_expr(inner),
        Rule::patch_block => parse_patch(inner),
        Rule::match_expr => parse_match_expr(inner),
        Rule::kw_delete => Expr::DeleteMark,
        r => panic!("unexpected primary rule: {:?}", r),
    }
}

/// Parse a `match scrutinee { pat when guard -> body, ... }` expression.
fn parse_match_expr(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner().filter(|p| !is_kw(p.as_rule()));
    let scrutinee = parse_expr(inner.next().expect("match scrutinee"));
    let mut arms: Vec<MatchArm> = Vec::new();
    for arm_pair in inner {
        if arm_pair.as_rule() != Rule::match_arm {
            continue;
        }
        let mut ai = arm_pair.into_inner().filter(|p| !is_kw(p.as_rule()));
        let pat_pair = ai.next().expect("match arm pattern");
        let pat = parse_pat(pat_pair);
        // Remaining: optional guard expr, then body expr.
        let rest: Vec<_> = ai.collect();
        let (guard, body) = match rest.len() {
            1 => (None, parse_expr(rest.into_iter().next().unwrap())),
            2 => {
                let mut it = rest.into_iter();
                let g = parse_expr(it.next().unwrap());
                let b = parse_expr(it.next().unwrap());
                (Some(g), b)
            }
            _ => panic!("match arm: expected 1 or 2 trailing exprs (guard?, body)"),
        };
        arms.push(MatchArm { pat, guard, body });
    }
    Expr::Match {
        scrutinee: Box::new(scrutinee),
        arms,
    }
}

/// Parse a `pat_or` / `pat_atom` rule into a `Pat` AST node.
fn parse_pat(pair: Pair<Rule>) -> Pat {
    match pair.as_rule() {
        Rule::pat_or => {
            let mut alts: Vec<Pat> = pair.into_inner().map(parse_pat).collect();
            if alts.len() == 1 {
                alts.pop().unwrap()
            } else {
                Pat::Or(alts)
            }
        }
        Rule::pat_atom => {
            let inner = pair.into_inner().next().expect("pat_atom inner");
            parse_pat(inner)
        }
        Rule::pat_range => {
            let mut it = pair.into_inner();
            let lo_pair = it.next().expect("pat_range: lo");
            let op_pair = it.next().expect("pat_range: op");
            let hi_pair = it.next().expect("pat_range: hi");
            let lo: f64 = lo_pair.as_str().parse().expect("range lo as f64");
            let hi: f64 = hi_pair.as_str().parse().expect("range hi as f64");
            let inclusive = op_pair.as_str() == "..=";
            Pat::Range { lo, hi, inclusive }
        }
        Rule::pat_wild => Pat::Wild,
        Rule::pat_literal => Pat::Lit(parse_pat_literal(pair)),
        Rule::pat_kind_bind => {
            let mut it = pair.into_inner();
            let name = it.next().unwrap().as_str().to_string();
            let kind = parse_kind_type(it.next().unwrap().as_str());
            Pat::Kind {
                name: Some(name),
                kind,
            }
        }
        Rule::pat_kind_only => {
            let kt = pair.into_inner().next().unwrap().as_str();
            Pat::Kind {
                name: None,
                kind: parse_kind_type(kt),
            }
        }
        Rule::pat_bind => Pat::Bind(pair.as_str().to_string()),
        Rule::pat_obj => {
            let mut fields: Vec<(String, Pat)> = Vec::new();
            let mut rest: Option<Option<String>> = None;
            for p in pair.into_inner() {
                match p.as_rule() {
                    Rule::pat_obj_field => {
                        // Either `key: pat` (Kv) or `key` (Short) — the
                        // grammar splits these into two named subrules so
                        // we can branch without re-checking presence of
                        // the trailing colon.
                        let inner = p.into_inner().next().unwrap();
                        match inner.as_rule() {
                            Rule::pat_obj_field_kv => {
                                let mut fi = inner.into_inner();
                                let k = fi.next().unwrap().as_str().to_string();
                                let v = parse_pat(fi.next().unwrap());
                                fields.push((k, v));
                            }
                            Rule::pat_obj_field_short => {
                                let k = inner.as_str().to_string();
                                fields.push((k.clone(), Pat::Bind(k)));
                            }
                            _ => {}
                        }
                    }
                    Rule::pat_obj_rest => {
                        let inner = p.into_inner().next().unwrap();
                        match inner.as_rule() {
                            Rule::pat_obj_rest_named => {
                                let nm = inner.into_inner().next().unwrap().as_str().to_string();
                                rest = Some(Some(nm));
                            }
                            Rule::pat_obj_rest_anon => rest = Some(None),
                            _ => {}
                        }
                    }
                    _ => {}
                }
            }
            Pat::Obj { fields, rest }
        }
        Rule::pat_arr => {
            let mut elems: Vec<Pat> = Vec::new();
            let mut rest: Option<Option<String>> = None;
            for p in pair.into_inner() {
                match p.as_rule() {
                    Rule::pat_or | Rule::pat_atom => elems.push(parse_pat(p)),
                    Rule::pat_arr_rest => {
                        let inner = p.into_inner().next().unwrap();
                        match inner.as_rule() {
                            Rule::pat_arr_rest_named => {
                                let nm = inner.into_inner().next().unwrap().as_str().to_string();
                                rest = Some(Some(nm));
                            }
                            Rule::pat_arr_rest_anon => rest = Some(None),
                            _ => {}
                        }
                    }
                    _ => {}
                }
            }
            Pat::Arr { elems, rest }
        }
        r => panic!("unexpected pattern rule: {:?}", r),
    }
}

/// Decode a `pat_literal` rule into a `PatLit`.
fn parse_pat_literal(pair: Pair<Rule>) -> PatLit {
    let inner = pair.into_inner().next().expect("pat_literal inner");
    match inner.as_rule() {
        Rule::pat_lit_null => PatLit::Null,
        Rule::pat_lit_true => PatLit::Bool(true),
        Rule::pat_lit_false => PatLit::Bool(false),
        Rule::pat_lit_int => PatLit::Int(inner.as_str().parse().expect("pat int")),
        Rule::pat_lit_float => PatLit::Float(inner.as_str().parse().expect("pat float")),
        Rule::pat_lit_str => {
            let raw = inner.as_str();
            // Strip surrounding quotes.
            let stripped = &raw[1..raw.len() - 1];
            PatLit::Str(stripped.to_string())
        }
        r => panic!("unexpected pat literal rule: {:?}", r),
    }
}

/// Map a kind type name (`number`, `string`, ...) to `KindType`.
fn parse_kind_type(name: &str) -> KindType {
    match name {
        "number" => KindType::Number,
        "string" => KindType::Str,
        "array" => KindType::Array,
        "object" => KindType::Object,
        "bool" => KindType::Bool,
        "null" => KindType::Null,
        other => panic!("unknown kind type: {other}"),
    }
}

/// Parse a literal token (null, true, false, integer, float, f-string, or
/// quoted string) into the corresponding `Expr` variant.
fn parse_literal(pair: Pair<Rule>) -> Expr {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::lit_null => Expr::Null,
        Rule::lit_true => Expr::Bool(true),
        Rule::lit_false => Expr::Bool(false),
        Rule::lit_int => Expr::Int(inner.as_str().parse().unwrap()),
        Rule::lit_float => Expr::Float(inner.as_str().parse().unwrap()),
        Rule::lit_fstring => {
            let raw = inner.as_str();
            let content = &raw[2..raw.len() - 1]; // strip `f"` prefix and `"` suffix
            let parts = parse_fstring_content(content);
            Expr::FString(parts)
        }
        Rule::lit_str => {
            let s = inner.into_inner().next().unwrap();
            let raw = s.as_str();
            // Strip surrounding quote characters and process backslash
            // escapes (`\n`, `\t`, `\r`, `\\`, `\"`, `\'`, `\0`, `\xNN`,
            // `\uXXXX`). Pre-fix this was raw passthrough — `"a\nb"`
            // became the literal 4-char string `a\nb`, breaking
            // every method that operates on real newlines (lines,
            // dedent, indent, words, regex with `\n`, etc.).
            Expr::Str(unescape_str_lit(&raw[1..raw.len() - 1]))
        }
        r => panic!("unexpected literal rule: {:?}", r),
    }
}

/// Parse the interior of an f-string (`f"…{expr}…"`) into a list of `FStringPart`
/// values. `{{` and `}}` are escape sequences for literal braces.
fn parse_fstring_content(raw: &str) -> Vec<FStringPart> {
    let mut parts = Vec::new();
    let mut lit = String::new();
    let mut chars = raw.chars().peekable();

    while let Some(c) = chars.next() {
        match c {
            '{' => {
                if chars.peek() == Some(&'{') {
                    chars.next();
                    lit.push('{');
                } else {
                    if !lit.is_empty() {
                        parts.push(FStringPart::Lit(std::mem::take(&mut lit)));
                    }
                    let mut inner = String::new();
                    let mut depth = 1usize;
                    for c2 in chars.by_ref() {
                        match c2 {
                            '{' => {
                                depth += 1;
                                inner.push(c2);
                            }
                            '}' => {
                                depth -= 1;
                                if depth == 0 {
                                    break;
                                }
                                inner.push(c2);
                            }
                            _ => inner.push(c2),
                        }
                    }
                    let (expr_str, fmt) = split_fstring_interp(&inner);
                    let expr = parse(expr_str.trim())
                        .unwrap_or_else(|e| panic!("f-string parse error in {{{}}}: {}", inner, e));
                    parts.push(FStringPart::Interp { expr, fmt });
                }
            }
            '}' if chars.peek() == Some(&'}') => {
                chars.next();
                lit.push('}');
            }
            _ => lit.push(c),
        }
    }
    if !lit.is_empty() {
        parts.push(FStringPart::Lit(lit));
    }
    parts
}

/// Split an f-string interpolation `{…}` interior at the first top-level `|`
/// (pipe format) or `:` (spec format), returning the expression substring and
/// an optional `FmtSpec`. Depth tracking avoids splitting inside nested braces.
fn split_fstring_interp(inner: &str) -> (&str, Option<FmtSpec>) {
    // Scan at depth 0 only; `(`, `[`, `{` increase depth.
    let mut depth = 0usize;
    let mut pipe_pos: Option<usize> = None;
    let mut colon_pos: Option<usize> = None;
    for (i, c) in inner.char_indices() {
        match c {
            '(' | '[' | '{' => depth += 1,
            ')' | ']' | '}' => {
                if depth > 0 {
                    depth -= 1;
                }
            }
            '|' if depth == 0 && pipe_pos.is_none() => pipe_pos = Some(i),
            ':' if depth == 0 && colon_pos.is_none() => colon_pos = Some(i),
            _ => {}
        }
    }
    if let Some(p) = pipe_pos {
        return (
            &inner[..p],
            Some(FmtSpec::Pipe(inner[p + 1..].trim().to_string())),
        );
    }
    if let Some(c) = colon_pos {
        return (&inner[..c], Some(FmtSpec::Spec(inner[c + 1..].to_string())));
    }
    (inner, None)
}

/// Parse a `let name = init in body` expression, supporting multiple bindings
/// (`let a = 1, b = 2 in …`) by folding them right-to-left into nested
/// `Expr::Let` nodes.
fn parse_let(pair: Pair<Rule>) -> Expr {
    // Filter out `let` and `in` keywords, then split: all but the last
    // pair are bindings; the last is the body expression.
    let inner: Vec<Pair<Rule>> = pair
        .into_inner()
        .filter(|p| !matches!(p.as_rule(), Rule::kw_let | Rule::kw_in))
        .collect();
    let (bindings, body_pair) = inner.split_at(inner.len() - 1);
    let body = parse_expr(body_pair[0].clone());
    let mut tuple_counter = 0u32;
    bindings.iter().rev().fold(body, |acc, b| {
        let mut bi = b.clone().into_inner();
        let target = bi.next().unwrap();
        let init = parse_expr(bi.next().unwrap());
        match target.as_rule() {
            Rule::ident => Expr::Let {
                name: target.as_str().to_string(),
                init: Box::new(init),
                body: Box::new(acc),
            },
            Rule::let_target => {
                let inner = target.into_inner().next().unwrap();
                match inner.as_rule() {
                    Rule::ident => Expr::Let {
                        name: inner.as_str().to_string(),
                        init: Box::new(init),
                        body: Box::new(acc),
                    },
                    Rule::let_tuple_target => lower_tuple_let_binding(
                        init,
                        parse_let_tuple_names(inner),
                        acc,
                        &mut tuple_counter,
                    ),
                    _ => unreachable!("unexpected let target inner: {:?}", inner.as_rule()),
                }
            }
            Rule::let_tuple_target => lower_tuple_let_binding(
                init,
                parse_let_tuple_names(target),
                acc,
                &mut tuple_counter,
            ),
            _ => unreachable!("unexpected let binding target: {:?}", target.as_rule()),
        }
    })
}

fn parse_let_tuple_names(pair: Pair<Rule>) -> Vec<String> {
    pair.into_inner()
        .filter(|p| matches!(p.as_rule(), Rule::ident))
        .map(|p| p.as_str().to_string())
        .collect()
}

fn lower_tuple_let_binding(init: Expr, names: Vec<String>, body: Expr, counter: &mut u32) -> Expr {
    let tmp = loop {
        let candidate = format!("__lettuple_{}", *counter);
        *counter += 1;
        if !names.iter().any(|name| name == &candidate) {
            break candidate;
        }
    };
    let destructured = names
        .into_iter()
        .enumerate()
        .rev()
        .fold(body, |acc, (i, name)| {
            let indexed = Expr::Chain(
                Box::new(Expr::Ident(tmp.clone())),
                vec![Step::Index(i as i64)],
            );
            Expr::Let {
                name,
                init: Box::new(indexed),
                body: Box::new(acc),
            }
        });
    Expr::Let {
        name: tmp,
        init: Box::new(init),
        body: Box::new(destructured),
    }
}

/// Parsed lambda parameter: either a single identifier or an `[a, b, ...]`
/// array-pattern destructure. Used internally by `parse_lambda` /
/// `parse_arrow_lambda` to desugar destructure params into
/// `synthetic_name + chained let-bindings` before constructing
/// `Expr::Lambda`.
enum LambdaPatParam {
    Ident(String),
    /// `[a, b, ...rest]` — bind each fixed-prefix name to the indexed
    /// element of the receiver, with an optional trailing identifier that
    /// captures the rest of the array (sliced from `len(prefix)` onward).
    Array {
        names: Vec<String>,
        rest: Option<String>,
    },
}

/// Walk a `lambda_params` / `arrow_params` pair and collect each child as
/// either an identifier or an array-pattern.
fn collect_lambda_params(params_pair: Pair<Rule>) -> Vec<LambdaPatParam> {
    let mut out = Vec::new();
    for p in params_pair.into_inner() {
        match p.as_rule() {
            Rule::ident => out.push(LambdaPatParam::Ident(p.as_str().to_string())),
            Rule::lambda_param => {
                // The `lambda_param` rule wraps either an `ident` or a
                // `lambda_array_pat`. Unwrap one level.
                let inner = p.into_inner().next().unwrap();
                match inner.as_rule() {
                    Rule::ident => out.push(LambdaPatParam::Ident(inner.as_str().to_string())),
                    Rule::lambda_array_pat => {
                        out.push(parse_lambda_array_pat(inner));
                    }
                    _ => {}
                }
            }
            Rule::lambda_array_pat => {
                out.push(parse_lambda_array_pat(p));
            }
            _ => {}
        }
    }
    out
}

/// Decode a `lambda_array_pat` Pest pair into the destructure shape:
/// the ordered list of fixed-prefix names, plus an optional trailing
/// `...rest` capture identifier.
fn parse_lambda_array_pat(pair: Pair<Rule>) -> LambdaPatParam {
    let mut names: Vec<String> = Vec::new();
    let mut rest: Option<String> = None;
    for child in pair.into_inner() {
        match child.as_rule() {
            Rule::ident => names.push(child.as_str().to_string()),
            Rule::lambda_array_rest => {
                let inner = child.into_inner().next().unwrap();
                rest = Some(inner.as_str().to_string());
            }
            _ => {}
        }
    }
    LambdaPatParam::Array { names, rest }
}

/// Lower a `[a, b, ...rest]` array-pattern into a synthetic ident plus a chain
/// of `let` bindings indexing the synthetic. Returns the synthetic name and
/// a function that wraps a body expression in the destructuring lets.
///
/// `__lampat_N` reuses the same naming scheme everywhere; the synthetic
/// shadows nothing user-visible since identifiers starting with `__` are
/// reserved.
fn synth_destructure(
    counter: &mut u32,
    pat: Vec<String>,
    rest: Option<String>,
) -> (String, Box<dyn FnOnce(Expr) -> Expr>) {
    let id = format!("__lampat_{}", *counter);
    *counter += 1;
    let synth = id.clone();
    let wrap: Box<dyn FnOnce(Expr) -> Expr> = Box::new(move |body| {
        // Optional rest: `let rest = synth[len(pat):] in body`. Innermost
        // because the index lookups for fixed names then bind around it.
        let mut acc = body;
        if let Some(rest_name) = rest {
            let prefix_len = pat.len() as i64;
            let slice = Expr::Chain(
                Box::new(Expr::Ident(synth.clone())),
                vec![Step::Slice(Some(prefix_len), None, None)],
            );
            acc = Expr::Let {
                name: rest_name,
                init: Box::new(slice),
                body: Box::new(acc),
            };
        }
        // Build nested Let from innermost outward: rightmost name is the
        // innermost binding. Walk pat in reverse so the leftmost binding
        // ends up outermost (so name-shadowing matches source order).
        pat.into_iter()
            .enumerate()
            .rev()
            .fold(acc, |acc, (i, name)| {
                // `let name = synth[i] in acc`
                let index = Expr::Chain(
                    Box::new(Expr::Ident(synth.clone())),
                    vec![Step::Index(i as i64)],
                );
                Expr::Let {
                    name,
                    init: Box::new(index),
                    body: Box::new(acc),
                }
            })
    });
    (id, wrap)
}

/// Lower a list of `LambdaPatParam` to (final param-name list, body wrapper).
/// Identifier params pass through; array-pattern params get synthetic names
/// and contribute let-binding wrappers.
fn lower_lambda_params(
    params: Vec<LambdaPatParam>,
) -> (Vec<String>, Box<dyn FnOnce(Expr) -> Expr>) {
    let mut names = Vec::with_capacity(params.len());
    let mut wrappers: Vec<Box<dyn FnOnce(Expr) -> Expr>> = Vec::new();
    let mut counter: u32 = 0;
    for p in params {
        match p {
            LambdaPatParam::Ident(n) => names.push(n),
            LambdaPatParam::Array { names: pat, rest } => {
                let (synth, wrap) = synth_destructure(&mut counter, pat, rest);
                names.push(synth);
                wrappers.push(wrap);
            }
        }
    }
    let body_wrap: Box<dyn FnOnce(Expr) -> Expr> = Box::new(move |body| {
        // Apply outermost-first so leftmost destructure is outer let.
        wrappers.into_iter().rev().fold(body, |acc, w| w(acc))
    });
    (names, body_wrap)
}

/// Parse a `lambda params body` expression (keyword-form lambda) into
/// `Expr::Lambda`. Array-pattern parameters are desugared via
/// `lower_lambda_params`.
fn parse_lambda(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner().filter(|p| p.as_rule() != Rule::kw_lambda);
    let params_pair = inner.next().unwrap();
    let raw = collect_lambda_params(params_pair);
    let (params, wrap) = lower_lambda_params(raw);
    let body = parse_expr(inner.next().unwrap());
    Expr::Lambda {
        params,
        body: Box::new(wrap(body)),
    }
}

/// Parse an arrow-lambda expression (`(params) => body`) into `Expr::Lambda`,
/// using the same representation as keyword-form lambdas.
fn parse_arrow_lambda(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let params_pair = inner.next().unwrap();
    let raw = collect_lambda_params(params_pair);
    let (params, wrap) = lower_lambda_params(raw);
    let body = parse_expr(inner.next().unwrap());
    Expr::Lambda {
        params,
        body: Box::new(wrap(body)),
    }
}

/// Filter keyword tokens (`for`, `in`, `if`) out of a comprehension pair's
/// children, returning only the meaningful sub-expressions and variable lists.
fn comp_inner_filter(pair: Pair<Rule>) -> impl Iterator<Item = Pair<Rule>> {
    pair.into_inner()
        .filter(|p| !matches!(p.as_rule(), Rule::kw_for | Rule::kw_in | Rule::kw_if))
}

/// Collect all `ident` children of a `comp_vars` pair as `Vec<String>`.
fn parse_comp_vars(pair: Pair<Rule>) -> Vec<String> {
    pair.into_inner()
        .filter(|p| p.as_rule() == Rule::ident)
        .map(|p| p.as_str().to_string())
        .collect()
}

/// Process backslash escapes inside a string literal body (the contents
/// between matching quotes, *not* including the quotes themselves).
/// Recognised escapes: `\n`, `\r`, `\t`, `\0`, `\\`, `\"`, `\'`, `\xNN`
/// (two hex digits → byte), `\uXXXX` (four hex digits → Unicode code
/// point). An unrecognised escape sequence is preserved literally
/// (e.g. `\d` stays `\d`) so regex patterns that pass through string
/// literals continue to work.
fn unescape_str_lit(s: &str) -> String {
    let bytes = s.as_bytes();
    let mut out = String::with_capacity(s.len());
    let mut i = 0;
    while i < bytes.len() {
        let b = bytes[i];
        if b != b'\\' || i + 1 >= bytes.len() {
            out.push(b as char);
            i += 1;
            continue;
        }
        let next = bytes[i + 1];
        match next {
            b'n' => {
                out.push('\n');
                i += 2;
            }
            b'r' => {
                out.push('\r');
                i += 2;
            }
            b't' => {
                out.push('\t');
                i += 2;
            }
            b'0' => {
                out.push('\0');
                i += 2;
            }
            b'\\' => {
                out.push('\\');
                i += 2;
            }
            b'"' => {
                out.push('"');
                i += 2;
            }
            b'\'' => {
                out.push('\'');
                i += 2;
            }
            b'x' if i + 3 < bytes.len() => {
                let hex = &s[i + 2..i + 4];
                if let Ok(n) = u8::from_str_radix(hex, 16) {
                    out.push(n as char);
                    i += 4;
                } else {
                    out.push('\\');
                    i += 1;
                }
            }
            b'u' if i + 5 < bytes.len() => {
                let hex = &s[i + 2..i + 6];
                if let Ok(n) = u32::from_str_radix(hex, 16) {
                    if let Some(c) = char::from_u32(n) {
                        out.push(c);
                        i += 6;
                        continue;
                    }
                }
                out.push('\\');
                i += 1;
            }
            // Unknown escape: keep the backslash + next char literal so
            // regex patterns (`\d`, `\w`, `\s`) survive intact.
            _ => {
                out.push('\\');
                i += 1;
            }
        }
    }
    out
}

/// Conjoin a sequence of `if` clauses with logical `and`. Returns `None`
/// when the iterator is empty so callers can keep `cond: None` for the
/// no-guard case (which lets the runtime skip the predicate check entirely).
fn collect_comp_conds<'a, I: Iterator<Item = Pair<'a, Rule>>>(rest: I) -> Option<Box<Expr>> {
    let exprs: Vec<Expr> = rest.map(parse_expr).collect();
    let mut it = exprs.into_iter();
    let first = it.next()?;
    let combined = it.fold(first, |acc, e| {
        Expr::BinOp(Box::new(acc), BinOp::And, Box::new(e))
    });
    Some(Box::new(combined))
}

/// Parse a list comprehension `[expr for vars in iter if cond ...]` into
/// `Expr::ListComp`. Multiple `if` clauses are folded together with `and`.
fn parse_list_comp(pair: Pair<Rule>) -> Expr {
    let mut inner = comp_inner_filter(pair);
    let expr = parse_expr(inner.next().unwrap());
    let vars = parse_comp_vars(inner.next().unwrap());
    let iter = parse_expr(inner.next().unwrap());
    let cond = collect_comp_conds(inner);
    Expr::ListComp {
        expr: Box::new(expr),
        vars,
        iter: Box::new(iter),
        cond,
    }
}

/// Parse a dict comprehension `{key: val for vars in iter if cond ...}` into
/// `Expr::DictComp`. Multiple `if` clauses are folded together with `and`.
fn parse_dict_comp(pair: Pair<Rule>) -> Expr {
    let mut inner = comp_inner_filter(pair);
    let key = parse_expr(inner.next().unwrap());
    let val = parse_expr(inner.next().unwrap());
    let vars = parse_comp_vars(inner.next().unwrap());
    let iter = parse_expr(inner.next().unwrap());
    let cond = collect_comp_conds(inner);
    Expr::DictComp {
        key: Box::new(key),
        val: Box::new(val),
        vars,
        iter: Box::new(iter),
        cond,
    }
}

/// Parse a set comprehension `{expr for vars in iter if cond ...}` into
/// `Expr::SetComp`. Multiple `if` clauses are folded together with `and`.
fn parse_set_comp(pair: Pair<Rule>) -> Expr {
    let mut inner = comp_inner_filter(pair);
    let expr = parse_expr(inner.next().unwrap());
    let vars = parse_comp_vars(inner.next().unwrap());
    let iter = parse_expr(inner.next().unwrap());
    let cond = collect_comp_conds(inner);
    Expr::SetComp {
        expr: Box::new(expr),
        vars,
        iter: Box::new(iter),
        cond,
    }
}

/// Parse a generator comprehension `(expr for vars in iter if cond ...)` into
/// `Expr::GenComp`. Semantically identical to `ListComp` but distinct in AST.
/// Multiple `if` clauses are folded together with `and`.
fn parse_gen_comp(pair: Pair<Rule>) -> Expr {
    let mut inner = comp_inner_filter(pair);
    let expr = parse_expr(inner.next().unwrap());
    let vars = parse_comp_vars(inner.next().unwrap());
    let iter = parse_expr(inner.next().unwrap());
    let cond = collect_comp_conds(inner);
    Expr::GenComp {
        expr: Box::new(expr),
        vars,
        iter: Box::new(iter),
        cond,
    }
}

/// Parse an object constructor `{ field, … }` into `Expr::Object`, collecting
/// all `obj_field` children via `parse_obj_field`.
fn parse_obj(pair: Pair<Rule>) -> Expr {
    let fields = pair
        .into_inner()
        .filter(|p| p.as_rule() == Rule::obj_field)
        .map(parse_obj_field)
        .collect();
    Expr::Object(fields)
}

/// Parse a single object field entry, dispatching on the field variant:
/// dynamic key-value, optional value, optional shorthand, spread, deep-spread,
/// conditional kv, or plain shorthand name.
fn parse_obj_field(pair: Pair<Rule>) -> ObjField {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::obj_field_dyn => {
            let mut i = inner.into_inner();
            let key = parse_expr(i.next().unwrap());
            let val = parse_expr(i.next().unwrap());
            ObjField::Dynamic { key, val }
        }
        Rule::obj_field_opt_v => {
            let mut i = inner.into_inner();
            let key = obj_key_str(i.next().unwrap());
            let val = parse_expr(i.next().unwrap());
            ObjField::Kv {
                key,
                val,
                optional: true,
                cond: None,
            }
        }
        Rule::obj_field_opt => {
            let key = obj_key_str(inner.into_inner().next().unwrap());
            ObjField::Kv {
                key: key.clone(),
                val: Expr::Ident(key),
                optional: true,
                cond: None,
            }
        }
        Rule::obj_field_spread | Rule::obj_field_spread_star => {
            let expr = parse_expr(inner.into_inner().next().unwrap());
            ObjField::Spread(expr)
        }
        Rule::obj_field_spread_deep => {
            let expr = parse_expr(inner.into_inner().next().unwrap());
            ObjField::SpreadDeep(expr)
        }
        Rule::obj_field_kv => {
            let mut cond: Option<Expr> = None;
            let mut key: Option<String> = None;
            let mut val: Option<Expr> = None;
            let mut saw_when = false;
            for p in inner.into_inner() {
                match p.as_rule() {
                    Rule::kw_when => saw_when = true,
                    Rule::obj_key_expr => key = Some(obj_key_str(p)),
                    Rule::expr => {
                        if saw_when {
                            cond = Some(parse_expr(p));
                        } else {
                            val = Some(parse_expr(p));
                        }
                    }
                    _ => {}
                }
            }
            ObjField::Kv {
                key: key.expect("obj_field_kv missing key"),
                val: val.expect("obj_field_kv missing val"),
                optional: false,
                cond,
            }
        }
        Rule::obj_field_short => {
            let name = inner.into_inner().next().unwrap().as_str().to_string();
            ObjField::Short(name)
        }
        r => panic!("unexpected obj_field rule: {:?}", r),
    }
}

/// Extract the string key from an `obj_key_expr` pair, unwrapping either a
/// loose identifier (which permits reserved keywords like `kind` in key
/// position) or a quoted string literal.
fn obj_key_str(pair: Pair<Rule>) -> String {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::ident | Rule::loose_ident => inner.as_str().to_string(),
        Rule::lit_str => {
            let s = inner.into_inner().next().unwrap();
            let raw = s.as_str();
            raw[1..raw.len() - 1].to_string()
        }
        r => panic!("unexpected obj_key_expr rule: {:?}", r),
    }
}

/// Parse an array constructor `[elem, …]` into `Expr::Array`, handling both
/// plain expressions and spread elements (`...expr`).
fn parse_arr(pair: Pair<Rule>) -> Expr {
    let elems = pair
        .into_inner()
        .filter(|p| p.as_rule() == Rule::arr_elem)
        .map(|elem| {
            let inner = elem.into_inner().next().unwrap();
            match inner.as_rule() {
                Rule::arr_spread => {
                    let expr = parse_expr(inner.into_inner().next().unwrap());
                    ArrayElem::Spread(expr)
                }
                _ => ArrayElem::Expr(parse_expr(inner)),
            }
        })
        .collect();
    Expr::Array(elems)
}

/// Parse a top-level global function call `name(args)` into
/// `Expr::GlobalCall`, used for functions that are not dot-method syntax
/// (e.g. `coalesce(…)`, `range(…)`).
fn parse_global_call(pair: Pair<Rule>) -> Expr {
    let mut inner = pair.into_inner();
    let name = inner.next().unwrap().as_str().to_string();
    let args = inner.next().map(parse_arg_list).unwrap_or_default();
    Expr::GlobalCall { name, args }
}

/// Parse a `patch root { field: val … }` block into `Expr::Patch`, collecting
/// all `patch_field` operations and the mandatory root expression.
fn parse_patch(pair: Pair<Rule>) -> Expr {
    let mut root: Option<Expr> = None;
    let mut ops: Vec<PatchOp> = Vec::new();
    for p in pair.into_inner() {
        match p.as_rule() {
            Rule::kw_patch => {}
            Rule::patch_field => ops.push(parse_patch_field(p)),
            _ => {
                // The root expression comes first (before any patch_field rules);
                // ignore the kw_patch token by matching it above.
                if root.is_none() {
                    root = Some(parse_expr(p));
                }
            }
        }
    }
    Expr::Patch {
        root: Box::new(root.expect("patch requires root expression")),
        ops,
    }
}

/// Parse a single `field: value [when cond]` entry inside a patch block into
/// a `PatchOp`, extracting the path, value, and optional condition.
fn parse_patch_field(pair: Pair<Rule>) -> PatchOp {
    let mut path: Vec<PathStep> = Vec::new();
    let mut val: Option<Expr> = None;
    let mut cond: Option<Expr> = None;
    let mut saw_when = false;
    for p in pair.into_inner() {
        match p.as_rule() {
            Rule::patch_key => path = parse_patch_key(p),
            Rule::kw_when => saw_when = true,
            Rule::expr => {
                if saw_when {
                    cond = Some(parse_expr(p));
                } else {
                    val = Some(parse_expr(p));
                }
            }
            _ => {}
        }
    }
    PatchOp {
        path,
        val: val.expect("patch_field missing val"),
        cond,
    }
}

/// Parse a patch key (`field.sub[0].*` etc.) into a `Vec<PathStep>`, starting
/// with the mandatory leading identifier and followed by zero or more
/// `patch_step` refinements.
fn parse_patch_key(pair: Pair<Rule>) -> Vec<PathStep> {
    let mut steps: Vec<PathStep> = Vec::new();
    let mut first = true;
    for p in pair.into_inner() {
        match p.as_rule() {
            Rule::ident if first => {
                steps.push(PathStep::Field(p.as_str().to_string()));
                first = false;
            }
            Rule::patch_step => steps.push(parse_patch_step(p)),
            _ => {}
        }
    }
    steps
}

/// Parse a single patch path step (`pp_dot_field`, `pp_index`, `pp_wild`,
/// `pp_wild_filter`, or `pp_descendant`) into a `PathStep`.
fn parse_patch_step(pair: Pair<Rule>) -> PathStep {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::pp_dot_field => {
            let name = inner.into_inner().next().unwrap().as_str().to_string();
            PathStep::Field(name)
        }
        Rule::pp_index => {
            let idx: i64 = inner.into_inner().next().unwrap().as_str().parse().unwrap();
            PathStep::Index(idx)
        }
        Rule::pp_wild => PathStep::Wildcard,
        Rule::pp_wild_filter => {
            // Extract the inner filter expression from `[* if expr]`.
            let mut e: Option<Expr> = None;
            for p in inner.into_inner() {
                if p.as_rule() == Rule::expr {
                    e = Some(parse_expr(p));
                }
            }
            PathStep::WildcardFilter(Box::new(e.expect("pp_wild_filter missing expr")))
        }
        Rule::pp_descendant => {
            let name = inner.into_inner().next().unwrap().as_str().to_string();
            PathStep::Descendant(name)
        }
        r => panic!("unexpected patch_step rule: {:?}", r),
    }
}

/// Parse an argument list into a `Vec<Arg>`, filtering out separator tokens
/// and mapping each `arg` rule to a positional or named `Arg`.
fn parse_arg_list(pair: Pair<Rule>) -> Vec<Arg> {
    pair.into_inner()
        .filter(|p| p.as_rule() == Rule::arg)
        .map(parse_arg)
        .collect()
}

/// Parse a single argument, returning `Arg::Named(name, expr)` for named args
/// (`key: expr`) and `Arg::Pos(expr)` for positional args.
fn parse_arg(pair: Pair<Rule>) -> Arg {
    let inner = pair.into_inner().next().unwrap();
    match inner.as_rule() {
        Rule::named_arg => {
            let mut i = inner.into_inner();
            let name = i.next().unwrap().as_str().to_string();
            let val = parse_expr(i.next().unwrap());
            Arg::Named(name, val)
        }
        Rule::pos_arg => Arg::Pos(parse_expr(inner.into_inner().next().unwrap())),
        r => panic!("unexpected arg rule: {:?}", r),
    }
}
