Skip to main content

jsonata_core/
evaluator.rs

1// Expression evaluator
2// Mirrors jsonata.js from the reference implementation
3
4#![allow(clippy::cloned_ref_to_slice_refs)]
5#![allow(clippy::explicit_counter_loop)]
6#![allow(clippy::too_many_arguments)]
7#![allow(clippy::manual_strip)]
8
9use std::cmp::Ordering;
10use std::collections::{HashMap, HashSet};
11
12use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
13use crate::parser;
14use crate::value::JValue;
15use indexmap::IndexMap;
16use std::rc::Rc;
17use thiserror::Error;
18
19/// Specialized sort comparator for `$l.field op $r.field` patterns.
20/// Bypasses the full AST evaluator for simple field-based sort comparisons.
21///
22/// In JSONata `$sort`, the comparator returns true when `$l` should come AFTER `$r`.
23/// `$l.field > $r.field` swaps when left > right, producing ascending order.
24/// `$l.field < $r.field` swaps when left < right, producing descending order.
25struct SpecializedSortComparator {
26    field: String,
27    descending: bool,
28}
29
30/// Pre-extracted sort key for the Schwartzian transform in specialized sorting.
31enum SortKey {
32    Num(f64),
33    Str(Rc<str>),
34    None,
35}
36
37fn compare_sort_keys(a: &SortKey, b: &SortKey, descending: bool) -> Ordering {
38    let ord = match (a, b) {
39        (SortKey::Num(x), SortKey::Num(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
40        (SortKey::Str(x), SortKey::Str(y)) => (**x).cmp(&**y),
41        (SortKey::None, SortKey::None) => Ordering::Equal,
42        (SortKey::None, _) => Ordering::Greater,
43        (_, SortKey::None) => Ordering::Less,
44        // Mixed types: maintain original order
45        _ => Ordering::Equal,
46    };
47    if descending { ord.reverse() } else { ord }
48}
49
50/// Try to extract a specialized sort comparator from a lambda AST node.
51/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
52fn try_specialize_sort_comparator(
53    body: &AstNode,
54    left_param: &str,
55    right_param: &str,
56) -> Option<SpecializedSortComparator> {
57    let AstNode::Binary { op, lhs, rhs } = body else {
58        return None;
59    };
60
61    // Returns true if op means "swap when left > right" (ascending order).
62    let is_ascending = |op: &BinaryOp| -> Option<bool> {
63        match op {
64            BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
65            BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
66            _ => None,
67        }
68    };
69
70    // Extract field name from a `$param.field` path with no stages.
71    let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
72        let AstNode::Path { steps } = node else { return None };
73        if steps.len() != 2 { return None; }
74        let AstNode::Variable(var) = &steps[0].node else { return None };
75        if var != param { return None; }
76        let AstNode::Name(field) = &steps[1].node else { return None };
77        if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() { return None; }
78        Some(field.clone())
79    };
80
81    // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
82    for flipped in [false, true] {
83        let (lhs_param, rhs_param) = if flipped {
84            (right_param, left_param)
85        } else {
86            (left_param, right_param)
87        };
88        if let (Some(lhs_field), Some(rhs_field)) = (
89            extract_var_field(lhs, lhs_param),
90            extract_var_field(rhs, rhs_param),
91        ) {
92            if lhs_field == rhs_field {
93                let descending = match op {
94                    // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
95                    // Flipped `$r.f - $l.f` → positive when r > l → descending.
96                    BinaryOp::Subtract => flipped,
97                    // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
98                    _ => {
99                        let ascending = is_ascending(op)?;
100                        if flipped { ascending } else { !ascending }
101                    }
102                };
103                return Some(SpecializedSortComparator {
104                    field: lhs_field,
105                    descending,
106                });
107            }
108        }
109    }
110    None
111}
112
113// ──────────────────────────────────────────────────────────────────────────────
114// CompiledExpr — unified compiled expression framework
115// ──────────────────────────────────────────────────────────────────────────────
116//
117// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
118// can represent arbitrary simple expressions without AST walking.  Evaluated in
119// a tight loop with no recursion tracking, no scope management, and no AstNode
120// pattern matching.
121
122/// Shape cache: maps field names to their positional index in an IndexMap.
123/// When all objects in an array share the same key ordering (extremely common
124/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
125/// instead of O(1)-amortized hash lookups.
126type ShapeCache = HashMap<String, usize>;
127
128/// Build a shape cache from the first object in an array.
129/// Returns None if the data is not an object.
130fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
131    match first_element {
132        JValue::Object(obj) => {
133            let mut cache = HashMap::with_capacity(obj.len());
134            for (idx, (key, _)) in obj.iter().enumerate() {
135                cache.insert(key.clone(), idx);
136            }
137            Some(cache)
138        }
139        _ => None,
140    }
141}
142
143/// Comparison operator for compiled expressions.
144#[derive(Debug, Clone, Copy)]
145pub(crate) enum CompiledCmp {
146    Eq,
147    Ne,
148    Lt,
149    Le,
150    Gt,
151    Ge,
152}
153
154/// Arithmetic operator for compiled expressions.
155#[derive(Debug, Clone, Copy)]
156pub(crate) enum CompiledArithOp {
157    Add,
158    Sub,
159    Mul,
160    Div,
161    Mod,
162}
163
164/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
165///
166/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
167/// expression-compile time (once), then `eval_compiled()` evaluates it per
168/// element in O(expression-size) with no heap allocation in the hot path.
169#[derive(Clone, Debug)]
170pub(crate) enum CompiledExpr {
171    // ── Leaves ──────────────────────────────────────────────────────────
172    /// A literal value known at compile time.
173    Literal(JValue),
174    /// Explicit `null` literal from `AstNode::Null`.
175    /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
176    /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
177    ExplicitNull,
178    /// Single-level field lookup on the current object: `obj.get("field")`.
179    FieldLookup(String),
180    /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
181    NestedFieldLookup(String, String),
182    /// Variable lookup from enclosing scope (e.g. `$var`).
183    /// Resolved at eval time via a provided variable map.
184    VariableLookup(String),
185
186    // ── Comparison ──────────────────────────────────────────────────────
187    Compare {
188        op: CompiledCmp,
189        lhs: Box<CompiledExpr>,
190        rhs: Box<CompiledExpr>,
191    },
192
193    // ── Arithmetic ──────────────────────────────────────────────────────
194    Arithmetic {
195        op: CompiledArithOp,
196        lhs: Box<CompiledExpr>,
197        rhs: Box<CompiledExpr>,
198    },
199
200    // ── String ──────────────────────────────────────────────────────────
201    Concat(Box<CompiledExpr>, Box<CompiledExpr>),
202
203    // ── Logical ─────────────────────────────────────────────────────────
204    And(Box<CompiledExpr>, Box<CompiledExpr>),
205    Or(Box<CompiledExpr>, Box<CompiledExpr>),
206    Not(Box<CompiledExpr>),
207    /// Negation of a numeric value.
208    Negate(Box<CompiledExpr>),
209
210    // ── Conditional ─────────────────────────────────────────────────────
211    Conditional {
212        condition: Box<CompiledExpr>,
213        then_expr: Box<CompiledExpr>,
214        else_expr: Option<Box<CompiledExpr>>,
215    },
216
217    // ── Compound ────────────────────────────────────────────────────────
218    /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
219    ObjectConstruct(Vec<(String, CompiledExpr)>),
220    /// Array construction: `[expr1, expr2, ...]`
221    ///
222    /// Each element carries a `bool` flag: `true` means the element originated
223    /// from an explicit `AstNode::Array` constructor and must be kept nested even
224    /// if it evaluates to an array. `false` means the element's array value is
225    /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
226    /// Undefined values are always skipped.
227    ArrayConstruct(Vec<(CompiledExpr, bool)>),
228
229    // ── Phase 2 extensions ──────────────────────────────────────────────
230    /// Named variable lookup from context scope (any `$name` not in lambda params).
231    /// Compiled when a named variable is encountered and no allowed_vars list is
232    /// provided (top-level compilation). At runtime, returns the value from the vars
233    /// map (lambda params or captured env), or Undefined if not present.
234    #[allow(dead_code)]
235    ContextVar(String),
236
237    /// Multi-step field path with optional per-step filters: `a.b[pred].c`
238    /// Applies implicit array-mapping semantics at each step.
239    FieldPath(Vec<CompiledStep>),
240
241    /// Call a pure, side-effect-free builtin with compiled arguments.
242    /// Only builtins in COMPILABLE_BUILTINS are allowed here.
243    BuiltinCall {
244        name: &'static str,
245        args: Vec<CompiledExpr>,
246    },
247
248    /// Sequential block: evaluate all expressions, return last value.
249    Block(Vec<CompiledExpr>),
250
251    /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
252    Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
253}
254
255/// One step in a compiled `FieldPath`.
256#[derive(Clone, Debug)]
257pub(crate) struct CompiledStep {
258    /// Field name to look up at this step.
259    pub field: String,
260    /// Optional predicate filter compiled from a `Stage::Filter` stage.
261    pub filter: Option<CompiledExpr>,
262}
263
264/// Try to compile an AstNode subtree into a CompiledExpr.
265/// Returns None for anything that requires full AST evaluation (lambda calls,
266/// function calls with side effects, complex paths, etc.).
267pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
268    try_compile_expr_inner(node, None)
269}
270
271/// Like `try_compile_expr` but additionally allows the specified variable names
272/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
273/// parameters are known and will be provided via the `vars` map at eval time.
274pub(crate) fn try_compile_expr_with_allowed_vars(
275    node: &AstNode,
276    allowed_vars: &[&str],
277) -> Option<CompiledExpr> {
278    try_compile_expr_inner(node, Some(allowed_vars))
279}
280
281fn try_compile_expr_inner(
282    node: &AstNode,
283    allowed_vars: Option<&[&str]>,
284) -> Option<CompiledExpr> {
285    match node {
286        // ── Literals ────────────────────────────────────────────────────
287        AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
288        AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
289        AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
290        AstNode::Null => Some(CompiledExpr::ExplicitNull),
291
292        // ── Field access ────────────────────────────────────────────────
293        AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
294
295        // ── Variable lookup ─────────────────────────────────────────────
296        // $ (empty name) always refers to the current element.
297        // Named variables: in HOF mode (allowed_vars=Some), only compile if the
298        // variable is in the allowed set (lambda params supplied via vars map).
299        // In top-level mode (allowed_vars=None), compile unknown variables as
300        // ContextVar — they return Undefined at runtime when no bindings are passed.
301        AstNode::Variable(var) if var.is_empty() => {
302            Some(CompiledExpr::VariableLookup(var.clone()))
303        }
304        AstNode::Variable(var) => {
305            if let Some(allowed) = allowed_vars {
306                // HOF mode: only compile if the variable is a known lambda param.
307                if allowed.contains(&var.as_str()) {
308                    return Some(CompiledExpr::VariableLookup(var.clone()));
309                }
310            }
311            // Named variables require Context for correct lookup (scope stack, builtins
312            // registry). The compiled fast path passes ctx=None, so fall back to the
313            // tree-walker for all non-empty variable references.
314            None
315        }
316
317        // ── Path expressions ────────────────────────────────────────────
318        AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
319
320        // ── Binary operations ───────────────────────────────────────────
321        AstNode::Binary { op, lhs, rhs } => {
322            let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
323            let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
324            match op {
325                // Comparison
326                BinaryOp::Equal => Some(CompiledExpr::Compare {
327                    op: CompiledCmp::Eq,
328                    lhs: Box::new(compiled_lhs),
329                    rhs: Box::new(compiled_rhs),
330                }),
331                BinaryOp::NotEqual => Some(CompiledExpr::Compare {
332                    op: CompiledCmp::Ne,
333                    lhs: Box::new(compiled_lhs),
334                    rhs: Box::new(compiled_rhs),
335                }),
336                BinaryOp::LessThan => Some(CompiledExpr::Compare {
337                    op: CompiledCmp::Lt,
338                    lhs: Box::new(compiled_lhs),
339                    rhs: Box::new(compiled_rhs),
340                }),
341                BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
342                    op: CompiledCmp::Le,
343                    lhs: Box::new(compiled_lhs),
344                    rhs: Box::new(compiled_rhs),
345                }),
346                BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
347                    op: CompiledCmp::Gt,
348                    lhs: Box::new(compiled_lhs),
349                    rhs: Box::new(compiled_rhs),
350                }),
351                BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
352                    op: CompiledCmp::Ge,
353                    lhs: Box::new(compiled_lhs),
354                    rhs: Box::new(compiled_rhs),
355                }),
356                // Arithmetic
357                BinaryOp::Add => Some(CompiledExpr::Arithmetic {
358                    op: CompiledArithOp::Add,
359                    lhs: Box::new(compiled_lhs),
360                    rhs: Box::new(compiled_rhs),
361                }),
362                BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
363                    op: CompiledArithOp::Sub,
364                    lhs: Box::new(compiled_lhs),
365                    rhs: Box::new(compiled_rhs),
366                }),
367                BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
368                    op: CompiledArithOp::Mul,
369                    lhs: Box::new(compiled_lhs),
370                    rhs: Box::new(compiled_rhs),
371                }),
372                BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
373                    op: CompiledArithOp::Div,
374                    lhs: Box::new(compiled_lhs),
375                    rhs: Box::new(compiled_rhs),
376                }),
377                BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
378                    op: CompiledArithOp::Mod,
379                    lhs: Box::new(compiled_lhs),
380                    rhs: Box::new(compiled_rhs),
381                }),
382                // Logical
383                BinaryOp::And => Some(CompiledExpr::And(
384                    Box::new(compiled_lhs),
385                    Box::new(compiled_rhs),
386                )),
387                BinaryOp::Or => Some(CompiledExpr::Or(
388                    Box::new(compiled_lhs),
389                    Box::new(compiled_rhs),
390                )),
391                // String concat
392                BinaryOp::Concatenate => Some(CompiledExpr::Concat(
393                    Box::new(compiled_lhs),
394                    Box::new(compiled_rhs),
395                )),
396                // Coalesce: return lhs if defined/non-null, else rhs
397                BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
398                    Box::new(compiled_lhs),
399                    Box::new(compiled_rhs),
400                )),
401                // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
402                _ => None,
403            }
404        }
405
406        // ── Unary operations ────────────────────────────────────────────
407        AstNode::Unary { op, operand } => {
408            let compiled = try_compile_expr_inner(operand, allowed_vars)?;
409            match op {
410                crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
411                crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
412            }
413        }
414
415        // ── Conditional ─────────────────────────────────────────────────
416        AstNode::Conditional {
417            condition,
418            then_branch,
419            else_branch,
420        } => {
421            let cond = try_compile_expr_inner(condition, allowed_vars)?;
422            let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
423            let else_e = match else_branch {
424                Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
425                None => None,
426            };
427            Some(CompiledExpr::Conditional {
428                condition: Box::new(cond),
429                then_expr: Box::new(then_e),
430                else_expr: else_e,
431            })
432        }
433
434        // ── Object construction ─────────────────────────────────────────
435        AstNode::Object(pairs) => {
436            let mut fields = Vec::with_capacity(pairs.len());
437            for (key_node, val_node) in pairs {
438                // Key must be a string literal
439                let key = match key_node {
440                    AstNode::String(s) => s.clone(),
441                    _ => return None,
442                };
443                let val = try_compile_expr_inner(val_node, allowed_vars)?;
444                fields.push((key, val));
445            }
446            Some(CompiledExpr::ObjectConstruct(fields))
447        }
448
449        // ── Array construction ──────────────────────────────────────────
450        AstNode::Array(elems) => {
451            let mut compiled = Vec::with_capacity(elems.len());
452            for elem in elems {
453                // Tag whether the element itself is an array constructor: if so, its
454                // array value must be kept nested rather than flattened (tree-walker parity).
455                let is_nested = matches!(elem, AstNode::Array(_));
456                compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
457            }
458            Some(CompiledExpr::ArrayConstruct(compiled))
459        }
460
461        // ── Block (sequential evaluation) ───────────────────────────────
462        AstNode::Block(exprs) if !exprs.is_empty() => {
463            let compiled: Option<Vec<CompiledExpr>> = exprs
464                .iter()
465                .map(|e| try_compile_expr_inner(e, allowed_vars))
466                .collect();
467            compiled.map(CompiledExpr::Block)
468        }
469
470        // ── Pure builtin function calls ──────────────────────────────────
471        AstNode::Function {
472            name,
473            args,
474            is_builtin: true,
475        } => {
476            if is_compilable_builtin(name) {
477                // Arity guard: if the call site passes more args than the builtin accepts,
478                // fall back to the tree-walker so it can raise the correct T0410 error.
479                if let Some(max) = compilable_builtin_max_args(name) {
480                    if args.len() > max {
481                        return None;
482                    }
483                }
484                let compiled_args: Option<Vec<CompiledExpr>> = args
485                    .iter()
486                    .map(|a| try_compile_expr_inner(a, allowed_vars))
487                    .collect();
488                compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
489                    name: static_builtin_name(name),
490                    args: cargs,
491                })
492            } else {
493                None
494            }
495        }
496
497        // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
498        _ => None,
499    }
500}
501
502/// Returns true if the named builtin is pure (no side effects, no context dependency)
503/// and can be safely compiled into a BuiltinCall.
504fn is_compilable_builtin(name: &str) -> bool {
505    matches!(
506        name,
507        "string"
508            | "length"
509            | "substring"
510            | "substringBefore"
511            | "substringAfter"
512            | "uppercase"
513            | "lowercase"
514            | "trim"
515            | "contains"
516            | "split"
517            | "join"
518            | "number"
519            | "floor"
520            | "ceil"
521            | "round"
522            | "abs"
523            | "sqrt"
524            | "sum"
525            | "max"
526            | "min"
527            | "average"
528            | "count"
529            | "boolean"
530            | "not"
531            | "keys"
532            | "append"
533            | "reverse"
534            | "distinct"
535            | "merge"
536    )
537}
538
539/// Maximum number of explicit arguments accepted by each compilable builtin.
540/// Returns `None` for variadic functions with no fixed upper bound.
541/// Used at compile time to fall back to the tree-walker for over-arity calls
542/// (which the tree-walker turns into the correct T0410/T0411 type errors).
543fn compilable_builtin_max_args(name: &str) -> Option<usize> {
544    match name {
545        "string" => Some(2),
546        "length" | "uppercase" | "lowercase" | "trim" => Some(1),
547        "substring" | "split" => Some(3),
548        "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
549        "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
550        "sum" | "max" | "min" | "average" | "count" => Some(1),
551        "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
552        "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
553        _ => None,
554    }
555}
556
557/// Return the `&'static str` for a known compilable builtin name.
558/// SAFETY: only called after `is_compilable_builtin` returns true.
559fn static_builtin_name(name: &str) -> &'static str {
560    match name {
561        "string" => "string",
562        "length" => "length",
563        "substring" => "substring",
564        "substringBefore" => "substringBefore",
565        "substringAfter" => "substringAfter",
566        "uppercase" => "uppercase",
567        "lowercase" => "lowercase",
568        "trim" => "trim",
569        "contains" => "contains",
570        "split" => "split",
571        "join" => "join",
572        "number" => "number",
573        "floor" => "floor",
574        "ceil" => "ceil",
575        "round" => "round",
576        "abs" => "abs",
577        "sqrt" => "sqrt",
578        "sum" => "sum",
579        "max" => "max",
580        "min" => "min",
581        "average" => "average",
582        "count" => "count",
583        "boolean" => "boolean",
584        "not" => "not",
585        "keys" => "keys",
586        "append" => "append",
587        "reverse" => "reverse",
588        "distinct" => "distinct",
589        "merge" => "merge",
590        _ => unreachable!("Not a compilable builtin: {}", name),
591    }
592}
593
594/// Evaluate a compiled expression against a single element.
595///
596/// `data` is the current element (typically an object from an array).
597/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
598///
599/// This is the tight inner loop — no recursion tracking, no scope push/pop,
600/// no AstNode pattern matching.
601#[inline(always)]
602pub(crate) fn eval_compiled(
603    expr: &CompiledExpr,
604    data: &JValue,
605    vars: Option<&HashMap<&str, &JValue>>,
606) -> Result<JValue, EvaluatorError> {
607    eval_compiled_inner(expr, data, vars, None, None)
608}
609
610/// Like `eval_compiled` but with an optional shape cache for O(1) positional
611/// field access. The shape cache maps field names to their index in the object's
612/// internal Vec, enabling `get_index()` instead of hash lookups.
613#[inline(always)]
614fn eval_compiled_shaped(
615    expr: &CompiledExpr,
616    data: &JValue,
617    vars: Option<&HashMap<&str, &JValue>>,
618    shape: &ShapeCache,
619) -> Result<JValue, EvaluatorError> {
620    eval_compiled_inner(expr, data, vars, None, Some(shape))
621}
622
623fn eval_compiled_inner(
624    expr: &CompiledExpr,
625    data: &JValue,
626    vars: Option<&HashMap<&str, &JValue>>,
627    ctx: Option<&Context>,
628    shape: Option<&ShapeCache>,
629) -> Result<JValue, EvaluatorError> {
630    match expr {
631        // ── Leaves ──────────────────────────────────────────────────────
632        CompiledExpr::Literal(v) => Ok(v.clone()),
633
634        // ExplicitNull evaluates to Null, but is flagged at compile-time for
635        // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
636        CompiledExpr::ExplicitNull => Ok(JValue::Null),
637
638        CompiledExpr::FieldLookup(field) => match data {
639            JValue::Object(obj) => {
640                // Shape-accelerated: use positional index if available
641                if let Some(shape) = shape {
642                    if let Some(&idx) = shape.get(field.as_str()) {
643                        return Ok(obj
644                            .get_index(idx)
645                            .map(|(_, v)| v.clone())
646                            .unwrap_or(JValue::Undefined));
647                    }
648                }
649                Ok(obj.get(field.as_str()).cloned().unwrap_or(JValue::Undefined))
650            }
651            _ => Ok(JValue::Undefined),
652        },
653
654        CompiledExpr::NestedFieldLookup(outer, inner) => match data {
655            JValue::Object(obj) => {
656                // Shape-accelerated outer lookup
657                let outer_val = if let Some(shape) = shape {
658                    if let Some(&idx) = shape.get(outer.as_str()) {
659                        obj.get_index(idx).map(|(_, v)| v)
660                    } else {
661                        obj.get(outer.as_str())
662                    }
663                } else {
664                    obj.get(outer.as_str())
665                };
666                Ok(outer_val
667                    .and_then(|v| match v {
668                        JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
669                        _ => None,
670                    })
671                    .unwrap_or(JValue::Undefined))
672            }
673            _ => Ok(JValue::Undefined),
674        },
675
676        CompiledExpr::VariableLookup(var) => {
677            if let Some(vars) = vars {
678                if let Some(val) = vars.get(var.as_str()) {
679                    return Ok((*val).clone());
680                }
681            }
682            // $ (empty var name) refers to the current data
683            if var.is_empty() {
684                return Ok(data.clone());
685            }
686            Ok(JValue::Undefined)
687        }
688
689        // ── Comparison ──────────────────────────────────────────────────
690        CompiledExpr::Compare { op, lhs, rhs } => {
691            let lhs_explicit_null = is_compiled_explicit_null(lhs);
692            let rhs_explicit_null = is_compiled_explicit_null(rhs);
693            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
694            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
695            match op {
696                CompiledCmp::Eq => Ok(JValue::Bool(
697                    crate::functions::array::values_equal(&left, &right),
698                )),
699                CompiledCmp::Ne => Ok(JValue::Bool(
700                    !crate::functions::array::values_equal(&left, &right),
701                )),
702                CompiledCmp::Lt => compiled_ordered_cmp(
703                    &left, &right, lhs_explicit_null, rhs_explicit_null,
704                    |a, b| a < b, |a, b| a < b,
705                ),
706                CompiledCmp::Le => compiled_ordered_cmp(
707                    &left, &right, lhs_explicit_null, rhs_explicit_null,
708                    |a, b| a <= b, |a, b| a <= b,
709                ),
710                CompiledCmp::Gt => compiled_ordered_cmp(
711                    &left, &right, lhs_explicit_null, rhs_explicit_null,
712                    |a, b| a > b, |a, b| a > b,
713                ),
714                CompiledCmp::Ge => compiled_ordered_cmp(
715                    &left, &right, lhs_explicit_null, rhs_explicit_null,
716                    |a, b| a >= b, |a, b| a >= b,
717                ),
718            }
719        }
720
721        // ── Arithmetic ──────────────────────────────────────────────────
722        CompiledExpr::Arithmetic { op, lhs, rhs } => {
723            let lhs_explicit_null = is_compiled_explicit_null(lhs);
724            let rhs_explicit_null = is_compiled_explicit_null(rhs);
725            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
726            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
727            compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
728        }
729
730        // ── String concat ───────────────────────────────────────────────
731        CompiledExpr::Concat(lhs, rhs) => {
732            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
733            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
734            let ls = compiled_to_concat_string(&left)?;
735            let rs = compiled_to_concat_string(&right)?;
736            Ok(JValue::string(format!("{}{}", ls, rs)))
737        }
738
739        // ── Logical ─────────────────────────────────────────────────────
740        CompiledExpr::And(lhs, rhs) => {
741            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
742            if !compiled_is_truthy(&left) {
743                return Ok(JValue::Bool(false));
744            }
745            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
746            Ok(JValue::Bool(compiled_is_truthy(&right)))
747        }
748        CompiledExpr::Or(lhs, rhs) => {
749            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
750            if compiled_is_truthy(&left) {
751                return Ok(JValue::Bool(true));
752            }
753            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
754            Ok(JValue::Bool(compiled_is_truthy(&right)))
755        }
756        CompiledExpr::Not(inner) => {
757            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
758            Ok(JValue::Bool(!compiled_is_truthy(&val)))
759        }
760        CompiledExpr::Negate(inner) => {
761            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
762            match val {
763                JValue::Number(n) => Ok(JValue::Number(-n)),
764                JValue::Null => Ok(JValue::Null),
765                // Undefined operand propagates through unary minus, matching the tree-walker.
766                v if v.is_undefined() => Ok(JValue::Undefined),
767                _ => Err(EvaluatorError::TypeError(
768                    "D1002: Cannot negate non-number value".to_string(),
769                )),
770            }
771        }
772
773        // ── Conditional ─────────────────────────────────────────────────
774        CompiledExpr::Conditional {
775            condition,
776            then_expr,
777            else_expr,
778        } => {
779            let cond = eval_compiled_inner(condition, data, vars, ctx, shape)?;
780            if compiled_is_truthy(&cond) {
781                eval_compiled_inner(then_expr, data, vars, ctx, shape)
782            } else if let Some(else_e) = else_expr {
783                eval_compiled_inner(else_e, data, vars, ctx, shape)
784            } else {
785                Ok(JValue::Undefined)
786            }
787        }
788
789        // ── Object construction ─────────────────────────────────────────
790        CompiledExpr::ObjectConstruct(fields) => {
791            let mut result = IndexMap::with_capacity(fields.len());
792            for (key, expr) in fields {
793                let value = eval_compiled_inner(expr, data, vars, ctx, shape)?;
794                if !value.is_undefined() {
795                    result.insert(key.clone(), value);
796                }
797            }
798            Ok(JValue::object(result))
799        }
800
801        // ── Array construction ──────────────────────────────────────────
802        CompiledExpr::ArrayConstruct(elems) => {
803            let mut result = Vec::new();
804            for (elem_expr, is_nested) in elems {
805                let value = eval_compiled_inner(elem_expr, data, vars, ctx, shape)?;
806                // Undefined values are excluded from array constructors (tree-walker parity)
807                if value.is_undefined() {
808                    continue;
809                }
810                if *is_nested {
811                    // Explicit array constructor [...] — keep nested even if it's an array
812                    result.push(value);
813                } else if let JValue::Array(arr) = value {
814                    // Non-constructor that evaluated to an array — flatten one level
815                    result.extend(arr.iter().cloned());
816                } else {
817                    result.push(value);
818                }
819            }
820            Ok(JValue::array(result))
821        }
822
823        // ── Phase 2 new variants ─────────────────────────────────────────
824
825        // ContextVar: named variable lookup from context scope.
826        // In top-level mode (ctx=None, no bindings), returns Undefined.
827        // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
828        // is only ever populated for top-level calls — always Undefined there.
829        CompiledExpr::ContextVar(name) => {
830            // Check vars map first (for lambda params that might shadow context)
831            if let Some(vars) = vars {
832                if let Some(val) = vars.get(name.as_str()) {
833                    return Ok((*val).clone());
834                }
835            }
836            // Then check context scope
837            if let Some(ctx) = ctx {
838                if let Some(val) = ctx.lookup(name) {
839                    return Ok(val.clone());
840                }
841            }
842            Ok(JValue::Undefined)
843        }
844
845        // FieldPath: multi-step field access with implicit array mapping.
846        CompiledExpr::FieldPath(steps) => {
847            compiled_eval_field_path(steps, data, vars, ctx, shape)
848        }
849
850        // BuiltinCall: evaluate all args, dispatch to pure builtin.
851        CompiledExpr::BuiltinCall { name, args } => {
852            let mut evaled_args = Vec::with_capacity(args.len());
853            for arg in args.iter() {
854                evaled_args.push(eval_compiled_inner(arg, data, vars, ctx, shape)?);
855            }
856            call_pure_builtin(name, &evaled_args, data)
857        }
858
859        // Block: evaluate each expression in sequence, return the last value.
860        CompiledExpr::Block(exprs) => {
861            let mut result = JValue::Undefined;
862            for expr in exprs.iter() {
863                result = eval_compiled_inner(expr, data, vars, ctx, shape)?;
864            }
865            Ok(result)
866        }
867
868        // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
869        // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
870        CompiledExpr::Coalesce(lhs, rhs) => {
871            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
872            if left.is_undefined() {
873                eval_compiled_inner(rhs, data, vars, ctx, shape)
874            } else {
875                Ok(left)
876            }
877        }
878    }
879}
880
881/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
882#[inline]
883pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
884    match value {
885        JValue::Null | JValue::Undefined => false,
886        JValue::Bool(b) => *b,
887        JValue::Number(n) => *n != 0.0,
888        JValue::String(s) => !s.is_empty(),
889        JValue::Array(a) => !a.is_empty(),
890        JValue::Object(o) => !o.is_empty(),
891        _ => false,
892    }
893}
894
895/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
896/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
897#[inline]
898fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
899    matches!(expr, CompiledExpr::ExplicitNull)
900}
901
902/// Ordered comparison for compiled expressions.
903/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
904#[inline]
905pub(crate) fn compiled_ordered_cmp(
906    left: &JValue,
907    right: &JValue,
908    left_is_explicit_null: bool,
909    right_is_explicit_null: bool,
910    cmp_num: fn(f64, f64) -> bool,
911    cmp_str: fn(&str, &str) -> bool,
912) -> Result<JValue, EvaluatorError> {
913    match (left, right) {
914        (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
915        (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
916        // Both null/undefined → undefined
917        (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
918        (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
919        // Explicit null literal with any non-null type → T2010 error
920        (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
921            "T2010: Type mismatch in comparison".to_string(),
922        )),
923        (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
924            "T2010: Type mismatch in comparison".to_string(),
925        )),
926        // Boolean with undefined → T2010 error
927        (JValue::Bool(_), JValue::Null | JValue::Undefined)
928        | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => {
929            Err(EvaluatorError::EvaluationError(
930                "T2010: Type mismatch in comparison".to_string(),
931            ))
932        }
933        // Number or String with implicit undefined (missing field) → undefined result
934        (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
935        | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
936            Ok(JValue::Null)
937        }
938        // Type mismatch (string vs number)
939        (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
940            Err(EvaluatorError::EvaluationError(
941                "T2009: The expressions on either side of operator must be of the same data type"
942                    .to_string(),
943            ))
944        }
945        _ => Err(EvaluatorError::EvaluationError(
946            "T2010: Type mismatch in comparison".to_string(),
947        )),
948    }
949}
950
951/// Arithmetic for compiled expressions.
952/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
953#[inline]
954pub(crate) fn compiled_arithmetic(
955    op: CompiledArithOp,
956    left: &JValue,
957    right: &JValue,
958    left_is_explicit_null: bool,
959    right_is_explicit_null: bool,
960) -> Result<JValue, EvaluatorError> {
961    let op_sym = match op {
962        CompiledArithOp::Add => "+",
963        CompiledArithOp::Sub => "-",
964        CompiledArithOp::Mul => "*",
965        CompiledArithOp::Div => "/",
966        CompiledArithOp::Mod => "%",
967    };
968    match (left, right) {
969        (JValue::Number(a), JValue::Number(b)) => {
970            let result = match op {
971                CompiledArithOp::Add => *a + *b,
972                CompiledArithOp::Sub => *a - *b,
973                CompiledArithOp::Mul => {
974                    let r = *a * *b;
975                    if r.is_infinite() {
976                        return Err(EvaluatorError::EvaluationError(
977                            "D1001: Number out of range".to_string(),
978                        ));
979                    }
980                    r
981                }
982                CompiledArithOp::Div => {
983                    if *b == 0.0 {
984                        return Err(EvaluatorError::EvaluationError(
985                            "Division by zero".to_string(),
986                        ));
987                    }
988                    *a / *b
989                }
990                CompiledArithOp::Mod => {
991                    if *b == 0.0 {
992                        return Err(EvaluatorError::EvaluationError(
993                            "Division by zero".to_string(),
994                        ));
995                    }
996                    *a % *b
997                }
998            };
999            Ok(JValue::Number(result))
1000        }
1001        // Explicit null literal → T2002 error (matching tree-walker behavior)
1002        (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1003            Err(EvaluatorError::TypeError(format!(
1004                "T2002: The left side of the {} operator must evaluate to a number",
1005                op_sym
1006            )))
1007        }
1008        (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1009            Err(EvaluatorError::TypeError(format!(
1010                "T2002: The right side of the {} operator must evaluate to a number",
1011                op_sym
1012            )))
1013        }
1014        // Implicit undefined propagation (from missing field) → undefined result
1015        (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1016            Ok(JValue::Null)
1017        }
1018        _ => Err(EvaluatorError::TypeError(format!(
1019            "Cannot apply {} to {:?} and {:?}",
1020            op_sym, left, right
1021        ))),
1022    }
1023}
1024
1025/// Convert a value to string for concatenation in compiled expressions.
1026#[inline]
1027pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1028    match value {
1029        JValue::String(s) => Ok(s.to_string()),
1030        JValue::Null | JValue::Undefined => Ok(String::new()),
1031        JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1032            match crate::functions::string::string(value, None) {
1033                Ok(JValue::String(s)) => Ok(s.to_string()),
1034                Ok(JValue::Null) => Ok(String::new()),
1035                _ => Err(EvaluatorError::TypeError(
1036                    "Cannot concatenate complex types".to_string(),
1037                )),
1038            }
1039        }
1040        _ => Ok(String::new()),
1041    }
1042}
1043
1044
1045// ──────────────────────────────────────────────────────────────────────────────
1046// Phase 2: path compilation, builtin dispatch, and supporting helpers
1047// ──────────────────────────────────────────────────────────────────────────────
1048
1049/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1050///
1051/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1052/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1053fn try_compile_path(steps: &[crate::ast::PathStep], allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
1054    use crate::ast::{AstNode, Stage};
1055
1056    if steps.is_empty() {
1057        return None;
1058    }
1059
1060    // Determine the start of the path:
1061    //   `$.field...`  → starts from current data (drop the leading `$` step)
1062    //   `$var.field`  → variable-prefixed paths: not compiled yet, fall back to tree-walker
1063    //   `field...`    → starts from current data
1064    let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1065        AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1066        AstNode::Variable(_) => return None,
1067        AstNode::Name(_) => steps,
1068        _ => return None,
1069    };
1070
1071    // Compile each field step (only Name nodes with at most one Filter stage each)
1072    let mut compiled_steps = Vec::with_capacity(field_steps.len());
1073    for step in field_steps {
1074        let field = match &step.node {
1075            AstNode::Name(name) => name.clone(),
1076            _ => return None,
1077        };
1078
1079        let filter = match step.stages.as_slice() {
1080            [] => None,
1081            [Stage::Filter(filter_node)] => {
1082                // Numeric literal predicates (`[0]`, `[1]`, etc.) mean index access in JSONata,
1083                // not boolean filtering. Fall back to tree-walker for these.
1084                if matches!(**filter_node, AstNode::Number(_)) {
1085                    return None;
1086                }
1087                Some(try_compile_expr_inner(filter_node, allowed_vars)?)
1088            }
1089            _ => return None,
1090        };
1091
1092        compiled_steps.push(CompiledStep { field, filter });
1093    }
1094
1095    if compiled_steps.is_empty() {
1096        // Bare `$` with no further field steps — current-data reference
1097        return Some(CompiledExpr::VariableLookup(String::new()));
1098    }
1099
1100    // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1101    // in HOF mode (allowed_vars=Some), where data is always a single Object element
1102    // from an array. In top-level mode (allowed_vars=None), data can itself be an
1103    // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1104    if allowed_vars.is_some() {
1105        if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1106            return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1107        }
1108        if compiled_steps.len() == 2
1109            && compiled_steps[0].filter.is_none()
1110            && compiled_steps[1].filter.is_none()
1111        {
1112            let outer = compiled_steps.remove(0).field;
1113            let inner = compiled_steps.remove(0).field;
1114            return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1115        }
1116    }
1117
1118    Some(CompiledExpr::FieldPath(compiled_steps))
1119}
1120
1121/// Evaluate a compiled `FieldPath` against `data`.
1122///
1123/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1124/// Filters are applied as predicates: truthy elements are kept.
1125///
1126/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1127/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1128/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1129fn compiled_eval_field_path(
1130    steps: &[CompiledStep],
1131    data: &JValue,
1132    vars: Option<&HashMap<&str, &JValue>>,
1133    ctx: Option<&Context>,
1134    shape: Option<&ShapeCache>,
1135) -> Result<JValue, EvaluatorError> {
1136    let mut current = data.clone();
1137    // Track whether the most recent field step mapped over an array (like the tree-walker's
1138    // `did_array_mapping` flag). Filters also count as array operations.
1139    let mut did_array_mapping = false;
1140    for step in steps {
1141        // Determine if this step will do array mapping before we overwrite `current`
1142        let is_array = matches!(current, JValue::Array(_));
1143        // Field access with implicit array mapping
1144        current = compiled_field_step(&step.field, &current);
1145        if is_array {
1146            did_array_mapping = true;
1147        } else {
1148            // Extracting from a single object resets the flag (tree-walker parity)
1149            did_array_mapping = false;
1150        }
1151        // Apply filter if present (filter is an array operation — keep the flag set)
1152        if let Some(filter) = &step.filter {
1153            current = compiled_apply_filter(filter, &current, vars, ctx, shape)?;
1154            // Filter always implies we operated on an array
1155            did_array_mapping = true;
1156        }
1157    }
1158    // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1159    if did_array_mapping {
1160        Ok(match current {
1161            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1162            other => other,
1163        })
1164    } else {
1165        Ok(current)
1166    }
1167}
1168
1169/// Perform a single-field access with implicit array-mapping semantics.
1170///
1171/// - Object: look up `field`, return its value or Undefined
1172/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1173/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1174/// - Other: Undefined
1175fn compiled_field_step(field: &str, value: &JValue) -> JValue {
1176    match value {
1177        JValue::Object(obj) => {
1178            // Check for tuple: extract from "@" inner object
1179            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1180                if let Some(JValue::Object(inner)) = obj.get("@") {
1181                    return inner.get(field).cloned().unwrap_or(JValue::Undefined);
1182                }
1183                return JValue::Undefined;
1184            }
1185            obj.get(field).cloned().unwrap_or(JValue::Undefined)
1186        }
1187        JValue::Array(arr) => {
1188            let mut result = Vec::new();
1189            for item in arr.iter() {
1190                let extracted = compiled_field_step(field, item);
1191                match extracted {
1192                    JValue::Undefined => {}
1193                    JValue::Array(inner) => result.extend(inner.iter().cloned()),
1194                    other => result.push(other),
1195                }
1196            }
1197            if result.is_empty() {
1198                JValue::Undefined
1199            } else {
1200                JValue::array(result)
1201            }
1202        }
1203        _ => JValue::Undefined,
1204    }
1205}
1206
1207/// Apply a compiled filter predicate to a value.
1208///
1209/// - Array: return elements for which the predicate is truthy
1210/// - Single value: return it if predicate is truthy, else Undefined
1211/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1212fn compiled_apply_filter(
1213    filter: &CompiledExpr,
1214    value: &JValue,
1215    vars: Option<&HashMap<&str, &JValue>>,
1216    ctx: Option<&Context>,
1217    shape: Option<&ShapeCache>,
1218) -> Result<JValue, EvaluatorError> {
1219    match value {
1220        JValue::Array(arr) => {
1221            let mut result = Vec::new();
1222            for item in arr.iter() {
1223                let pred = eval_compiled_inner(filter, item, vars, ctx, shape)?;
1224                if compiled_is_truthy(&pred) {
1225                    result.push(item.clone());
1226                }
1227            }
1228            if result.is_empty() {
1229                Ok(JValue::Undefined)
1230            } else if result.len() == 1 {
1231                Ok(result.remove(0))
1232            } else {
1233                Ok(JValue::array(result))
1234            }
1235        }
1236        JValue::Undefined => Ok(JValue::Undefined),
1237        _ => {
1238            let pred = eval_compiled_inner(filter, value, vars, ctx, shape)?;
1239            if compiled_is_truthy(&pred) {
1240                Ok(value.clone())
1241            } else {
1242                Ok(JValue::Undefined)
1243            }
1244        }
1245    }
1246}
1247
1248/// Dispatch a pure builtin function call.
1249///
1250/// Replicates the tree-walker's evaluation for the subset of builtins in
1251/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1252/// `data` is the current context value for implicit-argument insertion.
1253fn call_pure_builtin(
1254    name: &str,
1255    args: &[JValue],
1256    data: &JValue,
1257) -> Result<JValue, EvaluatorError> {
1258    use crate::functions;
1259
1260    // Apply implicit context insertion matching the tree-walker
1261    let args_storage: Vec<JValue>;
1262    let effective_args: &[JValue] = if args.is_empty() {
1263        match name {
1264            "string" => {
1265                // $string() with a null/undefined context returns undefined, not "null".
1266                // This mirrors the tree-walker's special case at the function-call site.
1267                if data.is_undefined() || data.is_null() {
1268                    return Ok(JValue::Undefined);
1269                }
1270                args_storage = vec![data.clone()];
1271                &args_storage
1272            }
1273            "number" | "boolean" | "uppercase" | "lowercase" => {
1274                args_storage = vec![data.clone()];
1275                &args_storage
1276            }
1277            _ => args,
1278        }
1279    } else if args.len() == 1 {
1280        match name {
1281            "substringBefore" | "substringAfter" | "contains" | "split" => {
1282                if matches!(data, JValue::String(_)) {
1283                    args_storage = std::iter::once(data.clone())
1284                        .chain(args.iter().cloned())
1285                        .collect();
1286                    &args_storage
1287                } else {
1288                    args
1289                }
1290            }
1291            _ => args,
1292        }
1293    } else {
1294        args
1295    };
1296
1297    // Apply undefined propagation: if the first effective argument is Undefined
1298    // and the function propagates undefined, return Undefined immediately.
1299    // This matches the tree-walker's `propagates_undefined` check.
1300    if effective_args.first().is_some_and(JValue::is_undefined)
1301        && propagates_undefined(name)
1302    {
1303        return Ok(JValue::Undefined);
1304    }
1305
1306    match name {
1307        // ── String functions ────────────────────────────────────────────
1308        "string" => {
1309            // Validate the optional prettify argument: must be a boolean.
1310            let prettify = match effective_args.get(1) {
1311                None => None,
1312                Some(JValue::Bool(b)) => Some(*b),
1313                Some(_) => {
1314                    return Err(EvaluatorError::TypeError(
1315                        "string() prettify parameter must be a boolean".to_string(),
1316                    ))
1317                }
1318            };
1319            let arg = effective_args.first().unwrap_or(&JValue::Null);
1320            Ok(functions::string::string(arg, prettify)?)
1321        }
1322        "length" => match effective_args.first() {
1323            Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1324            // Undefined input propagates (caught above by the undefined-propagation guard).
1325            Some(JValue::Undefined) => Ok(JValue::Undefined),
1326            // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1327            // so the test framework accepts it against any expected T-code).
1328            None => Err(EvaluatorError::EvaluationError(
1329                "length() requires exactly 1 argument".to_string(),
1330            )),
1331            // null and any other non-string type → T0410
1332            _ => Err(EvaluatorError::TypeError(
1333                "T0410: Argument 1 of function length does not match function signature"
1334                    .to_string(),
1335            )),
1336        },
1337        "uppercase" => match effective_args.first() {
1338            Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
1339            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1340            _ => Err(EvaluatorError::TypeError(
1341                "T0410: Argument 1 of function uppercase does not match function signature"
1342                    .to_string(),
1343            )),
1344        },
1345        "lowercase" => match effective_args.first() {
1346            Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
1347            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1348            _ => Err(EvaluatorError::TypeError(
1349                "T0410: Argument 1 of function lowercase does not match function signature"
1350                    .to_string(),
1351            )),
1352        },
1353        "trim" => match effective_args.first() {
1354            None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
1355            Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
1356            _ => Err(EvaluatorError::TypeError(
1357                "trim() requires a string argument".to_string(),
1358            )),
1359        },
1360        "substring" => {
1361            if effective_args.len() < 2 {
1362                return Err(EvaluatorError::EvaluationError(
1363                    "substring() requires at least 2 arguments".to_string(),
1364                ));
1365            }
1366            match (&effective_args[0], &effective_args[1]) {
1367                (JValue::String(s), JValue::Number(start)) => {
1368                    // Optional 3rd arg (length) must be a number if provided.
1369                    let length = match effective_args.get(2) {
1370                        None => None,
1371                        Some(JValue::Number(l)) => Some(*l as i64),
1372                        Some(_) => {
1373                            return Err(EvaluatorError::TypeError(
1374                                "T0410: Argument 3 of function substring does not match function signature"
1375                                    .to_string(),
1376                            ))
1377                        }
1378                    };
1379                    Ok(functions::string::substring(s, *start as i64, length)?)
1380                }
1381                _ => Err(EvaluatorError::TypeError(
1382                    "T0410: Argument 1 of function substring does not match function signature"
1383                        .to_string(),
1384                )),
1385            }
1386        }
1387        "substringBefore" => {
1388            if effective_args.len() != 2 {
1389                return Err(EvaluatorError::TypeError(
1390                    "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
1391                ));
1392            }
1393            match (&effective_args[0], &effective_args[1]) {
1394                (JValue::String(s), JValue::String(sep)) => {
1395                    Ok(functions::string::substring_before(s, sep)?)
1396                }
1397                // Undefined propagates; null is a type error.
1398                (JValue::Undefined, _) => Ok(JValue::Undefined),
1399                _ => Err(EvaluatorError::TypeError(
1400                    "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
1401                )),
1402            }
1403        }
1404        "substringAfter" => {
1405            if effective_args.len() != 2 {
1406                return Err(EvaluatorError::TypeError(
1407                    "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
1408                ));
1409            }
1410            match (&effective_args[0], &effective_args[1]) {
1411                (JValue::String(s), JValue::String(sep)) => {
1412                    Ok(functions::string::substring_after(s, sep)?)
1413                }
1414                // Undefined propagates; null is a type error.
1415                (JValue::Undefined, _) => Ok(JValue::Undefined),
1416                _ => Err(EvaluatorError::TypeError(
1417                    "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
1418                )),
1419            }
1420        }
1421        "contains" => {
1422            if effective_args.len() != 2 {
1423                return Err(EvaluatorError::EvaluationError(
1424                    "contains() requires exactly 2 arguments".to_string(),
1425                ));
1426            }
1427            match &effective_args[0] {
1428                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1429                JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
1430                _ => Err(EvaluatorError::TypeError(
1431                    "contains() requires a string as the first argument".to_string(),
1432                )),
1433            }
1434        }
1435        "split" => {
1436            if effective_args.len() < 2 {
1437                return Err(EvaluatorError::EvaluationError(
1438                    "split() requires at least 2 arguments".to_string(),
1439                ));
1440            }
1441            match &effective_args[0] {
1442                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1443                JValue::String(s) => {
1444                    // Validate the optional limit argument — must be a positive number.
1445                    let limit = match effective_args.get(2) {
1446                        None => None,
1447                        Some(JValue::Number(n)) => {
1448                            if *n < 0.0 {
1449                                return Err(EvaluatorError::EvaluationError(
1450                                    "D3020: Third argument of split function must be a positive number"
1451                                        .to_string(),
1452                                ));
1453                            }
1454                            Some(n.floor() as usize)
1455                        }
1456                        Some(_) => {
1457                            return Err(EvaluatorError::TypeError(
1458                                "split() limit must be a number".to_string(),
1459                            ))
1460                        }
1461                    };
1462                    Ok(functions::string::split(s, &effective_args[1], limit)?)
1463                }
1464                _ => Err(EvaluatorError::TypeError(
1465                    "split() requires a string as the first argument".to_string(),
1466                )),
1467            }
1468        }
1469        "join" => {
1470            if effective_args.is_empty() {
1471                return Err(EvaluatorError::TypeError(
1472                    "T0410: Argument 1 of function $join does not match function signature"
1473                        .to_string(),
1474                ));
1475            }
1476            match &effective_args[0] {
1477                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1478                // Signature: <a<s>s?:s> — first arg must be an array of strings.
1479                JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
1480                    Err(EvaluatorError::TypeError(
1481                        "T0412: Argument 1 of function $join must be an array of String"
1482                            .to_string(),
1483                    ))
1484                }
1485                JValue::Array(arr) => {
1486                    // All elements must be strings.
1487                    for item in arr.iter() {
1488                        if !matches!(item, JValue::String(_)) {
1489                            return Err(EvaluatorError::TypeError(
1490                                "T0412: Argument 1 of function $join must be an array of String"
1491                                    .to_string(),
1492                            ));
1493                        }
1494                    }
1495                    // Validate separator: must be a string if provided.
1496                    let separator = match effective_args.get(1) {
1497                        None | Some(JValue::Undefined) => None,
1498                        Some(JValue::String(s)) => Some(&**s),
1499                        Some(_) => {
1500                            return Err(EvaluatorError::TypeError(
1501                                "T0410: Argument 2 of function $join does not match function signature (expected String)"
1502                                    .to_string(),
1503                            ))
1504                        }
1505                    };
1506                    Ok(functions::string::join(arr, separator)?)
1507                }
1508                JValue::String(s) => Ok(JValue::String(s.clone())),
1509                _ => Err(EvaluatorError::TypeError(
1510                    "T0412: Argument 1 of function $join must be an array of String"
1511                        .to_string(),
1512                )),
1513            }
1514        }
1515
1516        // ── Numeric functions ───────────────────────────────────────────
1517        "number" => match effective_args.first() {
1518            Some(v) => Ok(functions::numeric::number(v)?),
1519            None => Err(EvaluatorError::EvaluationError(
1520                "number() requires at least 1 argument".to_string(),
1521            )),
1522        },
1523        "floor" => match effective_args.first() {
1524            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1525            Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
1526            _ => Err(EvaluatorError::TypeError(
1527                "floor() requires a number argument".to_string(),
1528            )),
1529        },
1530        "ceil" => match effective_args.first() {
1531            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1532            Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
1533            _ => Err(EvaluatorError::TypeError(
1534                "ceil() requires a number argument".to_string(),
1535            )),
1536        },
1537        "round" => match effective_args.first() {
1538            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1539            Some(JValue::Number(n)) => {
1540                let precision = effective_args.get(1).and_then(|v| {
1541                    if let JValue::Number(p) = v { Some(*p as i32) } else { None }
1542                });
1543                Ok(functions::numeric::round(*n, precision)?)
1544            }
1545            _ => Err(EvaluatorError::TypeError(
1546                "round() requires a number argument".to_string(),
1547            )),
1548        },
1549        "abs" => match effective_args.first() {
1550            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1551            Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
1552            _ => Err(EvaluatorError::TypeError(
1553                "abs() requires a number argument".to_string(),
1554            )),
1555        },
1556        "sqrt" => match effective_args.first() {
1557            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1558            Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
1559            _ => Err(EvaluatorError::TypeError(
1560                "sqrt() requires a number argument".to_string(),
1561            )),
1562        },
1563
1564        // ── Aggregation functions ───────────────────────────────────────
1565        "sum" => match effective_args.first() {
1566            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1567            None => Err(EvaluatorError::EvaluationError(
1568                "sum() requires exactly 1 argument".to_string(),
1569            )),
1570            Some(JValue::Null) => Ok(JValue::Null),
1571            Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
1572            Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
1573            Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
1574        },
1575        "max" => match effective_args.first() {
1576            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1577            Some(JValue::Null) | None => Ok(JValue::Null),
1578            Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
1579            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1580            _ => Err(EvaluatorError::TypeError(
1581                "max() requires an array or number argument".to_string(),
1582            )),
1583        },
1584        "min" => match effective_args.first() {
1585            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1586            Some(JValue::Null) | None => Ok(JValue::Null),
1587            Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
1588            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1589            _ => Err(EvaluatorError::TypeError(
1590                "min() requires an array or number argument".to_string(),
1591            )),
1592        },
1593        "average" => match effective_args.first() {
1594            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1595            Some(JValue::Null) | None => Ok(JValue::Null),
1596            Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
1597            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1598            _ => Err(EvaluatorError::TypeError(
1599                "average() requires an array or number argument".to_string(),
1600            )),
1601        },
1602        "count" => match effective_args.first() {
1603            Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
1604            Some(JValue::Null) | None => Ok(JValue::from(0i64)),
1605            Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
1606            _ => Ok(JValue::from(1i64)),
1607        },
1608
1609        // ── Boolean / logic ─────────────────────────────────────────────
1610        "boolean" => match effective_args.first() {
1611            Some(v) => Ok(functions::boolean::boolean(v)?),
1612            None => Err(EvaluatorError::EvaluationError(
1613                "boolean() requires 1 argument".to_string(),
1614            )),
1615        },
1616        "not" => match effective_args.first() {
1617            Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
1618            None => Err(EvaluatorError::EvaluationError(
1619                "not() requires 1 argument".to_string(),
1620            )),
1621        },
1622
1623        // ── Array functions ─────────────────────────────────────────────
1624        "append" => {
1625            if effective_args.len() != 2 {
1626                return Err(EvaluatorError::EvaluationError(
1627                    "append() requires exactly 2 arguments".to_string(),
1628                ));
1629            }
1630            let first = &effective_args[0];
1631            let second = &effective_args[1];
1632            if matches!(second, JValue::Null | JValue::Undefined) {
1633                return Ok(first.clone());
1634            }
1635            if matches!(first, JValue::Null | JValue::Undefined) {
1636                return Ok(second.clone());
1637            }
1638            let arr = match first {
1639                JValue::Array(a) => a.to_vec(),
1640                other => vec![other.clone()],
1641            };
1642            Ok(functions::array::append(&arr, second)?)
1643        }
1644        "reverse" => match effective_args.first() {
1645            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1646            Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
1647            _ => Err(EvaluatorError::TypeError(
1648                "reverse() requires an array argument".to_string(),
1649            )),
1650        },
1651        "distinct" => match effective_args.first() {
1652            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1653            Some(JValue::Array(arr)) => Ok(functions::array::distinct(arr)?),
1654            _ => Err(EvaluatorError::TypeError(
1655                "distinct() requires an array argument".to_string(),
1656            )),
1657        },
1658
1659        // ── Object functions ────────────────────────────────────────────
1660        "keys" => match effective_args.first() {
1661            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1662            Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
1663            Some(JValue::Object(obj)) => {
1664                if obj.is_empty() {
1665                    Ok(JValue::Null)
1666                } else {
1667                    let keys: Vec<JValue> =
1668                        obj.keys().map(|k| JValue::string(k.clone())).collect();
1669                    if keys.len() == 1 {
1670                        Ok(keys.into_iter().next().unwrap())
1671                    } else {
1672                        Ok(JValue::array(keys))
1673                    }
1674                }
1675            }
1676            Some(JValue::Array(arr)) => {
1677                let mut all_keys: Vec<JValue> = Vec::new();
1678                for item in arr.iter() {
1679                    if let JValue::Object(obj) = item {
1680                        for key in obj.keys() {
1681                            let k = JValue::string(key.clone());
1682                            if !all_keys.contains(&k) {
1683                                all_keys.push(k);
1684                            }
1685                        }
1686                    }
1687                }
1688                if all_keys.is_empty() {
1689                    Ok(JValue::Null)
1690                } else if all_keys.len() == 1 {
1691                    Ok(all_keys.into_iter().next().unwrap())
1692                } else {
1693                    Ok(JValue::array(all_keys))
1694                }
1695            }
1696            _ => Ok(JValue::Null),
1697        },
1698        "merge" => match effective_args.len() {
1699            0 => Err(EvaluatorError::EvaluationError(
1700                "merge() requires at least 1 argument".to_string(),
1701            )),
1702            1 => match &effective_args[0] {
1703                JValue::Array(arr) => Ok(functions::object::merge(arr)?),
1704                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1705                JValue::Object(_) => Ok(effective_args[0].clone()),
1706                _ => Err(EvaluatorError::TypeError(
1707                    "merge() requires objects or an array of objects".to_string(),
1708                )),
1709            },
1710            _ => Ok(functions::object::merge(effective_args)?),
1711        },
1712
1713        _ => unreachable!("call_pure_builtin called with non-compilable builtin: {}", name),
1714    }
1715}
1716
1717// ──────────────────────────────────────────────────────────────────────────────
1718// End of CompiledExpr framework
1719// ──────────────────────────────────────────────────────────────────────────────
1720
1721/// Functions that propagate undefined (return undefined when given an undefined argument).
1722/// These functions should return null/undefined when their input path doesn't exist,
1723/// rather than throwing a type error.
1724const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
1725    "not",
1726    "boolean",
1727    "length",
1728    "number",
1729    "uppercase",
1730    "lowercase",
1731    "substring",
1732    "substringBefore",
1733    "substringAfter",
1734    "string",
1735];
1736
1737/// Check whether a function propagates undefined values
1738fn propagates_undefined(name: &str) -> bool {
1739    UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
1740}
1741
1742/// Iterator-based numeric aggregation helpers.
1743/// These avoid cloning values by iterating over references and extracting f64 values directly.
1744mod aggregation {
1745    use super::*;
1746
1747    /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
1748    /// Returns Err if any non-numeric value is encountered.
1749    fn for_each_numeric(
1750        arr: &[JValue],
1751        func_name: &str,
1752        mut f: impl FnMut(f64),
1753    ) -> Result<(), EvaluatorError> {
1754        fn recurse(
1755            arr: &[JValue],
1756            func_name: &str,
1757            f: &mut dyn FnMut(f64),
1758        ) -> Result<(), EvaluatorError> {
1759            for value in arr.iter() {
1760                match value {
1761                    JValue::Array(inner) => recurse(inner, func_name, f)?,
1762                    JValue::Number(n) => {
1763                        f(*n);
1764                    }
1765                    _ => {
1766                        return Err(EvaluatorError::TypeError(format!(
1767                            "{}() requires all array elements to be numbers",
1768                            func_name
1769                        )));
1770                    }
1771                }
1772            }
1773            Ok(())
1774        }
1775        recurse(arr, func_name, &mut f)
1776    }
1777
1778    /// Count elements in a potentially nested array without cloning.
1779    fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
1780        let mut count = 0usize;
1781        for_each_numeric(arr, func_name, |_| count += 1)?;
1782        Ok(count)
1783    }
1784
1785    pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
1786        if arr.is_empty() {
1787            return Ok(JValue::from(0i64));
1788        }
1789        let mut total = 0.0f64;
1790        for_each_numeric(arr, "sum", |n| total += n)?;
1791        Ok(JValue::Number(total))
1792    }
1793
1794    pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
1795        if arr.is_empty() {
1796            return Ok(JValue::Null);
1797        }
1798        let mut max_val = f64::NEG_INFINITY;
1799        for_each_numeric(arr, "max", |n| {
1800            if n > max_val {
1801                max_val = n;
1802            }
1803        })?;
1804        Ok(JValue::Number(max_val))
1805    }
1806
1807    pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
1808        if arr.is_empty() {
1809            return Ok(JValue::Null);
1810        }
1811        let mut min_val = f64::INFINITY;
1812        for_each_numeric(arr, "min", |n| {
1813            if n < min_val {
1814                min_val = n;
1815            }
1816        })?;
1817        Ok(JValue::Number(min_val))
1818    }
1819
1820    pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
1821        if arr.is_empty() {
1822            return Ok(JValue::Null);
1823        }
1824        let mut total = 0.0f64;
1825        let count = count_numeric(arr, "average")?;
1826        for_each_numeric(arr, "average", |n| total += n)?;
1827        Ok(JValue::Number(total / count as f64))
1828    }
1829}
1830
1831/// Evaluator errors
1832#[derive(Error, Debug)]
1833pub enum EvaluatorError {
1834    #[error("Type error: {0}")]
1835    TypeError(String),
1836
1837    #[error("Reference error: {0}")]
1838    ReferenceError(String),
1839
1840    #[error("Evaluation error: {0}")]
1841    EvaluationError(String),
1842}
1843
1844impl From<crate::functions::FunctionError> for EvaluatorError {
1845    fn from(e: crate::functions::FunctionError) -> Self {
1846        EvaluatorError::EvaluationError(e.to_string())
1847    }
1848}
1849
1850impl From<crate::datetime::DateTimeError> for EvaluatorError {
1851    fn from(e: crate::datetime::DateTimeError) -> Self {
1852        EvaluatorError::EvaluationError(e.to_string())
1853    }
1854}
1855
1856/// Result of evaluating a lambda body that may be a tail call
1857/// Used for trampoline-based tail call optimization
1858enum LambdaResult {
1859    /// Final value - evaluation is complete
1860    JValue(JValue),
1861    /// Tail call - need to continue with another lambda invocation
1862    TailCall {
1863        /// The lambda to call (boxed to reduce enum size)
1864        lambda: Box<StoredLambda>,
1865        /// Arguments for the call
1866        args: Vec<JValue>,
1867        /// Data context for the call
1868        data: JValue,
1869    },
1870}
1871
1872/// Lambda storage
1873/// Stores the AST of a lambda function along with its parameters, optional signature,
1874/// and captured environment for closures
1875#[derive(Clone, Debug)]
1876pub struct StoredLambda {
1877    pub params: Vec<String>,
1878    pub body: AstNode,
1879    /// Pre-compiled body for use in tight inner loops (HOF fast path).
1880    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
1881    pub(crate) compiled_body: Option<CompiledExpr>,
1882    pub signature: Option<String>,
1883    /// Captured environment bindings for closures
1884    pub captured_env: HashMap<String, JValue>,
1885    /// Captured data context for lexical scoping of bare field names
1886    pub captured_data: Option<JValue>,
1887    /// Whether this lambda's body contains tail calls that can be optimized
1888    pub thunk: bool,
1889}
1890
1891/// A single scope in the scope stack
1892struct Scope {
1893    bindings: HashMap<String, JValue>,
1894    lambdas: HashMap<String, StoredLambda>,
1895}
1896
1897impl Scope {
1898    fn new() -> Self {
1899        Scope {
1900            bindings: HashMap::new(),
1901            lambdas: HashMap::new(),
1902        }
1903    }
1904}
1905
1906/// Evaluation context
1907///
1908/// Holds variable bindings and other state needed during evaluation.
1909/// Uses a scope stack for efficient push/pop instead of clone/restore.
1910pub struct Context {
1911    scope_stack: Vec<Scope>,
1912    parent_data: Option<JValue>,
1913}
1914
1915impl Context {
1916    pub fn new() -> Self {
1917        Context {
1918            scope_stack: vec![Scope::new()],
1919            parent_data: None,
1920        }
1921    }
1922
1923    /// Push a new scope onto the stack
1924    fn push_scope(&mut self) {
1925        self.scope_stack.push(Scope::new());
1926    }
1927
1928    /// Pop the top scope from the stack
1929    fn pop_scope(&mut self) {
1930        if self.scope_stack.len() > 1 {
1931            self.scope_stack.pop();
1932        }
1933    }
1934
1935    /// Pop scope but preserve specified lambdas by moving them to the current top scope
1936    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
1937        if self.scope_stack.len() > 1 {
1938            let popped = self.scope_stack.pop().unwrap();
1939            if !lambda_ids.is_empty() {
1940                let top = self.scope_stack.last_mut().unwrap();
1941                for id in lambda_ids {
1942                    if let Some(stored) = popped.lambdas.get(id) {
1943                        top.lambdas.insert(id.clone(), stored.clone());
1944                    }
1945                }
1946            }
1947        }
1948    }
1949
1950    /// Clear all bindings and lambdas in the top scope without deallocating
1951    fn clear_current_scope(&mut self) {
1952        let top = self.scope_stack.last_mut().unwrap();
1953        top.bindings.clear();
1954        top.lambdas.clear();
1955    }
1956
1957    pub fn bind(&mut self, name: String, value: JValue) {
1958        self.scope_stack
1959            .last_mut()
1960            .unwrap()
1961            .bindings
1962            .insert(name, value);
1963    }
1964
1965    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
1966        self.scope_stack
1967            .last_mut()
1968            .unwrap()
1969            .lambdas
1970            .insert(name, lambda);
1971    }
1972
1973    pub fn unbind(&mut self, name: &str) {
1974        // Remove from top scope only
1975        let top = self.scope_stack.last_mut().unwrap();
1976        top.bindings.remove(name);
1977        top.lambdas.remove(name);
1978    }
1979
1980    pub fn lookup(&self, name: &str) -> Option<&JValue> {
1981        // Walk scope stack from top to bottom
1982        for scope in self.scope_stack.iter().rev() {
1983            if let Some(value) = scope.bindings.get(name) {
1984                return Some(value);
1985            }
1986        }
1987        None
1988    }
1989
1990    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
1991        // Walk scope stack from top to bottom
1992        for scope in self.scope_stack.iter().rev() {
1993            if let Some(lambda) = scope.lambdas.get(name) {
1994                return Some(lambda);
1995            }
1996        }
1997        None
1998    }
1999
2000    pub fn set_parent(&mut self, data: JValue) {
2001        self.parent_data = Some(data);
2002    }
2003
2004    pub fn get_parent(&self) -> Option<&JValue> {
2005        self.parent_data.as_ref()
2006    }
2007
2008    /// Collect all bindings across all scopes (for environment capture).
2009    /// Higher scopes shadow lower scopes.
2010    fn all_bindings(&self) -> HashMap<String, JValue> {
2011        let mut result = HashMap::new();
2012        for scope in &self.scope_stack {
2013            for (k, v) in &scope.bindings {
2014                result.insert(k.clone(), v.clone());
2015            }
2016        }
2017        result
2018    }
2019}
2020
2021impl Default for Context {
2022    fn default() -> Self {
2023        Self::new()
2024    }
2025}
2026
2027/// Evaluator for JSONata expressions
2028pub struct Evaluator {
2029    context: Context,
2030    recursion_depth: usize,
2031    max_recursion_depth: usize,
2032}
2033
2034impl Evaluator {
2035    pub fn new() -> Self {
2036        Evaluator {
2037            context: Context::new(),
2038            recursion_depth: 0,
2039            // Limit recursion depth to prevent stack overflow
2040            // True TCO would allow deeper recursion but requires parser-level thunk marking
2041            max_recursion_depth: 302,
2042        }
2043    }
2044
2045    pub fn with_context(context: Context) -> Self {
2046        Evaluator {
2047            context,
2048            recursion_depth: 0,
2049            max_recursion_depth: 302,
2050        }
2051    }
2052
2053    /// Invoke a stored lambda with its captured environment and data.
2054    /// This is the standard way to call a StoredLambda, handling the
2055    /// captured_env and captured_data extraction boilerplate.
2056    fn invoke_stored_lambda(
2057        &mut self,
2058        stored: &StoredLambda,
2059        args: &[JValue],
2060        data: &JValue,
2061    ) -> Result<JValue, EvaluatorError> {
2062        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2063        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2064        // and no captured lambda/builtin values (those require Context for runtime lookup).
2065        if let Some(ref ce) = stored.compiled_body {
2066            if stored.signature.is_none()
2067                && !stored.thunk
2068                && !stored
2069                    .captured_env
2070                    .values()
2071                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2072            {
2073                let call_data = stored.captured_data.as_ref().unwrap_or(data);
2074                let vars: HashMap<&str, &JValue> = stored
2075                    .params
2076                    .iter()
2077                    .zip(args.iter())
2078                    .map(|(p, v)| (p.as_str(), v))
2079                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2080                    .collect();
2081                return eval_compiled(ce, call_data, Some(&vars));
2082            }
2083        }
2084
2085        let captured_env = if stored.captured_env.is_empty() {
2086            None
2087        } else {
2088            Some(&stored.captured_env)
2089        };
2090        let captured_data = stored.captured_data.as_ref();
2091        self.invoke_lambda_with_env(
2092            &stored.params,
2093            &stored.body,
2094            stored.signature.as_ref(),
2095            args,
2096            data,
2097            captured_env,
2098            captured_data,
2099            stored.thunk,
2100        )
2101    }
2102
2103    /// Look up a StoredLambda from a JValue that may be a lambda marker.
2104    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
2105    /// with a valid lambda_id that references a stored lambda.
2106    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
2107        if let JValue::Lambda { lambda_id, .. } = value {
2108            return self.context.lookup_lambda(lambda_id).cloned();
2109        }
2110        None
2111    }
2112
2113    /// Get the number of parameters a callback function expects by inspecting its AST.
2114    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
2115    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
2116    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
2117        match func_node {
2118            AstNode::Lambda { params, .. } => params.len(),
2119            AstNode::Variable(var_name) => {
2120                // Check if this variable holds a stored lambda
2121                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
2122                    return stored_lambda.params.len();
2123                }
2124                // Also check if it's a lambda value in bindings (e.g., from partial application)
2125                if let Some(value) = self.context.lookup(var_name) {
2126                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
2127                        return stored_lambda.params.len();
2128                    }
2129                }
2130                // Unknown, return max to be safe
2131                usize::MAX
2132            }
2133            AstNode::Function { .. } => {
2134                // For function references, we can't easily determine param count
2135                // Return max to be safe
2136                usize::MAX
2137            }
2138            _ => usize::MAX,
2139        }
2140    }
2141
2142    /// Specialized sort using pre-extracted keys (Schwartzian transform).
2143    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
2144    /// avoiding O(N log N) hash lookups during comparisons.
2145    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
2146        if arr.len() <= 1 {
2147            return;
2148        }
2149
2150        // Phase 1: Extract sort keys -- one IndexMap lookup per element
2151        let keys: Vec<SortKey> = arr
2152            .iter()
2153            .map(|item| match item {
2154                JValue::Object(obj) => match obj.get(&spec.field) {
2155                    Some(JValue::Number(n)) => SortKey::Num(*n),
2156                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
2157                    _ => SortKey::None,
2158                },
2159                _ => SortKey::None,
2160            })
2161            .collect();
2162
2163        // Phase 2: Build index permutation sorted by pre-extracted keys
2164        let mut perm: Vec<usize> = (0..arr.len()).collect();
2165        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
2166
2167        // Phase 3: Apply permutation in-place via cycle-following
2168        let mut placed = vec![false; arr.len()];
2169        for i in 0..arr.len() {
2170            if placed[i] || perm[i] == i {
2171                continue;
2172            }
2173            let mut j = i;
2174            loop {
2175                let target = perm[j];
2176                placed[j] = true;
2177                if target == i {
2178                    break;
2179                }
2180                arr.swap(j, target);
2181                j = target;
2182            }
2183        }
2184    }
2185
2186    /// Merge sort implementation using a comparator function.
2187    /// This replaces the O(n²) bubble sort for better performance on large arrays.
2188    /// The comparator returns true if the first element should come AFTER the second.
2189    fn merge_sort_with_comparator(
2190        &mut self,
2191        arr: &mut [JValue],
2192        comparator: &AstNode,
2193        data: &JValue,
2194    ) -> Result<(), EvaluatorError> {
2195        if arr.len() <= 1 {
2196            return Ok(());
2197        }
2198
2199        // Try specialized fast path for simple field comparisons like
2200        // function($l, $r) { $l.price > $r.price }
2201        if let AstNode::Lambda { params, body, .. } = comparator {
2202            if params.len() >= 2 {
2203                if let Some(spec) =
2204                    try_specialize_sort_comparator(body, &params[0], &params[1])
2205                {
2206                    Self::merge_sort_specialized(arr, &spec);
2207                    return Ok(());
2208                }
2209            }
2210        }
2211
2212        let mid = arr.len() / 2;
2213
2214        // Sort left half
2215        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
2216
2217        // Sort right half
2218        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
2219
2220        // Merge the sorted halves
2221        let mut temp = Vec::with_capacity(arr.len());
2222        let (left, right) = arr.split_at(mid);
2223
2224        let mut i = 0;
2225        let mut j = 0;
2226
2227        // For lambda comparators, use a reusable scope to avoid
2228        // push_scope/pop_scope per comparison (~n log n total comparisons)
2229        if let AstNode::Lambda { params, body, .. } = comparator {
2230            if params.len() >= 2 {
2231                // Pre-clone param names once outside the loop
2232                let param0 = params[0].clone();
2233                let param1 = params[1].clone();
2234                self.context.push_scope();
2235                while i < left.len() && j < right.len() {
2236                    // Reuse scope: clear and rebind instead of push/pop
2237                    self.context.clear_current_scope();
2238                    self.context.bind(param0.clone(), left[i].clone());
2239                    self.context.bind(param1.clone(), right[j].clone());
2240
2241                    let cmp_result = self.evaluate_internal(body, data)?;
2242
2243                    if self.is_truthy(&cmp_result) {
2244                        temp.push(right[j].clone());
2245                        j += 1;
2246                    } else {
2247                        temp.push(left[i].clone());
2248                        i += 1;
2249                    }
2250                }
2251                self.context.pop_scope();
2252            } else {
2253                // Unexpected param count - fall back to generic path
2254                while i < left.len() && j < right.len() {
2255                    let cmp_result = self.apply_function(
2256                        comparator,
2257                        &[left[i].clone(), right[j].clone()],
2258                        data,
2259                    )?;
2260                    if self.is_truthy(&cmp_result) {
2261                        temp.push(right[j].clone());
2262                        j += 1;
2263                    } else {
2264                        temp.push(left[i].clone());
2265                        i += 1;
2266                    }
2267                }
2268            }
2269        } else {
2270            // Non-lambda comparator: use generic apply_function path
2271            while i < left.len() && j < right.len() {
2272                let cmp_result = self.apply_function(
2273                    comparator,
2274                    &[left[i].clone(), right[j].clone()],
2275                    data,
2276                )?;
2277                if self.is_truthy(&cmp_result) {
2278                    temp.push(right[j].clone());
2279                    j += 1;
2280                } else {
2281                    temp.push(left[i].clone());
2282                    i += 1;
2283                }
2284            }
2285        }
2286
2287        // Copy remaining elements
2288        temp.extend_from_slice(&left[i..]);
2289        temp.extend_from_slice(&right[j..]);
2290
2291        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
2292        for (i, val) in temp.into_iter().enumerate() {
2293            arr[i] = val;
2294        }
2295
2296        Ok(())
2297    }
2298
2299    /// Evaluate an AST node against data
2300    ///
2301    /// This is the main entry point for evaluation. It sets up the parent context
2302    /// to be the root data if not already set.
2303    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
2304        // Set parent context to root data if not already set
2305        if self.context.get_parent().is_none() {
2306            self.context.set_parent(data.clone());
2307        }
2308
2309        self.evaluate_internal(node, data)
2310    }
2311
2312    /// Fast evaluation for leaf nodes that don't need recursion tracking.
2313    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
2314    /// Returns None for anything requiring the full evaluator.
2315    #[inline(always)]
2316    fn evaluate_leaf(&mut self, node: &AstNode, data: &JValue) -> Option<Result<JValue, EvaluatorError>> {
2317        match node {
2318            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
2319            AstNode::Number(n) => {
2320                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2321                    Some(Ok(JValue::from(*n as i64)))
2322                } else {
2323                    Some(Ok(JValue::Number(*n)))
2324                }
2325            }
2326            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
2327            AstNode::Null => Some(Ok(JValue::Null)),
2328            AstNode::Undefined => Some(Ok(JValue::Undefined)),
2329            AstNode::Name(field_name) => match data {
2330                // Array mapping and other cases need full evaluator
2331                JValue::Object(obj) => Some(Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null))),
2332                _ => None,
2333            },
2334            AstNode::Variable(name) if !name.is_empty() => {
2335                // Simple variable lookup — only fast-path when no tuple data
2336                if let JValue::Object(obj) = data {
2337                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2338                        return None; // Tuple data needs full evaluator
2339                    }
2340                }
2341                // May be a lambda/builtin — needs full evaluator if None
2342                self.context.lookup(name).map(|value| Ok(value.clone()))
2343            }
2344            _ => None,
2345        }
2346    }
2347
2348    /// Internal evaluation method
2349    fn evaluate_internal(
2350        &mut self,
2351        node: &AstNode,
2352        data: &JValue,
2353    ) -> Result<JValue, EvaluatorError> {
2354        // Fast path for leaf nodes — skip recursion tracking overhead
2355        if let Some(result) = self.evaluate_leaf(node, data) {
2356            return result;
2357        }
2358
2359        // Check recursion depth to prevent stack overflow
2360        self.recursion_depth += 1;
2361        if self.recursion_depth > self.max_recursion_depth {
2362            self.recursion_depth -= 1;
2363            return Err(EvaluatorError::EvaluationError(format!(
2364                "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
2365                self.max_recursion_depth
2366            )));
2367        }
2368
2369        let result = self.evaluate_internal_impl(node, data);
2370
2371        self.recursion_depth -= 1;
2372        result
2373    }
2374
2375    /// Internal evaluation implementation (separated to allow depth tracking)
2376    fn evaluate_internal_impl(
2377        &mut self,
2378        node: &AstNode,
2379        data: &JValue,
2380    ) -> Result<JValue, EvaluatorError> {
2381        match node {
2382            AstNode::String(s) => Ok(JValue::string(s.clone())),
2383
2384            // Name nodes represent field access on the current data
2385            AstNode::Name(field_name) => {
2386                match data {
2387                    JValue::Object(obj) => Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null)),
2388                    JValue::Array(arr) => {
2389                        // Map over array
2390                        let mut result = Vec::new();
2391                        for item in arr.iter() {
2392                            if let JValue::Object(obj) = item {
2393                                if let Some(val) = obj.get(field_name) {
2394                                    result.push(val.clone());
2395                                }
2396                            }
2397                        }
2398                        if result.is_empty() {
2399                            Ok(JValue::Null)
2400                        } else if result.len() == 1 {
2401                            Ok(result.into_iter().next().unwrap())
2402                        } else {
2403                            Ok(JValue::array(result))
2404                        }
2405                    }
2406                    _ => Ok(JValue::Null),
2407                }
2408            }
2409
2410            AstNode::Number(n) => {
2411                // Preserve integer-ness: if the number is a whole number, create an integer JValue
2412                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2413                    // It's a whole number that can be represented as i64
2414                    Ok(JValue::from(*n as i64))
2415                } else {
2416                    Ok(JValue::Number(*n))
2417                }
2418            }
2419            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
2420            AstNode::Null => Ok(JValue::Null),
2421            AstNode::Undefined => Ok(JValue::Undefined),
2422            AstNode::Placeholder => {
2423                // Placeholders should only appear as function arguments
2424                // If we reach here, it's an error
2425                Err(EvaluatorError::EvaluationError(
2426                    "Placeholder '?' can only be used as a function argument".to_string(),
2427                ))
2428            }
2429            AstNode::Regex { pattern, flags } => {
2430                // Return a regex object as a special JSON value
2431                // This will be recognized by functions like $split, $match, $replace
2432                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
2433            }
2434
2435            AstNode::Variable(name) => {
2436                // Special case: $ alone (empty name) refers to current context
2437                // First check if $ is bound in the context (for closures that captured $)
2438                // Otherwise, use the data parameter
2439                if name.is_empty() {
2440                    if let Some(value) = self.context.lookup("$") {
2441                        return Ok(value.clone());
2442                    }
2443                    // If data is a tuple, return the @ value
2444                    if let JValue::Object(obj) = data {
2445                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2446                            if let Some(inner) = obj.get("@") {
2447                                return Ok(inner.clone());
2448                            }
2449                        }
2450                    }
2451                    return Ok(data.clone());
2452                }
2453
2454                // Check variable bindings FIRST
2455                // This allows function parameters to shadow outer lambdas with the same name
2456                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
2457                if let Some(value) = self.context.lookup(name) {
2458                    return Ok(value.clone());
2459                }
2460
2461                // Check tuple bindings in data (for index binding operator #$var)
2462                // When iterating over a tuple stream, $var can reference the bound index
2463                if let JValue::Object(obj) = data {
2464                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2465                        // Check for the variable in tuple bindings (stored as "$name")
2466                        let binding_key = format!("${}", name);
2467                        if let Some(binding_value) = obj.get(&binding_key) {
2468                            return Ok(binding_value.clone());
2469                        }
2470                    }
2471                }
2472
2473                // Then check if this is a stored lambda (user-defined functions)
2474                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
2475                    // Return a lambda representation that can be passed to higher-order functions
2476                    // Include _lambda_id pointing to the stored lambda so it can be found
2477                    // when captured in closures
2478                    let lambda_repr = JValue::lambda(
2479                        name.as_str(),
2480                        stored_lambda.params.clone(),
2481                        Some(name.to_string()),
2482                        stored_lambda.signature.clone(),
2483                    );
2484                    return Ok(lambda_repr);
2485                }
2486
2487                // Check if this is a built-in function reference (only if not shadowed)
2488                if self.is_builtin_function(name) {
2489                    // Return a marker for built-in functions
2490                    // This allows built-in functions to be passed to higher-order functions
2491                    let builtin_repr = JValue::builtin(name.as_str());
2492                    return Ok(builtin_repr);
2493                }
2494
2495                // Undefined variable - return null (undefined in JSONata semantics)
2496                // This allows expressions like `$not(undefined_var)` to return undefined
2497                // and comparisons like `3 > $undefined` to return undefined
2498                Ok(JValue::Null)
2499            }
2500
2501            AstNode::ParentVariable(name) => {
2502                // Special case: $$ alone (empty name) refers to parent/root context
2503                if name.is_empty() {
2504                    return self.context.get_parent().cloned().ok_or_else(|| {
2505                        EvaluatorError::ReferenceError("Parent context not available".to_string())
2506                    });
2507                }
2508
2509                // For $$name, we need to evaluate name against parent context
2510                // This is similar to $.name but using parent data
2511                let parent_data = self.context.get_parent().ok_or_else(|| {
2512                    EvaluatorError::ReferenceError("Parent context not available".to_string())
2513                })?;
2514
2515                // Access field on parent context
2516                match parent_data {
2517                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
2518                    _ => Ok(JValue::Null),
2519                }
2520            }
2521
2522            AstNode::Path { steps } => self.evaluate_path(steps, data),
2523
2524            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
2525
2526            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
2527
2528            // Array constructor - JSONata semantics:
2529            AstNode::Array(elements) => {
2530                // - If element is itself an array constructor [...], keep it nested
2531                // - Otherwise, if element evaluates to an array, flatten it
2532                // - Undefined values are excluded
2533                let mut result = Vec::with_capacity(elements.len());
2534                for element in elements {
2535                    // Check if this element is itself an explicit array constructor
2536                    let is_array_constructor = matches!(element, AstNode::Array(_));
2537
2538                    let value = self.evaluate_internal(element, data)?;
2539
2540                    // Skip undefined values in array constructors
2541                    // Note: explicit null is preserved, only undefined (no value) is filtered
2542                    if value.is_undefined() {
2543                        continue;
2544                    }
2545
2546                    if is_array_constructor {
2547                        // Explicit array constructor - keep nested
2548                        result.push(value);
2549                    } else if let JValue::Array(arr) = value {
2550                        // Non-array-constructor that evaluated to array - flatten it
2551                        result.extend(arr.iter().cloned());
2552                    } else {
2553                        // Non-array value - add as-is
2554                        result.push(value);
2555                    }
2556                }
2557                Ok(JValue::array(result))
2558            }
2559
2560            AstNode::Object(pairs) => {
2561                let mut result = IndexMap::with_capacity(pairs.len());
2562
2563                // Check if all keys are string literals — can skip D1009 HashMap
2564                let all_literal_keys = pairs
2565                    .iter()
2566                    .all(|(k, _)| matches!(k, AstNode::String(_)));
2567
2568                if all_literal_keys {
2569                    // Fast path: literal keys, no need for D1009 tracking
2570                    for (key_node, value_node) in pairs.iter() {
2571                        let key = match key_node {
2572                            AstNode::String(s) => s,
2573                            _ => unreachable!(),
2574                        };
2575                        let value = self.evaluate_internal(value_node, data)?;
2576                        if value.is_undefined() {
2577                            continue;
2578                        }
2579                        result.insert(key.clone(), value);
2580                    }
2581                } else {
2582                    let mut key_sources: HashMap<String, usize> = HashMap::new();
2583                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
2584                        let key = match self.evaluate_internal(key_node, data)? {
2585                            JValue::String(s) => s,
2586                            JValue::Null => continue,
2587                            other => {
2588                                if other.is_undefined() {
2589                                    continue;
2590                                }
2591                                return Err(EvaluatorError::TypeError(format!(
2592                                    "Object key must be a string, got: {:?}",
2593                                    other
2594                                )));
2595                            }
2596                        };
2597
2598                        if let Some(&existing_idx) = key_sources.get(&*key) {
2599                            if existing_idx != pair_index {
2600                                return Err(EvaluatorError::EvaluationError(format!(
2601                                    "D1009: Multiple key expressions evaluate to same key: {}",
2602                                    key
2603                                )));
2604                            }
2605                        }
2606                        key_sources.insert(key.to_string(), pair_index);
2607
2608                        let value = self.evaluate_internal(value_node, data)?;
2609                        if value.is_undefined() {
2610                            continue;
2611                        }
2612                        result.insert(key.to_string(), value);
2613                    }
2614                }
2615                Ok(JValue::object(result))
2616            }
2617
2618            // Object transform: group items by key, then evaluate value once per group
2619            AstNode::ObjectTransform { input, pattern } => {
2620                // Evaluate the input expression
2621                let input_value = self.evaluate_internal(input, data)?;
2622
2623                // If input is undefined, return undefined (not empty object)
2624                if input_value.is_undefined() {
2625                    return Ok(JValue::Undefined);
2626                }
2627
2628                // Handle array input - process each item
2629                let items: Vec<JValue> = match input_value {
2630                    JValue::Array(ref arr) => (**arr).clone(),
2631                    JValue::Null => return Ok(JValue::Null),
2632                    other => vec![other],
2633                };
2634
2635                // If array is empty, add undefined to enable literal JSON object generation
2636                let items = if items.is_empty() {
2637                    vec![JValue::Undefined]
2638                } else {
2639                    items
2640                };
2641
2642                // Phase 1: Group items by key expression
2643                // groups maps key -> (grouped_data, expr_index)
2644                // When multiple items have same key, their data is appended together
2645                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
2646
2647                // Save the current $ binding to restore later
2648                let saved_dollar = self.context.lookup("$").cloned();
2649
2650                for item in &items {
2651                    // Bind $ to the current item for key evaluation
2652                    self.context.bind("$".to_string(), item.clone());
2653
2654                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
2655                        // Evaluate key with current item as context
2656                        let key = match self.evaluate_internal(key_node, item)? {
2657                            JValue::String(s) => s,
2658                            JValue::Null => continue, // Skip null keys
2659                            other => {
2660                                // Skip undefined keys
2661                                if other.is_undefined() {
2662                                    continue;
2663                                }
2664                                return Err(EvaluatorError::TypeError(format!(
2665                                    "T1003: Object key must be a string, got: {:?}",
2666                                    other
2667                                )));
2668                            }
2669                        };
2670
2671                        // Group items by key
2672                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
2673                            // Key already exists - check if from same expression index
2674                            if *existing_idx != pair_index {
2675                                // D1009: multiple key expressions evaluate to same key
2676                                return Err(EvaluatorError::EvaluationError(format!(
2677                                    "D1009: Multiple key expressions evaluate to same key: {}",
2678                                    key
2679                                )));
2680                            }
2681                            // Append item to the group
2682                            existing_data.push(item.clone());
2683                        } else {
2684                            // New key - create new group
2685                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
2686                        }
2687                    }
2688                }
2689
2690                // Phase 2: Evaluate value expression for each group
2691                let mut result = IndexMap::new();
2692
2693                for (key, (grouped_data, expr_index)) in groups {
2694                    // Get the value expression for this group
2695                    let (_key_node, value_node) = &pattern[expr_index];
2696
2697                    // Determine the context for value evaluation:
2698                    // - If single item, use that item directly
2699                    // - If multiple items, use the array of items
2700                    let context = if grouped_data.len() == 1 {
2701                        grouped_data.into_iter().next().unwrap()
2702                    } else {
2703                        JValue::array(grouped_data)
2704                    };
2705
2706                    // Bind $ to the context for value evaluation
2707                    self.context.bind("$".to_string(), context.clone());
2708
2709                    // Evaluate value expression with grouped context
2710                    let value = self.evaluate_internal(value_node, &context)?;
2711
2712                    // Skip undefined values
2713                    if !value.is_undefined() {
2714                        result.insert(key, value);
2715                    }
2716                }
2717
2718                // Restore the previous $ binding
2719                if let Some(saved) = saved_dollar {
2720                    self.context.bind("$".to_string(), saved);
2721                } else {
2722                    self.context.unbind("$");
2723                }
2724
2725                Ok(JValue::object(result))
2726            }
2727
2728            AstNode::Function {
2729                name,
2730                args,
2731                is_builtin,
2732            } => self.evaluate_function_call(name, args, *is_builtin, data),
2733
2734            // Call: invoke an arbitrary expression as a function
2735            // Used for IIFE patterns like (function($x){...})(5) or chained calls
2736            AstNode::Call { procedure, args } => {
2737                // Evaluate the procedure to get the callable value
2738                let callable = self.evaluate_internal(procedure, data)?;
2739
2740                // Check if it's a lambda value
2741                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
2742                    let mut evaluated_args = Vec::with_capacity(args.len());
2743                    for arg in args.iter() {
2744                        evaluated_args.push(self.evaluate_internal(arg, data)?);
2745                    }
2746                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
2747                }
2748
2749                // Not a callable value
2750                Err(EvaluatorError::TypeError(format!(
2751                    "Cannot call non-function value: {:?}",
2752                    callable
2753                )))
2754            }
2755
2756            AstNode::Conditional {
2757                condition,
2758                then_branch,
2759                else_branch,
2760            } => {
2761                let condition_value = self.evaluate_internal(condition, data)?;
2762                if self.is_truthy(&condition_value) {
2763                    self.evaluate_internal(then_branch, data)
2764                } else if let Some(else_branch) = else_branch {
2765                    self.evaluate_internal(else_branch, data)
2766                } else {
2767                    // No else branch - return undefined (not null)
2768                    // This allows $map to filter out results from conditionals without else
2769                    Ok(JValue::Undefined)
2770                }
2771            }
2772
2773            AstNode::Block(expressions) => {
2774                // Blocks create a new scope - push scope instead of clone/restore
2775                self.context.push_scope();
2776
2777                let mut result = JValue::Null;
2778                for expr in expressions {
2779                    result = self.evaluate_internal(expr, data)?;
2780                }
2781
2782                // Before popping, preserve any lambdas referenced by the result
2783                // This is essential for closures returned from blocks (IIFE pattern)
2784                let lambdas_to_keep = self.extract_lambda_ids(&result);
2785                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
2786
2787                Ok(result)
2788            }
2789
2790            // Lambda: capture current environment for closure support
2791            AstNode::Lambda {
2792                params,
2793                body,
2794                signature,
2795                thunk,
2796            } => {
2797                let lambda_id = format!("__lambda_{}_{:p}", params.len(), body.as_ref());
2798
2799                let compiled_body = if !thunk {
2800                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
2801                    try_compile_expr_with_allowed_vars(body, &var_refs)
2802                } else {
2803                    None
2804                };
2805                let stored_lambda = StoredLambda {
2806                    params: params.clone(),
2807                    body: (**body).clone(),
2808                    compiled_body,
2809                    signature: signature.clone(),
2810                    captured_env: self.capture_environment_for(body, params),
2811                    captured_data: Some(data.clone()),
2812                    thunk: *thunk,
2813                };
2814                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
2815
2816                let lambda_obj = JValue::lambda(
2817                    lambda_id.as_str(),
2818                    params.clone(),
2819                    None::<String>,
2820                    signature.clone(),
2821                );
2822
2823                Ok(lambda_obj)
2824            }
2825
2826            // Wildcard: collect all values from current object
2827            AstNode::Wildcard => {
2828                match data {
2829                    JValue::Object(obj) => {
2830                        let mut result = Vec::new();
2831                        for value in obj.values() {
2832                            // Flatten arrays into the result
2833                            match value {
2834                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
2835                                _ => result.push(value.clone()),
2836                            }
2837                        }
2838                        Ok(JValue::array(result))
2839                    }
2840                    JValue::Array(arr) => {
2841                        // For arrays, wildcard returns all elements
2842                        Ok(JValue::Array(arr.clone()))
2843                    }
2844                    _ => Ok(JValue::Null),
2845                }
2846            }
2847
2848            // Descendant: recursively traverse all nested values
2849            AstNode::Descendant => {
2850                let descendants = self.collect_descendants(data);
2851                if descendants.is_empty() {
2852                    Ok(JValue::Null) // No descendants means undefined
2853                } else {
2854                    Ok(JValue::array(descendants))
2855                }
2856            }
2857
2858            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
2859                "Predicate can only be used in path expressions".to_string(),
2860            )),
2861
2862            // Array grouping: same as Array but prevents flattening in path contexts
2863            AstNode::ArrayGroup(elements) => {
2864                let mut result = Vec::new();
2865                for element in elements {
2866                    let value = self.evaluate_internal(element, data)?;
2867                    result.push(value);
2868                }
2869                Ok(JValue::array(result))
2870            }
2871
2872            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
2873                "Function application can only be used in path expressions".to_string(),
2874            )),
2875
2876            AstNode::Sort { input, terms } => {
2877                let value = self.evaluate_internal(input, data)?;
2878                self.evaluate_sort(&value, terms)
2879            }
2880
2881            // Index binding: evaluates input and creates tuple stream with index variable
2882            AstNode::IndexBind { input, variable } => {
2883                let value = self.evaluate_internal(input, data)?;
2884
2885                // Store the variable name and create indexed results
2886                // This is a simplified implementation - full tuple stream would require more work
2887                match value {
2888                    JValue::Array(arr) => {
2889                        // Store the index binding metadata in a special wrapper
2890                        let mut result = Vec::new();
2891                        for (idx, item) in arr.iter().enumerate() {
2892                            // Create wrapper object with value and index
2893                            let mut wrapper = IndexMap::new();
2894                            wrapper.insert("@".to_string(), item.clone());
2895                            wrapper.insert(format!("${}", variable), JValue::Number(idx as f64));
2896                            wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
2897                            result.push(JValue::object(wrapper));
2898                        }
2899                        Ok(JValue::array(result))
2900                    }
2901                    // Single value: just return as-is with index 0
2902                    other => {
2903                        let mut wrapper = IndexMap::new();
2904                        wrapper.insert("@".to_string(), other);
2905                        wrapper.insert(format!("${}", variable), JValue::from(0i64));
2906                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
2907                        Ok(JValue::object(wrapper))
2908                    }
2909                }
2910            }
2911
2912            // Transform: |location|update[,delete]|
2913            AstNode::Transform {
2914                location,
2915                update,
2916                delete,
2917            } => {
2918                // Check if $ is bound (meaning we're being invoked as a lambda)
2919                if self.context.lookup("$").is_some() {
2920                    // Execute the transformation
2921                    self.execute_transform(location, update, delete.as_deref(), data)
2922                } else {
2923                    // Return a lambda representation
2924                    // The transform will be executed when the lambda is invoked
2925                    let transform_lambda = StoredLambda {
2926                        params: vec!["$".to_string()],
2927                        body: AstNode::Transform {
2928                            location: location.clone(),
2929                            update: update.clone(),
2930                            delete: delete.clone(),
2931                        },
2932                        compiled_body: None, // Transform is not a pure compilable expr
2933                        signature: None,
2934                        captured_env: HashMap::new(),
2935                        captured_data: None, // Transform takes $ as parameter
2936                        thunk: false,
2937                    };
2938
2939                    // Store with a generated unique name
2940                    let lambda_name = format!("__transform_{:p}", location);
2941                    self.context.bind_lambda(lambda_name, transform_lambda);
2942
2943                    // Return lambda marker
2944                    Ok(JValue::string("<lambda>"))
2945                }
2946            }
2947        }
2948    }
2949
2950    /// Apply stages (filters/predicates) to a value during field extraction
2951    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
2952    /// This matches the JavaScript reference where stages apply to sequences
2953    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
2954        // Wrap non-arrays in an array for filtering (JSONata semantics)
2955        let mut result = match value {
2956            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
2957            JValue::Array(_) => value,
2958            other => JValue::array(vec![other]),
2959        };
2960
2961        for stage in stages {
2962            match stage {
2963                Stage::Filter(predicate_expr) => {
2964                    // When applying stages, use stage-specific predicate logic
2965                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
2966                }
2967            }
2968        }
2969        Ok(result)
2970    }
2971
2972    /// Check if an AST node is definitely a filter expression (comparison/logical)
2973    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
2974    fn is_filter_predicate(predicate: &AstNode) -> bool {
2975        match predicate {
2976            AstNode::Binary { op, .. } => matches!(
2977                op,
2978                BinaryOp::GreaterThan
2979                    | BinaryOp::GreaterThanOrEqual
2980                    | BinaryOp::LessThan
2981                    | BinaryOp::LessThanOrEqual
2982                    | BinaryOp::Equal
2983                    | BinaryOp::NotEqual
2984                    | BinaryOp::And
2985                    | BinaryOp::Or
2986                    | BinaryOp::In
2987            ),
2988            AstNode::Unary {
2989                op: crate::ast::UnaryOp::Not,
2990                ..
2991            } => true,
2992            _ => false,
2993        }
2994    }
2995
2996    /// Evaluate a predicate as a stage during field extraction
2997    /// This has different semantics than standalone predicates:
2998    /// - Maps index operations over arrays of extracted values
2999    fn evaluate_predicate_as_stage(
3000        &mut self,
3001        current: &JValue,
3002        predicate: &AstNode,
3003    ) -> Result<JValue, EvaluatorError> {
3004        // Special case: empty brackets [] (represented as Boolean(true))
3005        if matches!(predicate, AstNode::Boolean(true)) {
3006            return match current {
3007                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
3008                JValue::Null => Ok(JValue::Null),
3009                other => Ok(JValue::array(vec![other.clone()])),
3010            };
3011        }
3012
3013        match current {
3014            JValue::Array(arr) => {
3015                // For stages: if we have an array of values (from field extraction),
3016                // apply the predicate to each value if appropriate
3017
3018                // Check if predicate is a numeric index
3019                if let AstNode::Number(n) = predicate {
3020                    // Check if this is an array of arrays (extracted array fields)
3021                    let is_array_of_arrays =
3022                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
3023
3024                    if !is_array_of_arrays {
3025                        // Simple values: just index normally
3026                        return self.array_index(current, &JValue::Number(*n));
3027                    }
3028
3029                    // Array of arrays: map index access over each extracted array
3030                    let mut result = Vec::new();
3031                    for item in arr.iter() {
3032                        match item {
3033                            JValue::Array(_) => {
3034                                let indexed = self.array_index(item, &JValue::Number(*n))?;
3035                                if !indexed.is_null() {
3036                                    result.push(indexed);
3037                                }
3038                            }
3039                            _ => {
3040                                if *n == 0.0 {
3041                                    result.push(item.clone());
3042                                }
3043                            }
3044                        }
3045                    }
3046                    return Ok(JValue::array(result));
3047                }
3048
3049                // Short-circuit: if predicate is definitely a comparison/logical expression,
3050                // skip speculative numeric evaluation and go directly to filter logic
3051                if Self::is_filter_predicate(predicate) {
3052                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
3053                    if let Some(compiled) = try_compile_expr(predicate) {
3054                        let shape = arr.first().and_then(build_shape_cache);
3055                        let mut filtered = Vec::with_capacity(arr.len());
3056                        for item in arr.iter() {
3057                            let result = if let Some(ref s) = shape {
3058                                eval_compiled_shaped(&compiled, item, None, s)?
3059                            } else {
3060                                eval_compiled(&compiled, item, None)?
3061                            };
3062                            if compiled_is_truthy(&result) {
3063                                filtered.push(item.clone());
3064                            }
3065                        }
3066                        return Ok(JValue::array(filtered));
3067                    }
3068                    // Fallback: full AST evaluation
3069                    let mut filtered = Vec::new();
3070                    for item in arr.iter() {
3071                        let item_result = self.evaluate_internal(predicate, item)?;
3072                        if self.is_truthy(&item_result) {
3073                            filtered.push(item.clone());
3074                        }
3075                    }
3076                    return Ok(JValue::array(filtered));
3077                }
3078
3079                // Try to evaluate the predicate to see if it's a numeric index or array of indices
3080                // If evaluation succeeds and yields a number, use it as an index
3081                // If it yields an array of numbers, use them as multiple indices
3082                // If evaluation fails (e.g., comparison error), treat as filter
3083                match self.evaluate_internal(predicate, current) {
3084                    Ok(JValue::Number(n)) => {
3085                        let n_val = n;
3086                        let is_array_of_arrays =
3087                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
3088
3089                        if !is_array_of_arrays {
3090                            let pred_result = JValue::Number(n_val);
3091                            return self.array_index(current, &pred_result);
3092                        }
3093
3094                        // Array of arrays: map index access
3095                        let mut result = Vec::new();
3096                        let pred_result = JValue::Number(n_val);
3097                        for item in arr.iter() {
3098                            match item {
3099                                JValue::Array(_) => {
3100                                    let indexed = self.array_index(item, &pred_result)?;
3101                                    if !indexed.is_null() {
3102                                        result.push(indexed);
3103                                    }
3104                                }
3105                                _ => {
3106                                    if n_val == 0.0 {
3107                                        result.push(item.clone());
3108                                    }
3109                                }
3110                            }
3111                        }
3112                        return Ok(JValue::array(result));
3113                    }
3114                    Ok(JValue::Array(indices)) => {
3115                        // Array of values - could be indices or filter results
3116                        // Check if all values are numeric
3117                        let has_non_numeric =
3118                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
3119
3120                        if has_non_numeric {
3121                            // Non-numeric values - treat as filter, fall through
3122                        } else {
3123                            // All numeric - use as indices
3124                            let arr_len = arr.len() as i64;
3125                            let mut resolved_indices: Vec<i64> = indices
3126                                .iter()
3127                                .filter_map(|v| {
3128                                    if let JValue::Number(n) = v {
3129                                        let idx = *n as i64;
3130                                        // Resolve negative indices
3131                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
3132                                        // Only include valid indices
3133                                        if actual_idx >= 0 && actual_idx < arr_len {
3134                                            Some(actual_idx)
3135                                        } else {
3136                                            None
3137                                        }
3138                                    } else {
3139                                        None
3140                                    }
3141                                })
3142                                .collect();
3143
3144                            // Sort and deduplicate indices
3145                            resolved_indices.sort();
3146                            resolved_indices.dedup();
3147
3148                            // Select elements at each sorted index
3149                            let result: Vec<JValue> = resolved_indices
3150                                .iter()
3151                                .map(|&idx| arr[idx as usize].clone())
3152                                .collect();
3153
3154                            return Ok(JValue::array(result));
3155                        }
3156                    }
3157                    Ok(_) => {
3158                        // Evaluated successfully but not a number or array - might be a filter
3159                        // Fall through to filter logic
3160                    }
3161                    Err(_) => {
3162                        // Evaluation failed - it's likely a filter expression
3163                        // Fall through to filter logic
3164                    }
3165                }
3166
3167                // It's a filter expression
3168                let mut filtered = Vec::new();
3169                for item in arr.iter() {
3170                    let item_result = self.evaluate_internal(predicate, item)?;
3171                    if self.is_truthy(&item_result) {
3172                        filtered.push(item.clone());
3173                    }
3174                }
3175                Ok(JValue::array(filtered))
3176            }
3177            JValue::Null => {
3178                // Null: return null
3179                Ok(JValue::Null)
3180            }
3181            other => {
3182                // Non-array values: treat as single-element conceptual array
3183                // For numeric predicates: index 0 returns the value, other indices return null
3184                // For boolean predicates: if truthy, return value; if falsy, return null
3185
3186                // Check if predicate is a numeric index
3187                if let AstNode::Number(n) = predicate {
3188                    // Index 0 returns the value, other indices return null
3189                    if *n == 0.0 {
3190                        return Ok(other.clone());
3191                    } else {
3192                        return Ok(JValue::Null);
3193                    }
3194                }
3195
3196                // Try to evaluate the predicate to see if it's a numeric index
3197                match self.evaluate_internal(predicate, other) {
3198                    Ok(JValue::Number(n)) => {
3199                        // Index 0 returns the value, other indices return null
3200                        if n == 0.0 {
3201                            Ok(other.clone())
3202                        } else {
3203                            Ok(JValue::Null)
3204                        }
3205                    }
3206                    Ok(pred_result) => {
3207                        // Boolean filter: return value if truthy, null if falsy
3208                        if self.is_truthy(&pred_result) {
3209                            Ok(other.clone())
3210                        } else {
3211                            Ok(JValue::Null)
3212                        }
3213                    }
3214                    Err(e) => Err(e),
3215                }
3216            }
3217        }
3218    }
3219
3220    /// Evaluate a path expression (e.g., foo.bar.baz)
3221    fn evaluate_path(
3222        &mut self,
3223        steps: &[PathStep],
3224        data: &JValue,
3225    ) -> Result<JValue, EvaluatorError> {
3226        // Avoid cloning by using references and only cloning when necessary
3227        if steps.is_empty() {
3228            return Ok(data.clone());
3229        }
3230
3231        // Fast path: single field access on object
3232        // This is a very common pattern, so optimize it
3233        if steps.len() == 1 {
3234            if let AstNode::Name(field_name) = &steps[0].node {
3235                return match data {
3236                    JValue::Object(obj) => {
3237                        // Check if this is a tuple - extract '@' value
3238                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3239                            if let Some(JValue::Object(inner)) = obj.get("@") {
3240                                Ok(inner.get(field_name).cloned().unwrap_or(JValue::Null))
3241                            } else {
3242                                Ok(JValue::Null)
3243                            }
3244                        } else {
3245                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null))
3246                        }
3247                    }
3248                    JValue::Array(arr) => {
3249                        // Array mapping: extract field from each element
3250                        // Optimized: use references to access fields without cloning entire objects
3251                        // Check first element for tuple-ness (tuples are all-or-nothing)
3252                        let has_tuples = arr.first().is_some_and(|item| {
3253                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
3254                        });
3255
3256                        if !has_tuples {
3257                            // Fast path: no tuples, just direct field lookups
3258                            let mut result = Vec::with_capacity(arr.len());
3259                            for item in arr.iter() {
3260                                if let JValue::Object(obj) = item {
3261                                    if let Some(val) = obj.get(field_name) {
3262                                        if !val.is_null() {
3263                                            match val {
3264                                                JValue::Array(arr_val) => {
3265                                                    result.extend(arr_val.iter().cloned());
3266                                                }
3267                                                other => result.push(other.clone()),
3268                                            }
3269                                        }
3270                                    }
3271                                } else if let JValue::Array(inner_arr) = item {
3272                                    let nested_result = self.evaluate_path(
3273                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
3274                                        &JValue::Array(inner_arr.clone()),
3275                                    )?;
3276                                    match nested_result {
3277                                        JValue::Array(nested) => {
3278                                            result.extend(nested.iter().cloned());
3279                                        }
3280                                        JValue::Null => {}
3281                                        other => result.push(other),
3282                                    }
3283                                }
3284                            }
3285
3286                            if result.is_empty() {
3287                                Ok(JValue::Null)
3288                            } else if result.len() == 1 {
3289                                Ok(result.into_iter().next().unwrap())
3290                            } else {
3291                                Ok(JValue::array(result))
3292                            }
3293                        } else {
3294                        // Tuple path: per-element tuple handling
3295                        let mut result = Vec::new();
3296                        for item in arr.iter() {
3297                            match item {
3298                                JValue::Object(obj) => {
3299                                    let is_tuple =
3300                                        obj.get("__tuple__") == Some(&JValue::Bool(true));
3301
3302                                    if is_tuple {
3303                                        let inner = match obj.get("@") {
3304                                            Some(JValue::Object(inner)) => inner,
3305                                            _ => continue,
3306                                        };
3307
3308                                        if let Some(val) = inner.get(field_name) {
3309                                            if !val.is_null() {
3310                                                // Build tuple wrapper - only clone bindings when needed
3311                                                let wrap = |v: JValue| -> JValue {
3312                                                    let mut wrapper = IndexMap::new();
3313                                                    wrapper.insert("@".to_string(), v);
3314                                                    wrapper.insert(
3315                                                        "__tuple__".to_string(),
3316                                                        JValue::Bool(true),
3317                                                    );
3318                                                    for (k, v) in obj.iter() {
3319                                                        if k.starts_with('$') {
3320                                                            wrapper.insert(k.clone(), v.clone());
3321                                                        }
3322                                                    }
3323                                                    JValue::object(wrapper)
3324                                                };
3325
3326                                                match val {
3327                                                    JValue::Array(arr_val) => {
3328                                                        for item in arr_val.iter() {
3329                                                            result.push(wrap(item.clone()));
3330                                                        }
3331                                                    }
3332                                                    other => result.push(wrap(other.clone())),
3333                                                }
3334                                            }
3335                                        }
3336                                    } else {
3337                                        // Non-tuple: access field directly by reference, only clone the field value
3338                                        if let Some(val) = obj.get(field_name) {
3339                                            if !val.is_null() {
3340                                                match val {
3341                                                    JValue::Array(arr_val) => {
3342                                                        for item in arr_val.iter() {
3343                                                            result.push(item.clone());
3344                                                        }
3345                                                    }
3346                                                    other => result.push(other.clone()),
3347                                                }
3348                                            }
3349                                        }
3350                                    }
3351                                }
3352                                JValue::Array(inner_arr) => {
3353                                    // Recursively map over nested array
3354                                    let nested_result = self.evaluate_path(
3355                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
3356                                        &JValue::Array(inner_arr.clone()),
3357                                    )?;
3358                                    // Add nested result to our results
3359                                    match nested_result {
3360                                        JValue::Array(nested) => {
3361                                            // Flatten nested arrays from recursive mapping
3362                                            result.extend(nested.iter().cloned());
3363                                        }
3364                                        JValue::Null => {} // Skip nulls from nested arrays
3365                                        other => result.push(other),
3366                                    }
3367                                }
3368                                _ => {} // Skip non-object items
3369                            }
3370                        }
3371
3372                        // Return array result
3373                        // JSONata singleton unwrapping: if we have exactly one result,
3374                        // unwrap it (even if it's an array)
3375                        if result.is_empty() {
3376                            Ok(JValue::Null)
3377                        } else if result.len() == 1 {
3378                            Ok(result.into_iter().next().unwrap())
3379                        } else {
3380                            Ok(JValue::array(result))
3381                        }
3382                        } // end else (tuple path)
3383                    }
3384                    _ => Ok(JValue::Null),
3385                };
3386            }
3387        }
3388
3389        // Fast path: 2-step $variable.field with no stages
3390        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
3391        if steps.len() == 2
3392            && steps[0].stages.is_empty()
3393            && steps[1].stages.is_empty()
3394        {
3395            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
3396                (&steps[0].node, &steps[1].node)
3397            {
3398                if !var_name.is_empty() {
3399                    if let Some(value) = self.context.lookup(var_name) {
3400                        match value {
3401                            JValue::Object(obj) => {
3402                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
3403                            }
3404                            JValue::Array(arr) => {
3405                                // Map field extraction over array (same as single-step Name on Array)
3406                                let mut result = Vec::with_capacity(arr.len());
3407                                for item in arr.iter() {
3408                                    if let JValue::Object(obj) = item {
3409                                        if let Some(val) = obj.get(field_name) {
3410                                            if !val.is_null() {
3411                                                match val {
3412                                                    JValue::Array(inner) => {
3413                                                        result.extend(inner.iter().cloned());
3414                                                    }
3415                                                    other => result.push(other.clone()),
3416                                                }
3417                                            }
3418                                        }
3419                                    }
3420                                }
3421                                return match result.len() {
3422                                    0 => Ok(JValue::Null),
3423                                    1 => Ok(result.pop().unwrap()),
3424                                    _ => Ok(JValue::array(result)),
3425                                };
3426                            }
3427                            _ => {} // Fall through to general path evaluation
3428                        }
3429                    }
3430                }
3431            }
3432        }
3433
3434        // Track whether we did array mapping (for singleton unwrapping)
3435        let mut did_array_mapping = false;
3436
3437        // For the first step, work with a reference
3438        let mut current: JValue = match &steps[0].node {
3439            AstNode::Wildcard => {
3440                // Wildcard as first step
3441                match data {
3442                    JValue::Object(obj) => {
3443                        let mut result = Vec::new();
3444                        for value in obj.values() {
3445                            // Flatten arrays into the result
3446                            match value {
3447                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3448                                _ => result.push(value.clone()),
3449                            }
3450                        }
3451                        JValue::array(result)
3452                    }
3453                    JValue::Array(arr) => JValue::Array(arr.clone()),
3454                    _ => JValue::Null,
3455                }
3456            }
3457            AstNode::Descendant => {
3458                // Descendant as first step
3459                let descendants = self.collect_descendants(data);
3460                JValue::array(descendants)
3461            }
3462            AstNode::ParentVariable(name) => {
3463                // Parent variable as first step
3464                let parent_data = self.context.get_parent().ok_or_else(|| {
3465                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3466                })?;
3467
3468                if name.is_empty() {
3469                    // $$ alone returns parent context
3470                    parent_data.clone()
3471                } else {
3472                    // $$field accesses field on parent
3473                    match parent_data {
3474                        JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
3475                        _ => JValue::Null,
3476                    }
3477                }
3478            }
3479            AstNode::Name(field_name) => {
3480                // Field/property access - get the stages for this step
3481                let stages = &steps[0].stages;
3482
3483                match data {
3484                    JValue::Object(obj) => {
3485                        let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
3486                        // Apply any stages to the extracted value
3487                        if !stages.is_empty() {
3488                            self.apply_stages(val, stages)?
3489                        } else {
3490                            val
3491                        }
3492                    }
3493                    JValue::Array(arr) => {
3494                        // Array mapping: extract field from each element and apply stages
3495                        let mut result = Vec::new();
3496                        for item in arr.iter() {
3497                            match item {
3498                                JValue::Object(obj) => {
3499                                    let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
3500                                    if !val.is_null() {
3501                                        if !stages.is_empty() {
3502                                            // Apply stages to the extracted value
3503                                            let processed_val = self.apply_stages(val, stages)?;
3504                                            // Stages always return an array (or null); extend results
3505                                            match processed_val {
3506                                                JValue::Array(arr) => {
3507                                                    result.extend(arr.iter().cloned())
3508                                                }
3509                                                JValue::Null => {} // Skip nulls from stage application
3510                                                other => result.push(other), // Shouldn't happen, but handle it
3511                                            }
3512                                        } else {
3513                                            // No stages: flatten arrays, push scalars
3514                                            match val {
3515                                                JValue::Array(arr) => {
3516                                                    result.extend(arr.iter().cloned())
3517                                                }
3518                                                other => result.push(other),
3519                                            }
3520                                        }
3521                                    }
3522                                }
3523                                JValue::Array(inner_arr) => {
3524                                    // Recursively map over nested array
3525                                    let nested_result = self.evaluate_path(
3526                                        &[steps[0].clone()],
3527                                        &JValue::Array(inner_arr.clone()),
3528                                    )?;
3529                                    match nested_result {
3530                                        JValue::Array(nested) => {
3531                                            result.extend(nested.iter().cloned())
3532                                        }
3533                                        JValue::Null => {} // Skip nulls from nested arrays
3534                                        other => result.push(other),
3535                                    }
3536                                }
3537                                _ => {} // Skip non-object items
3538                            }
3539                        }
3540                        JValue::array(result)
3541                    }
3542                    JValue::Null => JValue::Null,
3543                    // Accessing field on non-object returns undefined (not an error)
3544                    _ => JValue::Undefined,
3545                }
3546            }
3547            AstNode::String(string_literal) => {
3548                // String literal in path context - evaluate as literal and apply stages
3549                // This handles cases like "Red"[true] where "Red" is a literal, not a field access
3550                let stages = &steps[0].stages;
3551                let val = JValue::string(string_literal.clone());
3552
3553                if !stages.is_empty() {
3554                    // Apply stages (predicates) to the string literal
3555                    let result = self.apply_stages(val, stages)?;
3556                    // Unwrap single-element arrays back to scalar
3557                    // (string literals with predicates should return scalar or null, not arrays)
3558                    match result {
3559                        JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
3560                        JValue::Array(arr) if arr.is_empty() => JValue::Null,
3561                        other => other,
3562                    }
3563                } else {
3564                    val
3565                }
3566            }
3567            AstNode::Predicate(pred_expr) => {
3568                // Predicate as first step
3569                self.evaluate_predicate(data, pred_expr)?
3570            }
3571            AstNode::IndexBind { .. } => {
3572                // Index binding as first step - evaluate the IndexBind to create tuple array
3573                self.evaluate_internal(&steps[0].node, data)?
3574            }
3575            _ => {
3576                // Complex first step - evaluate it
3577                self.evaluate_path_step(&steps[0].node, data, data)?
3578            }
3579        };
3580
3581        // Process remaining steps
3582        for step in steps[1..].iter() {
3583            // Early return if current is null/undefined - no point continuing
3584            // This handles cases like `blah.{}` where blah doesn't exist
3585            if current.is_null() || current.is_undefined() {
3586                return Ok(JValue::Null);
3587            }
3588
3589            // Check if current is a tuple array - if so, we need to bind tuple variables
3590            // to context so they're available in nested expressions (like predicates)
3591            let is_tuple_array = if let JValue::Array(arr) = &current {
3592                arr.first().is_some_and(|first| {
3593                    if let JValue::Object(obj) = first {
3594                        obj.get("__tuple__") == Some(&JValue::Bool(true))
3595                    } else {
3596                        false
3597                    }
3598                })
3599            } else {
3600                false
3601            };
3602
3603            // For tuple arrays with certain step types, we need special handling to bind
3604            // tuple variables to context so they're available in nested expressions.
3605            // This is needed for:
3606            // - Object constructors: {"label": $$.items[$i]} needs $i in context
3607            // - Function applications: .($$.items[$i]) needs $i in context
3608            // - Variable lookups: .$i needs to find the tuple binding
3609            //
3610            // Steps like Name (field access) already have proper tuple handling in their
3611            // specific cases, so we don't intercept those here.
3612            let needs_tuple_context_binding = is_tuple_array
3613                && matches!(
3614                    &step.node,
3615                    AstNode::Object(_) | AstNode::FunctionApplication(_) | AstNode::Variable(_)
3616                );
3617
3618            if needs_tuple_context_binding {
3619                if let JValue::Array(arr) = &current {
3620                    let mut results = Vec::new();
3621
3622                    for tuple in arr.iter() {
3623                        if let JValue::Object(tuple_obj) = tuple {
3624                            // Extract tuple bindings (variables starting with $)
3625                            let bindings: Vec<(String, JValue)> = tuple_obj
3626                                .iter()
3627                                .filter(|(k, _)| k.starts_with('$') && k.len() > 1) // $i, $j, etc.
3628                                .map(|(k, v)| (k[1..].to_string(), v.clone())) // Remove $ prefix for context binding
3629                                .collect();
3630
3631                            // Save current bindings
3632                            let saved_bindings: Vec<(String, Option<JValue>)> = bindings
3633                                .iter()
3634                                .map(|(name, _)| (name.clone(), self.context.lookup(name).cloned()))
3635                                .collect();
3636
3637                            // Bind tuple variables to context
3638                            for (name, value) in &bindings {
3639                                self.context.bind(name.clone(), value.clone());
3640                            }
3641
3642                            // Get the actual value from the tuple (@ field)
3643                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
3644
3645                            // Evaluate the step
3646                            let step_result = match &step.node {
3647                                AstNode::Variable(_) => {
3648                                    // Variable lookup - check context (which now has bindings)
3649                                    self.evaluate_internal(&step.node, tuple)?
3650                                }
3651                                AstNode::Object(_) | AstNode::FunctionApplication(_) => {
3652                                    // Object constructor or function application - evaluate on actual data
3653                                    self.evaluate_internal(&step.node, &actual_data)?
3654                                }
3655                                _ => unreachable!(), // We only match specific types above
3656                            };
3657
3658                            // Restore previous bindings
3659                            for (name, saved_value) in &saved_bindings {
3660                                if let Some(value) = saved_value {
3661                                    self.context.bind(name.clone(), value.clone());
3662                                } else {
3663                                    self.context.unbind(name);
3664                                }
3665                            }
3666
3667                            // Collect result
3668                            if !step_result.is_null() && !step_result.is_undefined() {
3669                                // For object constructors, collect results directly
3670                                // For other steps, handle arrays
3671                                if matches!(&step.node, AstNode::Object(_)) {
3672                                    results.push(step_result);
3673                                } else if matches!(step_result, JValue::Array(_)) {
3674                                    if let JValue::Array(arr) = step_result {
3675                                        results.extend(arr.iter().cloned());
3676                                    }
3677                                } else {
3678                                    results.push(step_result);
3679                                }
3680                            }
3681                        }
3682                    }
3683
3684                    current = JValue::array(results);
3685                    continue; // Skip the regular step processing
3686                }
3687            }
3688
3689            current = match &step.node {
3690                AstNode::Wildcard => {
3691                    // Wildcard in path
3692                    let stages = &step.stages;
3693                    let wildcard_result = match &current {
3694                        JValue::Object(obj) => {
3695                            let mut result = Vec::new();
3696                            for value in obj.values() {
3697                                // Flatten arrays into the result
3698                                match value {
3699                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
3700                                    _ => result.push(value.clone()),
3701                                }
3702                            }
3703                            JValue::array(result)
3704                        }
3705                        JValue::Array(arr) => {
3706                            // Map wildcard over array
3707                            let mut all_values = Vec::new();
3708                            for item in arr.iter() {
3709                                match item {
3710                                    JValue::Object(obj) => {
3711                                        for value in obj.values() {
3712                                            // Flatten arrays
3713                                            match value {
3714                                                JValue::Array(arr) => {
3715                                                    all_values.extend(arr.iter().cloned())
3716                                                }
3717                                                _ => all_values.push(value.clone()),
3718                                            }
3719                                        }
3720                                    }
3721                                    JValue::Array(inner) => {
3722                                        all_values.extend(inner.iter().cloned());
3723                                    }
3724                                    _ => {}
3725                                }
3726                            }
3727                            JValue::array(all_values)
3728                        }
3729                        _ => JValue::Null,
3730                    };
3731
3732                    // Apply stages (predicates) if present
3733                    if !stages.is_empty() {
3734                        self.apply_stages(wildcard_result, stages)?
3735                    } else {
3736                        wildcard_result
3737                    }
3738                }
3739                AstNode::Descendant => {
3740                    // Descendant in path
3741                    match &current {
3742                        JValue::Array(arr) => {
3743                            // Collect descendants from all array elements
3744                            let mut all_descendants = Vec::new();
3745                            for item in arr.iter() {
3746                                all_descendants.extend(self.collect_descendants(item));
3747                            }
3748                            JValue::array(all_descendants)
3749                        }
3750                        _ => {
3751                            // Collect descendants from current value
3752                            let descendants = self.collect_descendants(&current);
3753                            JValue::array(descendants)
3754                        }
3755                    }
3756                }
3757                AstNode::Name(field_name) => {
3758                    // Navigate into object field or map over array, applying stages
3759                    let stages = &step.stages;
3760
3761                    match &current {
3762                        JValue::Object(obj) => {
3763                            // Single object field extraction - NOT array mapping
3764                            // This resets did_array_mapping because we're extracting from
3765                            // a single value, not mapping over an array. The field's value
3766                            // (even if it's an array) should be preserved as-is.
3767                            did_array_mapping = false;
3768                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
3769                            // Apply stages if present
3770                            if !stages.is_empty() {
3771                                self.apply_stages(val, stages)?
3772                            } else {
3773                                val
3774                            }
3775                        }
3776                        JValue::Array(arr) => {
3777                            // Array mapping: extract field from each element and apply stages
3778                            did_array_mapping = true; // Track that we did array mapping
3779
3780                            // Fast path: if no elements are tuples and no stages,
3781                            // skip all tuple checking overhead (common case for products.price etc.)
3782                            // Tuples are all-or-nothing (created by index binding #$i),
3783                            // so checking only the first element is sufficient.
3784                            let has_tuples = arr.first().is_some_and(|item| {
3785                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
3786                            });
3787
3788                            if !has_tuples && stages.is_empty() {
3789                                let mut result = Vec::with_capacity(arr.len());
3790                                for item in arr.iter() {
3791                                    match item {
3792                                        JValue::Object(obj) => {
3793                                            if let Some(val) = obj.get(field_name) {
3794                                                if !val.is_null() {
3795                                                    match val {
3796                                                        JValue::Array(arr_val) => {
3797                                                            result.extend(arr_val.iter().cloned())
3798                                                        }
3799                                                        other => result.push(other.clone()),
3800                                                    }
3801                                                }
3802                                            }
3803                                        }
3804                                        JValue::Array(_) => {
3805                                            let nested_result =
3806                                                self.evaluate_path(&[step.clone()], item)?;
3807                                            match nested_result {
3808                                                JValue::Array(nested) => {
3809                                                    result.extend(nested.iter().cloned())
3810                                                }
3811                                                JValue::Null => {}
3812                                                other => result.push(other),
3813                                            }
3814                                        }
3815                                        _ => {}
3816                                    }
3817                                }
3818                                JValue::array(result)
3819                            } else {
3820                                // Full path with tuple support and stages
3821                                let mut result = Vec::new();
3822
3823                                for item in arr.iter() {
3824                                    match item {
3825                                        JValue::Object(obj) => {
3826                                            // Check if this is a tuple stream element
3827                                            let (actual_obj, tuple_bindings) =
3828                                                if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3829                                                    // This is a tuple - extract '@' value and preserve bindings
3830                                                    if let Some(JValue::Object(inner)) = obj.get("@") {
3831                                                        // Collect index bindings (variables starting with $)
3832                                                        let bindings: Vec<(String, JValue)> = obj
3833                                                            .iter()
3834                                                            .filter(|(k, _)| k.starts_with('$'))
3835                                                            .map(|(k, v)| (k.clone(), v.clone()))
3836                                                            .collect();
3837                                                        (inner.clone(), Some(bindings))
3838                                                    } else {
3839                                                        continue; // Invalid tuple
3840                                                    }
3841                                                } else {
3842                                                    (obj.clone(), None)
3843                                                };
3844
3845                                            let val = actual_obj
3846                                                .get(field_name)
3847                                                .cloned()
3848                                                .unwrap_or(JValue::Null);
3849
3850                                            if !val.is_null() {
3851                                                // Helper to wrap value in tuple if we have bindings
3852                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
3853                                                    if let Some(b) = bindings {
3854                                                        let mut wrapper = IndexMap::new();
3855                                                        wrapper.insert("@".to_string(), v);
3856                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
3857                                                        for (k, val) in b {
3858                                                            wrapper.insert(k.clone(), val.clone());
3859                                                        }
3860                                                        JValue::object(wrapper)
3861                                                    } else {
3862                                                        v
3863                                                    }
3864                                                };
3865
3866                                                if !stages.is_empty() {
3867                                                    // Apply stages to the extracted value
3868                                                    let processed_val =
3869                                                        self.apply_stages(val, stages)?;
3870                                                    // Stages always return an array (or null); extend results
3871                                                    match processed_val {
3872                                                        JValue::Array(arr) => {
3873                                                            for item in arr.iter() {
3874                                                                result.push(wrap_in_tuple(
3875                                                                    item.clone(),
3876                                                                    &tuple_bindings,
3877                                                                ));
3878                                                            }
3879                                                        }
3880                                                        JValue::Null => {} // Skip nulls from stage application
3881                                                        other => result.push(wrap_in_tuple(
3882                                                            other,
3883                                                            &tuple_bindings,
3884                                                        )),
3885                                                    }
3886                                                } else {
3887                                                    // No stages: flatten arrays, push scalars
3888                                                    // But preserve tuple bindings!
3889                                                    match val {
3890                                                        JValue::Array(arr) => {
3891                                                            for item in arr.iter() {
3892                                                                result.push(wrap_in_tuple(
3893                                                                    item.clone(),
3894                                                                    &tuple_bindings,
3895                                                                ));
3896                                                            }
3897                                                        }
3898                                                        other => result.push(wrap_in_tuple(
3899                                                            other,
3900                                                            &tuple_bindings,
3901                                                        )),
3902                                                    }
3903                                                }
3904                                            }
3905                                        }
3906                                        JValue::Array(_) => {
3907                                            // Recursively map over nested array
3908                                            let nested_result =
3909                                                self.evaluate_path(&[step.clone()], item)?;
3910                                            match nested_result {
3911                                                JValue::Array(nested) => {
3912                                                    result.extend(nested.iter().cloned())
3913                                                }
3914                                                JValue::Null => {}
3915                                                other => result.push(other),
3916                                            }
3917                                        }
3918                                        _ => {}
3919                                    }
3920                                }
3921
3922                                JValue::array(result)
3923                            }
3924                        }
3925                        JValue::Null => JValue::Null,
3926                        // Accessing field on non-object returns undefined (not an error)
3927                        _ => JValue::Undefined,
3928                    }
3929                }
3930                AstNode::String(string_literal) => {
3931                    // String literal as a path step - evaluate as literal and apply stages
3932                    let stages = &step.stages;
3933                    let val = JValue::string(string_literal.clone());
3934
3935                    if !stages.is_empty() {
3936                        // Apply stages (predicates) to the string literal
3937                        let result = self.apply_stages(val, stages)?;
3938                        // Unwrap single-element arrays back to scalar
3939                        match result {
3940                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
3941                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
3942                            other => other,
3943                        }
3944                    } else {
3945                        val
3946                    }
3947                }
3948                AstNode::Predicate(pred_expr) => {
3949                    // Predicate in path - filter or index into current value
3950                    self.evaluate_predicate(&current, pred_expr)?
3951                }
3952                AstNode::ArrayGroup(elements) => {
3953                    // Array grouping: map expression over array but keep results grouped
3954                    // .[expr] means evaluate expr for each array element
3955                    match &current {
3956                        JValue::Array(arr) => {
3957                            let mut result = Vec::new();
3958                            for item in arr.iter() {
3959                                // For each array item, evaluate all elements and collect results
3960                                let mut group_values = Vec::new();
3961                                for element in elements {
3962                                    let value = self.evaluate_internal(element, item)?;
3963                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
3964                                    // This ensures [[expr]] produces properly nested arrays
3965                                    let should_preserve_array = matches!(
3966                                        element,
3967                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
3968                                    );
3969
3970                                    if should_preserve_array {
3971                                        // Keep the array as a single element to preserve nesting
3972                                        group_values.push(value);
3973                                    } else {
3974                                        // Flatten the value into group_values
3975                                        match value {
3976                                            JValue::Array(arr) => {
3977                                                group_values.extend(arr.iter().cloned())
3978                                            }
3979                                            other => group_values.push(other),
3980                                        }
3981                                    }
3982                                }
3983                                // Each array element gets its own sub-array with all values
3984                                result.push(JValue::array(group_values));
3985                            }
3986                            JValue::array(result)
3987                        }
3988                        _ => {
3989                            // For non-arrays, just evaluate the array constructor normally
3990                            let mut result = Vec::new();
3991                            for element in elements {
3992                                let value = self.evaluate_internal(element, &current)?;
3993                                result.push(value);
3994                            }
3995                            JValue::array(result)
3996                        }
3997                    }
3998                }
3999                AstNode::FunctionApplication(expr) => {
4000                    // Function application: map expr over the current value
4001                    // .(expr) means evaluate expr for each element, with $ bound to that element
4002                    // Null/undefined results are filtered out
4003                    match &current {
4004                        JValue::Array(arr) => {
4005                            // Produce the mapped result (compiled fast path or tree-walker fallback).
4006                            // Do NOT return early — singleton unwrapping is applied by the outer
4007                            // path evaluation code after all steps are processed.
4008                            let mapped: Vec<JValue> =
4009                                if let Some(compiled) = try_compile_expr(expr) {
4010                                    let shape = arr.first().and_then(build_shape_cache);
4011                                    let mut result = Vec::with_capacity(arr.len());
4012                                    for item in arr.iter() {
4013                                        let value = if let Some(ref s) = shape {
4014                                            eval_compiled_shaped(&compiled, item, None, s)?
4015                                        } else {
4016                                            eval_compiled(&compiled, item, None)?
4017                                        };
4018                                        if !value.is_null() && !value.is_undefined() {
4019                                            result.push(value);
4020                                        }
4021                                    }
4022                                    result
4023                                } else {
4024                                    let mut result = Vec::new();
4025                                    for item in arr.iter() {
4026                                        // Save the current $ binding
4027                                        let saved_dollar = self.context.lookup("$").cloned();
4028
4029                                        // Bind $ to the current item
4030                                        self.context.bind("$".to_string(), item.clone());
4031
4032                                        // Evaluate the expression in the context of this item
4033                                        let value = self.evaluate_internal(expr, item)?;
4034
4035                                        // Restore the previous $ binding
4036                                        if let Some(saved) = saved_dollar {
4037                                            self.context.bind("$".to_string(), saved);
4038                                        } else {
4039                                            self.context.unbind("$");
4040                                        }
4041
4042                                        // Only include non-null/undefined values
4043                                        if !value.is_null() && !value.is_undefined() {
4044                                            result.push(value);
4045                                        }
4046                                    }
4047                                    result
4048                                };
4049                            // Don't do singleton unwrapping here - let the path result
4050                            // handling deal with it, which respects has_explicit_array_keep
4051                            JValue::array(mapped)
4052                        }
4053                        _ => {
4054                            // For non-arrays, bind $ and evaluate
4055                            let saved_dollar = self.context.lookup("$").cloned();
4056                            self.context.bind("$".to_string(), current.clone());
4057
4058                            let value = self.evaluate_internal(expr, &current)?;
4059
4060                            if let Some(saved) = saved_dollar {
4061                                self.context.bind("$".to_string(), saved);
4062                            } else {
4063                                self.context.unbind("$");
4064                            }
4065
4066                            value
4067                        }
4068                    }
4069                }
4070                AstNode::Sort { terms, .. } => {
4071                    // Sort as a path step - sort 'current' by the terms
4072                    self.evaluate_sort(&current, terms)?
4073                }
4074                AstNode::IndexBind { variable, .. } => {
4075                    // Index binding as a path step - creates tuple stream from current
4076                    // This wraps each element with an index binding
4077                    match &current {
4078                        JValue::Array(arr) => {
4079                            let mut result = Vec::new();
4080                            for (idx, item) in arr.iter().enumerate() {
4081                                let mut wrapper = IndexMap::new();
4082                                wrapper.insert("@".to_string(), item.clone());
4083                                wrapper
4084                                    .insert(format!("${}", variable), JValue::Number(idx as f64));
4085                                wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4086                                result.push(JValue::object(wrapper));
4087                            }
4088                            JValue::array(result)
4089                        }
4090                        other => {
4091                            // Single value: wrap with index 0
4092                            let mut wrapper = IndexMap::new();
4093                            wrapper.insert("@".to_string(), other.clone());
4094                            wrapper.insert(format!("${}", variable), JValue::from(0i64));
4095                            wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4096                            JValue::object(wrapper)
4097                        }
4098                    }
4099                }
4100                // Handle complex path steps (e.g., computed properties, object construction)
4101                _ => self.evaluate_path_step(&step.node, &current, data)?,
4102            };
4103        }
4104
4105        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
4106        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
4107
4108        // Check for explicit array-keeping operations
4109        // Empty predicate [] can be represented as:
4110        // 1. Predicate(Boolean(true)) as a path step node
4111        // 2. Stage::Filter(Boolean(true)) as a stage
4112        let has_explicit_array_keep = steps.iter().any(|step| {
4113            // Check if the step itself is an empty predicate
4114            if let AstNode::Predicate(pred) = &step.node {
4115                if matches!(**pred, AstNode::Boolean(true)) {
4116                    return true;
4117                }
4118            }
4119            // Check if any stage is an empty predicate
4120            step.stages.iter().any(|stage| {
4121                let crate::ast::Stage::Filter(pred) = stage;
4122                matches!(**pred, AstNode::Boolean(true))
4123            })
4124        });
4125
4126        // Unwrap when:
4127        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
4128        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
4129        //    Note: did_array_mapping is reset to false when extracting from a single object,
4130        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
4131        // BUT NOT when there's an explicit array-keeping operation
4132        //
4133        // Important: We DON'T unwrap just because original data was an array - what matters is
4134        // whether the final extraction was from an array mapping context or a single object.
4135        let should_unwrap = !has_explicit_array_keep
4136            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
4137
4138        let result = match &current {
4139            // Empty arrays become null/undefined
4140            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4141            // Unwrap singleton arrays when appropriate
4142            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
4143            // Keep arrays otherwise
4144            _ => current,
4145        };
4146
4147        Ok(result)
4148    }
4149
4150    /// Helper to evaluate a complex path step
4151    fn evaluate_path_step(
4152        &mut self,
4153        step: &AstNode,
4154        current: &JValue,
4155        original_data: &JValue,
4156    ) -> Result<JValue, EvaluatorError> {
4157        // Special case: array mapping with object construction
4158        // e.g., items.{"name": name, "price": price}
4159        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
4160            match (current, step) {
4161                (JValue::Array(arr), AstNode::Object(pairs)) => {
4162                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
4163                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
4164                        let shape = arr.first().and_then(build_shape_cache);
4165                        let mut mapped = Vec::with_capacity(arr.len());
4166                        for item in arr.iter() {
4167                            let result = if let Some(ref s) = shape {
4168                                eval_compiled_shaped(&compiled, item, None, s)?
4169                            } else {
4170                                eval_compiled(&compiled, item, None)?
4171                            };
4172                            if !result.is_undefined() {
4173                                mapped.push(result);
4174                            }
4175                        }
4176                        return Ok(JValue::array(mapped));
4177                    }
4178                    // Fallback: full AST evaluation per element
4179                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
4180                        .iter()
4181                        .map(|item| self.evaluate_internal(step, item))
4182                        .collect();
4183                    Ok(JValue::array(mapped?))
4184                }
4185                _ => unreachable!(),
4186            }
4187        } else {
4188            // Special case: array.$ should map $ over the array, returning each element
4189            // e.g., [1, 2, 3].$ returns [1, 2, 3]
4190            if let AstNode::Variable(name) = step {
4191                if name.is_empty() {
4192                    // Bare $ - map over array if current is an array
4193                    if let JValue::Array(arr) = current {
4194                        // Map $ over each element - $ refers to each element in turn
4195                        return Ok(JValue::Array(arr.clone()));
4196                    } else {
4197                        // For non-arrays, $ refers to the current value
4198                        return Ok(current.clone());
4199                    }
4200                }
4201            }
4202
4203            // Special case: Variable access on tuple arrays (from index binding #$var)
4204            // When current is a tuple array, we need to evaluate the variable against each tuple
4205            // so that tuple bindings ($i, etc.) can be found
4206            if matches!(step, AstNode::Variable(_)) {
4207                if let JValue::Array(arr) = current {
4208                    // Check if this is a tuple array
4209                    let is_tuple_array = arr.first().is_some_and(|first| {
4210                        if let JValue::Object(obj) = first {
4211                            obj.get("__tuple__") == Some(&JValue::Bool(true))
4212                        } else {
4213                            false
4214                        }
4215                    });
4216
4217                    if is_tuple_array {
4218                        // Map the variable lookup over each tuple
4219                        let mut results = Vec::new();
4220                        for tuple in arr.iter() {
4221                            // Evaluate the variable in the context of this tuple
4222                            // This allows tuple bindings ($i, etc.) to be found
4223                            let val = self.evaluate_internal(step, tuple)?;
4224                            if !val.is_null() && !val.is_undefined() {
4225                                results.push(val);
4226                            }
4227                        }
4228                        return Ok(JValue::array(results));
4229                    }
4230                }
4231            }
4232
4233            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
4234            // rather than being used to index/access the current value
4235            // e.g., items[price > 50] where [price > 50] is a filter operation
4236            // or $x.price where $x is a variable binding
4237            // or $$.field where $$ is the parent context
4238            // or [0..9] where it's an array constructor
4239            // or $^(field) where it's a sort operator
4240            // or (expr).field where (expr) is a block that evaluates to a value
4241            if matches!(
4242                step,
4243                AstNode::Binary { .. }
4244                    | AstNode::Function { .. }
4245                    | AstNode::Variable(_)
4246                    | AstNode::ParentVariable(_)
4247                    | AstNode::Array(_)
4248                    | AstNode::Object(_)
4249                    | AstNode::Sort { .. }
4250                    | AstNode::Block(_)
4251            ) {
4252                // Evaluate the step in the context of original_data and return the result directly
4253                return self.evaluate_internal(step, original_data);
4254            }
4255
4256            // Standard path step evaluation for indexing/accessing current value
4257            let step_value = self.evaluate_internal(step, original_data)?;
4258            Ok(match (current, &step_value) {
4259                (JValue::Object(obj), JValue::String(key)) => {
4260                    obj.get(&**key).cloned().unwrap_or(JValue::Null)
4261                }
4262                (JValue::Array(arr), JValue::Number(n)) => {
4263                    let index = *n as i64;
4264                    let len = arr.len() as i64;
4265
4266                    // Handle negative indexing (offset from end)
4267                    let actual_idx = if index < 0 { len + index } else { index };
4268
4269                    if actual_idx < 0 || actual_idx >= len {
4270                        JValue::Null
4271                    } else {
4272                        arr[actual_idx as usize].clone()
4273                    }
4274                }
4275                _ => JValue::Null,
4276            })
4277        }
4278    }
4279
4280    /// Evaluate a binary operation
4281    fn evaluate_binary_op(
4282        &mut self,
4283        op: crate::ast::BinaryOp,
4284        lhs: &AstNode,
4285        rhs: &AstNode,
4286        data: &JValue,
4287    ) -> Result<JValue, EvaluatorError> {
4288        use crate::ast::BinaryOp;
4289
4290        // Special handling for coalescing operator (??)
4291        // Returns right side if left is undefined (produces no value)
4292        // Note: literal null is a value, so it's NOT replaced
4293        if op == BinaryOp::Coalesce {
4294            // Try to evaluate the left side
4295            return match self.evaluate_internal(lhs, data) {
4296                Ok(value) => {
4297                    // Successfully evaluated to a value (even if it's null)
4298                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
4299                    if matches!(lhs, AstNode::Null) {
4300                        Ok(value)
4301                    }
4302                    // For paths and variables, null means undefined - use RHS
4303                    else if value.is_null()
4304                        && (matches!(lhs, AstNode::Path { .. })
4305                            || matches!(lhs, AstNode::String(_))
4306                            || matches!(lhs, AstNode::Variable(_)))
4307                    {
4308                        self.evaluate_internal(rhs, data)
4309                    } else {
4310                        Ok(value)
4311                    }
4312                }
4313                Err(_) => {
4314                    // Evaluation failed (e.g., undefined variable) - use RHS
4315                    self.evaluate_internal(rhs, data)
4316                }
4317            };
4318        }
4319
4320        // Special handling for default operator (?:)
4321        // Returns right side if left is falsy or a non-value (like a function)
4322        if op == BinaryOp::Default {
4323            let left = self.evaluate_internal(lhs, data)?;
4324            if self.is_truthy_for_default(&left) {
4325                return Ok(left);
4326            }
4327            return self.evaluate_internal(rhs, data);
4328        }
4329
4330        // Special handling for chain/pipe operator (~>)
4331        // Pipes the LHS result to the RHS function as the first argument
4332        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
4333        if op == BinaryOp::ChainPipe {
4334            // Handle regex on RHS - treat as $match(lhs, regex)
4335            if let AstNode::Regex { pattern, flags } = rhs {
4336                // Evaluate LHS
4337                let lhs_value = self.evaluate_internal(lhs, data)?;
4338                // Do regex match inline
4339                return match lhs_value {
4340                    JValue::String(s) => {
4341                        // Build the regex
4342                        let case_insensitive = flags.contains('i');
4343                        let regex_pattern = if case_insensitive {
4344                            format!("(?i){}", pattern)
4345                        } else {
4346                            pattern.clone()
4347                        };
4348                        match regex::Regex::new(&regex_pattern) {
4349                            Ok(re) => {
4350                                if let Some(m) = re.find(&s) {
4351                                    // Return match object
4352                                    let mut result = IndexMap::new();
4353                                    result.insert(
4354                                        "match".to_string(),
4355                                        JValue::string(m.as_str().to_string()),
4356                                    );
4357                                    result.insert(
4358                                        "start".to_string(),
4359                                        JValue::Number(m.start() as f64),
4360                                    );
4361                                    result
4362                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
4363
4364                                    // Capture groups
4365                                    let mut groups = Vec::new();
4366                                    for cap in re.captures_iter(&s).take(1) {
4367                                        for i in 1..cap.len() {
4368                                            if let Some(c) = cap.get(i) {
4369                                                groups.push(JValue::string(c.as_str().to_string()));
4370                                            }
4371                                        }
4372                                    }
4373                                    if !groups.is_empty() {
4374                                        result.insert("groups".to_string(), JValue::array(groups));
4375                                    }
4376
4377                                    Ok(JValue::object(result))
4378                                } else {
4379                                    Ok(JValue::Null)
4380                                }
4381                            }
4382                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
4383                                "Invalid regex: {}",
4384                                e
4385                            ))),
4386                        }
4387                    }
4388                    JValue::Null => Ok(JValue::Null),
4389                    _ => Err(EvaluatorError::TypeError(
4390                        "Left side of ~> /regex/ must be a string".to_string(),
4391                    )),
4392                };
4393            }
4394
4395            // Early check: if LHS evaluates to undefined, return undefined
4396            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
4397            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
4398            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
4399                return Ok(JValue::Undefined);
4400            }
4401
4402            // Handle different RHS types
4403            match rhs {
4404                AstNode::Function {
4405                    name,
4406                    args,
4407                    is_builtin,
4408                } => {
4409                    // RHS is a function call
4410                    // Check if the function call has placeholder arguments (partial application)
4411                    let has_placeholder =
4412                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
4413
4414                    if has_placeholder {
4415                        // Partial application: replace the first placeholder with LHS value
4416                        let lhs_value = self.evaluate_internal(lhs, data)?;
4417                        let mut filled_args = Vec::new();
4418                        let mut lhs_used = false;
4419
4420                        for arg in args.iter() {
4421                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
4422                                // Replace first placeholder with evaluated LHS
4423                                // We need to create a temporary binding to pass the value
4424                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
4425                                self.context.bind(temp_name.clone(), lhs_value.clone());
4426                                filled_args.push(AstNode::Variable(temp_name));
4427                                lhs_used = true;
4428                            } else {
4429                                filled_args.push(arg.clone());
4430                            }
4431                        }
4432
4433                        // Evaluate the function with filled args
4434                        let result =
4435                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
4436
4437                        // Clean up temp bindings
4438                        for (i, arg) in args.iter().enumerate() {
4439                            if matches!(arg, AstNode::Placeholder) {
4440                                self.context.unbind(&format!("__pipe_arg_{}", i));
4441                            }
4442                        }
4443
4444                        // Unwrap singleton results from chain operator
4445                        return result.map(|v| self.unwrap_singleton(v));
4446                    } else {
4447                        // No placeholders: build args list with LHS as first argument
4448                        let mut all_args = vec![lhs.clone()];
4449                        all_args.extend_from_slice(args);
4450                        // Unwrap singleton results from chain operator
4451                        return self
4452                            .evaluate_function_call(name, &all_args, *is_builtin, data)
4453                            .map(|v| self.unwrap_singleton(v));
4454                    }
4455                }
4456                AstNode::Variable(var_name) => {
4457                    // RHS is a function reference (no parens)
4458                    // e.g., $average($tempReadings) ~> $round
4459                    let all_args = vec![lhs.clone()];
4460                    // Unwrap singleton results from chain operator
4461                    return self
4462                        .evaluate_function_call(var_name, &all_args, true, data)
4463                        .map(|v| self.unwrap_singleton(v));
4464                }
4465                AstNode::Binary {
4466                    op: BinaryOp::ChainPipe,
4467                    ..
4468                } => {
4469                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
4470                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
4471                    let lhs_value = self.evaluate_internal(lhs, data)?;
4472                    return self.evaluate_internal(rhs, &lhs_value);
4473                }
4474                AstNode::Transform { .. } => {
4475                    // RHS is a transform - invoke it with LHS as input
4476                    // Evaluate LHS first
4477                    let lhs_value = self.evaluate_internal(lhs, data)?;
4478
4479                    // Bind $ to the LHS value, then evaluate the transform
4480                    let saved_binding = self.context.lookup("$").cloned();
4481                    self.context.bind("$".to_string(), lhs_value.clone());
4482
4483                    let result = self.evaluate_internal(rhs, data);
4484
4485                    // Restore $ binding
4486                    if let Some(saved) = saved_binding {
4487                        self.context.bind("$".to_string(), saved);
4488                    } else {
4489                        self.context.unbind("$");
4490                    }
4491
4492                    // Unwrap singleton results from chain operator
4493                    return result.map(|v| self.unwrap_singleton(v));
4494                }
4495                AstNode::Lambda {
4496                    params,
4497                    body,
4498                    signature,
4499                    thunk,
4500                } => {
4501                    // RHS is a lambda - invoke it with LHS as argument
4502                    let lhs_value = self.evaluate_internal(lhs, data)?;
4503                    // Unwrap singleton results from chain operator
4504                    return self
4505                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
4506                        .map(|v| self.unwrap_singleton(v));
4507                }
4508                AstNode::Path { steps } => {
4509                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
4510                    // If the first step is a function call, we need to add LHS as first argument
4511                    if let Some(first_step) = steps.first() {
4512                        match &first_step.node {
4513                            AstNode::Function {
4514                                name,
4515                                args,
4516                                is_builtin,
4517                            } => {
4518                                // Prepend LHS to the function arguments
4519                                let mut all_args = vec![lhs.clone()];
4520                                all_args.extend_from_slice(args);
4521
4522                                // Call the function
4523                                let mut result = self.evaluate_function_call(
4524                                    name,
4525                                    &all_args,
4526                                    *is_builtin,
4527                                    data,
4528                                )?;
4529
4530                                // Apply stages from the first step (e.g., predicates)
4531                                for stage in &first_step.stages {
4532                                    match stage {
4533                                        Stage::Filter(filter_expr) => {
4534                                            result = self.evaluate_predicate_as_stage(
4535                                                &result,
4536                                                filter_expr,
4537                                            )?;
4538                                        }
4539                                    }
4540                                }
4541
4542                                // Apply remaining path steps if any
4543                                if steps.len() > 1 {
4544                                    let remaining_path = AstNode::Path {
4545                                        steps: steps[1..].to_vec(),
4546                                    };
4547                                    result = self.evaluate_internal(&remaining_path, &result)?;
4548                                }
4549
4550                                // Unwrap singleton results from chain operator, unless there are stages
4551                                // Stages (like predicates) indicate we want to preserve array structure
4552                                if !first_step.stages.is_empty() || steps.len() > 1 {
4553                                    return Ok(result);
4554                                } else {
4555                                    return Ok(self.unwrap_singleton(result));
4556                                }
4557                            }
4558                            AstNode::Variable(var_name) => {
4559                                // Variable that should resolve to a function
4560                                let all_args = vec![lhs.clone()];
4561                                let mut result =
4562                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
4563
4564                                // Apply stages from the first step
4565                                for stage in &first_step.stages {
4566                                    match stage {
4567                                        Stage::Filter(filter_expr) => {
4568                                            result = self.evaluate_predicate_as_stage(
4569                                                &result,
4570                                                filter_expr,
4571                                            )?;
4572                                        }
4573                                    }
4574                                }
4575
4576                                // Apply remaining path steps if any
4577                                if steps.len() > 1 {
4578                                    let remaining_path = AstNode::Path {
4579                                        steps: steps[1..].to_vec(),
4580                                    };
4581                                    result = self.evaluate_internal(&remaining_path, &result)?;
4582                                }
4583
4584                                // Unwrap singleton results from chain operator, unless there are stages
4585                                // Stages (like predicates) indicate we want to preserve array structure
4586                                if !first_step.stages.is_empty() || steps.len() > 1 {
4587                                    return Ok(result);
4588                                } else {
4589                                    return Ok(self.unwrap_singleton(result));
4590                                }
4591                            }
4592                            _ => {
4593                                // Other path types - just evaluate normally with LHS as context
4594                                let lhs_value = self.evaluate_internal(lhs, data)?;
4595                                return self
4596                                    .evaluate_internal(rhs, &lhs_value)
4597                                    .map(|v| self.unwrap_singleton(v));
4598                            }
4599                        }
4600                    }
4601
4602                    // Empty path? Shouldn't happen, but handle it
4603                    let lhs_value = self.evaluate_internal(lhs, data)?;
4604                    return self
4605                        .evaluate_internal(rhs, &lhs_value)
4606                        .map(|v| self.unwrap_singleton(v));
4607                }
4608                _ => {
4609                    return Err(EvaluatorError::TypeError(
4610                        "Right side of ~> must be a function call or function reference"
4611                            .to_string(),
4612                    ));
4613                }
4614            }
4615        }
4616
4617        // Special handling for variable binding (:=)
4618        if op == BinaryOp::ColonEqual {
4619            // Extract variable name from LHS
4620            let var_name = match lhs {
4621                AstNode::Variable(name) => name.clone(),
4622                _ => {
4623                    return Err(EvaluatorError::TypeError(
4624                        "Left side of := must be a variable".to_string(),
4625                    ))
4626                }
4627            };
4628
4629            // Check if RHS is a lambda - store it specially
4630            if let AstNode::Lambda {
4631                params,
4632                body,
4633                signature,
4634                thunk,
4635            } = rhs
4636            {
4637                // Store the lambda AST for later invocation
4638                // Capture only the free variables referenced by the lambda body
4639                let captured_env = self.capture_environment_for(body, params);
4640                let compiled_body = if !thunk {
4641                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
4642                    try_compile_expr_with_allowed_vars(body, &var_refs)
4643                } else {
4644                    None
4645                };
4646                let stored_lambda = StoredLambda {
4647                    params: params.clone(),
4648                    body: (**body).clone(),
4649                    compiled_body,
4650                    signature: signature.clone(),
4651                    captured_env,
4652                    captured_data: Some(data.clone()),
4653                    thunk: *thunk,
4654                };
4655                let lambda_params = stored_lambda.params.clone();
4656                let lambda_sig = stored_lambda.signature.clone();
4657                self.context.bind_lambda(var_name.clone(), stored_lambda);
4658
4659                // Return a lambda marker value (include _lambda_id so it can be found later)
4660                let lambda_repr = JValue::lambda(
4661                    var_name.as_str(),
4662                    lambda_params,
4663                    Some(var_name.clone()),
4664                    lambda_sig,
4665                );
4666                return Ok(lambda_repr);
4667            }
4668
4669            // Check if RHS is a pure function composition (ChainPipe between function references)
4670            // e.g., $uppertrim := $trim ~> $uppercase
4671            // This creates a lambda that composes the functions.
4672            // But NOT for data ~> function, which should be evaluated immediately.
4673            // e.g., $result := data ~> $map($fn) should evaluate the pipe
4674            if let AstNode::Binary {
4675                op: BinaryOp::ChainPipe,
4676                lhs: chain_lhs,
4677                rhs: chain_rhs,
4678            } = rhs
4679            {
4680                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
4681                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
4682                let is_function_composition = match chain_lhs.as_ref() {
4683                    // LHS is a function reference like $trim or $sum
4684                    AstNode::Variable(name)
4685                        if self.is_builtin_function(name)
4686                            || self.context.lookup_lambda(name).is_some() =>
4687                    {
4688                        true
4689                    }
4690                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
4691                    AstNode::Binary {
4692                        op: BinaryOp::ChainPipe,
4693                        ..
4694                    } => true,
4695                    // A function call with placeholder creates a partial application
4696                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
4697                    AstNode::Function { args, .. }
4698                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
4699                    {
4700                        true
4701                    }
4702                    // Anything else (data, function calls, arrays, etc.) is not pure composition
4703                    _ => false,
4704                };
4705
4706                if is_function_composition {
4707                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
4708                    // The original chain is $trim ~> $uppercase (left-associative)
4709                    // We want to create: ($ ~> $trim) ~> $uppercase
4710                    let param_name = "$".to_string();
4711
4712                    // First create $ ~> $trim
4713                    let first_pipe = AstNode::Binary {
4714                        op: BinaryOp::ChainPipe,
4715                        lhs: Box::new(AstNode::Variable(param_name.clone())),
4716                        rhs: chain_lhs.clone(),
4717                    };
4718
4719                    // Then wrap with ~> $uppercase (or the rest of the chain)
4720                    let composed_body = AstNode::Binary {
4721                        op: BinaryOp::ChainPipe,
4722                        lhs: Box::new(first_pipe),
4723                        rhs: chain_rhs.clone(),
4724                    };
4725
4726                    let stored_lambda = StoredLambda {
4727                        params: vec![param_name],
4728                        body: composed_body,
4729                        compiled_body: None, // ChainPipe body is not compilable
4730                        signature: None,
4731                        captured_env: self.capture_current_environment(),
4732                        captured_data: Some(data.clone()),
4733                        thunk: false,
4734                    };
4735                    self.context.bind_lambda(var_name.clone(), stored_lambda);
4736
4737                    // Return a lambda marker value (include _lambda_id for later lookup)
4738                    let lambda_repr = JValue::lambda(
4739                        var_name.as_str(),
4740                        vec!["$".to_string()],
4741                        Some(var_name.clone()),
4742                        None::<String>,
4743                    );
4744                    return Ok(lambda_repr);
4745                }
4746                // If not function composition, fall through to normal evaluation below
4747            }
4748
4749            // Evaluate the RHS
4750            let value = self.evaluate_internal(rhs, data)?;
4751
4752            // If the value is a lambda, copy the stored lambda to the new variable name
4753            if let Some(stored) = self.lookup_lambda_from_value(&value) {
4754                self.context.bind_lambda(var_name.clone(), stored);
4755            }
4756
4757            // Bind even if undefined (null) so inner scopes can shadow outer variables
4758            self.context.bind(var_name, value.clone());
4759            return Ok(value);
4760        }
4761
4762        // Special handling for 'In' operator - check for array filtering
4763        // Must evaluate lhs first to determine if this is array filtering
4764        if op == BinaryOp::In {
4765            let left = self.evaluate_internal(lhs, data)?;
4766
4767            // Check if this is array filtering: array[predicate]
4768            if matches!(left, JValue::Array(_)) {
4769                // Try evaluating rhs in current context to see if it's a simple index
4770                let right_result = self.evaluate_internal(rhs, data);
4771
4772                if let Ok(JValue::Number(_)) = right_result {
4773                    // Simple numeric index: array[n]
4774                    return self.array_index(&left, &right_result.unwrap());
4775                } else {
4776                    // This is array filtering: array[predicate]
4777                    // Evaluate the predicate for each array item
4778                    return self.array_filter(lhs, rhs, &left, data);
4779                }
4780            }
4781        }
4782
4783        // Special handling for logical operators (short-circuit evaluation)
4784        if op == BinaryOp::And {
4785            let left = self.evaluate_internal(lhs, data)?;
4786            if !self.is_truthy(&left) {
4787                // Short-circuit: if left is falsy, return false without evaluating right
4788                return Ok(JValue::Bool(false));
4789            }
4790            let right = self.evaluate_internal(rhs, data)?;
4791            return Ok(JValue::Bool(self.is_truthy(&right)));
4792        }
4793
4794        if op == BinaryOp::Or {
4795            let left = self.evaluate_internal(lhs, data)?;
4796            if self.is_truthy(&left) {
4797                // Short-circuit: if left is truthy, return true without evaluating right
4798                return Ok(JValue::Bool(true));
4799            }
4800            let right = self.evaluate_internal(rhs, data)?;
4801            return Ok(JValue::Bool(self.is_truthy(&right)));
4802        }
4803
4804        // Check if operands are explicit null literals (vs undefined from variables)
4805        let left_is_explicit_null = matches!(lhs, AstNode::Null);
4806        let right_is_explicit_null = matches!(rhs, AstNode::Null);
4807
4808        // Standard evaluation: evaluate both operands
4809        let left = self.evaluate_internal(lhs, data)?;
4810        let right = self.evaluate_internal(rhs, data)?;
4811
4812        match op {
4813            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
4814            BinaryOp::Subtract => {
4815                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
4816            }
4817            BinaryOp::Multiply => {
4818                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
4819            }
4820            BinaryOp::Divide => {
4821                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
4822            }
4823            BinaryOp::Modulo => {
4824                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
4825            }
4826
4827            BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
4828            BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
4829            BinaryOp::LessThan => {
4830                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
4831            }
4832            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
4833                &left,
4834                &right,
4835                left_is_explicit_null,
4836                right_is_explicit_null,
4837            ),
4838            BinaryOp::GreaterThan => {
4839                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
4840            }
4841            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
4842                &left,
4843                &right,
4844                left_is_explicit_null,
4845                right_is_explicit_null,
4846            ),
4847
4848            // And/Or handled above with short-circuit evaluation
4849            BinaryOp::And | BinaryOp::Or => unreachable!(),
4850
4851            BinaryOp::Concatenate => self.concatenate(&left, &right),
4852            BinaryOp::Range => self.range(&left, &right),
4853            BinaryOp::In => self.in_operator(&left, &right),
4854
4855            // These operators are all handled as special cases earlier in evaluate_binary_op
4856            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
4857                unreachable!()
4858            }
4859        }
4860    }
4861
4862    /// Evaluate a unary operation
4863    fn evaluate_unary_op(
4864        &mut self,
4865        op: crate::ast::UnaryOp,
4866        operand: &AstNode,
4867        data: &JValue,
4868    ) -> Result<JValue, EvaluatorError> {
4869        use crate::ast::UnaryOp;
4870
4871        let value = self.evaluate_internal(operand, data)?;
4872
4873        match op {
4874            UnaryOp::Negate => match value {
4875                // undefined returns undefined
4876                JValue::Null => Ok(JValue::Null),
4877                JValue::Number(n) => Ok(JValue::Number(-n)),
4878                _ => Err(EvaluatorError::TypeError(
4879                    "D1002: Cannot negate non-number value".to_string(),
4880                )),
4881            },
4882            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
4883        }
4884    }
4885
4886    /// Try to fuse an aggregate function call with its Path argument.
4887    /// Handles patterns like:
4888    /// - $sum(arr.field) → iterate arr, extract field, accumulate
4889    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
4890    ///
4891    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
4892    fn try_fused_aggregate(
4893        &mut self,
4894        name: &str,
4895        arg: &AstNode,
4896        data: &JValue,
4897    ) -> Result<Option<JValue>, EvaluatorError> {
4898        // Only applies to numeric aggregates
4899        if !matches!(name, "sum" | "max" | "min" | "average") {
4900            return Ok(None);
4901        }
4902
4903        // Argument must be a Path
4904        let AstNode::Path { steps } = arg else {
4905            return Ok(None);
4906        };
4907
4908        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
4909        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
4910        if steps.len() != 2 {
4911            return Ok(None);
4912        }
4913
4914        // Last step must be a simple Name (the field to extract)
4915        let field_step = &steps[1];
4916        if !field_step.stages.is_empty() {
4917            return Ok(None);
4918        }
4919        let AstNode::Name(extract_field) = &field_step.node else {
4920            return Ok(None);
4921        };
4922
4923        // First step: Name with optional filter stage
4924        let arr_step = &steps[0];
4925        let AstNode::Name(arr_name) = &arr_step.node else {
4926            return Ok(None);
4927        };
4928
4929        // Get the source array from data
4930        let arr = match data {
4931            JValue::Object(obj) => match obj.get(arr_name) {
4932                Some(JValue::Array(arr)) => arr,
4933                _ => return Ok(None),
4934            },
4935            _ => return Ok(None),
4936        };
4937
4938        // Check for filter stage — try CompiledExpr for the predicate
4939        let filter_compiled = match arr_step.stages.as_slice() {
4940            [] => None,
4941            [Stage::Filter(pred)] => try_compile_expr(pred),
4942            _ => return Ok(None),
4943        };
4944        // If filter stage exists but wasn't compilable, bail out
4945        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
4946            return Ok(None);
4947        }
4948
4949        // Build shape cache for the array
4950        let shape = arr.first().and_then(build_shape_cache);
4951
4952        // Fused iteration: filter (optional) + extract + aggregate
4953        let mut total = 0.0f64;
4954        let mut count = 0usize;
4955        let mut max_val = f64::NEG_INFINITY;
4956        let mut min_val = f64::INFINITY;
4957        let mut has_any = false;
4958
4959        for item in arr.iter() {
4960            // Apply compiled filter if present
4961            if let Some(ref compiled) = filter_compiled {
4962                let result = if let Some(ref s) = shape {
4963                    eval_compiled_shaped(compiled, item, None, s)?
4964                } else {
4965                    eval_compiled(compiled, item, None)?
4966                };
4967                if !compiled_is_truthy(&result) {
4968                    continue;
4969                }
4970            }
4971
4972            // Extract field value
4973            let val = match item {
4974                JValue::Object(obj) => match obj.get(extract_field) {
4975                    Some(JValue::Number(n)) => *n,
4976                    Some(_) | None => continue, // Skip non-numeric / missing
4977                },
4978                _ => continue,
4979            };
4980
4981            has_any = true;
4982            match name {
4983                "sum" => total += val,
4984                "max" => max_val = max_val.max(val),
4985                "min" => min_val = min_val.min(val),
4986                "average" => { total += val; count += 1; }
4987                _ => unreachable!(),
4988            }
4989        }
4990
4991        if !has_any {
4992            return Ok(Some(match name {
4993                "sum" => JValue::from(0i64),
4994                "average" | "max" | "min" => JValue::Null,
4995                _ => unreachable!(),
4996            }));
4997        }
4998
4999        Ok(Some(match name {
5000            "sum" => JValue::Number(total),
5001            "max" => JValue::Number(max_val),
5002            "min" => JValue::Number(min_val),
5003            "average" => JValue::Number(total / count as f64),
5004            _ => unreachable!(),
5005        }))
5006    }
5007
5008    /// Evaluate a function call
5009    fn evaluate_function_call(
5010        &mut self,
5011        name: &str,
5012        args: &[AstNode],
5013        is_builtin: bool,
5014        data: &JValue,
5015    ) -> Result<JValue, EvaluatorError> {
5016        use crate::functions;
5017
5018        // Check for partial application (any argument is a Placeholder)
5019        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
5020        if has_placeholder {
5021            return self.create_partial_application(name, args, is_builtin, data);
5022        }
5023
5024        // FIRST check if this variable holds a function value (lambda or builtin reference)
5025        // This is critical for:
5026        // 1. Allowing function parameters to shadow stored lambdas
5027        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
5028        // 2. Calling built-in functions passed as parameters
5029        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
5030        if let Some(value) = self.context.lookup(name).cloned() {
5031            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
5032                let mut evaluated_args = Vec::with_capacity(args.len());
5033                for arg in args {
5034                    evaluated_args.push(self.evaluate_internal(arg, data)?);
5035                }
5036                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
5037            }
5038            if let JValue::Builtin { name: builtin_name } = &value {
5039                // This is a built-in function reference (e.g., $f bound to $sum)
5040                let mut evaluated_args = Vec::with_capacity(args.len());
5041                for arg in args {
5042                    evaluated_args.push(self.evaluate_internal(arg, data)?);
5043                }
5044                return self.call_builtin_with_values(builtin_name, &evaluated_args);
5045            }
5046        }
5047
5048        // THEN check if this is a stored lambda (user-defined function by name)
5049        // This only applies if not shadowed by a binding above
5050        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
5051            let mut evaluated_args = Vec::with_capacity(args.len());
5052            for arg in args {
5053                evaluated_args.push(self.evaluate_internal(arg, data)?);
5054            }
5055            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
5056        }
5057
5058        // If the function was called without $ prefix and it's not a stored lambda,
5059        // it's an error (unknown function without $ prefix)
5060        if !is_builtin && name != "__lambda__" {
5061            return Err(EvaluatorError::ReferenceError(format!(
5062                "Unknown function: {}",
5063                name
5064            )));
5065        }
5066
5067        // Special handling for $exists function
5068        // It needs to know if the argument is explicit null vs undefined
5069        if name == "exists" && args.len() == 1 {
5070            let arg = &args[0];
5071
5072            // Check if it's an explicit null literal
5073            if matches!(arg, AstNode::Null) {
5074                return Ok(JValue::Bool(true)); // Explicit null exists
5075            }
5076
5077            // Check if it's a function reference
5078            if let AstNode::Variable(var_name) = arg {
5079                if self.is_builtin_function(var_name) {
5080                    return Ok(JValue::Bool(true)); // Built-in function exists
5081                }
5082
5083                // Check if it's a stored lambda
5084                if self.context.lookup_lambda(var_name).is_some() {
5085                    return Ok(JValue::Bool(true)); // Lambda exists
5086                }
5087
5088                // Check if the variable is defined
5089                if let Some(val) = self.context.lookup(var_name) {
5090                    // A variable bound to the undefined marker doesn't "exist"
5091                    if val.is_undefined() {
5092                        return Ok(JValue::Bool(false));
5093                    }
5094                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
5095                } else {
5096                    return Ok(JValue::Bool(false)); // Variable is undefined
5097                }
5098            }
5099
5100            // For other expressions, evaluate and check if non-null/non-undefined
5101            let value = self.evaluate_internal(arg, data)?;
5102            return Ok(JValue::Bool(
5103                !value.is_null() && !value.is_undefined(),
5104            ));
5105        }
5106
5107        // Check if any arguments are undefined variables or undefined paths
5108        // Functions like $not() should return undefined when given undefined values
5109        for arg in args {
5110            // Check for undefined variable (e.g., $undefined_var)
5111            if let AstNode::Variable(var_name) = arg {
5112                // Skip built-in function names - they're function references, not undefined variables
5113                if !var_name.is_empty()
5114                    && !self.is_builtin_function(var_name)
5115                    && self.context.lookup(var_name).is_none()
5116                {
5117                    // Undefined variable - for functions that should propagate undefined
5118                    if propagates_undefined(name) {
5119                        return Ok(JValue::Null); // Return undefined
5120                    }
5121                }
5122            }
5123            // Check for simple field name (e.g., blah) that evaluates to undefined
5124            if let AstNode::Name(field_name) = arg {
5125                let field_exists =
5126                    matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
5127                if !field_exists && propagates_undefined(name) {
5128                    return Ok(JValue::Null);
5129                }
5130            }
5131            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
5132            // Field accesses are represented as AstNode::Path. String literals should never
5133            // be checked for undefined propagation.
5134            // Check for Path expressions that evaluate to undefined
5135            if let AstNode::Path { steps } = arg {
5136                // For paths that evaluate to null, we need to determine if it's because:
5137                // 1. A field doesn't exist (undefined) - should propagate as undefined
5138                // 2. A field exists with value null - should throw T0410
5139                //
5140                // We can distinguish these by checking if the path is accessing a field
5141                // that doesn't exist on an object vs one that has an explicit null value.
5142                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
5143                    // Path evaluated to null - now check if it's truly undefined
5144                    // For single-step paths, check if the field exists
5145                    if steps.len() == 1 {
5146                        // Get field name - could be Name (identifier) or String (quoted)
5147                        let field_name = match &steps[0].node {
5148                            AstNode::Name(n) => Some(n.as_str()),
5149                            AstNode::String(s) => Some(s.as_str()),
5150                            _ => None,
5151                        };
5152                        if let Some(field) = field_name {
5153                            match data {
5154                                JValue::Object(obj) => {
5155                                    if !obj.contains_key(field) {
5156                                        // Field doesn't exist - return undefined
5157                                        if propagates_undefined(name) {
5158                                            return Ok(JValue::Null);
5159                                        }
5160                                    }
5161                                    // Field exists with value null - continue to throw T0410
5162                                }
5163                                JValue::Null => {
5164                                    // Trying to access field on null data - return undefined
5165                                    if propagates_undefined(name) {
5166                                        return Ok(JValue::Null);
5167                                    }
5168                                }
5169                                _ => {}
5170                            }
5171                        }
5172                    }
5173                    // For multi-step paths, check if any intermediate step failed
5174                    else if steps.len() > 1 {
5175                        // Evaluate each step to find where it breaks
5176                        let mut current = data;
5177                        let mut failed_due_to_missing_field = false;
5178
5179                        for (i, step) in steps.iter().enumerate() {
5180                            if let AstNode::Name(field_name) = &step.node {
5181                                match current {
5182                                    JValue::Object(obj) => {
5183                                        if let Some(val) = obj.get(field_name) {
5184                                            current = val;
5185                                        } else {
5186                                            // Field doesn't exist
5187                                            failed_due_to_missing_field = true;
5188                                            break;
5189                                        }
5190                                    }
5191                                    JValue::Array(_) => {
5192                                        // Array access - evaluate normally
5193                                        break;
5194                                    }
5195                                    JValue::Null => {
5196                                        // Hit null in the middle of the path
5197                                        if i > 0 {
5198                                            // Previous field had null value - not undefined
5199                                            failed_due_to_missing_field = false;
5200                                        }
5201                                        break;
5202                                    }
5203                                    _ => break,
5204                                }
5205                            }
5206                        }
5207
5208                        if failed_due_to_missing_field && propagates_undefined(name) {
5209                            return Ok(JValue::Null);
5210                        }
5211                    }
5212                }
5213            }
5214        }
5215
5216        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
5217        // try to fuse filter+extract+aggregate into a single pass.
5218        if args.len() == 1 {
5219            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
5220                return Ok(result);
5221            }
5222        }
5223
5224        let mut evaluated_args = Vec::with_capacity(args.len());
5225        for arg in args {
5226            evaluated_args.push(self.evaluate_internal(arg, data)?);
5227        }
5228
5229        // JSONata feature: when a function is called with no arguments but expects
5230        // at least one, use the current context value (data) as the implicit first argument
5231        // This also applies when functions expecting N arguments receive N-1 arguments,
5232        // in which case the context value becomes the first argument
5233        let context_functions_zero_arg = ["string", "number", "boolean", "uppercase", "lowercase"];
5234        let context_functions_missing_first = [
5235            "substringBefore",
5236            "substringAfter",
5237            "contains",
5238            "split",
5239            "replace",
5240        ];
5241
5242        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
5243            // Use the current context value as the implicit argument
5244            evaluated_args.push(data.clone());
5245        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
5246            // These functions expect 2+ arguments, but received 1
5247            // Only insert context if it's a compatible type (string for string functions)
5248            // Otherwise, let the function throw T0411 for wrong argument count
5249            if matches!(data, JValue::String(_)) {
5250                evaluated_args.insert(0, data.clone());
5251            }
5252        }
5253
5254        // Special handling for $string() with no explicit arguments
5255        // After context insertion, check if the argument is null (undefined context)
5256        if name == "string"
5257            && args.is_empty()
5258            && !evaluated_args.is_empty()
5259            && evaluated_args[0].is_null()
5260        {
5261            // Context was null/undefined, so return undefined
5262            return Ok(JValue::Null);
5263        }
5264
5265        match name {
5266            "string" => {
5267                if evaluated_args.len() > 2 {
5268                    return Err(EvaluatorError::EvaluationError(
5269                        "string() takes at most 2 arguments".to_string(),
5270                    ));
5271                }
5272
5273                let prettify = if evaluated_args.len() == 2 {
5274                    match &evaluated_args[1] {
5275                        JValue::Bool(b) => Some(*b),
5276                        _ => {
5277                            return Err(EvaluatorError::TypeError(
5278                                "string() prettify parameter must be a boolean".to_string(),
5279                            ))
5280                        }
5281                    }
5282                } else {
5283                    None
5284                };
5285
5286                Ok(functions::string::string(&evaluated_args[0], prettify)?)
5287            }
5288            "length" => {
5289                if evaluated_args.len() != 1 {
5290                    return Err(EvaluatorError::EvaluationError(
5291                        "length() requires exactly 1 argument".to_string(),
5292                    ));
5293                }
5294                match &evaluated_args[0] {
5295                    JValue::String(s) => Ok(functions::string::length(s)?),
5296                    _ => Err(EvaluatorError::TypeError(
5297                        "T0410: Argument 1 of function length does not match function signature"
5298                            .to_string(),
5299                    )),
5300                }
5301            }
5302            "uppercase" => {
5303                if evaluated_args.len() != 1 {
5304                    return Err(EvaluatorError::EvaluationError(
5305                        "uppercase() requires exactly 1 argument".to_string(),
5306                    ));
5307                }
5308                match &evaluated_args[0] {
5309                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
5310                    _ => Err(EvaluatorError::TypeError(
5311                        "T0410: Argument 1 of function uppercase does not match function signature"
5312                            .to_string(),
5313                    )),
5314                }
5315            }
5316            "lowercase" => {
5317                if evaluated_args.len() != 1 {
5318                    return Err(EvaluatorError::EvaluationError(
5319                        "lowercase() requires exactly 1 argument".to_string(),
5320                    ));
5321                }
5322                match &evaluated_args[0] {
5323                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
5324                    _ => Err(EvaluatorError::TypeError(
5325                        "T0410: Argument 1 of function lowercase does not match function signature"
5326                            .to_string(),
5327                    )),
5328                }
5329            }
5330            "number" => {
5331                if evaluated_args.is_empty() {
5332                    return Err(EvaluatorError::EvaluationError(
5333                        "number() requires at least 1 argument".to_string(),
5334                    ));
5335                }
5336                if evaluated_args.len() > 1 {
5337                    return Err(EvaluatorError::TypeError(
5338                        "T0410: Argument 2 of function number does not match function signature"
5339                            .to_string(),
5340                    ));
5341                }
5342                Ok(functions::numeric::number(&evaluated_args[0])?)
5343            }
5344            "sum" => {
5345                if evaluated_args.len() != 1 {
5346                    return Err(EvaluatorError::EvaluationError(
5347                        "sum() requires exactly 1 argument".to_string(),
5348                    ));
5349                }
5350                // Return undefined if argument is undefined
5351                if evaluated_args[0].is_undefined() {
5352                    return Ok(JValue::Undefined);
5353                }
5354                match &evaluated_args[0] {
5355                    JValue::Null => Ok(JValue::Null),
5356                    JValue::Array(arr) => {
5357                        // Use zero-clone iterator-based sum
5358                        Ok(aggregation::sum(arr)?)
5359                    }
5360                    // Non-array values: extract number directly
5361                    JValue::Number(n) => Ok(JValue::Number(*n)),
5362                    other => Ok(functions::numeric::sum(&[other.clone()])?),
5363                }
5364            }
5365            "count" => {
5366                if evaluated_args.len() != 1 {
5367                    return Err(EvaluatorError::EvaluationError(
5368                        "count() requires exactly 1 argument".to_string(),
5369                    ));
5370                }
5371                // Return 0 if argument is undefined
5372                if evaluated_args[0].is_undefined() {
5373                    return Ok(JValue::from(0i64));
5374                }
5375                match &evaluated_args[0] {
5376                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
5377                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
5378                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
5379                }
5380            }
5381            "substring" => {
5382                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
5383                    return Err(EvaluatorError::EvaluationError(
5384                        "substring() requires 2 or 3 arguments".to_string(),
5385                    ));
5386                }
5387                match (&evaluated_args[0], &evaluated_args[1]) {
5388                    (JValue::String(s), JValue::Number(start)) => {
5389                        let length = if evaluated_args.len() == 3 {
5390                            match &evaluated_args[2] {
5391                                JValue::Number(l) => Some(*l as i64),
5392                                _ => return Err(EvaluatorError::TypeError(
5393                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
5394                                )),
5395                            }
5396                        } else {
5397                            None
5398                        };
5399                        Ok(functions::string::substring(s, *start as i64, length)?)
5400                    }
5401                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5402                        "T0410: Argument 2 of function substring does not match function signature"
5403                            .to_string(),
5404                    )),
5405                    _ => Err(EvaluatorError::TypeError(
5406                        "T0410: Argument 1 of function substring does not match function signature"
5407                            .to_string(),
5408                    )),
5409                }
5410            }
5411            "substringBefore" => {
5412                if evaluated_args.len() != 2 {
5413                    return Err(EvaluatorError::TypeError(
5414                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
5415                    ));
5416                }
5417                match (&evaluated_args[0], &evaluated_args[1]) {
5418                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
5419                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5420                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
5421                    )),
5422                    _ => Err(EvaluatorError::TypeError(
5423                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
5424                    )),
5425                }
5426            }
5427            "substringAfter" => {
5428                if evaluated_args.len() != 2 {
5429                    return Err(EvaluatorError::TypeError(
5430                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
5431                    ));
5432                }
5433                match (&evaluated_args[0], &evaluated_args[1]) {
5434                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
5435                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5436                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
5437                    )),
5438                    _ => Err(EvaluatorError::TypeError(
5439                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
5440                    )),
5441                }
5442            }
5443            "pad" => {
5444                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
5445                    return Err(EvaluatorError::EvaluationError(
5446                        "pad() requires 2 or 3 arguments".to_string(),
5447                    ));
5448                }
5449
5450                // First argument: string to pad
5451                let string = match &evaluated_args[0] {
5452                    JValue::String(s) => s.clone(),
5453                    JValue::Null => return Ok(JValue::Null),
5454                    _ => {
5455                        return Err(EvaluatorError::TypeError(
5456                            "pad() first argument must be a string".to_string(),
5457                        ))
5458                    }
5459                };
5460
5461                // Second argument: width (negative = left pad, positive = right pad)
5462                let width = match &evaluated_args.get(1) {
5463                    Some(JValue::Number(n)) => *n as i32,
5464                    _ => {
5465                        return Err(EvaluatorError::TypeError(
5466                            "pad() second argument must be a number".to_string(),
5467                        ))
5468                    }
5469                };
5470
5471                // Third argument: padding string (optional, defaults to space)
5472                let pad_string = match evaluated_args.get(2) {
5473                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
5474                    _ => Rc::from(" "),
5475                };
5476
5477                let abs_width = width.unsigned_abs() as usize;
5478                // Count Unicode characters (code points), not bytes
5479                let char_count = string.chars().count();
5480
5481                if char_count >= abs_width {
5482                    // String is already long enough
5483                    return Ok(JValue::string(string));
5484                }
5485
5486                let padding_needed = abs_width - char_count;
5487
5488                let pad_chars: Vec<char> = pad_string.chars().collect();
5489                let mut padding = String::with_capacity(padding_needed);
5490                for i in 0..padding_needed {
5491                    padding.push(pad_chars[i % pad_chars.len()]);
5492                }
5493
5494                let result = if width < 0 {
5495                    // Left pad (negative width)
5496                    format!("{}{}", padding, string)
5497                } else {
5498                    // Right pad (positive width)
5499                    format!("{}{}", string, padding)
5500                };
5501
5502                Ok(JValue::string(result))
5503            }
5504
5505            "trim" => {
5506                if evaluated_args.is_empty() {
5507                    return Ok(JValue::Null); // undefined
5508                }
5509                if evaluated_args.len() != 1 {
5510                    return Err(EvaluatorError::EvaluationError(
5511                        "trim() requires at most 1 argument".to_string(),
5512                    ));
5513                }
5514                match &evaluated_args[0] {
5515                    JValue::Null => Ok(JValue::Null),
5516                    JValue::String(s) => Ok(functions::string::trim(s)?),
5517                    _ => Err(EvaluatorError::TypeError(
5518                        "trim() requires a string argument".to_string(),
5519                    )),
5520                }
5521            }
5522            "contains" => {
5523                if evaluated_args.len() != 2 {
5524                    return Err(EvaluatorError::EvaluationError(
5525                        "contains() requires exactly 2 arguments".to_string(),
5526                    ));
5527                }
5528                if evaluated_args[0].is_null() {
5529                    return Ok(JValue::Null);
5530                }
5531                match &evaluated_args[0] {
5532                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
5533                    _ => Err(EvaluatorError::TypeError(
5534                        "contains() requires a string as the first argument".to_string(),
5535                    )),
5536                }
5537            }
5538            "split" => {
5539                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
5540                    return Err(EvaluatorError::EvaluationError(
5541                        "split() requires 2 or 3 arguments".to_string(),
5542                    ));
5543                }
5544                if evaluated_args[0].is_null() {
5545                    return Ok(JValue::Null);
5546                }
5547                match &evaluated_args[0] {
5548                    JValue::String(s) => {
5549                        let limit = if evaluated_args.len() == 3 {
5550                            match &evaluated_args[2] {
5551                                JValue::Number(n) => {
5552                                    let f = *n;
5553                                    // Negative limit is an error
5554                                    if f < 0.0 {
5555                                        return Err(EvaluatorError::EvaluationError(
5556                                            "D3020: Third argument of split function must be a positive number".to_string(),
5557                                        ));
5558                                    }
5559                                    // Floor the value for non-integer limits
5560                                    Some(f.floor() as usize)
5561                                }
5562                                _ => {
5563                                    return Err(EvaluatorError::TypeError(
5564                                        "split() limit must be a number".to_string(),
5565                                    ))
5566                                }
5567                            }
5568                        } else {
5569                            None
5570                        };
5571                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
5572                    }
5573                    _ => Err(EvaluatorError::TypeError(
5574                        "split() requires a string as the first argument".to_string(),
5575                    )),
5576                }
5577            }
5578            "join" => {
5579                // Special case: if first arg is undefined, return undefined
5580                // But if separator (2nd arg) is undefined, use empty string (default)
5581                if evaluated_args.is_empty() {
5582                    return Err(EvaluatorError::TypeError(
5583                        "T0410: Argument 1 of function $join does not match function signature"
5584                            .to_string(),
5585                    ));
5586                }
5587                if evaluated_args[0].is_null() {
5588                    return Ok(JValue::Null);
5589                }
5590
5591                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
5592                // The signature handles coercion and validation
5593                use crate::signature::Signature;
5594
5595                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
5596                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
5597                })?;
5598
5599                let coerced_args = match signature.validate_and_coerce(&evaluated_args) {
5600                    Ok(args) => args,
5601                    Err(crate::signature::SignatureError::UndefinedArgument) => {
5602                        // This can happen if the separator is undefined
5603                        // In that case, just validate the first arg and use default separator
5604                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
5605                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
5606                        })?;
5607
5608                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1]) {
5609                            Ok(args) => args,
5610                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
5611                                index,
5612                                expected,
5613                            }) => {
5614                                return Err(EvaluatorError::TypeError(format!(
5615                                    "T0412: Argument {} of function $join must be an array of {}",
5616                                    index, expected
5617                                )));
5618                            }
5619                            Err(e) => {
5620                                return Err(EvaluatorError::TypeError(format!(
5621                                    "Signature validation failed: {}",
5622                                    e
5623                                )));
5624                            }
5625                        }
5626                    }
5627                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
5628                        index,
5629                        expected,
5630                    }) => {
5631                        return Err(EvaluatorError::TypeError(
5632                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
5633                        ));
5634                    }
5635                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
5636                        index,
5637                        expected,
5638                    }) => {
5639                        return Err(EvaluatorError::TypeError(format!(
5640                            "T0412: Argument {} of function $join must be an array of {}",
5641                            index, expected
5642                        )));
5643                    }
5644                    Err(e) => {
5645                        return Err(EvaluatorError::TypeError(format!(
5646                            "Signature validation failed: {}",
5647                            e
5648                        )));
5649                    }
5650                };
5651
5652                // After coercion, first arg is guaranteed to be an array of strings
5653                match &coerced_args[0] {
5654                    JValue::Array(arr) => {
5655                        let separator = if coerced_args.len() == 2 {
5656                            match &coerced_args[1] {
5657                                JValue::String(s) => Some(&**s),
5658                                JValue::Null => None, // Undefined separator -> use empty string
5659                                _ => None,            // Signature should have validated this
5660                            }
5661                        } else {
5662                            None // No separator provided -> use empty string
5663                        };
5664                        Ok(functions::string::join(arr, separator)?)
5665                    }
5666                    JValue::Null => Ok(JValue::Null),
5667                    _ => unreachable!("Signature validation should ensure array type"),
5668                }
5669            }
5670            "replace" => {
5671                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
5672                    return Err(EvaluatorError::EvaluationError(
5673                        "replace() requires 3 or 4 arguments".to_string(),
5674                    ));
5675                }
5676                if evaluated_args[0].is_null() {
5677                    return Ok(JValue::Null);
5678                }
5679
5680                // Check if replacement (3rd arg) is a function/lambda
5681                let replacement_is_lambda = matches!(
5682                    evaluated_args[2],
5683                    JValue::Lambda { .. } | JValue::Builtin { .. }
5684                );
5685
5686                if replacement_is_lambda {
5687                    // Lambda replacement mode
5688                    return self.replace_with_lambda(
5689                        &evaluated_args[0],
5690                        &evaluated_args[1],
5691                        &evaluated_args[2],
5692                        if evaluated_args.len() == 4 {
5693                            Some(&evaluated_args[3])
5694                        } else {
5695                            None
5696                        },
5697                        data,
5698                    );
5699                }
5700
5701                // String replacement mode
5702                match (&evaluated_args[0], &evaluated_args[2]) {
5703                    (JValue::String(s), JValue::String(replacement)) => {
5704                        let limit = if evaluated_args.len() == 4 {
5705                            match &evaluated_args[3] {
5706                                JValue::Number(n) => {
5707                                    let lim_f64 = *n;
5708                                    if lim_f64 < 0.0 {
5709                                        return Err(EvaluatorError::EvaluationError(format!(
5710                                            "D3011: Limit must be non-negative, got {}",
5711                                            lim_f64
5712                                        )));
5713                                    }
5714                                    Some(lim_f64 as usize)
5715                                }
5716                                _ => {
5717                                    return Err(EvaluatorError::TypeError(
5718                                        "replace() limit must be a number".to_string(),
5719                                    ))
5720                                }
5721                            }
5722                        } else {
5723                            None
5724                        };
5725                        Ok(functions::string::replace(
5726                            s,
5727                            &evaluated_args[1],
5728                            replacement,
5729                            limit,
5730                        )?)
5731                    }
5732                    _ => Err(EvaluatorError::TypeError(
5733                        "replace() requires string arguments".to_string(),
5734                    )),
5735                }
5736            }
5737            "match" => {
5738                // $match(str, pattern [, limit])
5739                // Returns array of match objects for regex matches or custom matcher function
5740                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
5741                    return Err(EvaluatorError::EvaluationError(
5742                        "match() requires 1 to 3 arguments".to_string(),
5743                    ));
5744                }
5745                if evaluated_args[0].is_null() {
5746                    return Ok(JValue::Null);
5747                }
5748
5749                let s = match &evaluated_args[0] {
5750                    JValue::String(s) => s.clone(),
5751                    _ => {
5752                        return Err(EvaluatorError::TypeError(
5753                            "match() first argument must be a string".to_string(),
5754                        ))
5755                    }
5756                };
5757
5758                // Get optional limit
5759                let limit = if evaluated_args.len() == 3 {
5760                    match &evaluated_args[2] {
5761                        JValue::Number(n) => Some(*n as usize),
5762                        JValue::Null => None,
5763                        _ => {
5764                            return Err(EvaluatorError::TypeError(
5765                                "match() limit must be a number".to_string(),
5766                            ))
5767                        }
5768                    }
5769                } else {
5770                    None
5771                };
5772
5773                // Check if second argument is a custom matcher function (lambda)
5774                let pattern_value = evaluated_args.get(1);
5775                let is_custom_matcher = pattern_value.is_some_and(|val| {
5776                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
5777                });
5778
5779                if is_custom_matcher {
5780                    // Custom matcher function support
5781                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
5782                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
5783                }
5784
5785                // Get regex pattern from second argument
5786                let (pattern, flags) = match pattern_value {
5787                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
5788                        EvaluatorError::TypeError(
5789                            "match() second argument must be a regex pattern or matcher function"
5790                                .to_string(),
5791                        )
5792                    })?,
5793                    None => (".*".to_string(), "".to_string()),
5794                };
5795
5796                // Build regex
5797                let is_global = flags.contains('g');
5798                let regex_pattern = if flags.contains('i') {
5799                    format!("(?i){}", pattern)
5800                } else {
5801                    pattern.clone()
5802                };
5803
5804                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
5805                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
5806                })?;
5807
5808                let mut results = Vec::new();
5809                let mut count = 0;
5810
5811                for caps in re.captures_iter(&s) {
5812                    if let Some(lim) = limit {
5813                        if count >= lim {
5814                            break;
5815                        }
5816                    }
5817
5818                    let full_match = caps.get(0).unwrap();
5819                    let mut match_obj = IndexMap::new();
5820                    match_obj.insert(
5821                        "match".to_string(),
5822                        JValue::string(full_match.as_str().to_string()),
5823                    );
5824                    match_obj.insert(
5825                        "index".to_string(),
5826                        JValue::Number(full_match.start() as f64),
5827                    );
5828
5829                    // Collect capture groups
5830                    let mut groups: Vec<JValue> = Vec::new();
5831                    for i in 1..caps.len() {
5832                        if let Some(group) = caps.get(i) {
5833                            groups.push(JValue::string(group.as_str().to_string()));
5834                        } else {
5835                            groups.push(JValue::Null);
5836                        }
5837                    }
5838                    if !groups.is_empty() {
5839                        match_obj.insert("groups".to_string(), JValue::array(groups));
5840                    }
5841
5842                    results.push(JValue::object(match_obj));
5843                    count += 1;
5844
5845                    // If not global, only return first match
5846                    if !is_global {
5847                        break;
5848                    }
5849                }
5850
5851                if results.is_empty() {
5852                    Ok(JValue::Null)
5853                } else if results.len() == 1 && !is_global {
5854                    // Single match (non-global) returns the match object directly
5855                    Ok(results.into_iter().next().unwrap())
5856                } else {
5857                    Ok(JValue::array(results))
5858                }
5859            }
5860            "max" => {
5861                if evaluated_args.len() != 1 {
5862                    return Err(EvaluatorError::EvaluationError(
5863                        "max() requires exactly 1 argument".to_string(),
5864                    ));
5865                }
5866                // Check for undefined
5867                if evaluated_args[0].is_undefined() {
5868                    return Ok(JValue::Undefined);
5869                }
5870                match &evaluated_args[0] {
5871                    JValue::Null => Ok(JValue::Null),
5872                    JValue::Array(arr) => {
5873                        // Use zero-clone iterator-based max
5874                        Ok(aggregation::max(arr)?)
5875                    }
5876                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
5877                    _ => Err(EvaluatorError::TypeError(
5878                        "max() requires an array or number argument".to_string(),
5879                    )),
5880                }
5881            }
5882            "min" => {
5883                if evaluated_args.len() != 1 {
5884                    return Err(EvaluatorError::EvaluationError(
5885                        "min() requires exactly 1 argument".to_string(),
5886                    ));
5887                }
5888                // Check for undefined
5889                if evaluated_args[0].is_undefined() {
5890                    return Ok(JValue::Undefined);
5891                }
5892                match &evaluated_args[0] {
5893                    JValue::Null => Ok(JValue::Null),
5894                    JValue::Array(arr) => {
5895                        // Use zero-clone iterator-based min
5896                        Ok(aggregation::min(arr)?)
5897                    }
5898                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
5899                    _ => Err(EvaluatorError::TypeError(
5900                        "min() requires an array or number argument".to_string(),
5901                    )),
5902                }
5903            }
5904            "average" => {
5905                if evaluated_args.len() != 1 {
5906                    return Err(EvaluatorError::EvaluationError(
5907                        "average() requires exactly 1 argument".to_string(),
5908                    ));
5909                }
5910                // Return undefined if argument is undefined
5911                if evaluated_args[0].is_undefined() {
5912                    return Ok(JValue::Undefined);
5913                }
5914                match &evaluated_args[0] {
5915                    JValue::Null => Ok(JValue::Null),
5916                    JValue::Array(arr) => {
5917                        // Use zero-clone iterator-based average
5918                        Ok(aggregation::average(arr)?)
5919                    }
5920                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
5921                    _ => Err(EvaluatorError::TypeError(
5922                        "average() requires an array or number argument".to_string(),
5923                    )),
5924                }
5925            }
5926            "abs" => {
5927                if evaluated_args.len() != 1 {
5928                    return Err(EvaluatorError::EvaluationError(
5929                        "abs() requires exactly 1 argument".to_string(),
5930                    ));
5931                }
5932                match &evaluated_args[0] {
5933                    JValue::Null => Ok(JValue::Null),
5934                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
5935                    _ => Err(EvaluatorError::TypeError(
5936                        "abs() requires a number argument".to_string(),
5937                    )),
5938                }
5939            }
5940            "floor" => {
5941                if evaluated_args.len() != 1 {
5942                    return Err(EvaluatorError::EvaluationError(
5943                        "floor() requires exactly 1 argument".to_string(),
5944                    ));
5945                }
5946                match &evaluated_args[0] {
5947                    JValue::Null => Ok(JValue::Null),
5948                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
5949                    _ => Err(EvaluatorError::TypeError(
5950                        "floor() requires a number argument".to_string(),
5951                    )),
5952                }
5953            }
5954            "ceil" => {
5955                if evaluated_args.len() != 1 {
5956                    return Err(EvaluatorError::EvaluationError(
5957                        "ceil() requires exactly 1 argument".to_string(),
5958                    ));
5959                }
5960                match &evaluated_args[0] {
5961                    JValue::Null => Ok(JValue::Null),
5962                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
5963                    _ => Err(EvaluatorError::TypeError(
5964                        "ceil() requires a number argument".to_string(),
5965                    )),
5966                }
5967            }
5968            "round" => {
5969                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
5970                    return Err(EvaluatorError::EvaluationError(
5971                        "round() requires 1 or 2 arguments".to_string(),
5972                    ));
5973                }
5974                match &evaluated_args[0] {
5975                    JValue::Null => Ok(JValue::Null),
5976                    JValue::Number(n) => {
5977                        let precision = if evaluated_args.len() == 2 {
5978                            match &evaluated_args[1] {
5979                                JValue::Number(p) => Some(*p as i32),
5980                                _ => {
5981                                    return Err(EvaluatorError::TypeError(
5982                                        "round() precision must be a number".to_string(),
5983                                    ))
5984                                }
5985                            }
5986                        } else {
5987                            None
5988                        };
5989                        Ok(functions::numeric::round(*n, precision)?)
5990                    }
5991                    _ => Err(EvaluatorError::TypeError(
5992                        "round() requires a number argument".to_string(),
5993                    )),
5994                }
5995            }
5996            "sqrt" => {
5997                if evaluated_args.len() != 1 {
5998                    return Err(EvaluatorError::EvaluationError(
5999                        "sqrt() requires exactly 1 argument".to_string(),
6000                    ));
6001                }
6002                match &evaluated_args[0] {
6003                    JValue::Null => Ok(JValue::Null),
6004                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
6005                    _ => Err(EvaluatorError::TypeError(
6006                        "sqrt() requires a number argument".to_string(),
6007                    )),
6008                }
6009            }
6010            "power" => {
6011                if evaluated_args.len() != 2 {
6012                    return Err(EvaluatorError::EvaluationError(
6013                        "power() requires exactly 2 arguments".to_string(),
6014                    ));
6015                }
6016                if evaluated_args[0].is_null() {
6017                    return Ok(JValue::Null);
6018                }
6019                match (&evaluated_args[0], &evaluated_args[1]) {
6020                    (JValue::Number(base), JValue::Number(exp)) => {
6021                        Ok(functions::numeric::power(*base, *exp)?)
6022                    }
6023                    _ => Err(EvaluatorError::TypeError(
6024                        "power() requires number arguments".to_string(),
6025                    )),
6026                }
6027            }
6028            "formatNumber" => {
6029                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6030                    return Err(EvaluatorError::EvaluationError(
6031                        "formatNumber() requires 2 or 3 arguments".to_string(),
6032                    ));
6033                }
6034                if evaluated_args[0].is_null() {
6035                    return Ok(JValue::Null);
6036                }
6037                match (&evaluated_args[0], &evaluated_args[1]) {
6038                    (JValue::Number(num), JValue::String(picture)) => {
6039                        let options = if evaluated_args.len() == 3 {
6040                            Some(&evaluated_args[2])
6041                        } else {
6042                            None
6043                        };
6044                        Ok(functions::numeric::format_number(*num, picture, options)?)
6045                    }
6046                    _ => Err(EvaluatorError::TypeError(
6047                        "formatNumber() requires a number and a string".to_string(),
6048                    )),
6049                }
6050            }
6051            "formatBase" => {
6052                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6053                    return Err(EvaluatorError::EvaluationError(
6054                        "formatBase() requires 1 or 2 arguments".to_string(),
6055                    ));
6056                }
6057                // Handle undefined input
6058                if evaluated_args[0].is_null() {
6059                    return Ok(JValue::Null);
6060                }
6061                match &evaluated_args[0] {
6062                    JValue::Number(num) => {
6063                        let radix = if evaluated_args.len() == 2 {
6064                            match &evaluated_args[1] {
6065                                JValue::Number(r) => Some(r.trunc() as i64),
6066                                _ => {
6067                                    return Err(EvaluatorError::TypeError(
6068                                        "formatBase() radix must be a number".to_string(),
6069                                    ))
6070                                }
6071                            }
6072                        } else {
6073                            None
6074                        };
6075                        Ok(functions::numeric::format_base(*num, radix)?)
6076                    }
6077                    _ => Err(EvaluatorError::TypeError(
6078                        "formatBase() requires a number".to_string(),
6079                    )),
6080                }
6081            }
6082            "append" => {
6083                if evaluated_args.len() != 2 {
6084                    return Err(EvaluatorError::EvaluationError(
6085                        "append() requires exactly 2 arguments".to_string(),
6086                    ));
6087                }
6088                // Handle null/undefined arguments
6089                let first = &evaluated_args[0];
6090                let second = &evaluated_args[1];
6091
6092                // If second arg is null, return first as-is (no change)
6093                if second.is_null() {
6094                    return Ok(first.clone());
6095                }
6096
6097                // If first arg is null, return second as-is (appending to nothing gives second)
6098                if first.is_null() {
6099                    return Ok(second.clone());
6100                }
6101
6102                // Convert both to arrays if needed, then append
6103                let arr = match first {
6104                    JValue::Array(a) => a.to_vec(),
6105                    other => vec![other.clone()], // Wrap non-array in array
6106                };
6107
6108                Ok(functions::array::append(&arr, second)?)
6109            }
6110            "reverse" => {
6111                if evaluated_args.len() != 1 {
6112                    return Err(EvaluatorError::EvaluationError(
6113                        "reverse() requires exactly 1 argument".to_string(),
6114                    ));
6115                }
6116                match &evaluated_args[0] {
6117                    JValue::Null => Ok(JValue::Null), // undefined returns undefined
6118                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
6119                    _ => Err(EvaluatorError::TypeError(
6120                        "reverse() requires an array argument".to_string(),
6121                    )),
6122                }
6123            }
6124            "shuffle" => {
6125                if evaluated_args.len() != 1 {
6126                    return Err(EvaluatorError::EvaluationError(
6127                        "shuffle() requires exactly 1 argument".to_string(),
6128                    ));
6129                }
6130                if evaluated_args[0].is_null() {
6131                    return Ok(JValue::Null);
6132                }
6133                match &evaluated_args[0] {
6134                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
6135                    _ => Err(EvaluatorError::TypeError(
6136                        "shuffle() requires an array argument".to_string(),
6137                    )),
6138                }
6139            }
6140
6141            "sift" => {
6142                // $sift(object, function) or $sift(function) - filter object by predicate
6143                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6144                    return Err(EvaluatorError::EvaluationError(
6145                        "sift() requires 1 or 2 arguments".to_string(),
6146                    ));
6147                }
6148
6149                // Determine which argument is the function
6150                let func_arg = if evaluated_args.len() == 1 {
6151                    &args[0]
6152                } else {
6153                    &args[1]
6154                };
6155
6156                // Detect how many parameters the callback expects
6157                let param_count = self.get_callback_param_count(func_arg);
6158
6159                // Helper function to sift a single object
6160                let sift_object = |evaluator: &mut Self,
6161                                   obj: &IndexMap<String, JValue>,
6162                                   func_node: &AstNode,
6163                                   context_data: &JValue,
6164                                   param_count: usize|
6165                 -> Result<JValue, EvaluatorError> {
6166                    // Only create the object value if callback uses 3 parameters
6167                    let obj_value = if param_count >= 3 {
6168                        Some(JValue::object(obj.clone()))
6169                    } else {
6170                        None
6171                    };
6172
6173                    let mut result = IndexMap::new();
6174                    for (key, value) in obj.iter() {
6175                        // Build argument list based on what callback expects
6176                        let call_args = match param_count {
6177                            1 => vec![value.clone()],
6178                            2 => vec![value.clone(), JValue::string(key.clone())],
6179                            _ => vec![
6180                                value.clone(),
6181                                JValue::string(key.clone()),
6182                                obj_value.as_ref().unwrap().clone(),
6183                            ],
6184                        };
6185
6186                        let pred_result =
6187                            evaluator.apply_function(func_node, &call_args, context_data)?;
6188                        if evaluator.is_truthy(&pred_result) {
6189                            result.insert(key.clone(), value.clone());
6190                        }
6191                    }
6192                    // Return undefined for empty results (will be filtered by function application)
6193                    if result.is_empty() {
6194                        Ok(JValue::Undefined)
6195                    } else {
6196                        Ok(JValue::object(result))
6197                    }
6198                };
6199
6200                // Handle partial application - if only 1 arg, use current context as object
6201                if evaluated_args.len() == 1 {
6202                    // $sift(function) - use current context data as object
6203                    match data {
6204                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
6205                        JValue::Array(arr) => {
6206                            // Map sift over each object in the array
6207                            let mut results = Vec::new();
6208                            for item in arr.iter() {
6209                                if let JValue::Object(o) = item {
6210                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
6211                                    // sift_object returns undefined for empty results
6212                                    if !sifted.is_undefined() {
6213                                        results.push(sifted);
6214                                    }
6215                                }
6216                            }
6217                            Ok(JValue::array(results))
6218                        }
6219                        JValue::Null => Ok(JValue::Null),
6220                        _ => Ok(JValue::Undefined),
6221                    }
6222                } else {
6223                    // $sift(object, function)
6224                    match &evaluated_args[0] {
6225                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
6226                        JValue::Null => Ok(JValue::Null),
6227                        _ => Err(EvaluatorError::TypeError(
6228                            "sift() first argument must be an object".to_string(),
6229                        )),
6230                    }
6231                }
6232            }
6233
6234            "zip" => {
6235                if evaluated_args.is_empty() {
6236                    return Err(EvaluatorError::EvaluationError(
6237                        "zip() requires at least 1 argument".to_string(),
6238                    ));
6239                }
6240
6241                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
6242                // If any argument is null/undefined, return empty array
6243                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
6244                for arg in &evaluated_args {
6245                    match arg {
6246                        JValue::Array(arr) => {
6247                            if arr.is_empty() {
6248                                // Empty array means result is empty
6249                                return Ok(JValue::array(vec![]));
6250                            }
6251                            arrays.push(arr.to_vec());
6252                        }
6253                        JValue::Null => {
6254                            // Null/undefined means result is empty
6255                            return Ok(JValue::array(vec![]));
6256                        }
6257                        other => {
6258                            // Wrap non-array values in single-element array
6259                            arrays.push(vec![other.clone()]);
6260                        }
6261                    }
6262                }
6263
6264                if arrays.is_empty() {
6265                    return Ok(JValue::array(vec![]));
6266                }
6267
6268                // Find the length of the shortest array
6269                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
6270
6271                // Zip the arrays together
6272                let mut result = Vec::with_capacity(min_len);
6273                for i in 0..min_len {
6274                    let mut tuple = Vec::with_capacity(arrays.len());
6275                    for array in &arrays {
6276                        tuple.push(array[i].clone());
6277                    }
6278                    result.push(JValue::array(tuple));
6279                }
6280
6281                Ok(JValue::array(result))
6282            }
6283
6284            "sort" => {
6285                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6286                    return Err(EvaluatorError::EvaluationError(
6287                        "sort() requires 1 or 2 arguments".to_string(),
6288                    ));
6289                }
6290
6291                // Use pre-evaluated first argument (avoid double evaluation)
6292                let array_value = &evaluated_args[0];
6293
6294                // Handle undefined input
6295                if array_value.is_null() {
6296                    return Ok(JValue::Null);
6297                }
6298
6299                let mut arr = match array_value {
6300                    JValue::Array(arr) => arr.to_vec(),
6301                    other => vec![other.clone()],
6302                };
6303
6304                if args.len() == 2 {
6305                    // Sort using the comparator from raw args (need unevaluated lambda AST)
6306                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
6307                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
6308                    Ok(JValue::array(arr))
6309                } else {
6310                    // Default sort (no comparator)
6311                    Ok(functions::array::sort(&arr)?)
6312                }
6313            }
6314            "distinct" => {
6315                if evaluated_args.len() != 1 {
6316                    return Err(EvaluatorError::EvaluationError(
6317                        "distinct() requires exactly 1 argument".to_string(),
6318                    ));
6319                }
6320                match &evaluated_args[0] {
6321                    JValue::Array(arr) => Ok(functions::array::distinct(arr)?),
6322                    _ => Err(EvaluatorError::TypeError(
6323                        "distinct() requires an array argument".to_string(),
6324                    )),
6325                }
6326            }
6327            "exists" => {
6328                if evaluated_args.len() != 1 {
6329                    return Err(EvaluatorError::EvaluationError(
6330                        "exists() requires exactly 1 argument".to_string(),
6331                    ));
6332                }
6333                Ok(functions::array::exists(&evaluated_args[0])?)
6334            }
6335            "keys" => {
6336                if evaluated_args.len() != 1 {
6337                    return Err(EvaluatorError::EvaluationError(
6338                        "keys() requires exactly 1 argument".to_string(),
6339                    ));
6340                }
6341
6342                // Helper to unwrap single-element arrays
6343                let unwrap_single = |keys: Vec<JValue>| -> JValue {
6344                    if keys.len() == 1 {
6345                        keys.into_iter().next().unwrap()
6346                    } else {
6347                        JValue::array(keys)
6348                    }
6349                };
6350
6351                match &evaluated_args[0] {
6352                    JValue::Null => Ok(JValue::Null),
6353                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
6354                    JValue::Object(obj) => {
6355                        // Return undefined for empty objects
6356                        if obj.is_empty() {
6357                            Ok(JValue::Null)
6358                        } else {
6359                            let keys: Vec<JValue> =
6360                                obj.keys().map(|k| JValue::string(k.clone())).collect();
6361                            Ok(unwrap_single(keys))
6362                        }
6363                    }
6364                    JValue::Array(arr) => {
6365                        // For arrays, collect keys from all objects
6366                        let mut all_keys = Vec::new();
6367                        for item in arr.iter() {
6368                            // Skip lambda/builtin values
6369                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
6370                                continue;
6371                            }
6372                            if let JValue::Object(obj) = item {
6373                                for key in obj.keys() {
6374                                    if !all_keys.contains(&JValue::string(key.clone())) {
6375                                        all_keys.push(JValue::string(key.clone()));
6376                                    }
6377                                }
6378                            }
6379                        }
6380                        if all_keys.is_empty() {
6381                            Ok(JValue::Null)
6382                        } else {
6383                            Ok(unwrap_single(all_keys))
6384                        }
6385                    }
6386                    // Non-object types return undefined
6387                    _ => Ok(JValue::Null),
6388                }
6389            }
6390            "lookup" => {
6391                if evaluated_args.len() != 2 {
6392                    return Err(EvaluatorError::EvaluationError(
6393                        "lookup() requires exactly 2 arguments".to_string(),
6394                    ));
6395                }
6396                if evaluated_args[0].is_null() {
6397                    return Ok(JValue::Null);
6398                }
6399
6400                let key = match &evaluated_args[1] {
6401                    JValue::String(k) => &**k,
6402                    _ => {
6403                        return Err(EvaluatorError::TypeError(
6404                            "lookup() requires a string key".to_string(),
6405                        ))
6406                    }
6407                };
6408
6409                // Helper function to recursively lookup in values
6410                fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
6411                    match val {
6412                        JValue::Array(arr) => {
6413                            let mut results = Vec::new();
6414                            for item in arr.iter() {
6415                                let nested = lookup_recursive(item, key);
6416                                results.extend(nested.iter().cloned());
6417                            }
6418                            results
6419                        }
6420                        JValue::Object(obj) => {
6421                            if let Some(v) = obj.get(key) {
6422                                vec![v.clone()]
6423                            } else {
6424                                vec![]
6425                            }
6426                        }
6427                        _ => vec![],
6428                    }
6429                }
6430
6431                let results = lookup_recursive(&evaluated_args[0], key);
6432                if results.is_empty() {
6433                    Ok(JValue::Null)
6434                } else if results.len() == 1 {
6435                    Ok(results[0].clone())
6436                } else {
6437                    Ok(JValue::array(results))
6438                }
6439            }
6440            "spread" => {
6441                if evaluated_args.len() != 1 {
6442                    return Err(EvaluatorError::EvaluationError(
6443                        "spread() requires exactly 1 argument".to_string(),
6444                    ));
6445                }
6446                match &evaluated_args[0] {
6447                    JValue::Null => Ok(JValue::Null),
6448                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Undefined),
6449                    JValue::Object(obj) => Ok(functions::object::spread(obj)?),
6450                    JValue::Array(arr) => {
6451                        // Spread each object in the array
6452                        let mut result = Vec::new();
6453                        for item in arr.iter() {
6454                            match item {
6455                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
6456                                    // Skip lambdas in array
6457                                    continue;
6458                                }
6459                                JValue::Object(obj) => {
6460                                    let spread_result = functions::object::spread(obj)?;
6461                                    if let JValue::Array(spread_items) = spread_result {
6462                                        result.extend(spread_items.iter().cloned());
6463                                    } else {
6464                                        result.push(spread_result);
6465                                    }
6466                                }
6467                                // Non-objects in array are returned unchanged
6468                                other => result.push(other.clone()),
6469                            }
6470                        }
6471                        Ok(JValue::array(result))
6472                    }
6473                    // Non-objects are returned unchanged
6474                    other => Ok(other.clone()),
6475                }
6476            }
6477            "merge" => {
6478                if evaluated_args.is_empty() {
6479                    return Err(EvaluatorError::EvaluationError(
6480                        "merge() requires at least 1 argument".to_string(),
6481                    ));
6482                }
6483                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
6484                // vs multiple object arguments: $merge(obj1, obj2)
6485                if evaluated_args.len() == 1 {
6486                    match &evaluated_args[0] {
6487                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
6488                        JValue::Null => Ok(JValue::Null), // $merge(undefined) returns undefined
6489                        JValue::Object(_) => {
6490                            // Single object - just return it
6491                            Ok(evaluated_args[0].clone())
6492                        }
6493                        _ => Err(EvaluatorError::TypeError(
6494                            "merge() requires objects or an array of objects".to_string(),
6495                        )),
6496                    }
6497                } else {
6498                    Ok(functions::object::merge(&evaluated_args)?)
6499                }
6500            }
6501
6502            "map" => {
6503                if args.len() != 2 {
6504                    return Err(EvaluatorError::EvaluationError(
6505                        "map() requires exactly 2 arguments".to_string(),
6506                    ));
6507                }
6508
6509                // Evaluate the array argument
6510                let array = self.evaluate_internal(&args[0], data)?;
6511
6512                match array {
6513                    JValue::Array(arr) => {
6514                        // Detect how many parameters the callback expects
6515                        let param_count = self.get_callback_param_count(&args[1]);
6516
6517                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
6518                        if param_count == 1 {
6519                            if let AstNode::Lambda {
6520                                params, body, signature: None, thunk: false,
6521                            } = &args[1]
6522                            {
6523                                let var_refs: Vec<&str> =
6524                                    params.iter().map(|s| s.as_str()).collect();
6525                                if let Some(compiled) =
6526                                    try_compile_expr_with_allowed_vars(body, &var_refs)
6527                                {
6528                                    let param_name = params[0].as_str();
6529                                    let mut result = Vec::with_capacity(arr.len());
6530                                    let mut vars = HashMap::new();
6531                                    for item in arr.iter() {
6532                                        vars.insert(param_name, item);
6533                                        let mapped = eval_compiled(&compiled, data, Some(&vars))?;
6534                                        if !mapped.is_undefined() {
6535                                            result.push(mapped);
6536                                        }
6537                                    }
6538                                    return Ok(JValue::array(result));
6539                                }
6540                            }
6541                            // Stored lambda variable fast path: $var with pre-compiled body
6542                            if let AstNode::Variable(var_name) = &args[1] {
6543                                if let Some(stored) = self.context.lookup_lambda(var_name) {
6544                                    if let Some(ref ce) = stored.compiled_body.clone() {
6545                                        let param_name = stored.params[0].clone();
6546                                        let captured_data = stored.captured_data.clone();
6547                                        let captured_env_clone = stored.captured_env.clone();
6548                                        let ce_clone = ce.clone();
6549                                        if !captured_env_clone
6550                                            .values()
6551                                            .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
6552                                        {
6553                                            let call_data =
6554                                                captured_data.as_ref().unwrap_or(data);
6555                                            let mut result = Vec::with_capacity(arr.len());
6556                                            let mut vars: HashMap<&str, &JValue> =
6557                                                captured_env_clone
6558                                                    .iter()
6559                                                    .map(|(k, v)| (k.as_str(), v))
6560                                                    .collect();
6561                                            for item in arr.iter() {
6562                                                vars.insert(param_name.as_str(), item);
6563                                                let mapped = eval_compiled(
6564                                                    &ce_clone,
6565                                                    call_data,
6566                                                    Some(&vars),
6567                                                )?;
6568                                                if !mapped.is_undefined() {
6569                                                    result.push(mapped);
6570                                                }
6571                                            }
6572                                            return Ok(JValue::array(result));
6573                                        }
6574                                    }
6575                                }
6576                            }
6577                        }
6578
6579                        // Only create the array value if callback uses 3 parameters
6580                        let arr_value = if param_count >= 3 {
6581                            Some(JValue::Array(arr.clone()))
6582                        } else {
6583                            None
6584                        };
6585
6586                        let mut result = Vec::with_capacity(arr.len());
6587                        for (index, item) in arr.iter().enumerate() {
6588                            // Build argument list based on what callback expects
6589                            let call_args = match param_count {
6590                                1 => vec![item.clone()],
6591                                2 => vec![item.clone(), JValue::Number(index as f64)],
6592                                _ => vec![
6593                                    item.clone(),
6594                                    JValue::Number(index as f64),
6595                                    arr_value.as_ref().unwrap().clone(),
6596                                ],
6597                            };
6598
6599                            let mapped = self.apply_function(&args[1], &call_args, data)?;
6600                            // Filter out undefined results but keep explicit null (JSONata map semantics)
6601                            // undefined comes from missing else clause, null is explicit
6602                            if !mapped.is_undefined() {
6603                                result.push(mapped);
6604                            }
6605                        }
6606                        Ok(JValue::array(result))
6607                    }
6608                    JValue::Null => Ok(JValue::Null),
6609                    _ => Err(EvaluatorError::TypeError(
6610                        "map() first argument must be an array".to_string(),
6611                    )),
6612                }
6613            }
6614
6615            "filter" => {
6616                if args.len() != 2 {
6617                    return Err(EvaluatorError::EvaluationError(
6618                        "filter() requires exactly 2 arguments".to_string(),
6619                    ));
6620                }
6621
6622                // Evaluate the array argument
6623                let array = self.evaluate_internal(&args[0], data)?;
6624
6625                // Handle undefined input - return undefined
6626                if array.is_undefined() {
6627                    return Ok(JValue::Undefined);
6628                }
6629
6630                // Handle null input
6631                if array.is_null() {
6632                    return Ok(JValue::Undefined);
6633                }
6634
6635                // Coerce non-array values to single-element arrays
6636                // Track if input was a single value to unwrap result appropriately
6637                // Use references to avoid upfront cloning of all elements
6638                let single_holder;
6639                let (items, was_single_value): (&[JValue], bool) = match &array {
6640                    JValue::Array(arr) => (arr.as_slice(), false),
6641                    _ => { single_holder = [array]; (&single_holder[..], true) }
6642                };
6643
6644                // Detect how many parameters the callback expects
6645                let param_count = self.get_callback_param_count(&args[1]);
6646
6647                // CompiledExpr fast path: direct lambda with 1 param, compilable body
6648                if param_count == 1 {
6649                    if let AstNode::Lambda {
6650                        params, body, signature: None, thunk: false,
6651                    } = &args[1]
6652                    {
6653                        let var_refs: Vec<&str> =
6654                            params.iter().map(|s| s.as_str()).collect();
6655                        if let Some(compiled) =
6656                            try_compile_expr_with_allowed_vars(body, &var_refs)
6657                        {
6658                            let param_name = params[0].as_str();
6659                            let mut result = Vec::with_capacity(items.len() / 2);
6660                            let mut vars = HashMap::new();
6661                            for item in items.iter() {
6662                                vars.insert(param_name, item);
6663                                let pred_result = eval_compiled(&compiled, data, Some(&vars))?;
6664                                if compiled_is_truthy(&pred_result) {
6665                                    result.push(item.clone());
6666                                }
6667                            }
6668                            if was_single_value {
6669                                if result.len() == 1 {
6670                                    return Ok(result.remove(0));
6671                                } else if result.is_empty() {
6672                                    return Ok(JValue::Undefined);
6673                                }
6674                            }
6675                            return Ok(JValue::array(result));
6676                        }
6677                    }
6678                    // Stored lambda variable fast path: $var with pre-compiled body
6679                    if let AstNode::Variable(var_name) = &args[1] {
6680                        if let Some(stored) = self.context.lookup_lambda(var_name) {
6681                            if let Some(ref ce) = stored.compiled_body.clone() {
6682                                let param_name = stored.params[0].clone();
6683                                let captured_data = stored.captured_data.clone();
6684                                let captured_env_clone = stored.captured_env.clone();
6685                                let ce_clone = ce.clone();
6686                                if !captured_env_clone
6687                                    .values()
6688                                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
6689                                {
6690                                    let call_data = captured_data.as_ref().unwrap_or(data);
6691                                    let mut result = Vec::with_capacity(items.len() / 2);
6692                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
6693                                        .iter()
6694                                        .map(|(k, v)| (k.as_str(), v))
6695                                        .collect();
6696                                    for item in items.iter() {
6697                                        vars.insert(param_name.as_str(), item);
6698                                        let pred_result =
6699                                            eval_compiled(&ce_clone, call_data, Some(&vars))?;
6700                                        if compiled_is_truthy(&pred_result) {
6701                                            result.push(item.clone());
6702                                        }
6703                                    }
6704                                    if was_single_value {
6705                                        if result.len() == 1 {
6706                                            return Ok(result.remove(0));
6707                                        } else if result.is_empty() {
6708                                            return Ok(JValue::Undefined);
6709                                        }
6710                                    }
6711                                    return Ok(JValue::array(result));
6712                                }
6713                            }
6714                        }
6715                    }
6716                }
6717
6718                // Only create the array value if callback uses 3 parameters
6719                let arr_value = if param_count >= 3 {
6720                    Some(JValue::array(items.to_vec()))
6721                } else {
6722                    None
6723                };
6724
6725                let mut result = Vec::with_capacity(items.len() / 2);
6726
6727                for (index, item) in items.iter().enumerate() {
6728                    // Build argument list based on what callback expects
6729                    let call_args = match param_count {
6730                        1 => vec![item.clone()],
6731                        2 => vec![item.clone(), JValue::Number(index as f64)],
6732                        _ => vec![
6733                            item.clone(),
6734                            JValue::Number(index as f64),
6735                            arr_value.as_ref().unwrap().clone(),
6736                        ],
6737                    };
6738
6739                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
6740                    if self.is_truthy(&predicate_result) {
6741                        result.push(item.clone());
6742                    }
6743                }
6744
6745                // If input was a single value, return the single matching item
6746                // (or undefined if no match)
6747                if was_single_value {
6748                    if result.len() == 1 {
6749                        return Ok(result.remove(0));
6750                    } else if result.is_empty() {
6751                        return Ok(JValue::Undefined);
6752                    }
6753                }
6754
6755                Ok(JValue::array(result))
6756            }
6757
6758            "reduce" => {
6759                if args.len() < 2 || args.len() > 3 {
6760                    return Err(EvaluatorError::EvaluationError(
6761                        "reduce() requires 2 or 3 arguments".to_string(),
6762                    ));
6763                }
6764
6765                // Check that the callback function has at least 2 parameters
6766                if let AstNode::Lambda { params, .. } = &args[1] {
6767                    if params.len() < 2 {
6768                        return Err(EvaluatorError::EvaluationError(
6769                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
6770                        ));
6771                    }
6772                } else if let AstNode::Function { name, .. } = &args[1] {
6773                    // For now, we can't validate built-in function signatures here
6774                    // But user-defined functions via lambda will be validated above
6775                    let _ = name; // avoid unused warning
6776                }
6777
6778                // Evaluate the array argument
6779                let array = self.evaluate_internal(&args[0], data)?;
6780
6781                // Convert single value to array (JSONata reduce accepts single values)
6782                // Use references to avoid upfront cloning of all elements
6783                let single_holder;
6784                let items: &[JValue] = match &array {
6785                    JValue::Array(arr) => arr.as_slice(),
6786                    JValue::Null => return Ok(JValue::Null),
6787                    _ => { single_holder = [array]; &single_holder[..] }
6788                };
6789
6790                if items.is_empty() {
6791                    // Return initial value if provided, otherwise null
6792                    return if args.len() == 3 {
6793                        self.evaluate_internal(&args[2], data)
6794                    } else {
6795                        Ok(JValue::Null)
6796                    };
6797                }
6798
6799                // Get initial accumulator
6800                let mut accumulator = if args.len() == 3 {
6801                    self.evaluate_internal(&args[2], data)?
6802                } else {
6803                    items[0].clone()
6804                };
6805
6806                let start_idx = if args.len() == 3 { 0 } else { 1 };
6807
6808                // Detect how many parameters the callback expects
6809                let param_count = self.get_callback_param_count(&args[1]);
6810
6811                // CompiledExpr fast path: direct lambda with 2 params, compilable body
6812                if param_count == 2 {
6813                    if let AstNode::Lambda {
6814                        params, body, signature: None, thunk: false,
6815                    } = &args[1]
6816                    {
6817                        let var_refs: Vec<&str> =
6818                            params.iter().map(|s| s.as_str()).collect();
6819                        if let Some(compiled) =
6820                            try_compile_expr_with_allowed_vars(body, &var_refs)
6821                        {
6822                            let acc_name = params[0].as_str();
6823                            let item_name = params[1].as_str();
6824                            for item in items[start_idx..].iter() {
6825                                let vars: HashMap<&str, &JValue> = HashMap::from([
6826                                    (acc_name, &accumulator),
6827                                    (item_name, item),
6828                                ]);
6829                                accumulator = eval_compiled(&compiled, data, Some(&vars))?;
6830                            }
6831                            return Ok(accumulator);
6832                        }
6833                    }
6834                    // Stored lambda variable fast path: $var with pre-compiled body
6835                    if let AstNode::Variable(var_name) = &args[1] {
6836                        if let Some(stored) = self.context.lookup_lambda(var_name) {
6837                            if stored.params.len() == 2 {
6838                                if let Some(ref ce) = stored.compiled_body.clone() {
6839                                    let acc_param = stored.params[0].clone();
6840                                    let item_param = stored.params[1].clone();
6841                                    let captured_data = stored.captured_data.clone();
6842                                    let captured_env_clone = stored.captured_env.clone();
6843                                    let ce_clone = ce.clone();
6844                                    if !captured_env_clone.values().any(|v| {
6845                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
6846                                    }) {
6847                                        let call_data =
6848                                            captured_data.as_ref().unwrap_or(data);
6849                                        for item in items[start_idx..].iter() {
6850                                            let mut vars: HashMap<&str, &JValue> =
6851                                                captured_env_clone
6852                                                    .iter()
6853                                                    .map(|(k, v)| (k.as_str(), v))
6854                                                    .collect();
6855                                            vars.insert(acc_param.as_str(), &accumulator);
6856                                            vars.insert(item_param.as_str(), item);
6857                                            // Evaluate and drop vars before assigning accumulator
6858                                            // to satisfy borrow checker (vars borrows accumulator)
6859                                            let new_acc =
6860                                                eval_compiled(&ce_clone, call_data, Some(&vars))?;
6861                                            drop(vars);
6862                                            accumulator = new_acc;
6863                                        }
6864                                        return Ok(accumulator);
6865                                    }
6866                                }
6867                            }
6868                        }
6869                    }
6870                }
6871
6872                // Only create the array value if callback uses 4 parameters
6873                let arr_value = if param_count >= 4 {
6874                    Some(JValue::array(items.to_vec()))
6875                } else {
6876                    None
6877                };
6878
6879                // Apply function to each element
6880                for (idx, item) in items[start_idx..].iter().enumerate() {
6881                    // For reduce, the function receives (accumulator, value, index, array)
6882                    // Callbacks may use any subset of these parameters
6883                    let actual_idx = start_idx + idx;
6884
6885                    // Build argument list based on what callback expects
6886                    let call_args = match param_count {
6887                        2 => vec![accumulator.clone(), item.clone()],
6888                        3 => vec![
6889                            accumulator.clone(),
6890                            item.clone(),
6891                            JValue::Number(actual_idx as f64),
6892                        ],
6893                        _ => vec![
6894                            accumulator.clone(),
6895                            item.clone(),
6896                            JValue::Number(actual_idx as f64),
6897                            arr_value.as_ref().unwrap().clone(),
6898                        ],
6899                    };
6900
6901                    accumulator = self.apply_function(&args[1], &call_args, data)?;
6902                }
6903
6904                Ok(accumulator)
6905            }
6906
6907            "single" => {
6908                if args.is_empty() || args.len() > 2 {
6909                    return Err(EvaluatorError::EvaluationError(
6910                        "single() requires 1 or 2 arguments".to_string(),
6911                    ));
6912                }
6913
6914                // Evaluate the array argument
6915                let array = self.evaluate_internal(&args[0], data)?;
6916
6917                // Convert to array (wrap single values)
6918                let arr = match array {
6919                    JValue::Array(arr) => arr.to_vec(),
6920                    JValue::Null => return Ok(JValue::Null),
6921                    other => vec![other],
6922                };
6923
6924                if args.len() == 1 {
6925                    // No predicate - array must have exactly 1 element
6926                    match arr.len() {
6927                        0 => Err(EvaluatorError::EvaluationError(
6928                            "single() argument is empty".to_string(),
6929                        )),
6930                        1 => Ok(arr.into_iter().next().unwrap()),
6931                        count => Err(EvaluatorError::EvaluationError(format!(
6932                            "single() argument has {} values (expected exactly 1)",
6933                            count
6934                        ))),
6935                    }
6936                } else {
6937                    // With predicate - find exactly 1 matching element
6938                    let arr_value = JValue::array(arr.clone());
6939                    let mut matches = Vec::new();
6940                    for (index, item) in arr.into_iter().enumerate() {
6941                        // Apply predicate function with (item, index, array)
6942                        let predicate_result = self.apply_function(
6943                            &args[1],
6944                            &[
6945                                item.clone(),
6946                                JValue::Number(index as f64),
6947                                arr_value.clone(),
6948                            ],
6949                            data,
6950                        )?;
6951                        if self.is_truthy(&predicate_result) {
6952                            matches.push(item);
6953                        }
6954                    }
6955
6956                    match matches.len() {
6957                        0 => Err(EvaluatorError::EvaluationError(
6958                            "single() predicate matches no values".to_string(),
6959                        )),
6960                        1 => Ok(matches.into_iter().next().unwrap()),
6961                        count => Err(EvaluatorError::EvaluationError(format!(
6962                            "single() predicate matches {} values (expected exactly 1)",
6963                            count
6964                        ))),
6965                    }
6966                }
6967            }
6968
6969            "each" => {
6970                // $each(object, function) - iterate over object, applying function to each value/key pair
6971                // Returns an array of the function results
6972                if args.is_empty() || args.len() > 2 {
6973                    return Err(EvaluatorError::EvaluationError(
6974                        "each() requires 1 or 2 arguments".to_string(),
6975                    ));
6976                }
6977
6978                // Determine which argument is the object and which is the function
6979                let (obj_value, func_arg) = if args.len() == 1 {
6980                    // Single argument: use current data as object
6981                    (data.clone(), &args[0])
6982                } else {
6983                    // Two arguments: first is object, second is function
6984                    (self.evaluate_internal(&args[0], data)?, &args[1])
6985                };
6986
6987                // Detect how many parameters the callback expects
6988                let param_count = self.get_callback_param_count(func_arg);
6989
6990                match obj_value {
6991                    JValue::Object(obj) => {
6992                        let mut result = Vec::new();
6993                        for (key, value) in obj.iter() {
6994                            // Build argument list based on what callback expects
6995                            // The callback receives the value as the first argument and key as second
6996                            let call_args = match param_count {
6997                                1 => vec![value.clone()],
6998                                _ => vec![value.clone(), JValue::string(key.clone())],
6999                            };
7000
7001                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
7002                            // Skip undefined results (similar to map behavior)
7003                            if !fn_result.is_null() && !fn_result.is_undefined() {
7004                                result.push(fn_result);
7005                            }
7006                        }
7007                        Ok(JValue::array(result))
7008                    }
7009                    JValue::Null => Ok(JValue::Null),
7010                    _ => Err(EvaluatorError::TypeError(
7011                        "each() first argument must be an object".to_string(),
7012                    )),
7013                }
7014            }
7015
7016            "not" => {
7017                if evaluated_args.len() != 1 {
7018                    return Err(EvaluatorError::EvaluationError(
7019                        "not() requires exactly 1 argument".to_string(),
7020                    ));
7021                }
7022                // $not(x) returns the logical negation of x
7023                // null is falsy, so $not(null) = true
7024                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
7025            }
7026            "boolean" => {
7027                if evaluated_args.len() != 1 {
7028                    return Err(EvaluatorError::EvaluationError(
7029                        "boolean() requires exactly 1 argument".to_string(),
7030                    ));
7031                }
7032                Ok(functions::boolean::boolean(&evaluated_args[0])?)
7033            }
7034            "type" => {
7035                if evaluated_args.len() != 1 {
7036                    return Err(EvaluatorError::EvaluationError(
7037                        "type() requires exactly 1 argument".to_string(),
7038                    ));
7039                }
7040                // Return type string
7041                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
7042                // We use a special marker object to distinguish undefined from null
7043                match &evaluated_args[0] {
7044                    JValue::Null => Ok(JValue::string("null")),
7045                    JValue::Bool(_) => Ok(JValue::string("boolean")),
7046                    JValue::Number(_) => Ok(JValue::string("number")),
7047                    JValue::String(_) => Ok(JValue::string("string")),
7048                    JValue::Array(_) => Ok(JValue::string("array")),
7049                    JValue::Object(_) => Ok(JValue::string("object")),
7050                    JValue::Undefined => Ok(JValue::Undefined),
7051                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
7052                        Ok(JValue::string("function"))
7053                    }
7054                    JValue::Regex { .. } => Ok(JValue::string("regex")),
7055                }
7056            }
7057
7058            "base64encode" => {
7059                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
7060                    return Ok(JValue::Null);
7061                }
7062                if evaluated_args.len() != 1 {
7063                    return Err(EvaluatorError::EvaluationError(
7064                        "base64encode() requires exactly 1 argument".to_string(),
7065                    ));
7066                }
7067                match &evaluated_args[0] {
7068                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
7069                    _ => Err(EvaluatorError::TypeError(
7070                        "base64encode() requires a string argument".to_string(),
7071                    )),
7072                }
7073            }
7074            "base64decode" => {
7075                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
7076                    return Ok(JValue::Null);
7077                }
7078                if evaluated_args.len() != 1 {
7079                    return Err(EvaluatorError::EvaluationError(
7080                        "base64decode() requires exactly 1 argument".to_string(),
7081                    ));
7082                }
7083                match &evaluated_args[0] {
7084                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
7085                    _ => Err(EvaluatorError::TypeError(
7086                        "base64decode() requires a string argument".to_string(),
7087                    )),
7088                }
7089            }
7090            "encodeUrlComponent" => {
7091                if evaluated_args.len() != 1 {
7092                    return Err(EvaluatorError::EvaluationError(
7093                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
7094                    ));
7095                }
7096                if evaluated_args[0].is_null() {
7097                    return Ok(JValue::Null);
7098                }
7099                match &evaluated_args[0] {
7100                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
7101                    _ => Err(EvaluatorError::TypeError(
7102                        "encodeUrlComponent() requires a string argument".to_string(),
7103                    )),
7104                }
7105            }
7106            "decodeUrlComponent" => {
7107                if evaluated_args.len() != 1 {
7108                    return Err(EvaluatorError::EvaluationError(
7109                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
7110                    ));
7111                }
7112                if evaluated_args[0].is_null() {
7113                    return Ok(JValue::Null);
7114                }
7115                match &evaluated_args[0] {
7116                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
7117                    _ => Err(EvaluatorError::TypeError(
7118                        "decodeUrlComponent() requires a string argument".to_string(),
7119                    )),
7120                }
7121            }
7122            "encodeUrl" => {
7123                if evaluated_args.len() != 1 {
7124                    return Err(EvaluatorError::EvaluationError(
7125                        "encodeUrl() requires exactly 1 argument".to_string(),
7126                    ));
7127                }
7128                if evaluated_args[0].is_null() {
7129                    return Ok(JValue::Null);
7130                }
7131                match &evaluated_args[0] {
7132                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
7133                    _ => Err(EvaluatorError::TypeError(
7134                        "encodeUrl() requires a string argument".to_string(),
7135                    )),
7136                }
7137            }
7138            "decodeUrl" => {
7139                if evaluated_args.len() != 1 {
7140                    return Err(EvaluatorError::EvaluationError(
7141                        "decodeUrl() requires exactly 1 argument".to_string(),
7142                    ));
7143                }
7144                if evaluated_args[0].is_null() {
7145                    return Ok(JValue::Null);
7146                }
7147                match &evaluated_args[0] {
7148                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
7149                    _ => Err(EvaluatorError::TypeError(
7150                        "decodeUrl() requires a string argument".to_string(),
7151                    )),
7152                }
7153            }
7154
7155            "error" => {
7156                // $error(message) - throw error with custom message
7157                if evaluated_args.is_empty() {
7158                    // No message provided
7159                    return Err(EvaluatorError::EvaluationError(
7160                        "D3137: $error() function evaluated".to_string(),
7161                    ));
7162                }
7163
7164                match &evaluated_args[0] {
7165                    JValue::String(s) => {
7166                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
7167                    }
7168                    _ => Err(EvaluatorError::TypeError(
7169                        "T0410: Argument 1 of function error does not match function signature"
7170                            .to_string(),
7171                    )),
7172                }
7173            }
7174            "assert" => {
7175                // $assert(condition, message) - throw error if condition is false
7176                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7177                    return Err(EvaluatorError::EvaluationError(
7178                        "assert() requires 1 or 2 arguments".to_string(),
7179                    ));
7180                }
7181
7182                // First argument must be a boolean
7183                let condition = match &evaluated_args[0] {
7184                    JValue::Bool(b) => *b,
7185                    _ => {
7186                        return Err(EvaluatorError::TypeError(
7187                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
7188                        ));
7189                    }
7190                };
7191
7192                if !condition {
7193                    let message = if evaluated_args.len() == 2 {
7194                        match &evaluated_args[1] {
7195                            JValue::String(s) => s.clone(),
7196                            _ => Rc::from("$assert() statement failed"),
7197                        }
7198                    } else {
7199                        Rc::from("$assert() statement failed")
7200                    };
7201                    return Err(EvaluatorError::EvaluationError(format!(
7202                        "D3141: {}",
7203                        message
7204                    )));
7205                }
7206
7207                Ok(JValue::Null)
7208            }
7209
7210            "eval" => {
7211                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
7212                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7213                    return Err(EvaluatorError::EvaluationError(
7214                        "T0410: Argument 1 of function $eval must be a string".to_string(),
7215                    ));
7216                }
7217
7218                // If the first argument is null/undefined, return undefined
7219                if evaluated_args[0].is_null() {
7220                    return Ok(JValue::Null);
7221                }
7222
7223                // First argument must be a string expression
7224                let expr_str = match &evaluated_args[0] {
7225                    JValue::String(s) => &**s,
7226                    _ => {
7227                        return Err(EvaluatorError::EvaluationError(
7228                            "T0410: Argument 1 of function $eval must be a string".to_string(),
7229                        ));
7230                    }
7231                };
7232
7233                // Parse the expression
7234                let parsed_ast = match parser::parse(expr_str) {
7235                    Ok(ast) => ast,
7236                    Err(e) => {
7237                        // D3120 is the error code for parse errors in $eval
7238                        return Err(EvaluatorError::EvaluationError(format!(
7239                            "D3120: The expression passed to $eval cannot be parsed: {}",
7240                            e
7241                        )));
7242                    }
7243                };
7244
7245                // Determine the context to use for evaluation
7246                let eval_context = if evaluated_args.len() == 2 {
7247                    &evaluated_args[1]
7248                } else {
7249                    data
7250                };
7251
7252                // Evaluate the parsed expression
7253                match self.evaluate_internal(&parsed_ast, eval_context) {
7254                    Ok(result) => Ok(result),
7255                    Err(e) => {
7256                        // D3121 is the error code for evaluation errors in $eval
7257                        let err_msg = e.to_string();
7258                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
7259                            Err(EvaluatorError::EvaluationError(format!(
7260                                "D3121: {}",
7261                                err_msg
7262                            )))
7263                        } else {
7264                            Err(e)
7265                        }
7266                    }
7267                }
7268            }
7269
7270            "now" => {
7271                if !evaluated_args.is_empty() {
7272                    return Err(EvaluatorError::EvaluationError(
7273                        "now() takes no arguments".to_string(),
7274                    ));
7275                }
7276                Ok(crate::datetime::now())
7277            }
7278
7279            "millis" => {
7280                if !evaluated_args.is_empty() {
7281                    return Err(EvaluatorError::EvaluationError(
7282                        "millis() takes no arguments".to_string(),
7283                    ));
7284                }
7285                Ok(crate::datetime::millis())
7286            }
7287
7288            "toMillis" => {
7289                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7290                    return Err(EvaluatorError::EvaluationError(
7291                        "toMillis() requires 1 or 2 arguments".to_string(),
7292                    ));
7293                }
7294
7295                match &evaluated_args[0] {
7296                    JValue::String(s) => {
7297                        // Optional second argument is a picture string for custom parsing
7298                        if evaluated_args.len() == 2 {
7299                            match &evaluated_args[1] {
7300                                JValue::String(picture) => {
7301                                    // Use custom picture format parsing
7302                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
7303                                }
7304                                JValue::Null => Ok(JValue::Null),
7305                                _ => Err(EvaluatorError::TypeError(
7306                                    "toMillis() second argument must be a string".to_string(),
7307                                )),
7308                            }
7309                        } else {
7310                            // Use ISO 8601 partial date parsing
7311                            Ok(crate::datetime::to_millis(s)?)
7312                        }
7313                    }
7314                    JValue::Null => Ok(JValue::Null),
7315                    _ => Err(EvaluatorError::TypeError(
7316                        "toMillis() requires a string argument".to_string(),
7317                    )),
7318                }
7319            }
7320
7321            "fromMillis" => {
7322                if evaluated_args.len() != 1 {
7323                    return Err(EvaluatorError::EvaluationError(
7324                        "fromMillis() requires exactly 1 argument".to_string(),
7325                    ));
7326                }
7327
7328                match &evaluated_args[0] {
7329                    JValue::Number(n) => {
7330                        let millis = (if n.fract() == 0.0 {
7331                            Ok(*n as i64)
7332                        } else {
7333                            Err(())
7334                        })
7335                        .map_err(|_| {
7336                            EvaluatorError::TypeError(
7337                                "fromMillis() requires an integer".to_string(),
7338                            )
7339                        })?;
7340                        Ok(crate::datetime::from_millis(millis)?)
7341                    }
7342                    JValue::Null => Ok(JValue::Null),
7343                    _ => Err(EvaluatorError::TypeError(
7344                        "fromMillis() requires a number argument".to_string(),
7345                    )),
7346                }
7347            }
7348
7349            _ => Err(EvaluatorError::ReferenceError(format!(
7350                "Unknown function: {}",
7351                name
7352            ))),
7353        }
7354    }
7355
7356    /// Apply a function (lambda or expression) to values
7357    ///
7358    /// This handles both:
7359    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
7360    /// 2. Simple expressions: price * 2 - evaluates with values as context
7361    fn apply_function(
7362        &mut self,
7363        func_node: &AstNode,
7364        values: &[JValue],
7365        data: &JValue,
7366    ) -> Result<JValue, EvaluatorError> {
7367        match func_node {
7368            AstNode::Lambda {
7369                params,
7370                body,
7371                signature,
7372                thunk,
7373            } => {
7374                // Direct lambda - invoke it
7375                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
7376            }
7377            AstNode::Function {
7378                name,
7379                args,
7380                is_builtin,
7381            } => {
7382                // Function call - check if it has placeholders (partial application)
7383                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
7384
7385                if has_placeholder {
7386                    // This is a partial application - evaluate it to get the lambda value
7387                    let partial_lambda =
7388                        self.create_partial_application(name, args, *is_builtin, data)?;
7389
7390                    // Now invoke the partial lambda with the provided values
7391                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
7392                        return self.invoke_stored_lambda(&stored, values, data);
7393                    }
7394                    Err(EvaluatorError::EvaluationError(
7395                        "Failed to apply partial application".to_string(),
7396                    ))
7397                } else {
7398                    // Regular function call without placeholders
7399                    // Evaluate it and apply if it returns a function
7400                    let result = self.evaluate_internal(func_node, data)?;
7401
7402                    // Check if result is a lambda value
7403                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
7404                        return self.invoke_stored_lambda(&stored, values, data);
7405                    }
7406
7407                    // Otherwise just return the result
7408                    Ok(result)
7409                }
7410            }
7411            AstNode::Variable(var_name) => {
7412                // Check if this variable holds a stored lambda
7413                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
7414                    self.invoke_stored_lambda(&stored_lambda, values, data)
7415                } else if let Some(value) = self.context.lookup(var_name).cloned() {
7416                    // Check if this variable holds a lambda value
7417                    // This handles lambdas passed as bound arguments in partial applications
7418                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
7419                        return self.invoke_stored_lambda(&stored, values, data);
7420                    }
7421                    // Regular variable value - evaluate with first value as context
7422                    if values.is_empty() {
7423                        self.evaluate_internal(func_node, data)
7424                    } else {
7425                        self.evaluate_internal(func_node, &values[0])
7426                    }
7427                } else if self.is_builtin_function(var_name) {
7428                    // This is a built-in function reference (e.g., $string, $number)
7429                    // Call it directly with the provided values (already evaluated)
7430                    self.call_builtin_with_values(var_name, values)
7431                } else {
7432                    // Unknown variable - evaluate with first value as context
7433                    if values.is_empty() {
7434                        self.evaluate_internal(func_node, data)
7435                    } else {
7436                        self.evaluate_internal(func_node, &values[0])
7437                    }
7438                }
7439            }
7440            _ => {
7441                // For non-lambda expressions, evaluate with first value as context
7442                if values.is_empty() {
7443                    self.evaluate_internal(func_node, data)
7444                } else {
7445                    self.evaluate_internal(func_node, &values[0])
7446                }
7447            }
7448        }
7449    }
7450
7451    /// Execute a transform operator on the bound $ value
7452    fn execute_transform(
7453        &mut self,
7454        location: &AstNode,
7455        update: &AstNode,
7456        delete: Option<&AstNode>,
7457        _original_data: &JValue,
7458    ) -> Result<JValue, EvaluatorError> {
7459        // Get the input value from $ binding
7460        let input = self
7461            .context
7462            .lookup("$")
7463            .ok_or_else(|| {
7464                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
7465            })?
7466            .clone();
7467
7468        // Evaluate location expression on the input to get objects to transform
7469        let located_objects = self.evaluate_internal(location, &input)?;
7470
7471        // Collect target objects into a vector for comparison
7472        let targets: Vec<JValue> = match located_objects {
7473            JValue::Array(arr) => arr.to_vec(),
7474            JValue::Object(_) => vec![located_objects],
7475            JValue::Null => Vec::new(),
7476            other => vec![other],
7477        };
7478
7479        // Validate update parameter - must be an object constructor
7480        // We need to check this before evaluation in case of errors
7481        // For now, we'll validate after evaluation in the transform helper
7482
7483        // Parse delete field names if provided
7484        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
7485            let delete_val = self.evaluate_internal(delete_node, &input)?;
7486            match delete_val {
7487                JValue::Array(arr) => arr
7488                    .iter()
7489                    .filter_map(|v| match v {
7490                        JValue::String(s) => Some(s.to_string()),
7491                        _ => None,
7492                    })
7493                    .collect(),
7494                JValue::String(s) => vec![s.to_string()],
7495                JValue::Null => Vec::new(), // Undefined variable is treated as no deletion
7496                _ => {
7497                    // Delete parameter must be an array of strings or a string
7498                    return Err(EvaluatorError::EvaluationError(
7499                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
7500                    ));
7501                }
7502            }
7503        } else {
7504            Vec::new()
7505        };
7506
7507        // Recursive helper to apply transformation throughout the structure
7508        fn apply_transform_deep(
7509            evaluator: &mut Evaluator,
7510            value: &JValue,
7511            targets: &[JValue],
7512            update: &AstNode,
7513            delete_fields: &[String],
7514        ) -> Result<JValue, EvaluatorError> {
7515            // Check if this value is one of the targets to transform
7516            // Use JValue's PartialEq for semantic equality comparison
7517            if targets.iter().any(|t| t == value) {
7518                // Transform this object
7519                if let JValue::Object(map_rc) = value.clone() {
7520                    let mut map = (*map_rc).clone();
7521                    let update_val = evaluator.evaluate_internal(update, value)?;
7522                    // Validate that update evaluates to an object or null (undefined)
7523                    match update_val {
7524                        JValue::Object(update_map) => {
7525                            for (key, val) in update_map.iter() {
7526                                map.insert(key.clone(), val.clone());
7527                            }
7528                        }
7529                        JValue::Null => {
7530                            // Null/undefined means no updates, just continue to deletions
7531                        }
7532                        _ => {
7533                            return Err(EvaluatorError::EvaluationError(
7534                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
7535                            ));
7536                        }
7537                    }
7538                    for field in delete_fields {
7539                        map.shift_remove(field);
7540                    }
7541                    return Ok(JValue::object(map));
7542                }
7543                return Ok(value.clone());
7544            }
7545
7546            // Otherwise, recursively process children to find and transform targets
7547            match value {
7548                JValue::Object(map) => {
7549                    let mut new_map = IndexMap::new();
7550                    for (k, v) in map.iter() {
7551                        new_map.insert(
7552                            k.clone(),
7553                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
7554                        );
7555                    }
7556                    Ok(JValue::object(new_map))
7557                }
7558                JValue::Array(arr) => {
7559                    let mut new_arr = Vec::new();
7560                    for item in arr.iter() {
7561                        new_arr.push(apply_transform_deep(
7562                            evaluator,
7563                            item,
7564                            targets,
7565                            update,
7566                            delete_fields,
7567                        )?);
7568                    }
7569                    Ok(JValue::array(new_arr))
7570                }
7571                _ => Ok(value.clone()),
7572            }
7573        }
7574
7575        // Apply transformation recursively starting from input
7576        apply_transform_deep(self, &input, &targets, update, &delete_fields)
7577    }
7578
7579    /// Helper to invoke a lambda with given parameters
7580    fn invoke_lambda(
7581        &mut self,
7582        params: &[String],
7583        body: &AstNode,
7584        signature: Option<&String>,
7585        values: &[JValue],
7586        data: &JValue,
7587        thunk: bool,
7588    ) -> Result<JValue, EvaluatorError> {
7589        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
7590    }
7591
7592    /// Invoke a lambda with optional captured environment (for closures)
7593    fn invoke_lambda_with_env(
7594        &mut self,
7595        params: &[String],
7596        body: &AstNode,
7597        signature: Option<&String>,
7598        values: &[JValue],
7599        data: &JValue,
7600        captured_env: Option<&HashMap<String, JValue>>,
7601        captured_data: Option<&JValue>,
7602        thunk: bool,
7603    ) -> Result<JValue, EvaluatorError> {
7604        // If this is a thunk (has tail calls), use TCO trampoline
7605        if thunk {
7606            let stored = StoredLambda {
7607                params: params.to_vec(),
7608                body: body.clone(),
7609                compiled_body: None, // Thunks use TCO, not the compiled fast path
7610                signature: signature.cloned(),
7611                captured_env: captured_env.cloned().unwrap_or_default(),
7612                captured_data: captured_data.cloned(),
7613                thunk,
7614            };
7615            return self.invoke_lambda_with_tco(&stored, values, data);
7616        }
7617
7618        // Validate signature if present, and get coerced arguments
7619        // Push a new scope for this lambda invocation
7620        self.context.push_scope();
7621
7622        // First apply captured environment (for closures)
7623        if let Some(env) = captured_env {
7624            for (name, value) in env {
7625                self.context.bind(name.clone(), value.clone());
7626            }
7627        }
7628
7629        if let Some(sig_str) = signature {
7630            // Validate and coerce arguments with signature
7631            let coerced_values = match crate::signature::Signature::parse(sig_str) {
7632                Ok(sig) => {
7633                    match sig.validate_and_coerce(values) {
7634                        Ok(coerced) => coerced,
7635                        Err(e) => {
7636                            self.context.pop_scope();
7637                            match e {
7638                                crate::signature::SignatureError::UndefinedArgument => {
7639                                    return Ok(JValue::Null);
7640                                }
7641                                crate::signature::SignatureError::ArgumentTypeMismatch {
7642                                    index,
7643                                    expected,
7644                                } => {
7645                                    return Err(EvaluatorError::TypeError(
7646                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
7647                                    ));
7648                                }
7649                                crate::signature::SignatureError::ArrayTypeMismatch {
7650                                    index,
7651                                    expected,
7652                                } => {
7653                                    return Err(EvaluatorError::TypeError(format!(
7654                                        "T0412: Argument {} of function must be an array of {}",
7655                                        index, expected
7656                                    )));
7657                                }
7658                                _ => {
7659                                    return Err(EvaluatorError::TypeError(format!(
7660                                        "Signature validation failed: {}",
7661                                        e
7662                                    )));
7663                                }
7664                            }
7665                        }
7666                    }
7667                }
7668                Err(e) => {
7669                    self.context.pop_scope();
7670                    return Err(EvaluatorError::EvaluationError(format!(
7671                        "Invalid signature: {}",
7672                        e
7673                    )));
7674                }
7675            };
7676            // Bind coerced values to params
7677            for (i, param) in params.iter().enumerate() {
7678                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
7679                self.context.bind(param.clone(), value);
7680            }
7681        } else {
7682            // No signature - bind directly from values slice (no allocation)
7683            for (i, param) in params.iter().enumerate() {
7684                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
7685                self.context.bind(param.clone(), value);
7686            }
7687        }
7688
7689        // Check if this is a partial application (body is a special marker string)
7690        if let AstNode::String(body_str) = body {
7691            if body_str.starts_with("__partial_call:") {
7692                // Parse the partial call info
7693                let parts: Vec<&str> = body_str.split(':').collect();
7694                if parts.len() >= 4 {
7695                    let func_name = parts[1];
7696                    let is_builtin = parts[2] == "true";
7697                    let total_args: usize = parts[3].parse().unwrap_or(0);
7698
7699                    // Get placeholder positions from captured env
7700                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
7701                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
7702                            positions
7703                                .iter()
7704                                .filter_map(|v| v.as_f64().map(|n| n as usize))
7705                                .collect()
7706                        } else {
7707                            vec![]
7708                        }
7709                    } else {
7710                        vec![]
7711                    };
7712
7713                    // Reconstruct the full argument list
7714                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
7715
7716                    // Fill in bound arguments from captured environment
7717                    if let Some(env) = captured_env {
7718                        for (key, value) in env {
7719                            if key.starts_with("__bound_arg_") {
7720                                if let Ok(pos) = key[12..].parse::<usize>() {
7721                                    if pos < total_args {
7722                                        full_args[pos] = value.clone();
7723                                    }
7724                                }
7725                            }
7726                        }
7727                    }
7728
7729                    // Fill in placeholder positions with provided values
7730                    for (i, &pos) in placeholder_positions.iter().enumerate() {
7731                        if pos < total_args {
7732                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
7733                            full_args[pos] = value;
7734                        }
7735                    }
7736
7737                    // Pop lambda scope, then push a new scope for temp args
7738                    self.context.pop_scope();
7739                    self.context.push_scope();
7740
7741                    // Build AST nodes for the function call arguments
7742                    let mut temp_args: Vec<AstNode> = Vec::new();
7743                    for (i, value) in full_args.iter().enumerate() {
7744                        let temp_name = format!("__temp_arg_{}", i);
7745                        self.context.bind(temp_name.clone(), value.clone());
7746                        temp_args.push(AstNode::Variable(temp_name));
7747                    }
7748
7749                    // Call the original function
7750                    let result =
7751                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
7752
7753                    // Pop temp scope
7754                    self.context.pop_scope();
7755
7756                    return result;
7757                }
7758            }
7759        }
7760
7761        // Evaluate lambda body (normal case)
7762        // Use captured_data for lexical scoping if available, otherwise use call-site data
7763        let body_data = captured_data.unwrap_or(data);
7764        let result = self.evaluate_internal(body, body_data)?;
7765
7766        // Pop lambda scope, preserving any lambdas referenced by the return value
7767        // Fast path: scalar results can never contain lambda references
7768        let is_scalar = matches!(&result,
7769            JValue::Number(_) | JValue::Bool(_) | JValue::String(_)
7770            | JValue::Null | JValue::Undefined);
7771        if is_scalar {
7772            self.context.pop_scope();
7773        } else {
7774            let lambdas_to_keep = self.extract_lambda_ids(&result);
7775            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
7776        }
7777
7778        Ok(result)
7779    }
7780
7781    /// Invoke a lambda with tail call optimization using a trampoline
7782    /// This method uses an iterative loop to handle tail-recursive calls without
7783    /// growing the stack, enabling deep recursion for tail-recursive functions.
7784    fn invoke_lambda_with_tco(
7785        &mut self,
7786        stored_lambda: &StoredLambda,
7787        initial_args: &[JValue],
7788        data: &JValue,
7789    ) -> Result<JValue, EvaluatorError> {
7790        let mut current_lambda = stored_lambda.clone();
7791        let mut current_args = initial_args.to_vec();
7792        let mut current_data = data.clone();
7793
7794        // Maximum number of tail call iterations to prevent infinite loops
7795        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
7796        const MAX_TCO_ITERATIONS: usize = 100_000;
7797        let mut iterations = 0;
7798
7799        // Push a persistent scope for the TCO trampoline loop.
7800        // This scope persists across all iterations so that lambdas defined
7801        // in one iteration (like recursive $iter) remain available in subsequent ones.
7802        self.context.push_scope();
7803
7804        // Trampoline loop - keeps evaluating until we get a final value
7805        let result = loop {
7806            iterations += 1;
7807            if iterations > MAX_TCO_ITERATIONS {
7808                self.context.pop_scope();
7809                return Err(EvaluatorError::EvaluationError(
7810                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
7811                ));
7812            }
7813
7814            // Evaluate the lambda body within the persistent scope
7815            let result =
7816                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
7817
7818            match result {
7819                LambdaResult::JValue(v) => break v,
7820                LambdaResult::TailCall { lambda, args, data } => {
7821                    // Continue with the tail call - no stack growth
7822                    current_lambda = *lambda;
7823                    current_args = args;
7824                    current_data = data;
7825                }
7826            }
7827        };
7828
7829        // Pop the persistent TCO scope, preserving lambdas referenced by the result
7830        let lambdas_to_keep = self.extract_lambda_ids(&result);
7831        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
7832
7833        Ok(result)
7834    }
7835
7836    /// Evaluate a lambda body, detecting tail calls for TCO
7837    /// Returns either a final value or a tail call continuation.
7838    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
7839    /// manages the persistent scope for the trampoline loop.
7840    fn invoke_lambda_body_for_tco(
7841        &mut self,
7842        lambda: &StoredLambda,
7843        values: &[JValue],
7844        data: &JValue,
7845    ) -> Result<LambdaResult, EvaluatorError> {
7846        // Validate signature if present
7847        let coerced_values = if let Some(sig_str) = &lambda.signature {
7848            match crate::signature::Signature::parse(sig_str) {
7849                Ok(sig) => match sig.validate_and_coerce(values) {
7850                    Ok(coerced) => coerced,
7851                    Err(e) => match e {
7852                        crate::signature::SignatureError::UndefinedArgument => {
7853                            return Ok(LambdaResult::JValue(JValue::Null));
7854                        }
7855                        crate::signature::SignatureError::ArgumentTypeMismatch {
7856                            index,
7857                            expected,
7858                        } => {
7859                            return Err(EvaluatorError::TypeError(
7860                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
7861                                    ));
7862                        }
7863                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
7864                            return Err(EvaluatorError::TypeError(format!(
7865                                "T0412: Argument {} of function must be an array of {}",
7866                                index, expected
7867                            )));
7868                        }
7869                        _ => {
7870                            return Err(EvaluatorError::TypeError(format!(
7871                                "Signature validation failed: {}",
7872                                e
7873                            )));
7874                        }
7875                    },
7876                },
7877                Err(e) => {
7878                    return Err(EvaluatorError::EvaluationError(format!(
7879                        "Invalid signature: {}",
7880                        e
7881                    )));
7882                }
7883            }
7884        } else {
7885            values.to_vec()
7886        };
7887
7888        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
7889        // Apply captured environment
7890        for (name, value) in &lambda.captured_env {
7891            self.context.bind(name.clone(), value.clone());
7892        }
7893
7894        // Bind parameters
7895        for (i, param) in lambda.params.iter().enumerate() {
7896            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
7897            self.context.bind(param.clone(), value);
7898        }
7899
7900        // Evaluate the body with tail call detection
7901        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
7902        self.evaluate_for_tco(&lambda.body, body_data)
7903    }
7904
7905    /// Evaluate an expression for TCO, detecting tail calls
7906    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
7907    fn evaluate_for_tco(
7908        &mut self,
7909        node: &AstNode,
7910        data: &JValue,
7911    ) -> Result<LambdaResult, EvaluatorError> {
7912        match node {
7913            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
7914            AstNode::Conditional {
7915                condition,
7916                then_branch,
7917                else_branch,
7918            } => {
7919                let cond_value = self.evaluate_internal(condition, data)?;
7920                let is_truthy = self.is_truthy(&cond_value);
7921
7922                if is_truthy {
7923                    self.evaluate_for_tco(then_branch, data)
7924                } else if let Some(else_expr) = else_branch {
7925                    self.evaluate_for_tco(else_expr, data)
7926                } else {
7927                    Ok(LambdaResult::JValue(JValue::Null))
7928                }
7929            }
7930
7931            // Block: evaluate all but last normally, last for TCO
7932            AstNode::Block(exprs) => {
7933                if exprs.is_empty() {
7934                    return Ok(LambdaResult::JValue(JValue::Null));
7935                }
7936
7937                // Evaluate all expressions except the last
7938                let mut result = JValue::Null;
7939                for (i, expr) in exprs.iter().enumerate() {
7940                    if i == exprs.len() - 1 {
7941                        // Last expression - evaluate for TCO
7942                        return self.evaluate_for_tco(expr, data);
7943                    } else {
7944                        result = self.evaluate_internal(expr, data)?;
7945                    }
7946                }
7947                Ok(LambdaResult::JValue(result))
7948            }
7949
7950            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
7951            AstNode::Binary {
7952                op: BinaryOp::ColonEqual,
7953                lhs,
7954                rhs,
7955            } => {
7956                // This is var := value; get the variable name
7957                let var_name = match lhs.as_ref() {
7958                    AstNode::Variable(name) => name.clone(),
7959                    _ => {
7960                        // Not a simple variable binding, evaluate normally
7961                        let result = self.evaluate_internal(node, data)?;
7962                        return Ok(LambdaResult::JValue(result));
7963                    }
7964                };
7965
7966                // Check if RHS is a lambda - store it specially
7967                if let AstNode::Lambda {
7968                    params,
7969                    body,
7970                    signature,
7971                    thunk,
7972                } = rhs.as_ref()
7973                {
7974                    let captured_env = self.capture_environment_for(body, params);
7975                    let compiled_body = if !thunk {
7976                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
7977                        try_compile_expr_with_allowed_vars(body, &var_refs)
7978                    } else {
7979                        None
7980                    };
7981                    let stored_lambda = StoredLambda {
7982                        params: params.clone(),
7983                        body: (**body).clone(),
7984                        compiled_body,
7985                        signature: signature.clone(),
7986                        captured_env,
7987                        captured_data: Some(data.clone()),
7988                        thunk: *thunk,
7989                    };
7990                    self.context.bind_lambda(var_name, stored_lambda);
7991                    let lambda_repr =
7992                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
7993                    return Ok(LambdaResult::JValue(lambda_repr));
7994                }
7995
7996                // Evaluate the RHS
7997                let value = self.evaluate_internal(rhs, data)?;
7998                self.context.bind(var_name, value.clone());
7999                Ok(LambdaResult::JValue(value))
8000            }
8001
8002            // Function call - this is where TCO happens
8003            AstNode::Function { name, args, .. } => {
8004                // Check if this is a call to a stored lambda (user function)
8005                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
8006                    if stored_lambda.thunk {
8007                        let mut evaluated_args = Vec::with_capacity(args.len());
8008                        for arg in args {
8009                            evaluated_args.push(self.evaluate_internal(arg, data)?);
8010                        }
8011                        return Ok(LambdaResult::TailCall {
8012                            lambda: Box::new(stored_lambda),
8013                            args: evaluated_args,
8014                            data: data.clone(),
8015                        });
8016                    }
8017                }
8018                // Not a thunk lambda - evaluate normally
8019                let result = self.evaluate_internal(node, data)?;
8020                Ok(LambdaResult::JValue(result))
8021            }
8022
8023            // Call node (calling a lambda value)
8024            AstNode::Call { procedure, args } => {
8025                // Evaluate the procedure to get the callable
8026                let callable = self.evaluate_internal(procedure, data)?;
8027
8028                // Check if it's a lambda with TCO
8029                if let JValue::Lambda { lambda_id, .. } = &callable {
8030                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
8031                        if stored_lambda.thunk {
8032                            let mut evaluated_args = Vec::with_capacity(args.len());
8033                            for arg in args {
8034                                evaluated_args.push(self.evaluate_internal(arg, data)?);
8035                            }
8036                            return Ok(LambdaResult::TailCall {
8037                                lambda: Box::new(stored_lambda),
8038                                args: evaluated_args,
8039                                data: data.clone(),
8040                            });
8041                        }
8042                    }
8043                }
8044                // Not a thunk - evaluate normally
8045                let result = self.evaluate_internal(node, data)?;
8046                Ok(LambdaResult::JValue(result))
8047            }
8048
8049            // Variable reference that might be a function call
8050            // This handles cases like $f($x) where $f is referenced by name
8051            AstNode::Variable(_) => {
8052                let result = self.evaluate_internal(node, data)?;
8053                Ok(LambdaResult::JValue(result))
8054            }
8055
8056            // Any other expression - evaluate normally
8057            _ => {
8058                let result = self.evaluate_internal(node, data)?;
8059                Ok(LambdaResult::JValue(result))
8060            }
8061        }
8062    }
8063
8064    /// Match with custom matcher function
8065    ///
8066    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
8067    /// The matcher function is called with the string and returns:
8068    /// { match: string, start: number, end: number, groups: [], next: function }
8069    /// The next function is called repeatedly to get subsequent matches
8070    fn match_with_custom_matcher(
8071        &mut self,
8072        str_value: &str,
8073        matcher_node: &AstNode,
8074        limit: Option<usize>,
8075        data: &JValue,
8076    ) -> Result<JValue, EvaluatorError> {
8077        let mut results = Vec::new();
8078        let mut count = 0;
8079
8080        // Call the matcher function with the string
8081        let str_val = JValue::string(str_value.to_string());
8082        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
8083
8084        // Iterate through matches following the 'next' chain
8085        while !current_match.is_undefined() && !current_match.is_null() {
8086            // Check limit
8087            if let Some(lim) = limit {
8088                if count >= lim {
8089                    break;
8090                }
8091            }
8092
8093            // Extract match information from the result object
8094            if let JValue::Object(ref match_obj) = current_match {
8095                // Validate that this is a proper match object
8096                let has_match = match_obj.contains_key("match");
8097                let has_start = match_obj.contains_key("start");
8098                let has_end = match_obj.contains_key("end");
8099                let has_groups = match_obj.contains_key("groups");
8100                let has_next = match_obj.contains_key("next");
8101
8102                if !has_match && !has_start && !has_end && !has_groups && !has_next {
8103                    // Invalid matcher result - T1010 error
8104                    return Err(EvaluatorError::EvaluationError(
8105                        "T1010: The matcher function did not return the correct object structure"
8106                            .to_string(),
8107                    ));
8108                }
8109
8110                // Build the result match object (match, index, groups)
8111                let mut result_obj = IndexMap::new();
8112
8113                if let Some(match_val) = match_obj.get("match") {
8114                    result_obj.insert("match".to_string(), match_val.clone());
8115                }
8116
8117                if let Some(start_val) = match_obj.get("start") {
8118                    result_obj.insert("index".to_string(), start_val.clone());
8119                }
8120
8121                if let Some(groups_val) = match_obj.get("groups") {
8122                    result_obj.insert("groups".to_string(), groups_val.clone());
8123                }
8124
8125                results.push(JValue::object(result_obj));
8126                count += 1;
8127
8128                // Get the next match by calling the 'next' function
8129                if let Some(next_func) = match_obj.get("next") {
8130                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
8131                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
8132                        continue;
8133                    }
8134                }
8135
8136                // No next function or couldn't call it - stop iteration
8137                break;
8138            } else {
8139                // Not a valid match object
8140                break;
8141            }
8142        }
8143
8144        // Return results
8145        if results.is_empty() {
8146            Ok(JValue::Undefined)
8147        } else {
8148            Ok(JValue::array(results))
8149        }
8150    }
8151
8152    /// Replace with lambda/function callback
8153    ///
8154    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
8155    /// The function receives a match object with: match, start, end, groups
8156    fn replace_with_lambda(
8157        &mut self,
8158        str_value: &JValue,
8159        pattern_value: &JValue,
8160        lambda_value: &JValue,
8161        limit_value: Option<&JValue>,
8162        data: &JValue,
8163    ) -> Result<JValue, EvaluatorError> {
8164        // Extract string
8165        let s = match str_value {
8166            JValue::String(s) => &**s,
8167            _ => {
8168                return Err(EvaluatorError::TypeError(
8169                    "replace() requires string arguments".to_string(),
8170                ))
8171            }
8172        };
8173
8174        // Extract regex pattern
8175        let (pattern, flags) =
8176            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
8177                EvaluatorError::TypeError(
8178                    "replace() pattern must be a regex when using lambda replacement".to_string(),
8179                )
8180            })?;
8181
8182        // Build regex
8183        let re = crate::functions::string::build_regex(&pattern, &flags)?;
8184
8185        // Parse limit
8186        let limit = if let Some(lim_val) = limit_value {
8187            match lim_val {
8188                JValue::Number(n) => {
8189                    let lim_f64 = *n;
8190                    if lim_f64 < 0.0 {
8191                        return Err(EvaluatorError::EvaluationError(format!(
8192                            "D3011: Limit must be non-negative, got {}",
8193                            lim_f64
8194                        )));
8195                    }
8196                    Some(lim_f64 as usize)
8197                }
8198                _ => {
8199                    return Err(EvaluatorError::TypeError(
8200                        "replace() limit must be a number".to_string(),
8201                    ))
8202                }
8203            }
8204        } else {
8205            None
8206        };
8207
8208        // Iterate through matches and replace using lambda
8209        let mut result = String::new();
8210        let mut last_end = 0;
8211        let mut count = 0;
8212
8213        for cap in re.captures_iter(s) {
8214            // Check limit
8215            if let Some(lim) = limit {
8216                if count >= lim {
8217                    break;
8218                }
8219            }
8220
8221            let m = cap.get(0).unwrap();
8222            let match_start = m.start();
8223            let match_end = m.end();
8224            let match_str = m.as_str();
8225
8226            // Add text before match
8227            result.push_str(&s[last_end..match_start]);
8228
8229            // Build match object
8230            let groups: Vec<JValue> = (1..cap.len())
8231                .map(|i| {
8232                    cap.get(i)
8233                        .map(|m| JValue::string(m.as_str().to_string()))
8234                        .unwrap_or(JValue::Null)
8235                })
8236                .collect();
8237
8238            let mut match_map = IndexMap::new();
8239            match_map.insert("match".to_string(), JValue::string(match_str));
8240            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
8241            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
8242            match_map.insert("groups".to_string(), JValue::array(groups));
8243            let match_obj = JValue::object(match_map);
8244
8245            // Invoke lambda with match object
8246            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
8247                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
8248            })?;
8249            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
8250            let replacement_str = match lambda_result {
8251                JValue::String(s) => s,
8252                _ => {
8253                    return Err(EvaluatorError::TypeError(format!(
8254                        "D3012: Replacement function must return a string, got {:?}",
8255                        lambda_result
8256                    )))
8257                }
8258            };
8259
8260            // Add replacement
8261            result.push_str(&replacement_str);
8262
8263            last_end = match_end;
8264            count += 1;
8265        }
8266
8267        // Add remaining text after last match
8268        result.push_str(&s[last_end..]);
8269
8270        Ok(JValue::string(result))
8271    }
8272
8273    /// Capture the current environment bindings for closure support
8274    fn capture_current_environment(&self) -> HashMap<String, JValue> {
8275        self.context.all_bindings()
8276    }
8277
8278    /// Capture only the variables referenced by a lambda body (selective capture).
8279    /// This avoids cloning the entire environment when only a few variables are needed.
8280    fn capture_environment_for(
8281        &self,
8282        body: &AstNode,
8283        params: &[String],
8284    ) -> HashMap<String, JValue> {
8285        let free_vars = Self::collect_free_variables(body, params);
8286        if free_vars.is_empty() {
8287            return HashMap::new();
8288        }
8289        let mut result = HashMap::new();
8290        for var_name in &free_vars {
8291            if let Some(value) = self.context.lookup(var_name) {
8292                result.insert(var_name.clone(), value.clone());
8293            }
8294        }
8295        result
8296    }
8297
8298    /// Collect all free variables in an AST node that are not bound by the given params.
8299    /// A "free variable" is one that is referenced but not defined within the expression.
8300    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
8301        let mut free_vars = HashSet::new();
8302        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
8303        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
8304        free_vars
8305    }
8306
8307    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
8308        match node {
8309            AstNode::Variable(name) => {
8310                if !bound.contains(name.as_str()) {
8311                    free.insert(name.clone());
8312                }
8313            }
8314            AstNode::Function { name, args, .. } => {
8315                // Function name references a variable (e.g., $f(...))
8316                if !bound.contains(name.as_str()) {
8317                    free.insert(name.clone());
8318                }
8319                for arg in args {
8320                    Self::collect_free_vars_walk(arg, bound, free);
8321                }
8322            }
8323            AstNode::Lambda { params, body, .. } => {
8324                // Inner lambda introduces new bindings
8325                let mut inner_bound = bound.clone();
8326                for p in params {
8327                    inner_bound.insert(p.as_str());
8328                }
8329                Self::collect_free_vars_walk(body, &inner_bound, free);
8330            }
8331            AstNode::Binary { op, lhs, rhs } => {
8332                Self::collect_free_vars_walk(lhs, bound, free);
8333                Self::collect_free_vars_walk(rhs, bound, free);
8334                // For ColonEqual, note: the binding is visible after this expr in blocks,
8335                // but block handling takes care of that separately
8336                let _ = op;
8337            }
8338            AstNode::Unary { operand, .. } => {
8339                Self::collect_free_vars_walk(operand, bound, free);
8340            }
8341            AstNode::Path { steps } => {
8342                for step in steps {
8343                    Self::collect_free_vars_walk(&step.node, bound, free);
8344                    for stage in &step.stages {
8345                        match stage {
8346                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
8347                        }
8348                    }
8349                }
8350            }
8351            AstNode::Call { procedure, args } => {
8352                Self::collect_free_vars_walk(procedure, bound, free);
8353                for arg in args {
8354                    Self::collect_free_vars_walk(arg, bound, free);
8355                }
8356            }
8357            AstNode::Conditional {
8358                condition,
8359                then_branch,
8360                else_branch,
8361            } => {
8362                Self::collect_free_vars_walk(condition, bound, free);
8363                Self::collect_free_vars_walk(then_branch, bound, free);
8364                if let Some(else_expr) = else_branch {
8365                    Self::collect_free_vars_walk(else_expr, bound, free);
8366                }
8367            }
8368            AstNode::Block(exprs) => {
8369                let mut block_bound = bound.clone();
8370                for expr in exprs {
8371                    Self::collect_free_vars_walk(expr, &block_bound, free);
8372                    // Bindings introduced via := become bound for subsequent expressions
8373                    if let AstNode::Binary {
8374                        op: BinaryOp::ColonEqual,
8375                        lhs,
8376                        ..
8377                    } = expr
8378                    {
8379                        if let AstNode::Variable(var_name) = lhs.as_ref() {
8380                            block_bound.insert(var_name.as_str());
8381                        }
8382                    }
8383                }
8384            }
8385            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
8386                for expr in exprs {
8387                    Self::collect_free_vars_walk(expr, bound, free);
8388                }
8389            }
8390            AstNode::Object(pairs) => {
8391                for (key, value) in pairs {
8392                    Self::collect_free_vars_walk(key, bound, free);
8393                    Self::collect_free_vars_walk(value, bound, free);
8394                }
8395            }
8396            AstNode::ObjectTransform { input, pattern } => {
8397                Self::collect_free_vars_walk(input, bound, free);
8398                for (key, value) in pattern {
8399                    Self::collect_free_vars_walk(key, bound, free);
8400                    Self::collect_free_vars_walk(value, bound, free);
8401                }
8402            }
8403            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
8404                Self::collect_free_vars_walk(expr, bound, free);
8405            }
8406            AstNode::Sort { input, terms } => {
8407                Self::collect_free_vars_walk(input, bound, free);
8408                for (expr, _) in terms {
8409                    Self::collect_free_vars_walk(expr, bound, free);
8410                }
8411            }
8412            AstNode::IndexBind { input, .. } => {
8413                Self::collect_free_vars_walk(input, bound, free);
8414            }
8415            AstNode::Transform {
8416                location,
8417                update,
8418                delete,
8419            } => {
8420                Self::collect_free_vars_walk(location, bound, free);
8421                Self::collect_free_vars_walk(update, bound, free);
8422                if let Some(del) = delete {
8423                    Self::collect_free_vars_walk(del, bound, free);
8424                }
8425            }
8426            // Leaf nodes with no variable references
8427            AstNode::String(_)
8428            | AstNode::Name(_)
8429            | AstNode::Number(_)
8430            | AstNode::Boolean(_)
8431            | AstNode::Null
8432            | AstNode::Undefined
8433            | AstNode::Placeholder
8434            | AstNode::Regex { .. }
8435            | AstNode::Wildcard
8436            | AstNode::Descendant
8437            | AstNode::ParentVariable(_) => {}
8438        }
8439    }
8440
8441    /// Check if a name refers to a built-in function
8442    fn is_builtin_function(&self, name: &str) -> bool {
8443        matches!(
8444            name,
8445            // String functions
8446            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
8447            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
8448            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
8449            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
8450
8451            // Numeric functions
8452            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
8453            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
8454
8455            // Aggregation functions
8456            "sum" | "max" | "min" | "average" |
8457
8458            // Boolean/logic functions
8459            "boolean" | "not" | "exists" |
8460
8461            // Array functions
8462            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
8463
8464            // Object functions
8465            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
8466
8467            // Higher-order functions
8468            "map" | "filter" | "reduce" | "singletonArray" |
8469
8470            // Date/time functions
8471            "now" | "millis" | "fromMillis" | "toMillis"
8472        )
8473    }
8474
8475    /// Call a built-in function directly with pre-evaluated Values
8476    /// This is used when passing built-in functions to higher-order functions like $map
8477    fn call_builtin_with_values(
8478        &mut self,
8479        name: &str,
8480        values: &[JValue],
8481    ) -> Result<JValue, EvaluatorError> {
8482        use crate::functions;
8483
8484        if values.is_empty() {
8485            return Err(EvaluatorError::EvaluationError(format!(
8486                "{}() requires at least 1 argument",
8487                name
8488            )));
8489        }
8490
8491        let arg = &values[0];
8492
8493        match name {
8494            "string" => Ok(functions::string::string(arg, None)?),
8495            "number" => Ok(functions::numeric::number(arg)?),
8496            "boolean" => Ok(functions::boolean::boolean(arg)?),
8497            "not" => {
8498                let b = functions::boolean::boolean(arg)?;
8499                match b {
8500                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
8501                    _ => Err(EvaluatorError::TypeError(
8502                        "not() requires a boolean".to_string(),
8503                    )),
8504                }
8505            }
8506            "exists" => Ok(JValue::Bool(!arg.is_null())),
8507            "abs" => match arg {
8508                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
8509                _ => Err(EvaluatorError::TypeError(
8510                    "abs() requires a number argument".to_string(),
8511                )),
8512            },
8513            "floor" => match arg {
8514                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
8515                _ => Err(EvaluatorError::TypeError(
8516                    "floor() requires a number argument".to_string(),
8517                )),
8518            },
8519            "ceil" => match arg {
8520                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
8521                _ => Err(EvaluatorError::TypeError(
8522                    "ceil() requires a number argument".to_string(),
8523                )),
8524            },
8525            "round" => match arg {
8526                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
8527                _ => Err(EvaluatorError::TypeError(
8528                    "round() requires a number argument".to_string(),
8529                )),
8530            },
8531            "sqrt" => match arg {
8532                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
8533                _ => Err(EvaluatorError::TypeError(
8534                    "sqrt() requires a number argument".to_string(),
8535                )),
8536            },
8537            "uppercase" => match arg {
8538                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
8539                JValue::Null => Ok(JValue::Null),
8540                _ => Err(EvaluatorError::TypeError(
8541                    "uppercase() requires a string argument".to_string(),
8542                )),
8543            },
8544            "lowercase" => match arg {
8545                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
8546                JValue::Null => Ok(JValue::Null),
8547                _ => Err(EvaluatorError::TypeError(
8548                    "lowercase() requires a string argument".to_string(),
8549                )),
8550            },
8551            "trim" => match arg {
8552                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
8553                JValue::Null => Ok(JValue::Null),
8554                _ => Err(EvaluatorError::TypeError(
8555                    "trim() requires a string argument".to_string(),
8556                )),
8557            },
8558            "length" => match arg {
8559                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
8560                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
8561                JValue::Null => Ok(JValue::Null),
8562                _ => Err(EvaluatorError::TypeError(
8563                    "length() requires a string or array argument".to_string(),
8564                )),
8565            },
8566            "sum" => match arg {
8567                JValue::Array(arr) => {
8568                    let mut total = 0.0;
8569                    for item in arr.iter() {
8570                        match item {
8571                            JValue::Number(n) => {
8572                                total += *n;
8573                            }
8574                            _ => {
8575                                return Err(EvaluatorError::TypeError(
8576                                    "sum() requires all array elements to be numbers".to_string(),
8577                                ));
8578                            }
8579                        }
8580                    }
8581                    Ok(JValue::Number(total))
8582                }
8583                JValue::Number(n) => Ok(JValue::Number(*n)),
8584                JValue::Null => Ok(JValue::Null),
8585                _ => Err(EvaluatorError::TypeError(
8586                    "sum() requires an array of numbers".to_string(),
8587                )),
8588            },
8589            "count" => {
8590                match arg {
8591                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
8592                    JValue::Null => Ok(JValue::Number(0.0)),
8593                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
8594                }
8595            }
8596            "max" => match arg {
8597                JValue::Array(arr) => {
8598                    let mut max_val: Option<f64> = None;
8599                    for item in arr.iter() {
8600                        if let JValue::Number(n) = item {
8601                            let f = *n;
8602                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
8603                        }
8604                    }
8605                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
8606                }
8607                JValue::Number(n) => Ok(JValue::Number(*n)),
8608                JValue::Null => Ok(JValue::Null),
8609                _ => Err(EvaluatorError::TypeError(
8610                    "max() requires an array of numbers".to_string(),
8611                )),
8612            },
8613            "min" => match arg {
8614                JValue::Array(arr) => {
8615                    let mut min_val: Option<f64> = None;
8616                    for item in arr.iter() {
8617                        if let JValue::Number(n) = item {
8618                            let f = *n;
8619                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
8620                        }
8621                    }
8622                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
8623                }
8624                JValue::Number(n) => Ok(JValue::Number(*n)),
8625                JValue::Null => Ok(JValue::Null),
8626                _ => Err(EvaluatorError::TypeError(
8627                    "min() requires an array of numbers".to_string(),
8628                )),
8629            },
8630            "average" => match arg {
8631                JValue::Array(arr) => {
8632                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
8633                    if nums.is_empty() {
8634                        Ok(JValue::Null)
8635                    } else {
8636                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
8637                        Ok(JValue::Number(avg))
8638                    }
8639                }
8640                JValue::Number(n) => Ok(JValue::Number(*n)),
8641                JValue::Null => Ok(JValue::Null),
8642                _ => Err(EvaluatorError::TypeError(
8643                    "average() requires an array of numbers".to_string(),
8644                )),
8645            },
8646            "append" => {
8647                // append(array1, array2) - append second array to first
8648                if values.len() < 2 {
8649                    return Err(EvaluatorError::EvaluationError(
8650                        "append() requires 2 arguments".to_string(),
8651                    ));
8652                }
8653                let first = &values[0];
8654                let second = &values[1];
8655
8656                // Convert first to array if needed
8657                let mut result = match first {
8658                    JValue::Array(arr) => arr.to_vec(),
8659                    JValue::Null => vec![],
8660                    other => vec![other.clone()],
8661                };
8662
8663                // Append second (flatten if array)
8664                match second {
8665                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
8666                    JValue::Null => {}
8667                    other => result.push(other.clone()),
8668                }
8669
8670                Ok(JValue::array(result))
8671            }
8672            "reverse" => match arg {
8673                JValue::Array(arr) => {
8674                    let mut reversed = arr.to_vec();
8675                    reversed.reverse();
8676                    Ok(JValue::array(reversed))
8677                }
8678                JValue::Null => Ok(JValue::Null),
8679                _ => Err(EvaluatorError::TypeError(
8680                    "reverse() requires an array".to_string(),
8681                )),
8682            },
8683            "keys" => match arg {
8684                JValue::Object(obj) => {
8685                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
8686                    Ok(JValue::array(keys))
8687                }
8688                JValue::Null => Ok(JValue::Null),
8689                _ => Err(EvaluatorError::TypeError(
8690                    "keys() requires an object".to_string(),
8691                )),
8692            },
8693
8694            // Add more functions as needed
8695            _ => Err(EvaluatorError::ReferenceError(format!(
8696                "Built-in function {} cannot be called with values directly",
8697                name
8698            ))),
8699        }
8700    }
8701
8702    /// Collect all descendant values recursively
8703    fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
8704        let mut descendants = Vec::new();
8705
8706        match value {
8707            JValue::Null => {
8708                // Null has no descendants, return empty
8709                return descendants;
8710            }
8711            JValue::Object(obj) => {
8712                // Include the current object
8713                descendants.push(value.clone());
8714
8715                for val in obj.values() {
8716                    // Recursively collect descendants
8717                    descendants.extend(self.collect_descendants(val));
8718                }
8719            }
8720            JValue::Array(arr) => {
8721                // DO NOT include the array itself - only recurse into elements
8722                // This matches JavaScript behavior: arrays are traversed but not collected
8723                for val in arr.iter() {
8724                    // Recursively collect descendants
8725                    descendants.extend(self.collect_descendants(val));
8726                }
8727            }
8728            _ => {
8729                // For primitives (string, number, boolean), just include the value itself
8730                descendants.push(value.clone());
8731            }
8732        }
8733
8734        descendants
8735    }
8736
8737    /// Evaluate a predicate (array filter or index)
8738    fn evaluate_predicate(
8739        &mut self,
8740        current: &JValue,
8741        predicate: &AstNode,
8742    ) -> Result<JValue, EvaluatorError> {
8743        // Special case: empty brackets [] (represented as Boolean(true))
8744        // This forces the value to be wrapped in an array
8745        if matches!(predicate, AstNode::Boolean(true)) {
8746            return match current {
8747                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
8748                JValue::Null => Ok(JValue::Null),
8749                other => Ok(JValue::array(vec![other.clone()])),
8750            };
8751        }
8752
8753        match current {
8754            JValue::Array(_arr) => {
8755                // Standalone predicates do simple array operations (no mapping over sub-arrays)
8756
8757                // First, try to evaluate predicate as a simple number (array index)
8758                if let AstNode::Number(n) = predicate {
8759                    // Direct array indexing
8760                    return self.array_index(current, &JValue::Number(*n));
8761                }
8762
8763                // Fast path: if predicate is definitely a filter expression (comparison/logical),
8764                // skip speculative numeric evaluation and go directly to filter logic
8765                if Self::is_filter_predicate(predicate) {
8766                    // Try CompiledExpr fast path
8767                    if let Some(compiled) = try_compile_expr(predicate) {
8768                        let shape = _arr.first().and_then(build_shape_cache);
8769                        let mut filtered = Vec::with_capacity(_arr.len());
8770                        for item in _arr.iter() {
8771                            let result = if let Some(ref s) = shape {
8772                                eval_compiled_shaped(&compiled, item, None, s)?
8773                            } else {
8774                                eval_compiled(&compiled, item, None)?
8775                            };
8776                            if compiled_is_truthy(&result) {
8777                                filtered.push(item.clone());
8778                            }
8779                        }
8780                        return Ok(JValue::array(filtered));
8781                    }
8782                    // Fallback: full AST evaluation per element
8783                    let mut filtered = Vec::new();
8784                    for item in _arr.iter() {
8785                        let item_result = self.evaluate_internal(predicate, item)?;
8786                        if self.is_truthy(&item_result) {
8787                            filtered.push(item.clone());
8788                        }
8789                    }
8790                    return Ok(JValue::array(filtered));
8791                }
8792
8793                // Try to evaluate the predicate to see if it's a numeric index
8794                // If evaluation succeeds and yields a number, use it as an index
8795                // If evaluation fails (e.g., comparison error), treat as filter
8796                match self.evaluate_internal(predicate, current) {
8797                    Ok(JValue::Number(_)) => {
8798                        // It's a numeric index
8799                        let pred_result = self.evaluate_internal(predicate, current)?;
8800                        return self.array_index(current, &pred_result);
8801                    }
8802                    Ok(JValue::Array(indices)) => {
8803                        // Multiple array selectors [[indices]]
8804                        // Check if array contains any non-numeric values
8805                        let has_non_numeric =
8806                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
8807
8808                        if has_non_numeric {
8809                            // If array contains non-numeric values, return entire array
8810                            return Ok(current.clone());
8811                        }
8812
8813                        // Collect numeric indices, handling negative indices
8814                        let arr_len = _arr.len() as i64;
8815                        let mut resolved_indices: Vec<i64> = indices
8816                            .iter()
8817                            .filter_map(|v| {
8818                                if let JValue::Number(n) = v {
8819                                    let idx = *n as i64;
8820                                    // Resolve negative indices
8821                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
8822                                    // Only include valid indices
8823                                    if actual_idx >= 0 && actual_idx < arr_len {
8824                                        Some(actual_idx)
8825                                    } else {
8826                                        None
8827                                    }
8828                                } else {
8829                                    None
8830                                }
8831                            })
8832                            .collect();
8833
8834                        // Sort and deduplicate indices
8835                        resolved_indices.sort();
8836                        resolved_indices.dedup();
8837
8838                        // Select elements at each sorted index
8839                        let result: Vec<JValue> = resolved_indices
8840                            .iter()
8841                            .map(|&idx| _arr[idx as usize].clone())
8842                            .collect();
8843
8844                        return Ok(JValue::array(result));
8845                    }
8846                    Ok(_) => {
8847                        // Evaluated successfully but not a number - might be a filter
8848                        // Fall through to filter logic
8849                    }
8850                    Err(_) => {
8851                        // Evaluation failed - it's likely a filter expression
8852                        // Fall through to filter logic
8853                    }
8854                }
8855
8856                // Try CompiledExpr fast path for filter expressions
8857                if let Some(compiled) = try_compile_expr(predicate) {
8858                    let shape = _arr.first().and_then(build_shape_cache);
8859                    let mut filtered = Vec::with_capacity(_arr.len());
8860                    for item in _arr.iter() {
8861                        let result = if let Some(ref s) = shape {
8862                            eval_compiled_shaped(&compiled, item, None, s)?
8863                        } else {
8864                            eval_compiled(&compiled, item, None)?
8865                        };
8866                        if compiled_is_truthy(&result) {
8867                            filtered.push(item.clone());
8868                        }
8869                    }
8870                    return Ok(JValue::array(filtered));
8871                }
8872
8873                // It's a filter expression - evaluate the predicate for each array element
8874                let mut filtered = Vec::new();
8875                for item in _arr.iter() {
8876                    let item_result = self.evaluate_internal(predicate, item)?;
8877
8878                    // If result is truthy, include this item
8879                    if self.is_truthy(&item_result) {
8880                        filtered.push(item.clone());
8881                    }
8882                }
8883
8884                Ok(JValue::array(filtered))
8885            }
8886            JValue::Object(obj) => {
8887                // For objects, predicate can be either:
8888                // 1. A string - property access (computed property name)
8889                // 2. A boolean expression - filter (return object if truthy)
8890                let pred_result = self.evaluate_internal(predicate, current)?;
8891
8892                // If it's a string, use it as a key for property access
8893                if let JValue::String(key) = &pred_result {
8894                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
8895                }
8896
8897                // Otherwise, treat as a filter expression
8898                // If the predicate is truthy, return the object; otherwise return undefined
8899                if self.is_truthy(&pred_result) {
8900                    Ok(current.clone())
8901                } else {
8902                    Ok(JValue::Undefined)
8903                }
8904            }
8905            _ => {
8906                // For primitive values (string, number, boolean):
8907                // In JSONata, scalars are treated as single-element arrays when indexed.
8908                // So value[0] returns value, value[1] returns undefined.
8909
8910                // First check if predicate is a numeric literal
8911                if let AstNode::Number(n) = predicate {
8912                    // For scalars, index 0 or -1 returns the value, others return undefined
8913                    let idx = n.floor() as i64;
8914                    if idx == 0 || idx == -1 {
8915                        return Ok(current.clone());
8916                    } else {
8917                        return Ok(JValue::Undefined);
8918                    }
8919                }
8920
8921                // Try to evaluate the predicate to see if it's a numeric index
8922                let pred_result = self.evaluate_internal(predicate, current)?;
8923
8924                if let JValue::Number(n) = &pred_result {
8925                    // It's a numeric index - treat scalar as single-element array
8926                    let idx = n.floor() as i64;
8927                    if idx == 0 || idx == -1 {
8928                        return Ok(current.clone());
8929                    } else {
8930                        return Ok(JValue::Undefined);
8931                    }
8932                }
8933
8934                // For non-numeric predicates, treat as a filter:
8935                // value[true] returns value, value[false] returns undefined
8936                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
8937                if self.is_truthy(&pred_result) {
8938                    Ok(current.clone())
8939                } else {
8940                    // Return undefined (not null) so $map can filter it out
8941                    Ok(JValue::Undefined)
8942                }
8943            }
8944        }
8945    }
8946
8947    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
8948    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
8949    fn evaluate_sort_term(
8950        &mut self,
8951        term_expr: &AstNode,
8952        element: &JValue,
8953    ) -> Result<JValue, EvaluatorError> {
8954        // For tuples (from index binding), extract the actual value from @ field
8955        let actual_element = if let JValue::Object(obj) = element {
8956            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
8957                obj.get("@").cloned().unwrap_or(JValue::Null)
8958            } else {
8959                element.clone()
8960            }
8961        } else {
8962            element.clone()
8963        };
8964
8965        // For simple field access (Path with single Name step), check if field exists
8966        if let AstNode::Path { steps } = term_expr {
8967            if steps.len() == 1 && steps[0].stages.is_empty() {
8968                if let AstNode::Name(field_name) = &steps[0].node {
8969                    // Check if the field exists in the element
8970                    if let JValue::Object(obj) = &actual_element {
8971                        return match obj.get(field_name) {
8972                            Some(val) => Ok(val.clone()),  // Field exists (may be null)
8973                            None => Ok(JValue::Undefined), // Field is missing
8974                        };
8975                    } else {
8976                        // Not an object - return undefined
8977                        return Ok(JValue::Undefined);
8978                    }
8979                }
8980            }
8981        }
8982
8983        // For complex expressions, evaluate normally against the actual element
8984        // but with the full tuple as the data context (so index bindings are accessible)
8985        let result = self.evaluate_internal(term_expr, element)?;
8986
8987        // If the result is null from a complex expression, we can't easily tell if it's
8988        // "missing field" or "explicit null". For now, treat null results as undefined
8989        // to maintain compatibility with existing tests.
8990        // TODO: For full JS compatibility, would need deeper analysis of the expression
8991        if result.is_null() {
8992            return Ok(JValue::Undefined);
8993        }
8994
8995        Ok(result)
8996    }
8997
8998    /// Evaluate sort operator
8999    fn evaluate_sort(
9000        &mut self,
9001        data: &JValue,
9002        terms: &[(AstNode, bool)],
9003    ) -> Result<JValue, EvaluatorError> {
9004        // If data is null, return null
9005        if data.is_null() {
9006            return Ok(JValue::Null);
9007        }
9008
9009        // If data is not an array, return it as-is (can't sort a single value)
9010        let array = match data {
9011            JValue::Array(arr) => arr.clone(),
9012            other => return Ok(other.clone()),
9013        };
9014
9015        // If empty array, return as-is
9016        if array.is_empty() {
9017            return Ok(JValue::Array(array));
9018        }
9019
9020        // Evaluate sort keys for each element
9021        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
9022
9023        for (idx, element) in array.iter().enumerate() {
9024            let mut sort_keys = Vec::new();
9025
9026            // Evaluate each sort term with $ bound to the element
9027            for (term_expr, _ascending) in terms {
9028                // Save current $ binding
9029                let saved_dollar = self.context.lookup("$").cloned();
9030
9031                // Bind $ to current element
9032                self.context.bind("$".to_string(), element.clone());
9033
9034                // Evaluate the sort expression, distinguishing missing fields from explicit null
9035                let sort_value = self.evaluate_sort_term(term_expr, element)?;
9036
9037                // Restore $ binding
9038                if let Some(val) = saved_dollar {
9039                    self.context.bind("$".to_string(), val);
9040                } else {
9041                    self.context.unbind("$");
9042                }
9043
9044                sort_keys.push(sort_value);
9045            }
9046
9047            indexed_array.push((idx, sort_keys));
9048        }
9049
9050        // Validate that all sort keys are comparable (same type, or undefined)
9051        // Undefined values (missing fields) are allowed and sort to the end
9052        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
9053        for term_idx in 0..terms.len() {
9054            let mut first_valid_type: Option<&str> = None;
9055
9056            for (_idx, sort_keys) in &indexed_array {
9057                let sort_value = &sort_keys[term_idx];
9058
9059                // Skip undefined markers (missing fields) - these are allowed and sort to end
9060                if sort_value.is_undefined() {
9061                    continue;
9062                }
9063
9064                // Get the type name for this value
9065                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
9066                let value_type = match sort_value {
9067                    JValue::Number(_) => "number",
9068                    JValue::String(_) => "string",
9069                    JValue::Bool(_) => "boolean",
9070                    JValue::Array(_) => "array",
9071                    JValue::Object(_) => "object", // This catches non-undefined objects
9072                    JValue::Null => "null",        // Explicit null from data
9073                    _ => "unknown",
9074                };
9075
9076                // Check that sort keys are only numbers or strings
9077                // Null, boolean, array, and object types are not valid for sorting
9078                if value_type != "number" && value_type != "string" {
9079                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
9080                }
9081
9082                // Check if this matches the first valid type we saw
9083                if let Some(first_type) = first_valid_type {
9084                    if first_type != value_type {
9085                        return Err(EvaluatorError::TypeError(format!(
9086                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
9087                            first_type, value_type
9088                        )));
9089                    }
9090                } else {
9091                    first_valid_type = Some(value_type);
9092                }
9093            }
9094        }
9095
9096        // Sort the indexed array
9097        indexed_array.sort_by(|a, b| {
9098            // Compare sort keys in order
9099            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
9100                let left = &a.1[i];
9101                let right = &b.1[i];
9102
9103                let cmp = self.compare_values(left, right);
9104
9105                if cmp != std::cmp::Ordering::Equal {
9106                    return if *ascending { cmp } else { cmp.reverse() };
9107                }
9108            }
9109
9110            // If all keys are equal, maintain original order (stable sort)
9111            a.0.cmp(&b.0)
9112        });
9113
9114        // Extract sorted elements
9115        let sorted: Vec<JValue> = indexed_array
9116            .iter()
9117            .map(|(idx, _)| array[*idx].clone())
9118            .collect();
9119
9120        Ok(JValue::array(sorted))
9121    }
9122
9123    /// Compare two values for sorting (JSONata semantics)
9124    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
9125        // Handle undefined markers first - they sort to the end
9126        let left_undef = left.is_undefined();
9127        let right_undef = right.is_undefined();
9128
9129        if left_undef && right_undef {
9130            return Ordering::Equal;
9131        }
9132        if left_undef {
9133            return Ordering::Greater; // Undefined sorts last
9134        }
9135        if right_undef {
9136            return Ordering::Less;
9137        }
9138
9139        match (left, right) {
9140            // Nulls also sort last (explicit null in data)
9141            (JValue::Null, JValue::Null) => Ordering::Equal,
9142            (JValue::Null, _) => Ordering::Greater,
9143            (_, JValue::Null) => Ordering::Less,
9144
9145            // Numbers
9146            (JValue::Number(a), JValue::Number(b)) => {
9147                let a_f64 = *a;
9148                let b_f64 = *b;
9149                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
9150            }
9151
9152            // Strings
9153            (JValue::String(a), JValue::String(b)) => a.cmp(b),
9154
9155            // Booleans
9156            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
9157
9158            // Arrays (lexicographic comparison)
9159            (JValue::Array(a), JValue::Array(b)) => {
9160                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
9161                    let cmp = self.compare_values(a_elem, b_elem);
9162                    if cmp != Ordering::Equal {
9163                        return cmp;
9164                    }
9165                }
9166                a.len().cmp(&b.len())
9167            }
9168
9169            // Different types: use type ordering
9170            // null < bool < number < string < array < object
9171            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
9172            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
9173            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
9174            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
9175
9176            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
9177            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
9178            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
9179            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
9180
9181            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
9182            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
9183            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
9184            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
9185
9186            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
9187            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
9188            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
9189            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
9190
9191            (JValue::Object(_), _) => Ordering::Greater,
9192            _ => Ordering::Equal,
9193        }
9194    }
9195
9196    /// Check if a value is truthy (JSONata semantics).
9197    fn is_truthy(&self, value: &JValue) -> bool {
9198        match value {
9199            JValue::Null | JValue::Undefined => false,
9200            JValue::Bool(b) => *b,
9201            JValue::Number(n) => *n != 0.0,
9202            JValue::String(s) => !s.is_empty(),
9203            JValue::Array(arr) => !arr.is_empty(),
9204            JValue::Object(obj) => !obj.is_empty(),
9205            _ => false,
9206        }
9207    }
9208
9209    /// Check if a value is truthy for the default operator (?:)
9210    /// This has special semantics:
9211    /// - Lambda/function objects are not values, so they're falsy
9212    /// - Arrays containing only falsy elements are falsy
9213    /// - Otherwise, use standard truthiness
9214    fn is_truthy_for_default(&self, value: &JValue) -> bool {
9215        match value {
9216            // Lambda/function values are not data values, so they're falsy
9217            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
9218            // Arrays need special handling - check if all elements are falsy
9219            JValue::Array(arr) => {
9220                if arr.is_empty() {
9221                    return false;
9222                }
9223                // Array is truthy only if it contains at least one truthy element
9224                arr.iter().any(|elem| self.is_truthy(elem))
9225            }
9226            // For all other types, use standard truthiness
9227            _ => self.is_truthy(value),
9228        }
9229    }
9230
9231    /// Unwrap singleton arrays to scalar values
9232    /// This is used when no explicit array-keeping operation (like []) was used
9233    fn unwrap_singleton(&self, value: JValue) -> JValue {
9234        match value {
9235            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
9236            _ => value,
9237        }
9238    }
9239
9240    /// Extract lambda IDs from a value (used for closure preservation)
9241    /// Finds any lambda_id references in the value so they can be preserved
9242    /// when exiting a block scope
9243    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
9244        // Fast path: scalars can never contain lambda references
9245        match value {
9246            JValue::Number(_) | JValue::Bool(_) | JValue::String(_)
9247            | JValue::Null | JValue::Undefined | JValue::Regex { .. }
9248            | JValue::Builtin { .. } => return Vec::new(),
9249            _ => {}
9250        }
9251        let mut ids = Vec::new();
9252        self.collect_lambda_ids(value, &mut ids);
9253        ids
9254    }
9255
9256    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
9257        match value {
9258            JValue::Lambda { lambda_id, .. } => {
9259                let id_str = lambda_id.to_string();
9260                if !ids.contains(&id_str) {
9261                    ids.push(id_str);
9262                    // Transitively follow the stored lambda's captured_env
9263                    // to find all referenced lambdas. This is critical for
9264                    // closures like the Y-combinator where returned lambdas
9265                    // capture other lambdas in their environment.
9266                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
9267                        let env_values: Vec<JValue> =
9268                            stored.captured_env.values().cloned().collect();
9269                        for env_value in &env_values {
9270                            self.collect_lambda_ids(env_value, ids);
9271                        }
9272                    }
9273                }
9274            }
9275            JValue::Object(map) => {
9276                // Recurse into object values
9277                for v in map.values() {
9278                    self.collect_lambda_ids(v, ids);
9279                }
9280            }
9281            JValue::Array(arr) => {
9282                // Recurse into array elements
9283                for v in arr.iter() {
9284                    self.collect_lambda_ids(v, ids);
9285                }
9286            }
9287            _ => {}
9288        }
9289    }
9290
9291    /// Equality comparison (JSONata semantics)
9292    fn equals(&self, left: &JValue, right: &JValue) -> bool {
9293        crate::functions::array::values_equal(left, right)
9294    }
9295
9296    /// Addition
9297    fn add(
9298        &self,
9299        left: &JValue,
9300        right: &JValue,
9301        left_is_explicit_null: bool,
9302        right_is_explicit_null: bool,
9303    ) -> Result<JValue, EvaluatorError> {
9304        match (left, right) {
9305            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
9306            // Explicit null literal with number -> T2002 error
9307            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
9308                Err(EvaluatorError::TypeError(
9309                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
9310                ))
9311            }
9312            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
9313                Err(EvaluatorError::TypeError(
9314                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
9315                ))
9316            }
9317            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
9318                Err(EvaluatorError::TypeError(
9319                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
9320                ))
9321            }
9322            // Undefined variable (null) with number -> undefined result
9323            (JValue::Null, JValue::Number(_)) | (JValue::Number(_), JValue::Null) => {
9324                Ok(JValue::Null)
9325            }
9326            // Boolean with anything (including undefined) -> T2001 error
9327            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
9328                "T2001: The left side of the '+' operator must evaluate to a number or a string"
9329                    .to_string(),
9330            )),
9331            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
9332                "T2001: The right side of the '+' operator must evaluate to a number or a string"
9333                    .to_string(),
9334            )),
9335            // Undefined with undefined -> undefined
9336            (JValue::Null, JValue::Null) => Ok(JValue::Null),
9337            _ => Err(EvaluatorError::TypeError(format!(
9338                "Cannot add {:?} and {:?}",
9339                left, right
9340            ))),
9341        }
9342    }
9343
9344    /// Subtraction
9345    fn subtract(
9346        &self,
9347        left: &JValue,
9348        right: &JValue,
9349        left_is_explicit_null: bool,
9350        right_is_explicit_null: bool,
9351    ) -> Result<JValue, EvaluatorError> {
9352        match (left, right) {
9353            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
9354            // Explicit null literal -> error
9355            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9356                "T2002: The left side of the - operator must evaluate to a number".to_string(),
9357            )),
9358            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9359                "T2002: The right side of the - operator must evaluate to a number".to_string(),
9360            )),
9361            // Undefined variables -> undefined result
9362            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9363            _ => Err(EvaluatorError::TypeError(format!(
9364                "Cannot subtract {:?} and {:?}",
9365                left, right
9366            ))),
9367        }
9368    }
9369
9370    /// Multiplication
9371    fn multiply(
9372        &self,
9373        left: &JValue,
9374        right: &JValue,
9375        left_is_explicit_null: bool,
9376        right_is_explicit_null: bool,
9377    ) -> Result<JValue, EvaluatorError> {
9378        match (left, right) {
9379            (JValue::Number(a), JValue::Number(b)) => {
9380                let result = *a * *b;
9381                // Check for overflow to Infinity
9382                if result.is_infinite() {
9383                    return Err(EvaluatorError::EvaluationError(
9384                        "D1001: Number out of range".to_string(),
9385                    ));
9386                }
9387                Ok(JValue::Number(result))
9388            }
9389            // Explicit null literal -> error
9390            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9391                "T2002: The left side of the * operator must evaluate to a number".to_string(),
9392            )),
9393            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9394                "T2002: The right side of the * operator must evaluate to a number".to_string(),
9395            )),
9396            // Undefined variables -> undefined result
9397            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9398            _ => Err(EvaluatorError::TypeError(format!(
9399                "Cannot multiply {:?} and {:?}",
9400                left, right
9401            ))),
9402        }
9403    }
9404
9405    /// Division
9406    fn divide(
9407        &self,
9408        left: &JValue,
9409        right: &JValue,
9410        left_is_explicit_null: bool,
9411        right_is_explicit_null: bool,
9412    ) -> Result<JValue, EvaluatorError> {
9413        match (left, right) {
9414            (JValue::Number(a), JValue::Number(b)) => {
9415                let denominator = *b;
9416                if denominator == 0.0 {
9417                    return Err(EvaluatorError::EvaluationError(
9418                        "Division by zero".to_string(),
9419                    ));
9420                }
9421                Ok(JValue::Number(*a / denominator))
9422            }
9423            // Explicit null literal -> error
9424            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9425                "T2002: The left side of the / operator must evaluate to a number".to_string(),
9426            )),
9427            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9428                "T2002: The right side of the / operator must evaluate to a number".to_string(),
9429            )),
9430            // Undefined variables -> undefined result
9431            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9432            _ => Err(EvaluatorError::TypeError(format!(
9433                "Cannot divide {:?} and {:?}",
9434                left, right
9435            ))),
9436        }
9437    }
9438
9439    /// Modulo
9440    fn modulo(
9441        &self,
9442        left: &JValue,
9443        right: &JValue,
9444        left_is_explicit_null: bool,
9445        right_is_explicit_null: bool,
9446    ) -> Result<JValue, EvaluatorError> {
9447        match (left, right) {
9448            (JValue::Number(a), JValue::Number(b)) => {
9449                let denominator = *b;
9450                if denominator == 0.0 {
9451                    return Err(EvaluatorError::EvaluationError(
9452                        "Division by zero".to_string(),
9453                    ));
9454                }
9455                Ok(JValue::Number(*a % denominator))
9456            }
9457            // Explicit null literal -> error
9458            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9459                "T2002: The left side of the % operator must evaluate to a number".to_string(),
9460            )),
9461            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9462                "T2002: The right side of the % operator must evaluate to a number".to_string(),
9463            )),
9464            // Undefined variables -> undefined result
9465            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9466            _ => Err(EvaluatorError::TypeError(format!(
9467                "Cannot compute modulo of {:?} and {:?}",
9468                left, right
9469            ))),
9470        }
9471    }
9472
9473    /// Get human-readable type name for error messages
9474    fn type_name(value: &JValue) -> &'static str {
9475        match value {
9476            JValue::Null => "null",
9477            JValue::Bool(_) => "boolean",
9478            JValue::Number(_) => "number",
9479            JValue::String(_) => "string",
9480            JValue::Array(_) => "array",
9481            JValue::Object(_) => "object",
9482            _ => "unknown",
9483        }
9484    }
9485
9486    /// Ordered comparison with null/type checking shared across <, <=, >, >=
9487    ///
9488    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
9489    /// `compare_strs` receives (left_str, right_str) for string operands.
9490    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
9491    fn ordered_compare(
9492        &self,
9493        left: &JValue,
9494        right: &JValue,
9495        left_is_explicit_null: bool,
9496        right_is_explicit_null: bool,
9497        op_symbol: &str,
9498        compare_nums: fn(f64, f64) -> bool,
9499        compare_strs: fn(&str, &str) -> bool,
9500    ) -> Result<JValue, EvaluatorError> {
9501        match (left, right) {
9502            (JValue::Number(a), JValue::Number(b)) => {
9503                Ok(JValue::Bool(compare_nums(*a, *b)))
9504            }
9505            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
9506            // Both null/undefined -> return undefined
9507            (JValue::Null, JValue::Null) => Ok(JValue::Null),
9508            // Explicit null literal with any type (except null) -> T2010 error
9509            (JValue::Null, _) if left_is_explicit_null => {
9510                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9511            }
9512            (_, JValue::Null) if right_is_explicit_null => {
9513                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9514            }
9515            // Boolean with undefined -> T2010 error
9516            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
9517                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9518            }
9519            // Number or String with undefined (not explicit null) -> undefined result
9520            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
9521            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
9522                Ok(JValue::Null)
9523            }
9524            // String vs Number -> T2009
9525            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
9526                Err(EvaluatorError::EvaluationError(format!(
9527                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
9528                    op_symbol
9529                )))
9530            }
9531            // Boolean comparisons -> T2010
9532            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
9533                Err(EvaluatorError::EvaluationError(format!(
9534                    "T2010: Cannot compare {} and {}",
9535                    Self::type_name(left), Self::type_name(right)
9536                )))
9537            }
9538            // Other type mismatches
9539            _ => Err(EvaluatorError::EvaluationError(format!(
9540                "T2010: Cannot compare {} and {}",
9541                Self::type_name(left), Self::type_name(right)
9542            ))),
9543        }
9544    }
9545
9546    /// Less than comparison
9547    fn less_than(
9548        &self,
9549        left: &JValue,
9550        right: &JValue,
9551        left_is_explicit_null: bool,
9552        right_is_explicit_null: bool,
9553    ) -> Result<JValue, EvaluatorError> {
9554        self.ordered_compare(
9555            left,
9556            right,
9557            left_is_explicit_null,
9558            right_is_explicit_null,
9559            "<",
9560            |a, b| a < b,
9561            |a, b| a < b,
9562        )
9563    }
9564
9565    /// Less than or equal comparison
9566    fn less_than_or_equal(
9567        &self,
9568        left: &JValue,
9569        right: &JValue,
9570        left_is_explicit_null: bool,
9571        right_is_explicit_null: bool,
9572    ) -> Result<JValue, EvaluatorError> {
9573        self.ordered_compare(
9574            left,
9575            right,
9576            left_is_explicit_null,
9577            right_is_explicit_null,
9578            "<=",
9579            |a, b| a <= b,
9580            |a, b| a <= b,
9581        )
9582    }
9583
9584    /// Greater than comparison
9585    fn greater_than(
9586        &self,
9587        left: &JValue,
9588        right: &JValue,
9589        left_is_explicit_null: bool,
9590        right_is_explicit_null: bool,
9591    ) -> Result<JValue, EvaluatorError> {
9592        self.ordered_compare(
9593            left,
9594            right,
9595            left_is_explicit_null,
9596            right_is_explicit_null,
9597            ">",
9598            |a, b| a > b,
9599            |a, b| a > b,
9600        )
9601    }
9602
9603    /// Greater than or equal comparison
9604    fn greater_than_or_equal(
9605        &self,
9606        left: &JValue,
9607        right: &JValue,
9608        left_is_explicit_null: bool,
9609        right_is_explicit_null: bool,
9610    ) -> Result<JValue, EvaluatorError> {
9611        self.ordered_compare(
9612            left,
9613            right,
9614            left_is_explicit_null,
9615            right_is_explicit_null,
9616            ">=",
9617            |a, b| a >= b,
9618            |a, b| a >= b,
9619        )
9620    }
9621
9622    /// Convert a value to a string for concatenation
9623    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
9624        match value {
9625            JValue::String(s) => Ok(s.to_string()),
9626            JValue::Null => Ok(String::new()),
9627            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
9628                match crate::functions::string::string(value, None) {
9629                    Ok(JValue::String(s)) => Ok(s.to_string()),
9630                    Ok(JValue::Null) => Ok(String::new()),
9631                    _ => Err(EvaluatorError::TypeError(
9632                        "Cannot concatenate complex types".to_string(),
9633                    )),
9634                }
9635            }
9636            _ => Ok(String::new()),
9637        }
9638    }
9639
9640    /// String concatenation
9641    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
9642        let left_str = Self::value_to_concat_string(left)?;
9643        let right_str = Self::value_to_concat_string(right)?;
9644        Ok(JValue::string(format!("{}{}", left_str, right_str)))
9645    }
9646
9647    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
9648    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
9649        // Check left operand is a number or null
9650        let start_f64 = match left {
9651            JValue::Number(n) => Some(*n),
9652            JValue::Null => None,
9653            _ => {
9654                return Err(EvaluatorError::EvaluationError(
9655                    "T2003: Left operand of range operator must be a number".to_string(),
9656                ));
9657            }
9658        };
9659
9660        // Check left operand is an integer (if it's a number)
9661        if let Some(val) = start_f64 {
9662            if val.fract() != 0.0 {
9663                return Err(EvaluatorError::EvaluationError(
9664                    "T2003: Left operand of range operator must be an integer".to_string(),
9665                ));
9666            }
9667        }
9668
9669        // Check right operand is a number or null
9670        let end_f64 = match right {
9671            JValue::Number(n) => Some(*n),
9672            JValue::Null => None,
9673            _ => {
9674                return Err(EvaluatorError::EvaluationError(
9675                    "T2004: Right operand of range operator must be a number".to_string(),
9676                ));
9677            }
9678        };
9679
9680        // Check right operand is an integer (if it's a number)
9681        if let Some(val) = end_f64 {
9682            if val.fract() != 0.0 {
9683                return Err(EvaluatorError::EvaluationError(
9684                    "T2004: Right operand of range operator must be an integer".to_string(),
9685                ));
9686            }
9687        }
9688
9689        // If either operand is null, return empty array
9690        if start_f64.is_none() || end_f64.is_none() {
9691            return Ok(JValue::array(vec![]));
9692        }
9693
9694        let start = start_f64.unwrap() as i64;
9695        let end = end_f64.unwrap() as i64;
9696
9697        // Check range size limit (10 million elements max)
9698        let size = if start <= end {
9699            (end - start + 1) as usize
9700        } else {
9701            0
9702        };
9703        if size > 10_000_000 {
9704            return Err(EvaluatorError::EvaluationError(
9705                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
9706            ));
9707        }
9708
9709        let mut result = Vec::with_capacity(size);
9710        if start <= end {
9711            for i in start..=end {
9712                result.push(JValue::Number(i as f64));
9713            }
9714        }
9715        // Note: if start > end, return empty array (not reversed)
9716        Ok(JValue::array(result))
9717    }
9718
9719    /// In operator (checks if left is in right array/object)
9720    /// Array indexing: array[index]
9721    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
9722        match (array, index) {
9723            (JValue::Array(arr), JValue::Number(n)) => {
9724                let idx = *n as i64;
9725                let len = arr.len() as i64;
9726
9727                // Handle negative indexing (offset from end)
9728                let actual_idx = if idx < 0 { len + idx } else { idx };
9729
9730                if actual_idx < 0 || actual_idx >= len {
9731                    Ok(JValue::Null)
9732                } else {
9733                    Ok(arr[actual_idx as usize].clone())
9734                }
9735            }
9736            _ => Err(EvaluatorError::TypeError(
9737                "Array indexing requires array and number".to_string(),
9738            )),
9739        }
9740    }
9741
9742    /// Array filtering: array[predicate]
9743    /// Evaluates the predicate for each item in the array and returns items where predicate is true
9744    fn array_filter(
9745        &mut self,
9746        _lhs_node: &AstNode,
9747        rhs_node: &AstNode,
9748        array: &JValue,
9749        _original_data: &JValue,
9750    ) -> Result<JValue, EvaluatorError> {
9751        match array {
9752            JValue::Array(arr) => {
9753                // Pre-allocate with estimated capacity (assume ~50% will match)
9754                let mut filtered = Vec::with_capacity(arr.len() / 2);
9755
9756                for item in arr.iter() {
9757                    // Evaluate the predicate in the context of this array item
9758                    // The item becomes the new "current context" ($)
9759                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
9760
9761                    // Check if the predicate is truthy
9762                    if self.is_truthy(&predicate_result) {
9763                        filtered.push(item.clone());
9764                    }
9765                }
9766
9767                Ok(JValue::array(filtered))
9768            }
9769            _ => Err(EvaluatorError::TypeError(
9770                "Array filtering requires an array".to_string(),
9771            )),
9772        }
9773    }
9774
9775    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
9776        // If either side is undefined/null, return false (not an error)
9777        // This matches JavaScript behavior
9778        if left.is_null() || right.is_null() {
9779            return Ok(JValue::Bool(false));
9780        }
9781
9782        match right {
9783            JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
9784            JValue::Object(obj) => {
9785                if let JValue::String(key) = left {
9786                    Ok(JValue::Bool(obj.contains_key(&**key)))
9787                } else {
9788                    Ok(JValue::Bool(false))
9789                }
9790            }
9791            // If right side is not an array or object (e.g., string, number),
9792            // wrap it in an array for comparison
9793            other => Ok(JValue::Bool(self.equals(left, other))),
9794        }
9795    }
9796
9797    /// Create a partially applied function from a function call with placeholder arguments
9798    /// This evaluates non-placeholder arguments and creates a new lambda that takes
9799    /// the placeholder positions as parameters.
9800    fn create_partial_application(
9801        &mut self,
9802        name: &str,
9803        args: &[AstNode],
9804        is_builtin: bool,
9805        data: &JValue,
9806    ) -> Result<JValue, EvaluatorError> {
9807        // First, look up the function to ensure it exists
9808        let is_lambda = self.context.lookup_lambda(name).is_some()
9809            || (self
9810                .context
9811                .lookup(name)
9812                .map(|v| matches!(v, JValue::Lambda { .. }))
9813                .unwrap_or(false));
9814
9815        // Built-in functions must be called with $ prefix for partial application
9816        // Without $, it's an error (T1007) suggesting the user forgot the $
9817        if !is_lambda && !is_builtin {
9818            // Check if it's a built-in function called without $
9819            if self.is_builtin_function(name) {
9820                return Err(EvaluatorError::EvaluationError(format!(
9821                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
9822                    name
9823                )));
9824            }
9825            return Err(EvaluatorError::EvaluationError(
9826                "T1008: Attempted to partially apply a non-function".to_string(),
9827            ));
9828        }
9829
9830        // Evaluate non-placeholder arguments and track placeholder positions
9831        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
9832        let mut placeholder_positions: Vec<usize> = Vec::new();
9833
9834        for (i, arg) in args.iter().enumerate() {
9835            if matches!(arg, AstNode::Placeholder) {
9836                placeholder_positions.push(i);
9837            } else {
9838                let value = self.evaluate_internal(arg, data)?;
9839                bound_args.push((i, value));
9840            }
9841        }
9842
9843        // Generate parameter names for each placeholder
9844        let param_names: Vec<String> = placeholder_positions
9845            .iter()
9846            .enumerate()
9847            .map(|(i, _)| format!("__p{}", i))
9848            .collect();
9849
9850        // Store the partial application info as a special lambda
9851        // When invoked, it will call the original function with bound + placeholder args
9852        let partial_id = format!(
9853            "__partial_{}_{}_{}",
9854            name,
9855            placeholder_positions.len(),
9856            bound_args.len()
9857        );
9858
9859        // Create a stored lambda that represents this partial application
9860        // The body is a marker that we'll interpret specially during invocation
9861        let stored_lambda = StoredLambda {
9862            params: param_names.clone(),
9863            body: AstNode::String(format!(
9864                "__partial_call:{}:{}:{}",
9865                name,
9866                is_builtin,
9867                args.len()
9868            )),
9869            compiled_body: None, // Partial application uses a special body marker
9870            signature: None,
9871            captured_env: {
9872                let mut env = self.capture_current_environment();
9873                // Store the bound arguments in the captured environment
9874                for (pos, value) in &bound_args {
9875                    env.insert(format!("__bound_arg_{}", pos), value.clone());
9876                }
9877                // Store placeholder positions
9878                env.insert(
9879                    "__placeholder_positions".to_string(),
9880                    JValue::array(
9881                        placeholder_positions
9882                            .iter()
9883                            .map(|p| JValue::Number(*p as f64))
9884                            .collect::<Vec<_>>(),
9885                    ),
9886                );
9887                // Store total argument count
9888                env.insert(
9889                    "__total_args".to_string(),
9890                    JValue::Number(args.len() as f64),
9891                );
9892                env
9893            },
9894            captured_data: Some(data.clone()),
9895            thunk: false,
9896        };
9897
9898        self.context.bind_lambda(partial_id.clone(), stored_lambda);
9899
9900        // Return a lambda object that can be invoked
9901        let lambda_obj = JValue::lambda(
9902            partial_id.as_str(),
9903            param_names,
9904            Some(name.to_string()),
9905            None::<String>,
9906        );
9907
9908        Ok(lambda_obj)
9909    }
9910}
9911
9912impl Default for Evaluator {
9913    fn default() -> Self {
9914        Self::new()
9915    }
9916}
9917
9918#[cfg(test)]
9919mod tests {
9920    use super::*;
9921    use crate::ast::{BinaryOp, UnaryOp};
9922
9923    #[test]
9924    fn test_evaluate_literals() {
9925        let mut evaluator = Evaluator::new();
9926        let data = JValue::Null;
9927
9928        // String literal
9929        let result = evaluator
9930            .evaluate(&AstNode::string("hello"), &data)
9931            .unwrap();
9932        assert_eq!(result, JValue::string("hello"));
9933
9934        // Number literal
9935        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
9936        assert_eq!(result, JValue::from(42i64));
9937
9938        // Boolean literal
9939        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
9940        assert_eq!(result, JValue::Bool(true));
9941
9942        // Null literal
9943        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
9944        assert_eq!(result, JValue::Null);
9945    }
9946
9947    #[test]
9948    fn test_evaluate_variables() {
9949        let mut evaluator = Evaluator::new();
9950        let data = JValue::Null;
9951
9952        // Bind a variable
9953        evaluator
9954            .context
9955            .bind("x".to_string(), JValue::from(100i64));
9956
9957        // Look up the variable
9958        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
9959        assert_eq!(result, JValue::from(100i64));
9960
9961        // Undefined variable returns null (undefined in JSONata semantics)
9962        let result = evaluator
9963            .evaluate(&AstNode::variable("undefined"), &data)
9964            .unwrap();
9965        assert_eq!(result, JValue::Null);
9966    }
9967
9968    #[test]
9969    fn test_evaluate_path() {
9970        let mut evaluator = Evaluator::new();
9971        let data = JValue::from(serde_json::json!({
9972            "foo": {
9973                "bar": {
9974                    "baz": 42
9975                }
9976            }
9977        }));
9978        // Simple path
9979        let path = AstNode::Path {
9980            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
9981        };
9982        let result = evaluator.evaluate(&path, &data).unwrap();
9983        assert_eq!(
9984            result,
9985            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
9986        );
9987
9988        // Nested path
9989        let path = AstNode::Path {
9990            steps: vec![
9991                PathStep::new(AstNode::Name("foo".to_string())),
9992                PathStep::new(AstNode::Name("bar".to_string())),
9993                PathStep::new(AstNode::Name("baz".to_string())),
9994            ],
9995        };
9996        let result = evaluator.evaluate(&path, &data).unwrap();
9997        assert_eq!(result, JValue::from(42i64));
9998
9999        // Missing path returns null
10000        let path = AstNode::Path {
10001            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
10002        };
10003        let result = evaluator.evaluate(&path, &data).unwrap();
10004        assert_eq!(result, JValue::Null);
10005    }
10006
10007    #[test]
10008    fn test_arithmetic_operations() {
10009        let mut evaluator = Evaluator::new();
10010        let data = JValue::Null;
10011
10012        // Addition
10013        let expr = AstNode::Binary {
10014            op: BinaryOp::Add,
10015            lhs: Box::new(AstNode::number(10.0)),
10016            rhs: Box::new(AstNode::number(5.0)),
10017        };
10018        let result = evaluator.evaluate(&expr, &data).unwrap();
10019        assert_eq!(result, JValue::Number(15.0));
10020
10021        // Subtraction
10022        let expr = AstNode::Binary {
10023            op: BinaryOp::Subtract,
10024            lhs: Box::new(AstNode::number(10.0)),
10025            rhs: Box::new(AstNode::number(5.0)),
10026        };
10027        let result = evaluator.evaluate(&expr, &data).unwrap();
10028        assert_eq!(result, JValue::Number(5.0));
10029
10030        // Multiplication
10031        let expr = AstNode::Binary {
10032            op: BinaryOp::Multiply,
10033            lhs: Box::new(AstNode::number(10.0)),
10034            rhs: Box::new(AstNode::number(5.0)),
10035        };
10036        let result = evaluator.evaluate(&expr, &data).unwrap();
10037        assert_eq!(result, JValue::Number(50.0));
10038
10039        // Division
10040        let expr = AstNode::Binary {
10041            op: BinaryOp::Divide,
10042            lhs: Box::new(AstNode::number(10.0)),
10043            rhs: Box::new(AstNode::number(5.0)),
10044        };
10045        let result = evaluator.evaluate(&expr, &data).unwrap();
10046        assert_eq!(result, JValue::Number(2.0));
10047
10048        // Modulo
10049        let expr = AstNode::Binary {
10050            op: BinaryOp::Modulo,
10051            lhs: Box::new(AstNode::number(10.0)),
10052            rhs: Box::new(AstNode::number(3.0)),
10053        };
10054        let result = evaluator.evaluate(&expr, &data).unwrap();
10055        assert_eq!(result, JValue::Number(1.0));
10056    }
10057
10058    #[test]
10059    fn test_division_by_zero() {
10060        let mut evaluator = Evaluator::new();
10061        let data = JValue::Null;
10062
10063        let expr = AstNode::Binary {
10064            op: BinaryOp::Divide,
10065            lhs: Box::new(AstNode::number(10.0)),
10066            rhs: Box::new(AstNode::number(0.0)),
10067        };
10068        let result = evaluator.evaluate(&expr, &data);
10069        assert!(result.is_err());
10070    }
10071
10072    #[test]
10073    fn test_comparison_operations() {
10074        let mut evaluator = Evaluator::new();
10075        let data = JValue::Null;
10076
10077        // Equal
10078        let expr = AstNode::Binary {
10079            op: BinaryOp::Equal,
10080            lhs: Box::new(AstNode::number(5.0)),
10081            rhs: Box::new(AstNode::number(5.0)),
10082        };
10083        assert_eq!(
10084            evaluator.evaluate(&expr, &data).unwrap(),
10085            JValue::Bool(true)
10086        );
10087
10088        // Not equal
10089        let expr = AstNode::Binary {
10090            op: BinaryOp::NotEqual,
10091            lhs: Box::new(AstNode::number(5.0)),
10092            rhs: Box::new(AstNode::number(3.0)),
10093        };
10094        assert_eq!(
10095            evaluator.evaluate(&expr, &data).unwrap(),
10096            JValue::Bool(true)
10097        );
10098
10099        // Less than
10100        let expr = AstNode::Binary {
10101            op: BinaryOp::LessThan,
10102            lhs: Box::new(AstNode::number(3.0)),
10103            rhs: Box::new(AstNode::number(5.0)),
10104        };
10105        assert_eq!(
10106            evaluator.evaluate(&expr, &data).unwrap(),
10107            JValue::Bool(true)
10108        );
10109
10110        // Greater than
10111        let expr = AstNode::Binary {
10112            op: BinaryOp::GreaterThan,
10113            lhs: Box::new(AstNode::number(5.0)),
10114            rhs: Box::new(AstNode::number(3.0)),
10115        };
10116        assert_eq!(
10117            evaluator.evaluate(&expr, &data).unwrap(),
10118            JValue::Bool(true)
10119        );
10120    }
10121
10122    #[test]
10123    fn test_logical_operations() {
10124        let mut evaluator = Evaluator::new();
10125        let data = JValue::Null;
10126
10127        // And - both true
10128        let expr = AstNode::Binary {
10129            op: BinaryOp::And,
10130            lhs: Box::new(AstNode::boolean(true)),
10131            rhs: Box::new(AstNode::boolean(true)),
10132        };
10133        assert_eq!(
10134            evaluator.evaluate(&expr, &data).unwrap(),
10135            JValue::Bool(true)
10136        );
10137
10138        // And - first false
10139        let expr = AstNode::Binary {
10140            op: BinaryOp::And,
10141            lhs: Box::new(AstNode::boolean(false)),
10142            rhs: Box::new(AstNode::boolean(true)),
10143        };
10144        assert_eq!(
10145            evaluator.evaluate(&expr, &data).unwrap(),
10146            JValue::Bool(false)
10147        );
10148
10149        // Or - first true
10150        let expr = AstNode::Binary {
10151            op: BinaryOp::Or,
10152            lhs: Box::new(AstNode::boolean(true)),
10153            rhs: Box::new(AstNode::boolean(false)),
10154        };
10155        assert_eq!(
10156            evaluator.evaluate(&expr, &data).unwrap(),
10157            JValue::Bool(true)
10158        );
10159
10160        // Or - both false
10161        let expr = AstNode::Binary {
10162            op: BinaryOp::Or,
10163            lhs: Box::new(AstNode::boolean(false)),
10164            rhs: Box::new(AstNode::boolean(false)),
10165        };
10166        assert_eq!(
10167            evaluator.evaluate(&expr, &data).unwrap(),
10168            JValue::Bool(false)
10169        );
10170    }
10171
10172    #[test]
10173    fn test_string_concatenation() {
10174        let mut evaluator = Evaluator::new();
10175        let data = JValue::Null;
10176
10177        let expr = AstNode::Binary {
10178            op: BinaryOp::Concatenate,
10179            lhs: Box::new(AstNode::string("Hello")),
10180            rhs: Box::new(AstNode::string(" World")),
10181        };
10182        let result = evaluator.evaluate(&expr, &data).unwrap();
10183        assert_eq!(result, JValue::string("Hello World"));
10184    }
10185
10186    #[test]
10187    fn test_range_operator() {
10188        let mut evaluator = Evaluator::new();
10189        let data = JValue::Null;
10190
10191        // Forward range
10192        let expr = AstNode::Binary {
10193            op: BinaryOp::Range,
10194            lhs: Box::new(AstNode::number(1.0)),
10195            rhs: Box::new(AstNode::number(5.0)),
10196        };
10197        let result = evaluator.evaluate(&expr, &data).unwrap();
10198        assert_eq!(
10199            result,
10200            JValue::array(vec![
10201                JValue::Number(1.0),
10202                JValue::Number(2.0),
10203                JValue::Number(3.0),
10204                JValue::Number(4.0),
10205                JValue::Number(5.0)
10206            ])
10207        );
10208
10209        // Backward range (start > end) returns empty array
10210        let expr = AstNode::Binary {
10211            op: BinaryOp::Range,
10212            lhs: Box::new(AstNode::number(5.0)),
10213            rhs: Box::new(AstNode::number(1.0)),
10214        };
10215        let result = evaluator.evaluate(&expr, &data).unwrap();
10216        assert_eq!(result, JValue::array(vec![]));
10217    }
10218
10219    #[test]
10220    fn test_in_operator() {
10221        let mut evaluator = Evaluator::new();
10222        let data = JValue::Null;
10223
10224        // In array
10225        let expr = AstNode::Binary {
10226            op: BinaryOp::In,
10227            lhs: Box::new(AstNode::number(3.0)),
10228            rhs: Box::new(AstNode::Array(vec![
10229                AstNode::number(1.0),
10230                AstNode::number(2.0),
10231                AstNode::number(3.0),
10232            ])),
10233        };
10234        let result = evaluator.evaluate(&expr, &data).unwrap();
10235        assert_eq!(result, JValue::Bool(true));
10236
10237        // Not in array
10238        let expr = AstNode::Binary {
10239            op: BinaryOp::In,
10240            lhs: Box::new(AstNode::number(5.0)),
10241            rhs: Box::new(AstNode::Array(vec![
10242                AstNode::number(1.0),
10243                AstNode::number(2.0),
10244                AstNode::number(3.0),
10245            ])),
10246        };
10247        let result = evaluator.evaluate(&expr, &data).unwrap();
10248        assert_eq!(result, JValue::Bool(false));
10249    }
10250
10251    #[test]
10252    fn test_unary_operations() {
10253        let mut evaluator = Evaluator::new();
10254        let data = JValue::Null;
10255
10256        // Negation
10257        let expr = AstNode::Unary {
10258            op: UnaryOp::Negate,
10259            operand: Box::new(AstNode::number(5.0)),
10260        };
10261        let result = evaluator.evaluate(&expr, &data).unwrap();
10262        assert_eq!(result, JValue::Number(-5.0));
10263
10264        // Not
10265        let expr = AstNode::Unary {
10266            op: UnaryOp::Not,
10267            operand: Box::new(AstNode::boolean(true)),
10268        };
10269        let result = evaluator.evaluate(&expr, &data).unwrap();
10270        assert_eq!(result, JValue::Bool(false));
10271    }
10272
10273    #[test]
10274    fn test_array_construction() {
10275        let mut evaluator = Evaluator::new();
10276        let data = JValue::Null;
10277
10278        let expr = AstNode::Array(vec![
10279            AstNode::number(1.0),
10280            AstNode::number(2.0),
10281            AstNode::number(3.0),
10282        ]);
10283        let result = evaluator.evaluate(&expr, &data).unwrap();
10284        // Whole number literals are preserved as integers
10285        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
10286    }
10287
10288    #[test]
10289    fn test_object_construction() {
10290        let mut evaluator = Evaluator::new();
10291        let data = JValue::Null;
10292
10293        let expr = AstNode::Object(vec![
10294            (AstNode::string("name"), AstNode::string("Alice")),
10295            (AstNode::string("age"), AstNode::number(30.0)),
10296        ]);
10297        let result = evaluator.evaluate(&expr, &data).unwrap();
10298        // Whole number literals are preserved as integers
10299        let mut expected = IndexMap::new();
10300        expected.insert("name".to_string(), JValue::string("Alice"));
10301        expected.insert("age".to_string(), JValue::Number(30.0));
10302        assert_eq!(result, JValue::object(expected));
10303    }
10304
10305    #[test]
10306    fn test_conditional() {
10307        let mut evaluator = Evaluator::new();
10308        let data = JValue::Null;
10309
10310        // True condition
10311        let expr = AstNode::Conditional {
10312            condition: Box::new(AstNode::boolean(true)),
10313            then_branch: Box::new(AstNode::string("yes")),
10314            else_branch: Some(Box::new(AstNode::string("no"))),
10315        };
10316        let result = evaluator.evaluate(&expr, &data).unwrap();
10317        assert_eq!(result, JValue::string("yes"));
10318
10319        // False condition
10320        let expr = AstNode::Conditional {
10321            condition: Box::new(AstNode::boolean(false)),
10322            then_branch: Box::new(AstNode::string("yes")),
10323            else_branch: Some(Box::new(AstNode::string("no"))),
10324        };
10325        let result = evaluator.evaluate(&expr, &data).unwrap();
10326        assert_eq!(result, JValue::string("no"));
10327
10328        // No else branch returns undefined (not null)
10329        let expr = AstNode::Conditional {
10330            condition: Box::new(AstNode::boolean(false)),
10331            then_branch: Box::new(AstNode::string("yes")),
10332            else_branch: None,
10333        };
10334        let result = evaluator.evaluate(&expr, &data).unwrap();
10335        assert_eq!(result, JValue::Undefined);
10336    }
10337
10338    #[test]
10339    fn test_block_expression() {
10340        let mut evaluator = Evaluator::new();
10341        let data = JValue::Null;
10342
10343        let expr = AstNode::Block(vec![
10344            AstNode::number(1.0),
10345            AstNode::number(2.0),
10346            AstNode::number(3.0),
10347        ]);
10348        let result = evaluator.evaluate(&expr, &data).unwrap();
10349        // Block returns the last expression; whole numbers are preserved as integers
10350        assert_eq!(result, JValue::from(3i64));
10351    }
10352
10353    #[test]
10354    fn test_function_calls() {
10355        let mut evaluator = Evaluator::new();
10356        let data = JValue::Null;
10357
10358        // uppercase function
10359        let expr = AstNode::Function {
10360            name: "uppercase".to_string(),
10361            args: vec![AstNode::string("hello")],
10362            is_builtin: true,
10363        };
10364        let result = evaluator.evaluate(&expr, &data).unwrap();
10365        assert_eq!(result, JValue::string("HELLO"));
10366
10367        // lowercase function
10368        let expr = AstNode::Function {
10369            name: "lowercase".to_string(),
10370            args: vec![AstNode::string("HELLO")],
10371            is_builtin: true,
10372        };
10373        let result = evaluator.evaluate(&expr, &data).unwrap();
10374        assert_eq!(result, JValue::string("hello"));
10375
10376        // length function
10377        let expr = AstNode::Function {
10378            name: "length".to_string(),
10379            args: vec![AstNode::string("hello")],
10380            is_builtin: true,
10381        };
10382        let result = evaluator.evaluate(&expr, &data).unwrap();
10383        assert_eq!(result, JValue::from(5i64));
10384
10385        // sum function
10386        let expr = AstNode::Function {
10387            name: "sum".to_string(),
10388            args: vec![AstNode::Array(vec![
10389                AstNode::number(1.0),
10390                AstNode::number(2.0),
10391                AstNode::number(3.0),
10392            ])],
10393            is_builtin: true,
10394        };
10395        let result = evaluator.evaluate(&expr, &data).unwrap();
10396        assert_eq!(result, JValue::Number(6.0));
10397
10398        // count function
10399        let expr = AstNode::Function {
10400            name: "count".to_string(),
10401            args: vec![AstNode::Array(vec![
10402                AstNode::number(1.0),
10403                AstNode::number(2.0),
10404                AstNode::number(3.0),
10405            ])],
10406            is_builtin: true,
10407        };
10408        let result = evaluator.evaluate(&expr, &data).unwrap();
10409        assert_eq!(result, JValue::from(3i64));
10410    }
10411
10412    #[test]
10413    fn test_complex_nested_data() {
10414        let mut evaluator = Evaluator::new();
10415        let data = JValue::from(serde_json::json!({
10416            "users": [
10417                {"name": "Alice", "age": 30},
10418                {"name": "Bob", "age": 25},
10419                {"name": "Charlie", "age": 35}
10420            ],
10421            "metadata": {
10422                "total": 3,
10423                "version": "1.0"
10424            }
10425        }));
10426        // Access nested field
10427        let path = AstNode::Path {
10428            steps: vec![
10429                PathStep::new(AstNode::Name("metadata".to_string())),
10430                PathStep::new(AstNode::Name("version".to_string())),
10431            ],
10432        };
10433        let result = evaluator.evaluate(&path, &data).unwrap();
10434        assert_eq!(result, JValue::string("1.0"));
10435    }
10436
10437    #[test]
10438    fn test_error_handling() {
10439        let mut evaluator = Evaluator::new();
10440        let data = JValue::Null;
10441
10442        // Type error: adding string and number
10443        let expr = AstNode::Binary {
10444            op: BinaryOp::Add,
10445            lhs: Box::new(AstNode::string("hello")),
10446            rhs: Box::new(AstNode::number(5.0)),
10447        };
10448        let result = evaluator.evaluate(&expr, &data);
10449        assert!(result.is_err());
10450
10451        // Reference error: undefined function
10452        let expr = AstNode::Function {
10453            name: "undefined_function".to_string(),
10454            args: vec![],
10455            is_builtin: false,
10456        };
10457        let result = evaluator.evaluate(&expr, &data);
10458        assert!(result.is_err());
10459    }
10460
10461    #[test]
10462    fn test_truthiness() {
10463        let evaluator = Evaluator::new();
10464
10465        assert!(!evaluator.is_truthy(&JValue::Null));
10466        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
10467        assert!(evaluator.is_truthy(&JValue::Bool(true)));
10468        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
10469        assert!(evaluator.is_truthy(&JValue::from(1i64)));
10470        assert!(!evaluator.is_truthy(&JValue::string("")));
10471        assert!(evaluator.is_truthy(&JValue::string("hello")));
10472        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
10473        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
10474    }
10475
10476    #[test]
10477    fn test_integration_with_parser() {
10478        use crate::parser::parse;
10479
10480        let mut evaluator = Evaluator::new();
10481        let data = JValue::from(serde_json::json!({
10482            "price": 10,
10483            "quantity": 5
10484        }));
10485        // Test simple path
10486        let ast = parse("price").unwrap();
10487        let result = evaluator.evaluate(&ast, &data).unwrap();
10488        assert_eq!(result, JValue::from(10i64));
10489
10490        // Test arithmetic
10491        let ast = parse("price * quantity").unwrap();
10492        let result = evaluator.evaluate(&ast, &data).unwrap();
10493        // Note: Arithmetic operations produce f64 results in JSON
10494        assert_eq!(result, JValue::Number(50.0));
10495
10496        // Test comparison
10497        let ast = parse("price > 5").unwrap();
10498        let result = evaluator.evaluate(&ast, &data).unwrap();
10499        assert_eq!(result, JValue::Bool(true));
10500    }
10501
10502    #[test]
10503    fn test_evaluate_dollar_function_uppercase() {
10504        use crate::parser::parse;
10505
10506        let mut evaluator = Evaluator::new();
10507        let ast = parse(r#"$uppercase("hello")"#).unwrap();
10508        let empty = JValue::object(IndexMap::new());
10509        let result = evaluator.evaluate(&ast, &empty).unwrap();
10510        assert_eq!(result, JValue::string("HELLO"));
10511    }
10512
10513    #[test]
10514    fn test_evaluate_dollar_function_sum() {
10515        use crate::parser::parse;
10516
10517        let mut evaluator = Evaluator::new();
10518        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
10519        let empty = JValue::object(IndexMap::new());
10520        let result = evaluator.evaluate(&ast, &empty).unwrap();
10521        assert_eq!(result, JValue::Number(15.0));
10522    }
10523
10524    #[test]
10525    fn test_evaluate_nested_dollar_functions() {
10526        use crate::parser::parse;
10527
10528        let mut evaluator = Evaluator::new();
10529        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
10530        let empty = JValue::object(IndexMap::new());
10531        let result = evaluator.evaluate(&ast, &empty).unwrap();
10532        // length() returns an integer, not a float
10533        assert_eq!(result, JValue::Number(5.0));
10534    }
10535
10536    #[test]
10537    fn test_array_mapping() {
10538        use crate::parser::parse;
10539
10540        let mut evaluator = Evaluator::new();
10541        let data: JValue = serde_json::from_str(
10542            r#"{
10543            "products": [
10544                {"id": 1, "name": "Laptop", "price": 999.99},
10545                {"id": 2, "name": "Mouse", "price": 29.99},
10546                {"id": 3, "name": "Keyboard", "price": 79.99}
10547            ]
10548        }"#,
10549        )
10550        .map(|v: serde_json::Value| JValue::from(v))
10551        .unwrap();
10552
10553        // Test mapping over array to extract field
10554        let ast = parse("products.name").unwrap();
10555        let result = evaluator.evaluate(&ast, &data).unwrap();
10556        assert_eq!(
10557            result,
10558            JValue::array(vec![
10559                JValue::string("Laptop"),
10560                JValue::string("Mouse"),
10561                JValue::string("Keyboard")
10562            ])
10563        );
10564
10565        // Test mapping over array to extract prices
10566        let ast = parse("products.price").unwrap();
10567        let result = evaluator.evaluate(&ast, &data).unwrap();
10568        assert_eq!(
10569            result,
10570            JValue::array(vec![
10571                JValue::Number(999.99),
10572                JValue::Number(29.99),
10573                JValue::Number(79.99)
10574            ])
10575        );
10576
10577        // Test with $sum function on mapped array
10578        let ast = parse("$sum(products.price)").unwrap();
10579        let result = evaluator.evaluate(&ast, &data).unwrap();
10580        assert_eq!(result, JValue::Number(1109.97));
10581    }
10582
10583    #[test]
10584    fn test_empty_brackets() {
10585        use crate::parser::parse;
10586
10587        let mut evaluator = Evaluator::new();
10588
10589        // Test empty brackets on simple value - should wrap in array
10590        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
10591        let ast = parse("foo[]").unwrap();
10592        let result = evaluator.evaluate(&ast, &data).unwrap();
10593        assert_eq!(
10594            result,
10595            JValue::array(vec![JValue::string("bar")]),
10596            "Empty brackets should wrap value in array"
10597        );
10598
10599        // Test empty brackets on array - should return array as-is
10600        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
10601        let ast2 = parse("arr[]").unwrap();
10602        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
10603        assert_eq!(
10604            result2,
10605            JValue::array(vec![
10606                JValue::Number(1.0),
10607                JValue::Number(2.0),
10608                JValue::Number(3.0)
10609            ]),
10610            "Empty brackets should preserve array"
10611        );
10612    }
10613}