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