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 {
48        ord.reverse()
49    } else {
50        ord
51    }
52}
53
54/// Try to extract a specialized sort comparator from a lambda AST node.
55/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
56fn try_specialize_sort_comparator(
57    body: &AstNode,
58    left_param: &str,
59    right_param: &str,
60) -> Option<SpecializedSortComparator> {
61    let AstNode::Binary { op, lhs, rhs } = body else {
62        return None;
63    };
64
65    // Returns true if op means "swap when left > right" (ascending order).
66    let is_ascending = |op: &BinaryOp| -> Option<bool> {
67        match op {
68            BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
69            BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
70            _ => None,
71        }
72    };
73
74    // Extract field name from a `$param.field` path with no stages.
75    let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
76        let AstNode::Path { steps } = node else {
77            return None;
78        };
79        if steps.len() != 2 {
80            return None;
81        }
82        let AstNode::Variable(var) = &steps[0].node else {
83            return None;
84        };
85        if var != param {
86            return None;
87        }
88        let AstNode::Name(field) = &steps[1].node else {
89            return None;
90        };
91        if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() {
92            return None;
93        }
94        Some(field.clone())
95    };
96
97    // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
98    for flipped in [false, true] {
99        let (lhs_param, rhs_param) = if flipped {
100            (right_param, left_param)
101        } else {
102            (left_param, right_param)
103        };
104        if let (Some(lhs_field), Some(rhs_field)) = (
105            extract_var_field(lhs, lhs_param),
106            extract_var_field(rhs, rhs_param),
107        ) {
108            if lhs_field == rhs_field {
109                let descending = match op {
110                    // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
111                    // Flipped `$r.f - $l.f` → positive when r > l → descending.
112                    BinaryOp::Subtract => flipped,
113                    // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
114                    _ => {
115                        let ascending = is_ascending(op)?;
116                        if flipped {
117                            ascending
118                        } else {
119                            !ascending
120                        }
121                    }
122                };
123                return Some(SpecializedSortComparator {
124                    field: lhs_field,
125                    descending,
126                });
127            }
128        }
129    }
130    None
131}
132
133// ──────────────────────────────────────────────────────────────────────────────
134// CompiledExpr — unified compiled expression framework
135// ──────────────────────────────────────────────────────────────────────────────
136//
137// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
138// can represent arbitrary simple expressions without AST walking.  Evaluated in
139// a tight loop with no recursion tracking, no scope management, and no AstNode
140// pattern matching.
141
142/// Shape cache: maps field names to their positional index in an IndexMap.
143/// When all objects in an array share the same key ordering (extremely common
144/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
145/// instead of O(1)-amortized hash lookups.
146type ShapeCache = HashMap<String, usize>;
147
148/// Build a shape cache from the first object in an array.
149/// Returns None if the data is not an object.
150fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
151    match first_element {
152        JValue::Object(obj) => {
153            let mut cache = HashMap::with_capacity(obj.len());
154            for (idx, (key, _)) in obj.iter().enumerate() {
155                cache.insert(key.clone(), idx);
156            }
157            Some(cache)
158        }
159        _ => None,
160    }
161}
162
163/// Comparison operator for compiled expressions.
164#[derive(Debug, Clone, Copy)]
165pub(crate) enum CompiledCmp {
166    Eq,
167    Ne,
168    Lt,
169    Le,
170    Gt,
171    Ge,
172}
173
174/// Arithmetic operator for compiled expressions.
175#[derive(Debug, Clone, Copy)]
176pub(crate) enum CompiledArithOp {
177    Add,
178    Sub,
179    Mul,
180    Div,
181    Mod,
182}
183
184/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
185///
186/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
187/// expression-compile time (once), then `eval_compiled()` evaluates it per
188/// element in O(expression-size) with no heap allocation in the hot path.
189#[derive(Clone, Debug)]
190pub(crate) enum CompiledExpr {
191    // ── Leaves ──────────────────────────────────────────────────────────
192    /// A literal value known at compile time.
193    Literal(JValue),
194    /// Explicit `null` literal from `AstNode::Null`.
195    /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
196    /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
197    ExplicitNull,
198    /// Single-level field lookup on the current object: `obj.get("field")`.
199    FieldLookup(String),
200    /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
201    NestedFieldLookup(String, String),
202    /// Variable lookup from enclosing scope (e.g. `$var`).
203    /// Resolved at eval time via a provided variable map.
204    VariableLookup(String),
205
206    // ── Comparison ──────────────────────────────────────────────────────
207    Compare {
208        op: CompiledCmp,
209        lhs: Box<CompiledExpr>,
210        rhs: Box<CompiledExpr>,
211    },
212
213    // ── Arithmetic ──────────────────────────────────────────────────────
214    Arithmetic {
215        op: CompiledArithOp,
216        lhs: Box<CompiledExpr>,
217        rhs: Box<CompiledExpr>,
218    },
219
220    // ── String ──────────────────────────────────────────────────────────
221    Concat(Box<CompiledExpr>, Box<CompiledExpr>),
222
223    // ── Logical ─────────────────────────────────────────────────────────
224    And(Box<CompiledExpr>, Box<CompiledExpr>),
225    Or(Box<CompiledExpr>, Box<CompiledExpr>),
226    Not(Box<CompiledExpr>),
227    /// Negation of a numeric value.
228    Negate(Box<CompiledExpr>),
229
230    // ── Conditional ─────────────────────────────────────────────────────
231    Conditional {
232        condition: Box<CompiledExpr>,
233        then_expr: Box<CompiledExpr>,
234        else_expr: Option<Box<CompiledExpr>>,
235    },
236
237    // ── Compound ────────────────────────────────────────────────────────
238    /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
239    ObjectConstruct(Vec<(String, CompiledExpr)>),
240    /// Array construction: `[expr1, expr2, ...]`
241    ///
242    /// Each element carries a `bool` flag: `true` means the element originated
243    /// from an explicit `AstNode::Array` constructor and must be kept nested even
244    /// if it evaluates to an array. `false` means the element's array value is
245    /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
246    /// Undefined values are always skipped.
247    ArrayConstruct(Vec<(CompiledExpr, bool)>),
248
249    // ── Phase 2 extensions ──────────────────────────────────────────────
250    /// Named variable lookup from context scope (any `$name` not in lambda params).
251    /// Compiled when a named variable is encountered and no allowed_vars list is
252    /// provided (top-level compilation). At runtime, returns the value from the vars
253    /// map (lambda params or captured env), or Undefined if not present.
254    #[allow(dead_code)]
255    ContextVar(String),
256
257    /// Multi-step field path with optional per-step filters: `a.b[pred].c`
258    /// Applies implicit array-mapping semantics at each step.
259    FieldPath(Vec<CompiledStep>),
260
261    /// Call a pure, side-effect-free builtin with compiled arguments.
262    /// Only builtins in COMPILABLE_BUILTINS are allowed here.
263    BuiltinCall {
264        name: &'static str,
265        args: Vec<CompiledExpr>,
266    },
267
268    /// Sequential block: evaluate all expressions, return last value.
269    Block(Vec<CompiledExpr>),
270
271    /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
272    Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
273
274    // ── Higher-order functions with inline lambdas ───────────────────────
275    /// `$map(array, function($v [, $i]) { body })` — compiled when the second
276    /// argument is an inline lambda literal (not a stored variable).
277    /// `params` holds the lambda parameter names (without `$`), 1 or 2 elements.
278    MapCall {
279        array: Box<CompiledExpr>,
280        params: Vec<String>,
281        body: Box<CompiledExpr>,
282    },
283    /// `$filter(array, function($v [, $i]) { body })` — compiled when the second
284    /// argument is an inline lambda literal.
285    FilterCall {
286        array: Box<CompiledExpr>,
287        params: Vec<String>,
288        body: Box<CompiledExpr>,
289    },
290    /// `$reduce(array, function($acc, $v) { body } [, initial])` — compiled when the
291    /// second argument is an inline lambda literal with exactly 2 parameters.
292    ReduceCall {
293        array: Box<CompiledExpr>,
294        params: Vec<String>,
295        body: Box<CompiledExpr>,
296        initial: Option<Box<CompiledExpr>>,
297    },
298}
299
300/// One step in a compiled `FieldPath`.
301#[derive(Clone, Debug)]
302pub(crate) struct CompiledStep {
303    /// Field name to look up at this step.
304    pub field: String,
305    /// Optional predicate filter compiled from a `Stage::Filter` stage.
306    pub filter: Option<CompiledExpr>,
307}
308
309/// Try to compile an AstNode subtree into a CompiledExpr.
310/// Returns None for anything that requires full AST evaluation (lambda calls,
311/// function calls with side effects, complex paths, etc.).
312pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
313    try_compile_expr_inner(node, None)
314}
315
316/// Like `try_compile_expr` but additionally allows the specified variable names
317/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
318/// parameters are known and will be provided via the `vars` map at eval time.
319pub(crate) fn try_compile_expr_with_allowed_vars(
320    node: &AstNode,
321    allowed_vars: &[&str],
322) -> Option<CompiledExpr> {
323    try_compile_expr_inner(node, Some(allowed_vars))
324}
325
326fn try_compile_expr_inner(node: &AstNode, allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
327    match node {
328        // ── Literals ────────────────────────────────────────────────────
329        AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
330        AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
331        AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
332        AstNode::Null => Some(CompiledExpr::ExplicitNull),
333
334        // ── Field access ────────────────────────────────────────────────
335        AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
336
337        // ── Variable lookup ─────────────────────────────────────────────
338        // $ (empty name) always refers to the current element.
339        // Named variables: in HOF mode (allowed_vars=Some), only compile if the
340        // variable is in the allowed set (lambda params supplied via vars map).
341        // In top-level mode (allowed_vars=None), compile unknown variables as
342        // ContextVar — they return Undefined at runtime when no bindings are passed.
343        AstNode::Variable(var) if var.is_empty() => Some(CompiledExpr::VariableLookup(var.clone())),
344        AstNode::Variable(var) => {
345            if let Some(allowed) = allowed_vars {
346                // HOF mode: only compile if the variable is a known lambda param.
347                if allowed.contains(&var.as_str()) {
348                    return Some(CompiledExpr::VariableLookup(var.clone()));
349                }
350            }
351            // Named variables require Context for correct lookup (scope stack, builtins
352            // registry). The compiled fast path passes ctx=None, so fall back to the
353            // tree-walker for all non-empty variable references.
354            None
355        }
356
357        // ── Path expressions ────────────────────────────────────────────
358        AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
359
360        // ── Binary operations ───────────────────────────────────────────
361        AstNode::Binary { op, lhs, rhs } => {
362            let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
363            let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
364            match op {
365                // Comparison
366                BinaryOp::Equal => Some(CompiledExpr::Compare {
367                    op: CompiledCmp::Eq,
368                    lhs: Box::new(compiled_lhs),
369                    rhs: Box::new(compiled_rhs),
370                }),
371                BinaryOp::NotEqual => Some(CompiledExpr::Compare {
372                    op: CompiledCmp::Ne,
373                    lhs: Box::new(compiled_lhs),
374                    rhs: Box::new(compiled_rhs),
375                }),
376                BinaryOp::LessThan => Some(CompiledExpr::Compare {
377                    op: CompiledCmp::Lt,
378                    lhs: Box::new(compiled_lhs),
379                    rhs: Box::new(compiled_rhs),
380                }),
381                BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
382                    op: CompiledCmp::Le,
383                    lhs: Box::new(compiled_lhs),
384                    rhs: Box::new(compiled_rhs),
385                }),
386                BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
387                    op: CompiledCmp::Gt,
388                    lhs: Box::new(compiled_lhs),
389                    rhs: Box::new(compiled_rhs),
390                }),
391                BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
392                    op: CompiledCmp::Ge,
393                    lhs: Box::new(compiled_lhs),
394                    rhs: Box::new(compiled_rhs),
395                }),
396                // Arithmetic
397                BinaryOp::Add => Some(CompiledExpr::Arithmetic {
398                    op: CompiledArithOp::Add,
399                    lhs: Box::new(compiled_lhs),
400                    rhs: Box::new(compiled_rhs),
401                }),
402                BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
403                    op: CompiledArithOp::Sub,
404                    lhs: Box::new(compiled_lhs),
405                    rhs: Box::new(compiled_rhs),
406                }),
407                BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
408                    op: CompiledArithOp::Mul,
409                    lhs: Box::new(compiled_lhs),
410                    rhs: Box::new(compiled_rhs),
411                }),
412                BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
413                    op: CompiledArithOp::Div,
414                    lhs: Box::new(compiled_lhs),
415                    rhs: Box::new(compiled_rhs),
416                }),
417                BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
418                    op: CompiledArithOp::Mod,
419                    lhs: Box::new(compiled_lhs),
420                    rhs: Box::new(compiled_rhs),
421                }),
422                // Logical
423                BinaryOp::And => Some(CompiledExpr::And(
424                    Box::new(compiled_lhs),
425                    Box::new(compiled_rhs),
426                )),
427                BinaryOp::Or => Some(CompiledExpr::Or(
428                    Box::new(compiled_lhs),
429                    Box::new(compiled_rhs),
430                )),
431                // String concat
432                BinaryOp::Concatenate => Some(CompiledExpr::Concat(
433                    Box::new(compiled_lhs),
434                    Box::new(compiled_rhs),
435                )),
436                // Coalesce: return lhs if defined/non-null, else rhs
437                BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
438                    Box::new(compiled_lhs),
439                    Box::new(compiled_rhs),
440                )),
441                // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
442                _ => None,
443            }
444        }
445
446        // ── Unary operations ────────────────────────────────────────────
447        AstNode::Unary { op, operand } => {
448            let compiled = try_compile_expr_inner(operand, allowed_vars)?;
449            match op {
450                crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
451                crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
452            }
453        }
454
455        // ── Conditional ─────────────────────────────────────────────────
456        AstNode::Conditional {
457            condition,
458            then_branch,
459            else_branch,
460        } => {
461            let cond = try_compile_expr_inner(condition, allowed_vars)?;
462            let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
463            let else_e = match else_branch {
464                Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
465                None => None,
466            };
467            Some(CompiledExpr::Conditional {
468                condition: Box::new(cond),
469                then_expr: Box::new(then_e),
470                else_expr: else_e,
471            })
472        }
473
474        // ── Object construction ─────────────────────────────────────────
475        AstNode::Object(pairs) => {
476            let mut fields = Vec::with_capacity(pairs.len());
477            for (key_node, val_node) in pairs {
478                // Key must be a string literal
479                let key = match key_node {
480                    AstNode::String(s) => s.clone(),
481                    _ => return None,
482                };
483                let val = try_compile_expr_inner(val_node, allowed_vars)?;
484                fields.push((key, val));
485            }
486            Some(CompiledExpr::ObjectConstruct(fields))
487        }
488
489        // ── Array construction ──────────────────────────────────────────
490        AstNode::Array(elems) => {
491            let mut compiled = Vec::with_capacity(elems.len());
492            for elem in elems {
493                // Tag whether the element itself is an array constructor: if so, its
494                // array value must be kept nested rather than flattened (tree-walker parity).
495                let is_nested = matches!(elem, AstNode::Array(_));
496                compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
497            }
498            Some(CompiledExpr::ArrayConstruct(compiled))
499        }
500
501        // ── Block (sequential evaluation) ───────────────────────────────
502        AstNode::Block(exprs) if !exprs.is_empty() => {
503            let compiled: Option<Vec<CompiledExpr>> = exprs
504                .iter()
505                .map(|e| try_compile_expr_inner(e, allowed_vars))
506                .collect();
507            compiled.map(CompiledExpr::Block)
508        }
509
510        // ── Pure builtin function calls ──────────────────────────────────
511        AstNode::Function {
512            name,
513            args,
514            is_builtin: true,
515        } => {
516            if is_compilable_builtin(name) {
517                // Arity guard: if the call site passes more args than the builtin accepts,
518                // fall back to the tree-walker so it can raise the correct T0410 error.
519                if let Some(max) = compilable_builtin_max_args(name) {
520                    if args.len() > max {
521                        return None;
522                    }
523                }
524                let compiled_args: Option<Vec<CompiledExpr>> = args
525                    .iter()
526                    .map(|a| try_compile_expr_inner(a, allowed_vars))
527                    .collect();
528                compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
529                    name: static_builtin_name(name),
530                    args: cargs,
531                })
532            } else {
533                try_compile_hof_expr(name, args, allowed_vars)
534            }
535        }
536
537        // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
538        _ => None,
539    }
540}
541
542/// Extract an inline lambda's params and body from an AST node, returning `None` if the
543/// node is not a simple lambda (i.e. has a signature or is a TCO thunk).
544fn extract_inline_lambda(node: &AstNode) -> Option<(&Vec<String>, &AstNode)> {
545    match node {
546        AstNode::Lambda {
547            params,
548            body,
549            signature: None,
550            thunk: false,
551        } => Some((params, body)),
552        _ => None,
553    }
554}
555
556/// Compile the array argument + lambda body for a HOF call, returning `None` if either
557/// fails to compile. The lambda params are added to the allowed-vars set so the body
558/// can reference them.
559fn compile_hof_array_and_body(
560    array_node: &AstNode,
561    params: &[String],
562    body: &AstNode,
563    allowed_vars: Option<&[&str]>,
564) -> Option<(Box<CompiledExpr>, Box<CompiledExpr>)> {
565    let array = try_compile_expr_inner(array_node, allowed_vars)?;
566    let param_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
567    let compiled_body = try_compile_expr_inner(body, Some(&param_refs))?;
568    Some((Box::new(array), Box::new(compiled_body)))
569}
570
571/// Try to compile a higher-order function call (`$map`, `$filter`, `$reduce`) when the
572/// callback argument is an inline lambda literal with a compilable body.
573///
574/// Returns `None` when:
575/// - The callback is not an inline lambda (e.g. a stored variable `$f`) — fall back so
576///   the tree-walker can look up the lambda at runtime.
577/// - The lambda has a signature or is a TCO thunk — semantics require the full evaluator.
578/// - The lambda body is not fully compilable — fall back transparently.
579/// - Param count is outside the supported range (see per-function constraints below).
580fn try_compile_hof_expr(
581    name: &str,
582    args: &[AstNode],
583    allowed_vars: Option<&[&str]>,
584) -> Option<CompiledExpr> {
585    match name {
586        "map" | "filter" => {
587            if args.len() != 2 {
588                return None;
589            }
590            let (params, body) = extract_inline_lambda(&args[1])?;
591            if params.is_empty() || params.len() > 2 {
592                return None;
593            }
594            let (array, compiled_body) =
595                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
596            if name == "map" {
597                Some(CompiledExpr::MapCall {
598                    array,
599                    params: params.clone(),
600                    body: compiled_body,
601                })
602            } else {
603                Some(CompiledExpr::FilterCall {
604                    array,
605                    params: params.clone(),
606                    body: compiled_body,
607                })
608            }
609        }
610        "reduce" => {
611            if args.len() < 2 || args.len() > 3 {
612                return None;
613            }
614            let (params, body) = extract_inline_lambda(&args[1])?;
615            if params.len() != 2 {
616                return None;
617            }
618            let (array, compiled_body) =
619                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
620            let initial = if args.len() == 3 {
621                Some(Box::new(try_compile_expr_inner(&args[2], allowed_vars)?))
622            } else {
623                None
624            };
625            Some(CompiledExpr::ReduceCall {
626                array,
627                params: params.clone(),
628                body: compiled_body,
629                initial,
630            })
631        }
632        _ => None,
633    }
634}
635
636/// Returns true if the named builtin is pure (no side effects, no context dependency)
637/// and can be safely compiled into a BuiltinCall.
638fn is_compilable_builtin(name: &str) -> bool {
639    matches!(
640        name,
641        "string"
642            | "length"
643            | "substring"
644            | "substringBefore"
645            | "substringAfter"
646            | "uppercase"
647            | "lowercase"
648            | "trim"
649            | "contains"
650            | "split"
651            | "join"
652            | "number"
653            | "floor"
654            | "ceil"
655            | "round"
656            | "abs"
657            | "sqrt"
658            | "sum"
659            | "max"
660            | "min"
661            | "average"
662            | "count"
663            | "boolean"
664            | "not"
665            | "keys"
666            | "append"
667            | "reverse"
668            | "distinct"
669            | "merge"
670    )
671}
672
673/// Maximum number of explicit arguments accepted by each compilable builtin.
674/// Returns `None` for variadic functions with no fixed upper bound.
675/// Used at compile time to fall back to the tree-walker for over-arity calls
676/// (which the tree-walker turns into the correct T0410/T0411 type errors).
677fn compilable_builtin_max_args(name: &str) -> Option<usize> {
678    match name {
679        "string" => Some(2),
680        "length" | "uppercase" | "lowercase" | "trim" => Some(1),
681        "substring" | "split" => Some(3),
682        "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
683        "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
684        "sum" | "max" | "min" | "average" | "count" => Some(1),
685        "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
686        "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
687        _ => None,
688    }
689}
690
691/// Return the `&'static str` for a known compilable builtin name.
692/// SAFETY: only called after `is_compilable_builtin` returns true.
693fn static_builtin_name(name: &str) -> &'static str {
694    match name {
695        "string" => "string",
696        "length" => "length",
697        "substring" => "substring",
698        "substringBefore" => "substringBefore",
699        "substringAfter" => "substringAfter",
700        "uppercase" => "uppercase",
701        "lowercase" => "lowercase",
702        "trim" => "trim",
703        "contains" => "contains",
704        "split" => "split",
705        "join" => "join",
706        "number" => "number",
707        "floor" => "floor",
708        "ceil" => "ceil",
709        "round" => "round",
710        "abs" => "abs",
711        "sqrt" => "sqrt",
712        "sum" => "sum",
713        "max" => "max",
714        "min" => "min",
715        "average" => "average",
716        "count" => "count",
717        "boolean" => "boolean",
718        "not" => "not",
719        "keys" => "keys",
720        "append" => "append",
721        "reverse" => "reverse",
722        "distinct" => "distinct",
723        "merge" => "merge",
724        _ => unreachable!("Not a compilable builtin: {}", name),
725    }
726}
727
728/// Evaluate a compiled expression against a single element.
729///
730/// `data` is the current element (typically an object from an array).
731/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
732///
733/// This is the tight inner loop — no recursion tracking, no scope push/pop,
734/// no AstNode pattern matching.
735#[inline(always)]
736pub(crate) fn eval_compiled(
737    expr: &CompiledExpr,
738    data: &JValue,
739    vars: Option<&HashMap<&str, &JValue>>,
740) -> Result<JValue, EvaluatorError> {
741    eval_compiled_inner(expr, data, vars, None, None)
742}
743
744/// Like `eval_compiled` but with an optional shape cache for O(1) positional
745/// field access. The shape cache maps field names to their index in the object's
746/// internal Vec, enabling `get_index()` instead of hash lookups.
747#[inline(always)]
748fn eval_compiled_shaped(
749    expr: &CompiledExpr,
750    data: &JValue,
751    vars: Option<&HashMap<&str, &JValue>>,
752    shape: &ShapeCache,
753) -> Result<JValue, EvaluatorError> {
754    eval_compiled_inner(expr, data, vars, None, Some(shape))
755}
756
757/// Clone the outer variable bindings into a new HashMap with the given capacity hint.
758/// Used by HOF eval arms to create per-iteration variable scopes that merge outer vars
759/// with lambda parameters.
760#[inline]
761fn clone_outer_vars<'a>(
762    vars: Option<&HashMap<&'a str, &'a JValue>>,
763    capacity: usize,
764) -> HashMap<&'a str, &'a JValue> {
765    vars.map(|v| v.iter().map(|(&k, v)| (k, *v)).collect())
766        .unwrap_or_else(|| HashMap::with_capacity(capacity))
767}
768
769fn eval_compiled_inner(
770    expr: &CompiledExpr,
771    data: &JValue,
772    vars: Option<&HashMap<&str, &JValue>>,
773    ctx: Option<&Context>,
774    shape: Option<&ShapeCache>,
775) -> Result<JValue, EvaluatorError> {
776    match expr {
777        // ── Leaves ──────────────────────────────────────────────────────
778        CompiledExpr::Literal(v) => Ok(v.clone()),
779
780        // ExplicitNull evaluates to Null, but is flagged at compile-time for
781        // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
782        CompiledExpr::ExplicitNull => Ok(JValue::Null),
783
784        CompiledExpr::FieldLookup(field) => match data {
785            JValue::Object(obj) => {
786                // Shape-accelerated: use positional index if available
787                if let Some(shape) = shape {
788                    if let Some(&idx) = shape.get(field.as_str()) {
789                        return Ok(obj
790                            .get_index(idx)
791                            .map(|(_, v)| v.clone())
792                            .unwrap_or(JValue::Undefined));
793                    }
794                }
795                Ok(obj
796                    .get(field.as_str())
797                    .cloned()
798                    .unwrap_or(JValue::Undefined))
799            }
800            _ => Ok(JValue::Undefined),
801        },
802
803        CompiledExpr::NestedFieldLookup(outer, inner) => match data {
804            JValue::Object(obj) => {
805                // Shape-accelerated outer lookup
806                let outer_val = if let Some(shape) = shape {
807                    if let Some(&idx) = shape.get(outer.as_str()) {
808                        obj.get_index(idx).map(|(_, v)| v)
809                    } else {
810                        obj.get(outer.as_str())
811                    }
812                } else {
813                    obj.get(outer.as_str())
814                };
815                Ok(outer_val
816                    .and_then(|v| match v {
817                        JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
818                        _ => None,
819                    })
820                    .unwrap_or(JValue::Undefined))
821            }
822            _ => Ok(JValue::Undefined),
823        },
824
825        CompiledExpr::VariableLookup(var) => {
826            if let Some(vars) = vars {
827                if let Some(val) = vars.get(var.as_str()) {
828                    return Ok((*val).clone());
829                }
830            }
831            // $ (empty var name) refers to the current data
832            if var.is_empty() {
833                return Ok(data.clone());
834            }
835            Ok(JValue::Undefined)
836        }
837
838        // ── Comparison ──────────────────────────────────────────────────
839        CompiledExpr::Compare { op, lhs, rhs } => {
840            let lhs_explicit_null = is_compiled_explicit_null(lhs);
841            let rhs_explicit_null = is_compiled_explicit_null(rhs);
842            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
843            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
844            match op {
845                CompiledCmp::Eq => Ok(JValue::Bool(crate::functions::array::values_equal(
846                    &left, &right,
847                ))),
848                CompiledCmp::Ne => Ok(JValue::Bool(!crate::functions::array::values_equal(
849                    &left, &right,
850                ))),
851                CompiledCmp::Lt => compiled_ordered_cmp(
852                    &left,
853                    &right,
854                    lhs_explicit_null,
855                    rhs_explicit_null,
856                    |a, b| a < b,
857                    |a, b| a < b,
858                ),
859                CompiledCmp::Le => compiled_ordered_cmp(
860                    &left,
861                    &right,
862                    lhs_explicit_null,
863                    rhs_explicit_null,
864                    |a, b| a <= b,
865                    |a, b| a <= b,
866                ),
867                CompiledCmp::Gt => compiled_ordered_cmp(
868                    &left,
869                    &right,
870                    lhs_explicit_null,
871                    rhs_explicit_null,
872                    |a, b| a > b,
873                    |a, b| a > b,
874                ),
875                CompiledCmp::Ge => compiled_ordered_cmp(
876                    &left,
877                    &right,
878                    lhs_explicit_null,
879                    rhs_explicit_null,
880                    |a, b| a >= b,
881                    |a, b| a >= b,
882                ),
883            }
884        }
885
886        // ── Arithmetic ──────────────────────────────────────────────────
887        CompiledExpr::Arithmetic { op, lhs, rhs } => {
888            let lhs_explicit_null = is_compiled_explicit_null(lhs);
889            let rhs_explicit_null = is_compiled_explicit_null(rhs);
890            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
891            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
892            compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
893        }
894
895        // ── String concat ───────────────────────────────────────────────
896        CompiledExpr::Concat(lhs, rhs) => {
897            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
898            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
899            let ls = compiled_to_concat_string(&left)?;
900            let rs = compiled_to_concat_string(&right)?;
901            Ok(JValue::string(format!("{}{}", ls, rs)))
902        }
903
904        // ── Logical ─────────────────────────────────────────────────────
905        CompiledExpr::And(lhs, rhs) => {
906            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
907            if !compiled_is_truthy(&left) {
908                return Ok(JValue::Bool(false));
909            }
910            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
911            Ok(JValue::Bool(compiled_is_truthy(&right)))
912        }
913        CompiledExpr::Or(lhs, rhs) => {
914            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
915            if compiled_is_truthy(&left) {
916                return Ok(JValue::Bool(true));
917            }
918            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
919            Ok(JValue::Bool(compiled_is_truthy(&right)))
920        }
921        CompiledExpr::Not(inner) => {
922            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
923            Ok(JValue::Bool(!compiled_is_truthy(&val)))
924        }
925        CompiledExpr::Negate(inner) => {
926            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
927            match val {
928                JValue::Number(n) => Ok(JValue::Number(-n)),
929                JValue::Null => Ok(JValue::Null),
930                // Undefined operand propagates through unary minus, matching the tree-walker.
931                v if v.is_undefined() => Ok(JValue::Undefined),
932                _ => Err(EvaluatorError::TypeError(
933                    "D1002: Cannot negate non-number value".to_string(),
934                )),
935            }
936        }
937
938        // ── Conditional ─────────────────────────────────────────────────
939        CompiledExpr::Conditional {
940            condition,
941            then_expr,
942            else_expr,
943        } => {
944            let cond = eval_compiled_inner(condition, data, vars, ctx, shape)?;
945            if compiled_is_truthy(&cond) {
946                eval_compiled_inner(then_expr, data, vars, ctx, shape)
947            } else if let Some(else_e) = else_expr {
948                eval_compiled_inner(else_e, data, vars, ctx, shape)
949            } else {
950                Ok(JValue::Undefined)
951            }
952        }
953
954        // ── Object construction ─────────────────────────────────────────
955        CompiledExpr::ObjectConstruct(fields) => {
956            let mut result = IndexMap::with_capacity(fields.len());
957            for (key, expr) in fields {
958                let value = eval_compiled_inner(expr, data, vars, ctx, shape)?;
959                if !value.is_undefined() {
960                    result.insert(key.clone(), value);
961                }
962            }
963            Ok(JValue::object(result))
964        }
965
966        // ── Array construction ──────────────────────────────────────────
967        CompiledExpr::ArrayConstruct(elems) => {
968            let mut result = Vec::new();
969            for (elem_expr, is_nested) in elems {
970                let value = eval_compiled_inner(elem_expr, data, vars, ctx, shape)?;
971                // Undefined values are excluded from array constructors (tree-walker parity)
972                if value.is_undefined() {
973                    continue;
974                }
975                if *is_nested {
976                    // Explicit array constructor [...] — keep nested even if it's an array
977                    result.push(value);
978                } else if let JValue::Array(arr) = value {
979                    // Non-constructor that evaluated to an array — flatten one level
980                    result.extend(arr.iter().cloned());
981                } else {
982                    result.push(value);
983                }
984            }
985            Ok(JValue::array(result))
986        }
987
988        // ── Phase 2 new variants ─────────────────────────────────────────
989
990        // ContextVar: named variable lookup from context scope.
991        // In top-level mode (ctx=None, no bindings), returns Undefined.
992        // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
993        // is only ever populated for top-level calls — always Undefined there.
994        CompiledExpr::ContextVar(name) => {
995            // Check vars map first (for lambda params that might shadow context)
996            if let Some(vars) = vars {
997                if let Some(val) = vars.get(name.as_str()) {
998                    return Ok((*val).clone());
999                }
1000            }
1001            // Then check context scope
1002            if let Some(ctx) = ctx {
1003                if let Some(val) = ctx.lookup(name) {
1004                    return Ok(val.clone());
1005                }
1006            }
1007            Ok(JValue::Undefined)
1008        }
1009
1010        // FieldPath: multi-step field access with implicit array mapping.
1011        CompiledExpr::FieldPath(steps) => compiled_eval_field_path(steps, data, vars, ctx, shape),
1012
1013        // BuiltinCall: evaluate all args, dispatch to pure builtin.
1014        CompiledExpr::BuiltinCall { name, args } => {
1015            let mut evaled_args = Vec::with_capacity(args.len());
1016            for arg in args.iter() {
1017                evaled_args.push(eval_compiled_inner(arg, data, vars, ctx, shape)?);
1018            }
1019            call_pure_builtin(name, &evaled_args, data)
1020        }
1021
1022        // Block: evaluate each expression in sequence, return the last value.
1023        CompiledExpr::Block(exprs) => {
1024            let mut result = JValue::Undefined;
1025            for expr in exprs.iter() {
1026                result = eval_compiled_inner(expr, data, vars, ctx, shape)?;
1027            }
1028            Ok(result)
1029        }
1030
1031        // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1032        // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1033        CompiledExpr::Coalesce(lhs, rhs) => {
1034            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
1035            if left.is_undefined() {
1036                eval_compiled_inner(rhs, data, vars, ctx, shape)
1037            } else {
1038                Ok(left)
1039            }
1040        }
1041
1042        // ── Higher-order functions ─────────────────────────────────────────────
1043        //
1044        // These variants are emitted by try_compile_hof_expr when the HOF argument
1045        // is an inline lambda literal with a compilable body. Outer vars are merged
1046        // with the lambda params so that nested HOF can access variables from
1047        // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1048        CompiledExpr::MapCall {
1049            array,
1050            params,
1051            body,
1052        } => {
1053            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1054            let single_holder;
1055            let items: &[JValue] = match &arr_val {
1056                JValue::Array(a) => a.as_slice(),
1057                JValue::Undefined => return Ok(JValue::Undefined),
1058                other => {
1059                    single_holder = [other.clone()];
1060                    &single_holder[..]
1061                }
1062            };
1063            let mut result = Vec::with_capacity(items.len());
1064            let p0 = params.first().map(|s| s.as_str());
1065
1066            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1067                // 2-param lambda (element + index): build per-iteration because idx_val
1068                // is loop-local and cannot outlive the iteration.
1069                for (idx, item) in items.iter().enumerate() {
1070                    let idx_val = JValue::Number(idx as f64);
1071                    let mut call_vars = clone_outer_vars(vars, 2);
1072                    if let Some(p) = p0 {
1073                        call_vars.insert(p, item);
1074                    }
1075                    call_vars.insert(p1, &idx_val);
1076                    let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1077                    if !mapped.is_undefined() {
1078                        result.push(mapped);
1079                    }
1080                }
1081            } else if let Some(p0) = p0 {
1082                // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1083                let mut call_vars = clone_outer_vars(vars, 1);
1084                for item in items.iter() {
1085                    call_vars.insert(p0, item);
1086                    let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1087                    if !mapped.is_undefined() {
1088                        result.push(mapped);
1089                    }
1090                }
1091            }
1092            Ok(if result.is_empty() {
1093                JValue::Undefined
1094            } else {
1095                JValue::array(result)
1096            })
1097        }
1098
1099        CompiledExpr::FilterCall {
1100            array,
1101            params,
1102            body,
1103        } => {
1104            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1105            if arr_val.is_undefined() || arr_val.is_null() {
1106                return Ok(JValue::Undefined);
1107            }
1108            let single_holder;
1109            let (items, was_single) = match &arr_val {
1110                JValue::Array(a) => (a.as_slice(), false),
1111                other => {
1112                    single_holder = [other.clone()];
1113                    (&single_holder[..], true)
1114                }
1115            };
1116            let mut result = Vec::with_capacity(items.len() / 2);
1117            let p0 = params.first().map(|s| s.as_str());
1118
1119            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1120                for (idx, item) in items.iter().enumerate() {
1121                    let idx_val = JValue::Number(idx as f64);
1122                    let mut call_vars = clone_outer_vars(vars, 2);
1123                    if let Some(p) = p0 {
1124                        call_vars.insert(p, item);
1125                    }
1126                    call_vars.insert(p1, &idx_val);
1127                    let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1128                    if compiled_is_truthy(&pred) {
1129                        result.push(item.clone());
1130                    }
1131                }
1132            } else if let Some(p0) = p0 {
1133                let mut call_vars = clone_outer_vars(vars, 1);
1134                for item in items.iter() {
1135                    call_vars.insert(p0, item);
1136                    let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1137                    if compiled_is_truthy(&pred) {
1138                        result.push(item.clone());
1139                    }
1140                }
1141            }
1142            if was_single {
1143                Ok(match result.len() {
1144                    0 => JValue::Undefined,
1145                    1 => result.remove(0),
1146                    _ => JValue::array(result),
1147                })
1148            } else {
1149                Ok(JValue::array(result))
1150            }
1151        }
1152
1153        CompiledExpr::ReduceCall {
1154            array,
1155            params,
1156            body,
1157            initial,
1158        } => {
1159            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1160            let single_holder;
1161            let items: &[JValue] = match &arr_val {
1162                JValue::Array(a) => a.as_slice(),
1163                JValue::Null => return Ok(JValue::Null),
1164                JValue::Undefined => return Ok(JValue::Undefined),
1165                other => {
1166                    single_holder = [other.clone()];
1167                    &single_holder[..]
1168                }
1169            };
1170            let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1171                let init_val = eval_compiled_inner(init_expr, data, vars, ctx, shape)?;
1172                if items.is_empty() {
1173                    return Ok(init_val);
1174                }
1175                (0usize, init_val)
1176            } else {
1177                if items.is_empty() {
1178                    return Ok(JValue::Null);
1179                }
1180                (1, items[0].clone())
1181            };
1182            let acc_param = params[0].as_str();
1183            let item_param = params[1].as_str();
1184            for item in items[start_idx..].iter() {
1185                // Per-iteration HashMap: &accumulator borrow must be released before we
1186                // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1187                let mut call_vars = clone_outer_vars(vars, 2);
1188                call_vars.insert(acc_param, &accumulator);
1189                call_vars.insert(item_param, item);
1190                let new_acc = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1191                drop(call_vars);
1192                accumulator = new_acc;
1193            }
1194            Ok(accumulator)
1195        }
1196    }
1197}
1198
1199/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1200#[inline]
1201pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1202    match value {
1203        JValue::Null | JValue::Undefined => false,
1204        JValue::Bool(b) => *b,
1205        JValue::Number(n) => *n != 0.0,
1206        JValue::String(s) => !s.is_empty(),
1207        JValue::Array(a) => !a.is_empty(),
1208        JValue::Object(o) => !o.is_empty(),
1209        _ => false,
1210    }
1211}
1212
1213/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1214/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1215#[inline]
1216fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1217    matches!(expr, CompiledExpr::ExplicitNull)
1218}
1219
1220/// Ordered comparison for compiled expressions.
1221/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1222#[inline]
1223pub(crate) fn compiled_ordered_cmp(
1224    left: &JValue,
1225    right: &JValue,
1226    left_is_explicit_null: bool,
1227    right_is_explicit_null: bool,
1228    cmp_num: fn(f64, f64) -> bool,
1229    cmp_str: fn(&str, &str) -> bool,
1230) -> Result<JValue, EvaluatorError> {
1231    match (left, right) {
1232        (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1233        (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1234        // Both null/undefined → undefined
1235        (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1236        (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1237        // Explicit null literal with any non-null type → T2010 error
1238        (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1239            "T2010: Type mismatch in comparison".to_string(),
1240        )),
1241        (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1242            "T2010: Type mismatch in comparison".to_string(),
1243        )),
1244        // Boolean with undefined → T2010 error
1245        (JValue::Bool(_), JValue::Null | JValue::Undefined)
1246        | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1247            EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1248        ),
1249        // Number or String with implicit undefined (missing field) → undefined result
1250        (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1251        | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1252            Ok(JValue::Null)
1253        }
1254        // Type mismatch (string vs number)
1255        (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1256            Err(EvaluatorError::EvaluationError(
1257                "T2009: The expressions on either side of operator must be of the same data type"
1258                    .to_string(),
1259            ))
1260        }
1261        _ => Err(EvaluatorError::EvaluationError(
1262            "T2010: Type mismatch in comparison".to_string(),
1263        )),
1264    }
1265}
1266
1267/// Arithmetic for compiled expressions.
1268/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1269#[inline]
1270pub(crate) fn compiled_arithmetic(
1271    op: CompiledArithOp,
1272    left: &JValue,
1273    right: &JValue,
1274    left_is_explicit_null: bool,
1275    right_is_explicit_null: bool,
1276) -> Result<JValue, EvaluatorError> {
1277    let op_sym = match op {
1278        CompiledArithOp::Add => "+",
1279        CompiledArithOp::Sub => "-",
1280        CompiledArithOp::Mul => "*",
1281        CompiledArithOp::Div => "/",
1282        CompiledArithOp::Mod => "%",
1283    };
1284    match (left, right) {
1285        (JValue::Number(a), JValue::Number(b)) => {
1286            let result = match op {
1287                CompiledArithOp::Add => *a + *b,
1288                CompiledArithOp::Sub => *a - *b,
1289                CompiledArithOp::Mul => {
1290                    let r = *a * *b;
1291                    if r.is_infinite() {
1292                        return Err(EvaluatorError::EvaluationError(
1293                            "D1001: Number out of range".to_string(),
1294                        ));
1295                    }
1296                    r
1297                }
1298                CompiledArithOp::Div => {
1299                    if *b == 0.0 {
1300                        return Err(EvaluatorError::EvaluationError(
1301                            "Division by zero".to_string(),
1302                        ));
1303                    }
1304                    *a / *b
1305                }
1306                CompiledArithOp::Mod => {
1307                    if *b == 0.0 {
1308                        return Err(EvaluatorError::EvaluationError(
1309                            "Division by zero".to_string(),
1310                        ));
1311                    }
1312                    *a % *b
1313                }
1314            };
1315            Ok(JValue::Number(result))
1316        }
1317        // Explicit null literal → T2002 error (matching tree-walker behavior)
1318        (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1319            Err(EvaluatorError::TypeError(format!(
1320                "T2002: The left side of the {} operator must evaluate to a number",
1321                op_sym
1322            )))
1323        }
1324        (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1325            Err(EvaluatorError::TypeError(format!(
1326                "T2002: The right side of the {} operator must evaluate to a number",
1327                op_sym
1328            )))
1329        }
1330        // Implicit undefined propagation (from missing field) → undefined result
1331        (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1332            Ok(JValue::Null)
1333        }
1334        _ => Err(EvaluatorError::TypeError(format!(
1335            "Cannot apply {} to {:?} and {:?}",
1336            op_sym, left, right
1337        ))),
1338    }
1339}
1340
1341/// Convert a value to string for concatenation in compiled expressions.
1342#[inline]
1343pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1344    match value {
1345        JValue::String(s) => Ok(s.to_string()),
1346        JValue::Null | JValue::Undefined => Ok(String::new()),
1347        JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1348            match crate::functions::string::string(value, None) {
1349                Ok(JValue::String(s)) => Ok(s.to_string()),
1350                Ok(JValue::Null) => Ok(String::new()),
1351                _ => Err(EvaluatorError::TypeError(
1352                    "Cannot concatenate complex types".to_string(),
1353                )),
1354            }
1355        }
1356        _ => Ok(String::new()),
1357    }
1358}
1359
1360/// Equality comparison for the bytecode VM.
1361#[inline]
1362pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> JValue {
1363    JValue::Bool(crate::functions::array::values_equal(lhs, rhs))
1364}
1365
1366/// String concatenation for the bytecode VM.
1367#[inline]
1368pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1369    let l = compiled_to_concat_string(&lhs)?;
1370    let r = compiled_to_concat_string(&rhs)?;
1371    Ok(JValue::string(l + &r))
1372}
1373
1374/// Entry point for the bytecode VM to call pure builtins by name.
1375#[inline]
1376pub(crate) fn call_pure_builtin_by_name(
1377    name: &str,
1378    args: &[JValue],
1379    data: &JValue,
1380) -> Result<JValue, EvaluatorError> {
1381    call_pure_builtin(name, args, data)
1382}
1383
1384// ──────────────────────────────────────────────────────────────────────────────
1385// Phase 2: path compilation, builtin dispatch, and supporting helpers
1386// ──────────────────────────────────────────────────────────────────────────────
1387
1388/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1389///
1390/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1391/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1392fn try_compile_path(
1393    steps: &[crate::ast::PathStep],
1394    allowed_vars: Option<&[&str]>,
1395) -> Option<CompiledExpr> {
1396    use crate::ast::{AstNode, Stage};
1397
1398    if steps.is_empty() {
1399        return None;
1400    }
1401
1402    // Determine the start of the path:
1403    //   `$.field...`  → starts from current data (drop the leading `$` step)
1404    //   `$var.field`  → variable-prefixed paths: not compiled yet, fall back to tree-walker
1405    //   `field...`    → starts from current data
1406    let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1407        AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1408        AstNode::Variable(_) => return None,
1409        AstNode::Name(_) => steps,
1410        _ => return None,
1411    };
1412
1413    // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1414    // which represent index access in JSONata, not boolean filtering.
1415    let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1416        if matches!(node, AstNode::Number(_)) {
1417            return None;
1418        }
1419        try_compile_expr_inner(node, allowed_vars)
1420    };
1421
1422    // Compile each field step.
1423    // Handles:
1424    //   - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1425    //   - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1426    //     previous step's filter slot, since both encodings have identical runtime semantics.
1427    let mut compiled_steps = Vec::with_capacity(field_steps.len());
1428    for step in field_steps {
1429        match &step.node {
1430            AstNode::Name(name) => {
1431                let filter = match step.stages.as_slice() {
1432                    [] => None,
1433                    [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1434                    _ => return None,
1435                };
1436                compiled_steps.push(CompiledStep {
1437                    field: name.clone(),
1438                    filter,
1439                });
1440            }
1441            AstNode::Predicate(filter_node) => {
1442                // Standalone predicate step — fold into the previous Name step's filter slot.
1443                if !step.stages.is_empty() {
1444                    return None;
1445                }
1446                let last = compiled_steps.last_mut()?;
1447                if last.filter.is_some() {
1448                    return None;
1449                }
1450                last.filter = Some(compile_filter(filter_node)?);
1451            }
1452            _ => return None,
1453        }
1454    }
1455
1456    if compiled_steps.is_empty() {
1457        // Bare `$` with no further field steps — current-data reference
1458        return Some(CompiledExpr::VariableLookup(String::new()));
1459    }
1460
1461    // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1462    // in HOF mode (allowed_vars=Some), where data is always a single Object element
1463    // from an array. In top-level mode (allowed_vars=None), data can itself be an
1464    // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1465    if allowed_vars.is_some() {
1466        if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1467            return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1468        }
1469        if compiled_steps.len() == 2
1470            && compiled_steps[0].filter.is_none()
1471            && compiled_steps[1].filter.is_none()
1472        {
1473            let outer = compiled_steps.remove(0).field;
1474            let inner = compiled_steps.remove(0).field;
1475            return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1476        }
1477    }
1478
1479    Some(CompiledExpr::FieldPath(compiled_steps))
1480}
1481
1482/// Evaluate a compiled `FieldPath` against `data`.
1483///
1484/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1485/// Filters are applied as predicates: truthy elements are kept.
1486///
1487/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1488/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1489/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1490fn compiled_eval_field_path(
1491    steps: &[CompiledStep],
1492    data: &JValue,
1493    vars: Option<&HashMap<&str, &JValue>>,
1494    ctx: Option<&Context>,
1495    shape: Option<&ShapeCache>,
1496) -> Result<JValue, EvaluatorError> {
1497    let mut current = data.clone();
1498    // Track whether the most recent field step mapped over an array (like the tree-walker's
1499    // `did_array_mapping` flag). Filters also count as array operations.
1500    let mut did_array_mapping = false;
1501    for step in steps {
1502        // Determine if this step will do array mapping before we overwrite `current`
1503        let is_array = matches!(current, JValue::Array(_));
1504        // Field access with implicit array mapping
1505        current = compiled_field_step(&step.field, &current);
1506        if is_array {
1507            did_array_mapping = true;
1508        } else {
1509            // Extracting from a single object resets the flag (tree-walker parity)
1510            did_array_mapping = false;
1511        }
1512        // Apply filter if present (filter is an array operation — keep the flag set)
1513        if let Some(filter) = &step.filter {
1514            current = compiled_apply_filter(filter, &current, vars, ctx, shape)?;
1515            // Filter always implies we operated on an array
1516            did_array_mapping = true;
1517        }
1518    }
1519    // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1520    if did_array_mapping {
1521        Ok(match current {
1522            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1523            other => other,
1524        })
1525    } else {
1526        Ok(current)
1527    }
1528}
1529
1530/// Perform a single-field access with implicit array-mapping semantics.
1531///
1532/// - Object: look up `field`, return its value or Undefined
1533/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1534/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1535/// - Other: Undefined
1536fn compiled_field_step(field: &str, value: &JValue) -> JValue {
1537    match value {
1538        JValue::Object(obj) => {
1539            // Check for tuple: extract from "@" inner object
1540            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1541                if let Some(JValue::Object(inner)) = obj.get("@") {
1542                    return inner.get(field).cloned().unwrap_or(JValue::Undefined);
1543                }
1544                return JValue::Undefined;
1545            }
1546            obj.get(field).cloned().unwrap_or(JValue::Undefined)
1547        }
1548        JValue::Array(arr) => {
1549            // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1550            let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1551                if let JValue::Object(obj) = v {
1552                    if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1553                        return build_shape_cache(v);
1554                    }
1555                }
1556                None
1557            });
1558            let mut result = Vec::new();
1559            for item in arr.iter() {
1560                let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1561                    // Tuple objects need the recursive path for "@" inner lookup.
1562                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1563                        compiled_field_step(field, item)
1564                    } else if let Some(&pos) = sh.get(field) {
1565                        // Positional access with key verification: guards against heterogeneous
1566                        // schemas (objects where the same field is at a different index).
1567                        // On a mismatch, fall back to a regular hash lookup.
1568                        match obj.get_index(pos) {
1569                            Some((k, v)) if k.as_str() == field => v.clone(),
1570                            _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1571                        }
1572                    } else {
1573                        // Field not in the first object's schema — fall back to hash lookup
1574                        // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1575                        obj.get(field).cloned().unwrap_or(JValue::Undefined)
1576                    }
1577                } else {
1578                    compiled_field_step(field, item)
1579                };
1580                match extracted {
1581                    JValue::Undefined => {}
1582                    JValue::Array(inner) => result.extend(inner.iter().cloned()),
1583                    other => result.push(other),
1584                }
1585            }
1586            if result.is_empty() {
1587                JValue::Undefined
1588            } else {
1589                JValue::array(result)
1590            }
1591        }
1592        _ => JValue::Undefined,
1593    }
1594}
1595
1596/// Apply a compiled filter predicate to a value.
1597///
1598/// - Array: return elements for which the predicate is truthy
1599/// - Single value: return it if predicate is truthy, else Undefined
1600/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1601fn compiled_apply_filter(
1602    filter: &CompiledExpr,
1603    value: &JValue,
1604    vars: Option<&HashMap<&str, &JValue>>,
1605    ctx: Option<&Context>,
1606    shape: Option<&ShapeCache>,
1607) -> Result<JValue, EvaluatorError> {
1608    match value {
1609        JValue::Array(arr) => {
1610            let mut result = Vec::new();
1611            // Auto-build shape cache from first element when not provided.
1612            // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1613            let local_shape: Option<ShapeCache> = if shape.is_none() {
1614                arr.first().and_then(build_shape_cache)
1615            } else {
1616                None
1617            };
1618            let effective_shape = shape.or(local_shape.as_ref());
1619            for item in arr.iter() {
1620                let pred = eval_compiled_inner(filter, item, vars, ctx, effective_shape)?;
1621                if compiled_is_truthy(&pred) {
1622                    result.push(item.clone());
1623                }
1624            }
1625            if result.is_empty() {
1626                Ok(JValue::Undefined)
1627            } else if result.len() == 1 {
1628                Ok(result.remove(0))
1629            } else {
1630                Ok(JValue::array(result))
1631            }
1632        }
1633        JValue::Undefined => Ok(JValue::Undefined),
1634        _ => {
1635            let pred = eval_compiled_inner(filter, value, vars, ctx, shape)?;
1636            if compiled_is_truthy(&pred) {
1637                Ok(value.clone())
1638            } else {
1639                Ok(JValue::Undefined)
1640            }
1641        }
1642    }
1643}
1644
1645/// Dispatch a pure builtin function call.
1646///
1647/// Replicates the tree-walker's evaluation for the subset of builtins in
1648/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1649/// `data` is the current context value for implicit-argument insertion.
1650fn call_pure_builtin(name: &str, args: &[JValue], data: &JValue) -> Result<JValue, EvaluatorError> {
1651    use crate::functions;
1652
1653    // Apply implicit context insertion matching the tree-walker
1654    let args_storage: Vec<JValue>;
1655    let effective_args: &[JValue] = if args.is_empty() {
1656        match name {
1657            "string" => {
1658                // $string() with a null/undefined context returns undefined, not "null".
1659                // This mirrors the tree-walker's special case at the function-call site.
1660                if data.is_undefined() || data.is_null() {
1661                    return Ok(JValue::Undefined);
1662                }
1663                args_storage = vec![data.clone()];
1664                &args_storage
1665            }
1666            "number" | "boolean" | "uppercase" | "lowercase" => {
1667                args_storage = vec![data.clone()];
1668                &args_storage
1669            }
1670            _ => args,
1671        }
1672    } else if args.len() == 1 {
1673        match name {
1674            "substringBefore" | "substringAfter" | "contains" | "split" => {
1675                if matches!(data, JValue::String(_)) {
1676                    args_storage = std::iter::once(data.clone())
1677                        .chain(args.iter().cloned())
1678                        .collect();
1679                    &args_storage
1680                } else {
1681                    args
1682                }
1683            }
1684            _ => args,
1685        }
1686    } else {
1687        args
1688    };
1689
1690    // Apply undefined propagation: if the first effective argument is Undefined
1691    // and the function propagates undefined, return Undefined immediately.
1692    // This matches the tree-walker's `propagates_undefined` check.
1693    if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
1694        return Ok(JValue::Undefined);
1695    }
1696
1697    match name {
1698        // ── String functions ────────────────────────────────────────────
1699        "string" => {
1700            // Validate the optional prettify argument: must be a boolean.
1701            let prettify = match effective_args.get(1) {
1702                None => None,
1703                Some(JValue::Bool(b)) => Some(*b),
1704                Some(_) => {
1705                    return Err(EvaluatorError::TypeError(
1706                        "string() prettify parameter must be a boolean".to_string(),
1707                    ))
1708                }
1709            };
1710            let arg = effective_args.first().unwrap_or(&JValue::Null);
1711            Ok(functions::string::string(arg, prettify)?)
1712        }
1713        "length" => match effective_args.first() {
1714            Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1715            // Undefined input propagates (caught above by the undefined-propagation guard).
1716            Some(JValue::Undefined) => Ok(JValue::Undefined),
1717            // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1718            // so the test framework accepts it against any expected T-code).
1719            None => Err(EvaluatorError::EvaluationError(
1720                "length() requires exactly 1 argument".to_string(),
1721            )),
1722            // null and any other non-string type → T0410
1723            _ => Err(EvaluatorError::TypeError(
1724                "T0410: Argument 1 of function length does not match function signature"
1725                    .to_string(),
1726            )),
1727        },
1728        "uppercase" => match effective_args.first() {
1729            Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
1730            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1731            _ => Err(EvaluatorError::TypeError(
1732                "T0410: Argument 1 of function uppercase does not match function signature"
1733                    .to_string(),
1734            )),
1735        },
1736        "lowercase" => match effective_args.first() {
1737            Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
1738            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1739            _ => Err(EvaluatorError::TypeError(
1740                "T0410: Argument 1 of function lowercase does not match function signature"
1741                    .to_string(),
1742            )),
1743        },
1744        "trim" => match effective_args.first() {
1745            None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
1746            Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
1747            _ => Err(EvaluatorError::TypeError(
1748                "trim() requires a string argument".to_string(),
1749            )),
1750        },
1751        "substring" => {
1752            if effective_args.len() < 2 {
1753                return Err(EvaluatorError::EvaluationError(
1754                    "substring() requires at least 2 arguments".to_string(),
1755                ));
1756            }
1757            match (&effective_args[0], &effective_args[1]) {
1758                (JValue::String(s), JValue::Number(start)) => {
1759                    // Optional 3rd arg (length) must be a number if provided.
1760                    let length = match effective_args.get(2) {
1761                        None => None,
1762                        Some(JValue::Number(l)) => Some(*l as i64),
1763                        Some(_) => {
1764                            return Err(EvaluatorError::TypeError(
1765                                "T0410: Argument 3 of function substring does not match function signature"
1766                                    .to_string(),
1767                            ))
1768                        }
1769                    };
1770                    Ok(functions::string::substring(s, *start as i64, length)?)
1771                }
1772                _ => Err(EvaluatorError::TypeError(
1773                    "T0410: Argument 1 of function substring does not match function signature"
1774                        .to_string(),
1775                )),
1776            }
1777        }
1778        "substringBefore" => {
1779            if effective_args.len() != 2 {
1780                return Err(EvaluatorError::TypeError(
1781                    "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
1782                ));
1783            }
1784            match (&effective_args[0], &effective_args[1]) {
1785                (JValue::String(s), JValue::String(sep)) => {
1786                    Ok(functions::string::substring_before(s, sep)?)
1787                }
1788                // Undefined propagates; null is a type error.
1789                (JValue::Undefined, _) => Ok(JValue::Undefined),
1790                _ => Err(EvaluatorError::TypeError(
1791                    "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
1792                )),
1793            }
1794        }
1795        "substringAfter" => {
1796            if effective_args.len() != 2 {
1797                return Err(EvaluatorError::TypeError(
1798                    "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
1799                ));
1800            }
1801            match (&effective_args[0], &effective_args[1]) {
1802                (JValue::String(s), JValue::String(sep)) => {
1803                    Ok(functions::string::substring_after(s, sep)?)
1804                }
1805                // Undefined propagates; null is a type error.
1806                (JValue::Undefined, _) => Ok(JValue::Undefined),
1807                _ => Err(EvaluatorError::TypeError(
1808                    "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
1809                )),
1810            }
1811        }
1812        "contains" => {
1813            if effective_args.len() != 2 {
1814                return Err(EvaluatorError::EvaluationError(
1815                    "contains() requires exactly 2 arguments".to_string(),
1816                ));
1817            }
1818            match &effective_args[0] {
1819                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1820                JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
1821                _ => Err(EvaluatorError::TypeError(
1822                    "contains() requires a string as the first argument".to_string(),
1823                )),
1824            }
1825        }
1826        "split" => {
1827            if effective_args.len() < 2 {
1828                return Err(EvaluatorError::EvaluationError(
1829                    "split() requires at least 2 arguments".to_string(),
1830                ));
1831            }
1832            match &effective_args[0] {
1833                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1834                JValue::String(s) => {
1835                    // Validate the optional limit argument — must be a positive number.
1836                    let limit = match effective_args.get(2) {
1837                        None => None,
1838                        Some(JValue::Number(n)) => {
1839                            if *n < 0.0 {
1840                                return Err(EvaluatorError::EvaluationError(
1841                                    "D3020: Third argument of split function must be a positive number"
1842                                        .to_string(),
1843                                ));
1844                            }
1845                            Some(n.floor() as usize)
1846                        }
1847                        Some(_) => {
1848                            return Err(EvaluatorError::TypeError(
1849                                "split() limit must be a number".to_string(),
1850                            ))
1851                        }
1852                    };
1853                    Ok(functions::string::split(s, &effective_args[1], limit)?)
1854                }
1855                _ => Err(EvaluatorError::TypeError(
1856                    "split() requires a string as the first argument".to_string(),
1857                )),
1858            }
1859        }
1860        "join" => {
1861            if effective_args.is_empty() {
1862                return Err(EvaluatorError::TypeError(
1863                    "T0410: Argument 1 of function $join does not match function signature"
1864                        .to_string(),
1865                ));
1866            }
1867            match &effective_args[0] {
1868                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1869                // Signature: <a<s>s?:s> — first arg must be an array of strings.
1870                JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
1871                    Err(EvaluatorError::TypeError(
1872                        "T0412: Argument 1 of function $join must be an array of String"
1873                            .to_string(),
1874                    ))
1875                }
1876                JValue::Array(arr) => {
1877                    // All elements must be strings.
1878                    for item in arr.iter() {
1879                        if !matches!(item, JValue::String(_)) {
1880                            return Err(EvaluatorError::TypeError(
1881                                "T0412: Argument 1 of function $join must be an array of String"
1882                                    .to_string(),
1883                            ));
1884                        }
1885                    }
1886                    // Validate separator: must be a string if provided.
1887                    let separator = match effective_args.get(1) {
1888                        None | Some(JValue::Undefined) => None,
1889                        Some(JValue::String(s)) => Some(&**s),
1890                        Some(_) => {
1891                            return Err(EvaluatorError::TypeError(
1892                                "T0410: Argument 2 of function $join does not match function signature (expected String)"
1893                                    .to_string(),
1894                            ))
1895                        }
1896                    };
1897                    Ok(functions::string::join(arr, separator)?)
1898                }
1899                JValue::String(s) => Ok(JValue::String(s.clone())),
1900                _ => Err(EvaluatorError::TypeError(
1901                    "T0412: Argument 1 of function $join must be an array of String".to_string(),
1902                )),
1903            }
1904        }
1905
1906        // ── Numeric functions ───────────────────────────────────────────
1907        "number" => match effective_args.first() {
1908            Some(v) => Ok(functions::numeric::number(v)?),
1909            None => Err(EvaluatorError::EvaluationError(
1910                "number() requires at least 1 argument".to_string(),
1911            )),
1912        },
1913        "floor" => match effective_args.first() {
1914            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1915            Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
1916            _ => Err(EvaluatorError::TypeError(
1917                "floor() requires a number argument".to_string(),
1918            )),
1919        },
1920        "ceil" => match effective_args.first() {
1921            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1922            Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
1923            _ => Err(EvaluatorError::TypeError(
1924                "ceil() requires a number argument".to_string(),
1925            )),
1926        },
1927        "round" => match effective_args.first() {
1928            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1929            Some(JValue::Number(n)) => {
1930                let precision = effective_args.get(1).and_then(|v| {
1931                    if let JValue::Number(p) = v {
1932                        Some(*p as i32)
1933                    } else {
1934                        None
1935                    }
1936                });
1937                Ok(functions::numeric::round(*n, precision)?)
1938            }
1939            _ => Err(EvaluatorError::TypeError(
1940                "round() requires a number argument".to_string(),
1941            )),
1942        },
1943        "abs" => match effective_args.first() {
1944            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1945            Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
1946            _ => Err(EvaluatorError::TypeError(
1947                "abs() requires a number argument".to_string(),
1948            )),
1949        },
1950        "sqrt" => match effective_args.first() {
1951            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1952            Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
1953            _ => Err(EvaluatorError::TypeError(
1954                "sqrt() requires a number argument".to_string(),
1955            )),
1956        },
1957
1958        // ── Aggregation functions ───────────────────────────────────────
1959        "sum" => match effective_args.first() {
1960            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1961            None => Err(EvaluatorError::EvaluationError(
1962                "sum() requires exactly 1 argument".to_string(),
1963            )),
1964            Some(JValue::Null) => Ok(JValue::Null),
1965            Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
1966            Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
1967            Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
1968        },
1969        "max" => match effective_args.first() {
1970            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1971            Some(JValue::Null) | None => Ok(JValue::Null),
1972            Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
1973            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1974            _ => Err(EvaluatorError::TypeError(
1975                "max() requires an array or number argument".to_string(),
1976            )),
1977        },
1978        "min" => match effective_args.first() {
1979            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1980            Some(JValue::Null) | None => Ok(JValue::Null),
1981            Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
1982            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1983            _ => Err(EvaluatorError::TypeError(
1984                "min() requires an array or number argument".to_string(),
1985            )),
1986        },
1987        "average" => match effective_args.first() {
1988            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1989            Some(JValue::Null) | None => Ok(JValue::Null),
1990            Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
1991            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1992            _ => Err(EvaluatorError::TypeError(
1993                "average() requires an array or number argument".to_string(),
1994            )),
1995        },
1996        "count" => match effective_args.first() {
1997            Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
1998            Some(JValue::Null) | None => Ok(JValue::from(0i64)),
1999            Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2000            _ => Ok(JValue::from(1i64)),
2001        },
2002
2003        // ── Boolean / logic ─────────────────────────────────────────────
2004        "boolean" => match effective_args.first() {
2005            Some(v) => Ok(functions::boolean::boolean(v)?),
2006            None => Err(EvaluatorError::EvaluationError(
2007                "boolean() requires 1 argument".to_string(),
2008            )),
2009        },
2010        "not" => match effective_args.first() {
2011            Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2012            None => Err(EvaluatorError::EvaluationError(
2013                "not() requires 1 argument".to_string(),
2014            )),
2015        },
2016
2017        // ── Array functions ─────────────────────────────────────────────
2018        "append" => {
2019            if effective_args.len() != 2 {
2020                return Err(EvaluatorError::EvaluationError(
2021                    "append() requires exactly 2 arguments".to_string(),
2022                ));
2023            }
2024            let first = &effective_args[0];
2025            let second = &effective_args[1];
2026            if matches!(second, JValue::Null | JValue::Undefined) {
2027                return Ok(first.clone());
2028            }
2029            if matches!(first, JValue::Null | JValue::Undefined) {
2030                return Ok(second.clone());
2031            }
2032            let arr = match first {
2033                JValue::Array(a) => a.to_vec(),
2034                other => vec![other.clone()],
2035            };
2036            Ok(functions::array::append(&arr, second)?)
2037        }
2038        "reverse" => match effective_args.first() {
2039            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2040            Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2041            _ => Err(EvaluatorError::TypeError(
2042                "reverse() requires an array argument".to_string(),
2043            )),
2044        },
2045        "distinct" => match effective_args.first() {
2046            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2047            Some(JValue::Array(arr)) => Ok(functions::array::distinct(arr)?),
2048            _ => Err(EvaluatorError::TypeError(
2049                "distinct() requires an array argument".to_string(),
2050            )),
2051        },
2052
2053        // ── Object functions ────────────────────────────────────────────
2054        "keys" => match effective_args.first() {
2055            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2056            Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2057            Some(JValue::Object(obj)) => {
2058                if obj.is_empty() {
2059                    Ok(JValue::Null)
2060                } else {
2061                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2062                    if keys.len() == 1 {
2063                        Ok(keys.into_iter().next().unwrap())
2064                    } else {
2065                        Ok(JValue::array(keys))
2066                    }
2067                }
2068            }
2069            Some(JValue::Array(arr)) => {
2070                let mut all_keys: Vec<JValue> = Vec::new();
2071                for item in arr.iter() {
2072                    if let JValue::Object(obj) = item {
2073                        for key in obj.keys() {
2074                            let k = JValue::string(key.clone());
2075                            if !all_keys.contains(&k) {
2076                                all_keys.push(k);
2077                            }
2078                        }
2079                    }
2080                }
2081                if all_keys.is_empty() {
2082                    Ok(JValue::Null)
2083                } else if all_keys.len() == 1 {
2084                    Ok(all_keys.into_iter().next().unwrap())
2085                } else {
2086                    Ok(JValue::array(all_keys))
2087                }
2088            }
2089            _ => Ok(JValue::Null),
2090        },
2091        "merge" => match effective_args.len() {
2092            0 => Err(EvaluatorError::EvaluationError(
2093                "merge() requires at least 1 argument".to_string(),
2094            )),
2095            1 => match &effective_args[0] {
2096                JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2097                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2098                JValue::Object(_) => Ok(effective_args[0].clone()),
2099                _ => Err(EvaluatorError::TypeError(
2100                    "merge() requires objects or an array of objects".to_string(),
2101                )),
2102            },
2103            _ => Ok(functions::object::merge(effective_args)?),
2104        },
2105
2106        _ => unreachable!(
2107            "call_pure_builtin called with non-compilable builtin: {}",
2108            name
2109        ),
2110    }
2111}
2112
2113// ──────────────────────────────────────────────────────────────────────────────
2114// End of CompiledExpr framework
2115// ──────────────────────────────────────────────────────────────────────────────
2116
2117/// Functions that propagate undefined (return undefined when given an undefined argument).
2118/// These functions should return null/undefined when their input path doesn't exist,
2119/// rather than throwing a type error.
2120const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2121    "not",
2122    "boolean",
2123    "length",
2124    "number",
2125    "uppercase",
2126    "lowercase",
2127    "substring",
2128    "substringBefore",
2129    "substringAfter",
2130    "string",
2131];
2132
2133/// Check whether a function propagates undefined values
2134fn propagates_undefined(name: &str) -> bool {
2135    UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2136}
2137
2138/// Iterator-based numeric aggregation helpers.
2139/// These avoid cloning values by iterating over references and extracting f64 values directly.
2140mod aggregation {
2141    use super::*;
2142
2143    /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2144    /// Returns Err if any non-numeric value is encountered.
2145    fn for_each_numeric(
2146        arr: &[JValue],
2147        func_name: &str,
2148        mut f: impl FnMut(f64),
2149    ) -> Result<(), EvaluatorError> {
2150        fn recurse(
2151            arr: &[JValue],
2152            func_name: &str,
2153            f: &mut dyn FnMut(f64),
2154        ) -> Result<(), EvaluatorError> {
2155            for value in arr.iter() {
2156                match value {
2157                    JValue::Array(inner) => recurse(inner, func_name, f)?,
2158                    JValue::Number(n) => {
2159                        f(*n);
2160                    }
2161                    _ => {
2162                        return Err(EvaluatorError::TypeError(format!(
2163                            "{}() requires all array elements to be numbers",
2164                            func_name
2165                        )));
2166                    }
2167                }
2168            }
2169            Ok(())
2170        }
2171        recurse(arr, func_name, &mut f)
2172    }
2173
2174    /// Count elements in a potentially nested array without cloning.
2175    fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2176        let mut count = 0usize;
2177        for_each_numeric(arr, func_name, |_| count += 1)?;
2178        Ok(count)
2179    }
2180
2181    pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2182        if arr.is_empty() {
2183            return Ok(JValue::from(0i64));
2184        }
2185        let mut total = 0.0f64;
2186        for_each_numeric(arr, "sum", |n| total += n)?;
2187        Ok(JValue::Number(total))
2188    }
2189
2190    pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2191        if arr.is_empty() {
2192            return Ok(JValue::Null);
2193        }
2194        let mut max_val = f64::NEG_INFINITY;
2195        for_each_numeric(arr, "max", |n| {
2196            if n > max_val {
2197                max_val = n;
2198            }
2199        })?;
2200        Ok(JValue::Number(max_val))
2201    }
2202
2203    pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2204        if arr.is_empty() {
2205            return Ok(JValue::Null);
2206        }
2207        let mut min_val = f64::INFINITY;
2208        for_each_numeric(arr, "min", |n| {
2209            if n < min_val {
2210                min_val = n;
2211            }
2212        })?;
2213        Ok(JValue::Number(min_val))
2214    }
2215
2216    pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2217        if arr.is_empty() {
2218            return Ok(JValue::Null);
2219        }
2220        let mut total = 0.0f64;
2221        let count = count_numeric(arr, "average")?;
2222        for_each_numeric(arr, "average", |n| total += n)?;
2223        Ok(JValue::Number(total / count as f64))
2224    }
2225}
2226
2227/// Evaluator errors
2228#[derive(Error, Debug)]
2229pub enum EvaluatorError {
2230    #[error("Type error: {0}")]
2231    TypeError(String),
2232
2233    #[error("Reference error: {0}")]
2234    ReferenceError(String),
2235
2236    #[error("Evaluation error: {0}")]
2237    EvaluationError(String),
2238}
2239
2240impl From<crate::functions::FunctionError> for EvaluatorError {
2241    fn from(e: crate::functions::FunctionError) -> Self {
2242        EvaluatorError::EvaluationError(e.to_string())
2243    }
2244}
2245
2246impl From<crate::datetime::DateTimeError> for EvaluatorError {
2247    fn from(e: crate::datetime::DateTimeError) -> Self {
2248        EvaluatorError::EvaluationError(e.to_string())
2249    }
2250}
2251
2252/// Result of evaluating a lambda body that may be a tail call
2253/// Used for trampoline-based tail call optimization
2254enum LambdaResult {
2255    /// Final value - evaluation is complete
2256    JValue(JValue),
2257    /// Tail call - need to continue with another lambda invocation
2258    TailCall {
2259        /// The lambda to call (boxed to reduce enum size)
2260        lambda: Box<StoredLambda>,
2261        /// Arguments for the call
2262        args: Vec<JValue>,
2263        /// Data context for the call
2264        data: JValue,
2265    },
2266}
2267
2268/// Lambda storage
2269/// Stores the AST of a lambda function along with its parameters, optional signature,
2270/// and captured environment for closures
2271#[derive(Clone, Debug)]
2272pub struct StoredLambda {
2273    pub params: Vec<String>,
2274    pub body: AstNode,
2275    /// Pre-compiled body for use in tight inner loops (HOF fast path).
2276    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2277    pub(crate) compiled_body: Option<CompiledExpr>,
2278    pub signature: Option<String>,
2279    /// Captured environment bindings for closures
2280    pub captured_env: HashMap<String, JValue>,
2281    /// Captured data context for lexical scoping of bare field names
2282    pub captured_data: Option<JValue>,
2283    /// Whether this lambda's body contains tail calls that can be optimized
2284    pub thunk: bool,
2285}
2286
2287/// A single scope in the scope stack
2288struct Scope {
2289    bindings: HashMap<String, JValue>,
2290    lambdas: HashMap<String, StoredLambda>,
2291}
2292
2293impl Scope {
2294    fn new() -> Self {
2295        Scope {
2296            bindings: HashMap::new(),
2297            lambdas: HashMap::new(),
2298        }
2299    }
2300}
2301
2302/// Evaluation context
2303///
2304/// Holds variable bindings and other state needed during evaluation.
2305/// Uses a scope stack for efficient push/pop instead of clone/restore.
2306pub struct Context {
2307    scope_stack: Vec<Scope>,
2308    parent_data: Option<JValue>,
2309}
2310
2311impl Context {
2312    pub fn new() -> Self {
2313        Context {
2314            scope_stack: vec![Scope::new()],
2315            parent_data: None,
2316        }
2317    }
2318
2319    /// Push a new scope onto the stack
2320    fn push_scope(&mut self) {
2321        self.scope_stack.push(Scope::new());
2322    }
2323
2324    /// Pop the top scope from the stack
2325    fn pop_scope(&mut self) {
2326        if self.scope_stack.len() > 1 {
2327            self.scope_stack.pop();
2328        }
2329    }
2330
2331    /// Pop scope but preserve specified lambdas by moving them to the current top scope
2332    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2333        if self.scope_stack.len() > 1 {
2334            let popped = self.scope_stack.pop().unwrap();
2335            if !lambda_ids.is_empty() {
2336                let top = self.scope_stack.last_mut().unwrap();
2337                for id in lambda_ids {
2338                    if let Some(stored) = popped.lambdas.get(id) {
2339                        top.lambdas.insert(id.clone(), stored.clone());
2340                    }
2341                }
2342            }
2343        }
2344    }
2345
2346    /// Clear all bindings and lambdas in the top scope without deallocating
2347    fn clear_current_scope(&mut self) {
2348        let top = self.scope_stack.last_mut().unwrap();
2349        top.bindings.clear();
2350        top.lambdas.clear();
2351    }
2352
2353    pub fn bind(&mut self, name: String, value: JValue) {
2354        self.scope_stack
2355            .last_mut()
2356            .unwrap()
2357            .bindings
2358            .insert(name, value);
2359    }
2360
2361    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2362        self.scope_stack
2363            .last_mut()
2364            .unwrap()
2365            .lambdas
2366            .insert(name, lambda);
2367    }
2368
2369    pub fn unbind(&mut self, name: &str) {
2370        // Remove from top scope only
2371        let top = self.scope_stack.last_mut().unwrap();
2372        top.bindings.remove(name);
2373        top.lambdas.remove(name);
2374    }
2375
2376    pub fn lookup(&self, name: &str) -> Option<&JValue> {
2377        // Walk scope stack from top to bottom
2378        for scope in self.scope_stack.iter().rev() {
2379            if let Some(value) = scope.bindings.get(name) {
2380                return Some(value);
2381            }
2382        }
2383        None
2384    }
2385
2386    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2387        // Walk scope stack from top to bottom
2388        for scope in self.scope_stack.iter().rev() {
2389            if let Some(lambda) = scope.lambdas.get(name) {
2390                return Some(lambda);
2391            }
2392        }
2393        None
2394    }
2395
2396    pub fn set_parent(&mut self, data: JValue) {
2397        self.parent_data = Some(data);
2398    }
2399
2400    pub fn get_parent(&self) -> Option<&JValue> {
2401        self.parent_data.as_ref()
2402    }
2403
2404    /// Collect all bindings across all scopes (for environment capture).
2405    /// Higher scopes shadow lower scopes.
2406    fn all_bindings(&self) -> HashMap<String, JValue> {
2407        let mut result = HashMap::new();
2408        for scope in &self.scope_stack {
2409            for (k, v) in &scope.bindings {
2410                result.insert(k.clone(), v.clone());
2411            }
2412        }
2413        result
2414    }
2415}
2416
2417impl Default for Context {
2418    fn default() -> Self {
2419        Self::new()
2420    }
2421}
2422
2423/// Evaluator for JSONata expressions
2424pub struct Evaluator {
2425    context: Context,
2426    recursion_depth: usize,
2427    max_recursion_depth: usize,
2428}
2429
2430impl Evaluator {
2431    pub fn new() -> Self {
2432        Evaluator {
2433            context: Context::new(),
2434            recursion_depth: 0,
2435            // Limit recursion depth to prevent stack overflow
2436            // True TCO would allow deeper recursion but requires parser-level thunk marking
2437            max_recursion_depth: 302,
2438        }
2439    }
2440
2441    pub fn with_context(context: Context) -> Self {
2442        Evaluator {
2443            context,
2444            recursion_depth: 0,
2445            max_recursion_depth: 302,
2446        }
2447    }
2448
2449    /// Invoke a stored lambda with its captured environment and data.
2450    /// This is the standard way to call a StoredLambda, handling the
2451    /// captured_env and captured_data extraction boilerplate.
2452    fn invoke_stored_lambda(
2453        &mut self,
2454        stored: &StoredLambda,
2455        args: &[JValue],
2456        data: &JValue,
2457    ) -> Result<JValue, EvaluatorError> {
2458        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2459        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2460        // and no captured lambda/builtin values (those require Context for runtime lookup).
2461        if let Some(ref ce) = stored.compiled_body {
2462            if stored.signature.is_none()
2463                && !stored.thunk
2464                && !stored
2465                    .captured_env
2466                    .values()
2467                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2468            {
2469                let call_data = stored.captured_data.as_ref().unwrap_or(data);
2470                let vars: HashMap<&str, &JValue> = stored
2471                    .params
2472                    .iter()
2473                    .zip(args.iter())
2474                    .map(|(p, v)| (p.as_str(), v))
2475                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2476                    .collect();
2477                return eval_compiled(ce, call_data, Some(&vars));
2478            }
2479        }
2480
2481        let captured_env = if stored.captured_env.is_empty() {
2482            None
2483        } else {
2484            Some(&stored.captured_env)
2485        };
2486        let captured_data = stored.captured_data.as_ref();
2487        self.invoke_lambda_with_env(
2488            &stored.params,
2489            &stored.body,
2490            stored.signature.as_ref(),
2491            args,
2492            data,
2493            captured_env,
2494            captured_data,
2495            stored.thunk,
2496        )
2497    }
2498
2499    /// Look up a StoredLambda from a JValue that may be a lambda marker.
2500    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
2501    /// with a valid lambda_id that references a stored lambda.
2502    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
2503        if let JValue::Lambda { lambda_id, .. } = value {
2504            return self.context.lookup_lambda(lambda_id).cloned();
2505        }
2506        None
2507    }
2508
2509    /// Get the number of parameters a callback function expects by inspecting its AST.
2510    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
2511    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
2512    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
2513        match func_node {
2514            AstNode::Lambda { params, .. } => params.len(),
2515            AstNode::Variable(var_name) => {
2516                // Check if this variable holds a stored lambda
2517                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
2518                    return stored_lambda.params.len();
2519                }
2520                // Also check if it's a lambda value in bindings (e.g., from partial application)
2521                if let Some(value) = self.context.lookup(var_name) {
2522                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
2523                        return stored_lambda.params.len();
2524                    }
2525                }
2526                // Unknown, return max to be safe
2527                usize::MAX
2528            }
2529            AstNode::Function { .. } => {
2530                // For function references, we can't easily determine param count
2531                // Return max to be safe
2532                usize::MAX
2533            }
2534            _ => usize::MAX,
2535        }
2536    }
2537
2538    /// Specialized sort using pre-extracted keys (Schwartzian transform).
2539    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
2540    /// avoiding O(N log N) hash lookups during comparisons.
2541    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
2542        if arr.len() <= 1 {
2543            return;
2544        }
2545
2546        // Phase 1: Extract sort keys -- one IndexMap lookup per element
2547        let keys: Vec<SortKey> = arr
2548            .iter()
2549            .map(|item| match item {
2550                JValue::Object(obj) => match obj.get(&spec.field) {
2551                    Some(JValue::Number(n)) => SortKey::Num(*n),
2552                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
2553                    _ => SortKey::None,
2554                },
2555                _ => SortKey::None,
2556            })
2557            .collect();
2558
2559        // Phase 2: Build index permutation sorted by pre-extracted keys
2560        let mut perm: Vec<usize> = (0..arr.len()).collect();
2561        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
2562
2563        // Phase 3: Apply permutation in-place via cycle-following
2564        let mut placed = vec![false; arr.len()];
2565        for i in 0..arr.len() {
2566            if placed[i] || perm[i] == i {
2567                continue;
2568            }
2569            let mut j = i;
2570            loop {
2571                let target = perm[j];
2572                placed[j] = true;
2573                if target == i {
2574                    break;
2575                }
2576                arr.swap(j, target);
2577                j = target;
2578            }
2579        }
2580    }
2581
2582    /// Merge sort implementation using a comparator function.
2583    /// This replaces the O(n²) bubble sort for better performance on large arrays.
2584    /// The comparator returns true if the first element should come AFTER the second.
2585    fn merge_sort_with_comparator(
2586        &mut self,
2587        arr: &mut [JValue],
2588        comparator: &AstNode,
2589        data: &JValue,
2590    ) -> Result<(), EvaluatorError> {
2591        if arr.len() <= 1 {
2592            return Ok(());
2593        }
2594
2595        // Try specialized fast path for simple field comparisons like
2596        // function($l, $r) { $l.price > $r.price }
2597        if let AstNode::Lambda { params, body, .. } = comparator {
2598            if params.len() >= 2 {
2599                if let Some(spec) = try_specialize_sort_comparator(body, &params[0], &params[1]) {
2600                    Self::merge_sort_specialized(arr, &spec);
2601                    return Ok(());
2602                }
2603            }
2604        }
2605
2606        let mid = arr.len() / 2;
2607
2608        // Sort left half
2609        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
2610
2611        // Sort right half
2612        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
2613
2614        // Merge the sorted halves
2615        let mut temp = Vec::with_capacity(arr.len());
2616        let (left, right) = arr.split_at(mid);
2617
2618        let mut i = 0;
2619        let mut j = 0;
2620
2621        // For lambda comparators, use a reusable scope to avoid
2622        // push_scope/pop_scope per comparison (~n log n total comparisons)
2623        if let AstNode::Lambda { params, body, .. } = comparator {
2624            if params.len() >= 2 {
2625                // Pre-clone param names once outside the loop
2626                let param0 = params[0].clone();
2627                let param1 = params[1].clone();
2628                self.context.push_scope();
2629                while i < left.len() && j < right.len() {
2630                    // Reuse scope: clear and rebind instead of push/pop
2631                    self.context.clear_current_scope();
2632                    self.context.bind(param0.clone(), left[i].clone());
2633                    self.context.bind(param1.clone(), right[j].clone());
2634
2635                    let cmp_result = self.evaluate_internal(body, data)?;
2636
2637                    if self.is_truthy(&cmp_result) {
2638                        temp.push(right[j].clone());
2639                        j += 1;
2640                    } else {
2641                        temp.push(left[i].clone());
2642                        i += 1;
2643                    }
2644                }
2645                self.context.pop_scope();
2646            } else {
2647                // Unexpected param count - fall back to generic path
2648                while i < left.len() && j < right.len() {
2649                    let cmp_result = self.apply_function(
2650                        comparator,
2651                        &[left[i].clone(), right[j].clone()],
2652                        data,
2653                    )?;
2654                    if self.is_truthy(&cmp_result) {
2655                        temp.push(right[j].clone());
2656                        j += 1;
2657                    } else {
2658                        temp.push(left[i].clone());
2659                        i += 1;
2660                    }
2661                }
2662            }
2663        } else {
2664            // Non-lambda comparator: use generic apply_function path
2665            while i < left.len() && j < right.len() {
2666                let cmp_result =
2667                    self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
2668                if self.is_truthy(&cmp_result) {
2669                    temp.push(right[j].clone());
2670                    j += 1;
2671                } else {
2672                    temp.push(left[i].clone());
2673                    i += 1;
2674                }
2675            }
2676        }
2677
2678        // Copy remaining elements
2679        temp.extend_from_slice(&left[i..]);
2680        temp.extend_from_slice(&right[j..]);
2681
2682        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
2683        for (i, val) in temp.into_iter().enumerate() {
2684            arr[i] = val;
2685        }
2686
2687        Ok(())
2688    }
2689
2690    /// Evaluate an AST node against data
2691    ///
2692    /// This is the main entry point for evaluation. It sets up the parent context
2693    /// to be the root data if not already set.
2694    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
2695        // Set parent context to root data if not already set
2696        if self.context.get_parent().is_none() {
2697            self.context.set_parent(data.clone());
2698        }
2699
2700        self.evaluate_internal(node, data)
2701    }
2702
2703    /// Fast evaluation for leaf nodes that don't need recursion tracking.
2704    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
2705    /// Returns None for anything requiring the full evaluator.
2706    #[inline(always)]
2707    fn evaluate_leaf(
2708        &mut self,
2709        node: &AstNode,
2710        data: &JValue,
2711    ) -> Option<Result<JValue, EvaluatorError>> {
2712        match node {
2713            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
2714            AstNode::Number(n) => {
2715                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2716                    Some(Ok(JValue::from(*n as i64)))
2717                } else {
2718                    Some(Ok(JValue::Number(*n)))
2719                }
2720            }
2721            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
2722            AstNode::Null => Some(Ok(JValue::Null)),
2723            AstNode::Undefined => Some(Ok(JValue::Undefined)),
2724            AstNode::Name(field_name) => match data {
2725                // Array mapping and other cases need full evaluator
2726                JValue::Object(obj) => {
2727                    Some(Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null)))
2728                }
2729                _ => None,
2730            },
2731            AstNode::Variable(name) if !name.is_empty() => {
2732                // Simple variable lookup — only fast-path when no tuple data
2733                if let JValue::Object(obj) = data {
2734                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2735                        return None; // Tuple data needs full evaluator
2736                    }
2737                }
2738                // May be a lambda/builtin — needs full evaluator if None
2739                self.context.lookup(name).map(|value| Ok(value.clone()))
2740            }
2741            _ => None,
2742        }
2743    }
2744
2745    /// Internal evaluation method
2746    fn evaluate_internal(
2747        &mut self,
2748        node: &AstNode,
2749        data: &JValue,
2750    ) -> Result<JValue, EvaluatorError> {
2751        // Fast path for leaf nodes — skip recursion tracking overhead
2752        if let Some(result) = self.evaluate_leaf(node, data) {
2753            return result;
2754        }
2755
2756        // Check recursion depth to prevent stack overflow
2757        self.recursion_depth += 1;
2758        if self.recursion_depth > self.max_recursion_depth {
2759            self.recursion_depth -= 1;
2760            return Err(EvaluatorError::EvaluationError(format!(
2761                "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
2762                self.max_recursion_depth
2763            )));
2764        }
2765
2766        let result = self.evaluate_internal_impl(node, data);
2767
2768        self.recursion_depth -= 1;
2769        result
2770    }
2771
2772    /// Internal evaluation implementation (separated to allow depth tracking)
2773    fn evaluate_internal_impl(
2774        &mut self,
2775        node: &AstNode,
2776        data: &JValue,
2777    ) -> Result<JValue, EvaluatorError> {
2778        match node {
2779            AstNode::String(s) => Ok(JValue::string(s.clone())),
2780
2781            // Name nodes represent field access on the current data
2782            AstNode::Name(field_name) => {
2783                match data {
2784                    JValue::Object(obj) => Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null)),
2785                    JValue::Array(arr) => {
2786                        // Map over array
2787                        let mut result = Vec::new();
2788                        for item in arr.iter() {
2789                            if let JValue::Object(obj) = item {
2790                                if let Some(val) = obj.get(field_name) {
2791                                    result.push(val.clone());
2792                                }
2793                            }
2794                        }
2795                        if result.is_empty() {
2796                            Ok(JValue::Null)
2797                        } else if result.len() == 1 {
2798                            Ok(result.into_iter().next().unwrap())
2799                        } else {
2800                            Ok(JValue::array(result))
2801                        }
2802                    }
2803                    _ => Ok(JValue::Null),
2804                }
2805            }
2806
2807            AstNode::Number(n) => {
2808                // Preserve integer-ness: if the number is a whole number, create an integer JValue
2809                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2810                    // It's a whole number that can be represented as i64
2811                    Ok(JValue::from(*n as i64))
2812                } else {
2813                    Ok(JValue::Number(*n))
2814                }
2815            }
2816            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
2817            AstNode::Null => Ok(JValue::Null),
2818            AstNode::Undefined => Ok(JValue::Undefined),
2819            AstNode::Placeholder => {
2820                // Placeholders should only appear as function arguments
2821                // If we reach here, it's an error
2822                Err(EvaluatorError::EvaluationError(
2823                    "Placeholder '?' can only be used as a function argument".to_string(),
2824                ))
2825            }
2826            AstNode::Regex { pattern, flags } => {
2827                // Return a regex object as a special JSON value
2828                // This will be recognized by functions like $split, $match, $replace
2829                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
2830            }
2831
2832            AstNode::Variable(name) => {
2833                // Special case: $ alone (empty name) refers to current context
2834                // First check if $ is bound in the context (for closures that captured $)
2835                // Otherwise, use the data parameter
2836                if name.is_empty() {
2837                    if let Some(value) = self.context.lookup("$") {
2838                        return Ok(value.clone());
2839                    }
2840                    // If data is a tuple, return the @ value
2841                    if let JValue::Object(obj) = data {
2842                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2843                            if let Some(inner) = obj.get("@") {
2844                                return Ok(inner.clone());
2845                            }
2846                        }
2847                    }
2848                    return Ok(data.clone());
2849                }
2850
2851                // Check variable bindings FIRST
2852                // This allows function parameters to shadow outer lambdas with the same name
2853                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
2854                if let Some(value) = self.context.lookup(name) {
2855                    return Ok(value.clone());
2856                }
2857
2858                // Check tuple bindings in data (for index binding operator #$var)
2859                // When iterating over a tuple stream, $var can reference the bound index
2860                if let JValue::Object(obj) = data {
2861                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2862                        // Check for the variable in tuple bindings (stored as "$name")
2863                        let binding_key = format!("${}", name);
2864                        if let Some(binding_value) = obj.get(&binding_key) {
2865                            return Ok(binding_value.clone());
2866                        }
2867                    }
2868                }
2869
2870                // Then check if this is a stored lambda (user-defined functions)
2871                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
2872                    // Return a lambda representation that can be passed to higher-order functions
2873                    // Include _lambda_id pointing to the stored lambda so it can be found
2874                    // when captured in closures
2875                    let lambda_repr = JValue::lambda(
2876                        name.as_str(),
2877                        stored_lambda.params.clone(),
2878                        Some(name.to_string()),
2879                        stored_lambda.signature.clone(),
2880                    );
2881                    return Ok(lambda_repr);
2882                }
2883
2884                // Check if this is a built-in function reference (only if not shadowed)
2885                if self.is_builtin_function(name) {
2886                    // Return a marker for built-in functions
2887                    // This allows built-in functions to be passed to higher-order functions
2888                    let builtin_repr = JValue::builtin(name.as_str());
2889                    return Ok(builtin_repr);
2890                }
2891
2892                // Undefined variable - return null (undefined in JSONata semantics)
2893                // This allows expressions like `$not(undefined_var)` to return undefined
2894                // and comparisons like `3 > $undefined` to return undefined
2895                Ok(JValue::Null)
2896            }
2897
2898            AstNode::ParentVariable(name) => {
2899                // Special case: $$ alone (empty name) refers to parent/root context
2900                if name.is_empty() {
2901                    return self.context.get_parent().cloned().ok_or_else(|| {
2902                        EvaluatorError::ReferenceError("Parent context not available".to_string())
2903                    });
2904                }
2905
2906                // For $$name, we need to evaluate name against parent context
2907                // This is similar to $.name but using parent data
2908                let parent_data = self.context.get_parent().ok_or_else(|| {
2909                    EvaluatorError::ReferenceError("Parent context not available".to_string())
2910                })?;
2911
2912                // Access field on parent context
2913                match parent_data {
2914                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
2915                    _ => Ok(JValue::Null),
2916                }
2917            }
2918
2919            AstNode::Path { steps } => self.evaluate_path(steps, data),
2920
2921            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
2922
2923            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
2924
2925            // Array constructor - JSONata semantics:
2926            AstNode::Array(elements) => {
2927                // - If element is itself an array constructor [...], keep it nested
2928                // - Otherwise, if element evaluates to an array, flatten it
2929                // - Undefined values are excluded
2930                let mut result = Vec::with_capacity(elements.len());
2931                for element in elements {
2932                    // Check if this element is itself an explicit array constructor
2933                    let is_array_constructor = matches!(element, AstNode::Array(_));
2934
2935                    let value = self.evaluate_internal(element, data)?;
2936
2937                    // Skip undefined values in array constructors
2938                    // Note: explicit null is preserved, only undefined (no value) is filtered
2939                    if value.is_undefined() {
2940                        continue;
2941                    }
2942
2943                    if is_array_constructor {
2944                        // Explicit array constructor - keep nested
2945                        result.push(value);
2946                    } else if let JValue::Array(arr) = value {
2947                        // Non-array-constructor that evaluated to array - flatten it
2948                        result.extend(arr.iter().cloned());
2949                    } else {
2950                        // Non-array value - add as-is
2951                        result.push(value);
2952                    }
2953                }
2954                Ok(JValue::array(result))
2955            }
2956
2957            AstNode::Object(pairs) => {
2958                let mut result = IndexMap::with_capacity(pairs.len());
2959
2960                // Check if all keys are string literals — can skip D1009 HashMap
2961                let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
2962
2963                if all_literal_keys {
2964                    // Fast path: literal keys, no need for D1009 tracking
2965                    for (key_node, value_node) in pairs.iter() {
2966                        let key = match key_node {
2967                            AstNode::String(s) => s,
2968                            _ => unreachable!(),
2969                        };
2970                        let value = self.evaluate_internal(value_node, data)?;
2971                        if value.is_undefined() {
2972                            continue;
2973                        }
2974                        result.insert(key.clone(), value);
2975                    }
2976                } else {
2977                    let mut key_sources: HashMap<String, usize> = HashMap::new();
2978                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
2979                        let key = match self.evaluate_internal(key_node, data)? {
2980                            JValue::String(s) => s,
2981                            JValue::Null => continue,
2982                            other => {
2983                                if other.is_undefined() {
2984                                    continue;
2985                                }
2986                                return Err(EvaluatorError::TypeError(format!(
2987                                    "Object key must be a string, got: {:?}",
2988                                    other
2989                                )));
2990                            }
2991                        };
2992
2993                        if let Some(&existing_idx) = key_sources.get(&*key) {
2994                            if existing_idx != pair_index {
2995                                return Err(EvaluatorError::EvaluationError(format!(
2996                                    "D1009: Multiple key expressions evaluate to same key: {}",
2997                                    key
2998                                )));
2999                            }
3000                        }
3001                        key_sources.insert(key.to_string(), pair_index);
3002
3003                        let value = self.evaluate_internal(value_node, data)?;
3004                        if value.is_undefined() {
3005                            continue;
3006                        }
3007                        result.insert(key.to_string(), value);
3008                    }
3009                }
3010                Ok(JValue::object(result))
3011            }
3012
3013            // Object transform: group items by key, then evaluate value once per group
3014            AstNode::ObjectTransform { input, pattern } => {
3015                // Evaluate the input expression
3016                let input_value = self.evaluate_internal(input, data)?;
3017
3018                // If input is undefined, return undefined (not empty object)
3019                if input_value.is_undefined() {
3020                    return Ok(JValue::Undefined);
3021                }
3022
3023                // Handle array input - process each item
3024                let items: Vec<JValue> = match input_value {
3025                    JValue::Array(ref arr) => (**arr).clone(),
3026                    JValue::Null => return Ok(JValue::Null),
3027                    other => vec![other],
3028                };
3029
3030                // If array is empty, add undefined to enable literal JSON object generation
3031                let items = if items.is_empty() {
3032                    vec![JValue::Undefined]
3033                } else {
3034                    items
3035                };
3036
3037                // Phase 1: Group items by key expression
3038                // groups maps key -> (grouped_data, expr_index)
3039                // When multiple items have same key, their data is appended together
3040                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3041
3042                // Save the current $ binding to restore later
3043                let saved_dollar = self.context.lookup("$").cloned();
3044
3045                for item in &items {
3046                    // Bind $ to the current item for key evaluation
3047                    self.context.bind("$".to_string(), item.clone());
3048
3049                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3050                        // Evaluate key with current item as context
3051                        let key = match self.evaluate_internal(key_node, item)? {
3052                            JValue::String(s) => s,
3053                            JValue::Null => continue, // Skip null keys
3054                            other => {
3055                                // Skip undefined keys
3056                                if other.is_undefined() {
3057                                    continue;
3058                                }
3059                                return Err(EvaluatorError::TypeError(format!(
3060                                    "T1003: Object key must be a string, got: {:?}",
3061                                    other
3062                                )));
3063                            }
3064                        };
3065
3066                        // Group items by key
3067                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3068                            // Key already exists - check if from same expression index
3069                            if *existing_idx != pair_index {
3070                                // D1009: multiple key expressions evaluate to same key
3071                                return Err(EvaluatorError::EvaluationError(format!(
3072                                    "D1009: Multiple key expressions evaluate to same key: {}",
3073                                    key
3074                                )));
3075                            }
3076                            // Append item to the group
3077                            existing_data.push(item.clone());
3078                        } else {
3079                            // New key - create new group
3080                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3081                        }
3082                    }
3083                }
3084
3085                // Phase 2: Evaluate value expression for each group
3086                let mut result = IndexMap::new();
3087
3088                for (key, (grouped_data, expr_index)) in groups {
3089                    // Get the value expression for this group
3090                    let (_key_node, value_node) = &pattern[expr_index];
3091
3092                    // Determine the context for value evaluation:
3093                    // - If single item, use that item directly
3094                    // - If multiple items, use the array of items
3095                    let context = if grouped_data.len() == 1 {
3096                        grouped_data.into_iter().next().unwrap()
3097                    } else {
3098                        JValue::array(grouped_data)
3099                    };
3100
3101                    // Bind $ to the context for value evaluation
3102                    self.context.bind("$".to_string(), context.clone());
3103
3104                    // Evaluate value expression with grouped context
3105                    let value = self.evaluate_internal(value_node, &context)?;
3106
3107                    // Skip undefined values
3108                    if !value.is_undefined() {
3109                        result.insert(key, value);
3110                    }
3111                }
3112
3113                // Restore the previous $ binding
3114                if let Some(saved) = saved_dollar {
3115                    self.context.bind("$".to_string(), saved);
3116                } else {
3117                    self.context.unbind("$");
3118                }
3119
3120                Ok(JValue::object(result))
3121            }
3122
3123            AstNode::Function {
3124                name,
3125                args,
3126                is_builtin,
3127            } => self.evaluate_function_call(name, args, *is_builtin, data),
3128
3129            // Call: invoke an arbitrary expression as a function
3130            // Used for IIFE patterns like (function($x){...})(5) or chained calls
3131            AstNode::Call { procedure, args } => {
3132                // Evaluate the procedure to get the callable value
3133                let callable = self.evaluate_internal(procedure, data)?;
3134
3135                // Check if it's a lambda value
3136                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3137                    let mut evaluated_args = Vec::with_capacity(args.len());
3138                    for arg in args.iter() {
3139                        evaluated_args.push(self.evaluate_internal(arg, data)?);
3140                    }
3141                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3142                }
3143
3144                // Not a callable value
3145                Err(EvaluatorError::TypeError(format!(
3146                    "Cannot call non-function value: {:?}",
3147                    callable
3148                )))
3149            }
3150
3151            AstNode::Conditional {
3152                condition,
3153                then_branch,
3154                else_branch,
3155            } => {
3156                let condition_value = self.evaluate_internal(condition, data)?;
3157                if self.is_truthy(&condition_value) {
3158                    self.evaluate_internal(then_branch, data)
3159                } else if let Some(else_branch) = else_branch {
3160                    self.evaluate_internal(else_branch, data)
3161                } else {
3162                    // No else branch - return undefined (not null)
3163                    // This allows $map to filter out results from conditionals without else
3164                    Ok(JValue::Undefined)
3165                }
3166            }
3167
3168            AstNode::Block(expressions) => {
3169                // Blocks create a new scope - push scope instead of clone/restore
3170                self.context.push_scope();
3171
3172                let mut result = JValue::Null;
3173                for expr in expressions {
3174                    result = self.evaluate_internal(expr, data)?;
3175                }
3176
3177                // Before popping, preserve any lambdas referenced by the result
3178                // This is essential for closures returned from blocks (IIFE pattern)
3179                let lambdas_to_keep = self.extract_lambda_ids(&result);
3180                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3181
3182                Ok(result)
3183            }
3184
3185            // Lambda: capture current environment for closure support
3186            AstNode::Lambda {
3187                params,
3188                body,
3189                signature,
3190                thunk,
3191            } => {
3192                let lambda_id = format!("__lambda_{}_{:p}", params.len(), body.as_ref());
3193
3194                let compiled_body = if !thunk {
3195                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3196                    try_compile_expr_with_allowed_vars(body, &var_refs)
3197                } else {
3198                    None
3199                };
3200                let stored_lambda = StoredLambda {
3201                    params: params.clone(),
3202                    body: (**body).clone(),
3203                    compiled_body,
3204                    signature: signature.clone(),
3205                    captured_env: self.capture_environment_for(body, params),
3206                    captured_data: Some(data.clone()),
3207                    thunk: *thunk,
3208                };
3209                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3210
3211                let lambda_obj = JValue::lambda(
3212                    lambda_id.as_str(),
3213                    params.clone(),
3214                    None::<String>,
3215                    signature.clone(),
3216                );
3217
3218                Ok(lambda_obj)
3219            }
3220
3221            // Wildcard: collect all values from current object
3222            AstNode::Wildcard => {
3223                match data {
3224                    JValue::Object(obj) => {
3225                        let mut result = Vec::new();
3226                        for value in obj.values() {
3227                            // Flatten arrays into the result
3228                            match value {
3229                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3230                                _ => result.push(value.clone()),
3231                            }
3232                        }
3233                        Ok(JValue::array(result))
3234                    }
3235                    JValue::Array(arr) => {
3236                        // For arrays, wildcard returns all elements
3237                        Ok(JValue::Array(arr.clone()))
3238                    }
3239                    _ => Ok(JValue::Null),
3240                }
3241            }
3242
3243            // Descendant: recursively traverse all nested values
3244            AstNode::Descendant => {
3245                let descendants = self.collect_descendants(data);
3246                if descendants.is_empty() {
3247                    Ok(JValue::Null) // No descendants means undefined
3248                } else {
3249                    Ok(JValue::array(descendants))
3250                }
3251            }
3252
3253            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3254                "Predicate can only be used in path expressions".to_string(),
3255            )),
3256
3257            // Array grouping: same as Array but prevents flattening in path contexts
3258            AstNode::ArrayGroup(elements) => {
3259                let mut result = Vec::new();
3260                for element in elements {
3261                    let value = self.evaluate_internal(element, data)?;
3262                    result.push(value);
3263                }
3264                Ok(JValue::array(result))
3265            }
3266
3267            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3268                "Function application can only be used in path expressions".to_string(),
3269            )),
3270
3271            AstNode::Sort { input, terms } => {
3272                let value = self.evaluate_internal(input, data)?;
3273                self.evaluate_sort(&value, terms)
3274            }
3275
3276            // Index binding: evaluates input and creates tuple stream with index variable
3277            AstNode::IndexBind { input, variable } => {
3278                let value = self.evaluate_internal(input, data)?;
3279
3280                // Store the variable name and create indexed results
3281                // This is a simplified implementation - full tuple stream would require more work
3282                match value {
3283                    JValue::Array(arr) => {
3284                        // Store the index binding metadata in a special wrapper
3285                        let mut result = Vec::new();
3286                        for (idx, item) in arr.iter().enumerate() {
3287                            // Create wrapper object with value and index
3288                            let mut wrapper = IndexMap::new();
3289                            wrapper.insert("@".to_string(), item.clone());
3290                            wrapper.insert(format!("${}", variable), JValue::Number(idx as f64));
3291                            wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
3292                            result.push(JValue::object(wrapper));
3293                        }
3294                        Ok(JValue::array(result))
3295                    }
3296                    // Single value: just return as-is with index 0
3297                    other => {
3298                        let mut wrapper = IndexMap::new();
3299                        wrapper.insert("@".to_string(), other);
3300                        wrapper.insert(format!("${}", variable), JValue::from(0i64));
3301                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
3302                        Ok(JValue::object(wrapper))
3303                    }
3304                }
3305            }
3306
3307            // Transform: |location|update[,delete]|
3308            AstNode::Transform {
3309                location,
3310                update,
3311                delete,
3312            } => {
3313                // Check if $ is bound (meaning we're being invoked as a lambda)
3314                if self.context.lookup("$").is_some() {
3315                    // Execute the transformation
3316                    self.execute_transform(location, update, delete.as_deref(), data)
3317                } else {
3318                    // Return a lambda representation
3319                    // The transform will be executed when the lambda is invoked
3320                    let transform_lambda = StoredLambda {
3321                        params: vec!["$".to_string()],
3322                        body: AstNode::Transform {
3323                            location: location.clone(),
3324                            update: update.clone(),
3325                            delete: delete.clone(),
3326                        },
3327                        compiled_body: None, // Transform is not a pure compilable expr
3328                        signature: None,
3329                        captured_env: HashMap::new(),
3330                        captured_data: None, // Transform takes $ as parameter
3331                        thunk: false,
3332                    };
3333
3334                    // Store with a generated unique name
3335                    let lambda_name = format!("__transform_{:p}", location);
3336                    self.context.bind_lambda(lambda_name, transform_lambda);
3337
3338                    // Return lambda marker
3339                    Ok(JValue::string("<lambda>"))
3340                }
3341            }
3342        }
3343    }
3344
3345    /// Apply stages (filters/predicates) to a value during field extraction
3346    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
3347    /// This matches the JavaScript reference where stages apply to sequences
3348    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
3349        // Wrap non-arrays in an array for filtering (JSONata semantics)
3350        let mut result = match value {
3351            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
3352            JValue::Array(_) => value,
3353            other => JValue::array(vec![other]),
3354        };
3355
3356        for stage in stages {
3357            match stage {
3358                Stage::Filter(predicate_expr) => {
3359                    // When applying stages, use stage-specific predicate logic
3360                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
3361                }
3362            }
3363        }
3364        Ok(result)
3365    }
3366
3367    /// Check if an AST node is definitely a filter expression (comparison/logical)
3368    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
3369    fn is_filter_predicate(predicate: &AstNode) -> bool {
3370        match predicate {
3371            AstNode::Binary { op, .. } => matches!(
3372                op,
3373                BinaryOp::GreaterThan
3374                    | BinaryOp::GreaterThanOrEqual
3375                    | BinaryOp::LessThan
3376                    | BinaryOp::LessThanOrEqual
3377                    | BinaryOp::Equal
3378                    | BinaryOp::NotEqual
3379                    | BinaryOp::And
3380                    | BinaryOp::Or
3381                    | BinaryOp::In
3382            ),
3383            AstNode::Unary {
3384                op: crate::ast::UnaryOp::Not,
3385                ..
3386            } => true,
3387            _ => false,
3388        }
3389    }
3390
3391    /// Evaluate a predicate as a stage during field extraction
3392    /// This has different semantics than standalone predicates:
3393    /// - Maps index operations over arrays of extracted values
3394    fn evaluate_predicate_as_stage(
3395        &mut self,
3396        current: &JValue,
3397        predicate: &AstNode,
3398    ) -> Result<JValue, EvaluatorError> {
3399        // Special case: empty brackets [] (represented as Boolean(true))
3400        if matches!(predicate, AstNode::Boolean(true)) {
3401            return match current {
3402                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
3403                JValue::Null => Ok(JValue::Null),
3404                other => Ok(JValue::array(vec![other.clone()])),
3405            };
3406        }
3407
3408        match current {
3409            JValue::Array(arr) => {
3410                // For stages: if we have an array of values (from field extraction),
3411                // apply the predicate to each value if appropriate
3412
3413                // Check if predicate is a numeric index
3414                if let AstNode::Number(n) = predicate {
3415                    // Check if this is an array of arrays (extracted array fields)
3416                    let is_array_of_arrays =
3417                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
3418
3419                    if !is_array_of_arrays {
3420                        // Simple values: just index normally
3421                        return self.array_index(current, &JValue::Number(*n));
3422                    }
3423
3424                    // Array of arrays: map index access over each extracted array
3425                    let mut result = Vec::new();
3426                    for item in arr.iter() {
3427                        match item {
3428                            JValue::Array(_) => {
3429                                let indexed = self.array_index(item, &JValue::Number(*n))?;
3430                                if !indexed.is_null() {
3431                                    result.push(indexed);
3432                                }
3433                            }
3434                            _ => {
3435                                if *n == 0.0 {
3436                                    result.push(item.clone());
3437                                }
3438                            }
3439                        }
3440                    }
3441                    return Ok(JValue::array(result));
3442                }
3443
3444                // Short-circuit: if predicate is definitely a comparison/logical expression,
3445                // skip speculative numeric evaluation and go directly to filter logic
3446                if Self::is_filter_predicate(predicate) {
3447                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
3448                    if let Some(compiled) = try_compile_expr(predicate) {
3449                        let shape = arr.first().and_then(build_shape_cache);
3450                        let mut filtered = Vec::with_capacity(arr.len());
3451                        for item in arr.iter() {
3452                            let result = if let Some(ref s) = shape {
3453                                eval_compiled_shaped(&compiled, item, None, s)?
3454                            } else {
3455                                eval_compiled(&compiled, item, None)?
3456                            };
3457                            if compiled_is_truthy(&result) {
3458                                filtered.push(item.clone());
3459                            }
3460                        }
3461                        return Ok(JValue::array(filtered));
3462                    }
3463                    // Fallback: full AST evaluation
3464                    let mut filtered = Vec::new();
3465                    for item in arr.iter() {
3466                        let item_result = self.evaluate_internal(predicate, item)?;
3467                        if self.is_truthy(&item_result) {
3468                            filtered.push(item.clone());
3469                        }
3470                    }
3471                    return Ok(JValue::array(filtered));
3472                }
3473
3474                // Try to evaluate the predicate to see if it's a numeric index or array of indices
3475                // If evaluation succeeds and yields a number, use it as an index
3476                // If it yields an array of numbers, use them as multiple indices
3477                // If evaluation fails (e.g., comparison error), treat as filter
3478                match self.evaluate_internal(predicate, current) {
3479                    Ok(JValue::Number(n)) => {
3480                        let n_val = n;
3481                        let is_array_of_arrays =
3482                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
3483
3484                        if !is_array_of_arrays {
3485                            let pred_result = JValue::Number(n_val);
3486                            return self.array_index(current, &pred_result);
3487                        }
3488
3489                        // Array of arrays: map index access
3490                        let mut result = Vec::new();
3491                        let pred_result = JValue::Number(n_val);
3492                        for item in arr.iter() {
3493                            match item {
3494                                JValue::Array(_) => {
3495                                    let indexed = self.array_index(item, &pred_result)?;
3496                                    if !indexed.is_null() {
3497                                        result.push(indexed);
3498                                    }
3499                                }
3500                                _ => {
3501                                    if n_val == 0.0 {
3502                                        result.push(item.clone());
3503                                    }
3504                                }
3505                            }
3506                        }
3507                        return Ok(JValue::array(result));
3508                    }
3509                    Ok(JValue::Array(indices)) => {
3510                        // Array of values - could be indices or filter results
3511                        // Check if all values are numeric
3512                        let has_non_numeric =
3513                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
3514
3515                        if has_non_numeric {
3516                            // Non-numeric values - treat as filter, fall through
3517                        } else {
3518                            // All numeric - use as indices
3519                            let arr_len = arr.len() as i64;
3520                            let mut resolved_indices: Vec<i64> = indices
3521                                .iter()
3522                                .filter_map(|v| {
3523                                    if let JValue::Number(n) = v {
3524                                        let idx = *n as i64;
3525                                        // Resolve negative indices
3526                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
3527                                        // Only include valid indices
3528                                        if actual_idx >= 0 && actual_idx < arr_len {
3529                                            Some(actual_idx)
3530                                        } else {
3531                                            None
3532                                        }
3533                                    } else {
3534                                        None
3535                                    }
3536                                })
3537                                .collect();
3538
3539                            // Sort and deduplicate indices
3540                            resolved_indices.sort();
3541                            resolved_indices.dedup();
3542
3543                            // Select elements at each sorted index
3544                            let result: Vec<JValue> = resolved_indices
3545                                .iter()
3546                                .map(|&idx| arr[idx as usize].clone())
3547                                .collect();
3548
3549                            return Ok(JValue::array(result));
3550                        }
3551                    }
3552                    Ok(_) => {
3553                        // Evaluated successfully but not a number or array - might be a filter
3554                        // Fall through to filter logic
3555                    }
3556                    Err(_) => {
3557                        // Evaluation failed - it's likely a filter expression
3558                        // Fall through to filter logic
3559                    }
3560                }
3561
3562                // It's a filter expression
3563                let mut filtered = Vec::new();
3564                for item in arr.iter() {
3565                    let item_result = self.evaluate_internal(predicate, item)?;
3566                    if self.is_truthy(&item_result) {
3567                        filtered.push(item.clone());
3568                    }
3569                }
3570                Ok(JValue::array(filtered))
3571            }
3572            JValue::Null => {
3573                // Null: return null
3574                Ok(JValue::Null)
3575            }
3576            other => {
3577                // Non-array values: treat as single-element conceptual array
3578                // For numeric predicates: index 0 returns the value, other indices return null
3579                // For boolean predicates: if truthy, return value; if falsy, return null
3580
3581                // Check if predicate is a numeric index
3582                if let AstNode::Number(n) = predicate {
3583                    // Index 0 returns the value, other indices return null
3584                    if *n == 0.0 {
3585                        return Ok(other.clone());
3586                    } else {
3587                        return Ok(JValue::Null);
3588                    }
3589                }
3590
3591                // Try to evaluate the predicate to see if it's a numeric index
3592                match self.evaluate_internal(predicate, other) {
3593                    Ok(JValue::Number(n)) => {
3594                        // Index 0 returns the value, other indices return null
3595                        if n == 0.0 {
3596                            Ok(other.clone())
3597                        } else {
3598                            Ok(JValue::Null)
3599                        }
3600                    }
3601                    Ok(pred_result) => {
3602                        // Boolean filter: return value if truthy, null if falsy
3603                        if self.is_truthy(&pred_result) {
3604                            Ok(other.clone())
3605                        } else {
3606                            Ok(JValue::Null)
3607                        }
3608                    }
3609                    Err(e) => Err(e),
3610                }
3611            }
3612        }
3613    }
3614
3615    /// Evaluate a path expression (e.g., foo.bar.baz)
3616    fn evaluate_path(
3617        &mut self,
3618        steps: &[PathStep],
3619        data: &JValue,
3620    ) -> Result<JValue, EvaluatorError> {
3621        // Avoid cloning by using references and only cloning when necessary
3622        if steps.is_empty() {
3623            return Ok(data.clone());
3624        }
3625
3626        // Fast path: single field access on object
3627        // This is a very common pattern, so optimize it
3628        if steps.len() == 1 {
3629            if let AstNode::Name(field_name) = &steps[0].node {
3630                return match data {
3631                    JValue::Object(obj) => {
3632                        // Check if this is a tuple - extract '@' value
3633                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3634                            if let Some(JValue::Object(inner)) = obj.get("@") {
3635                                Ok(inner.get(field_name).cloned().unwrap_or(JValue::Null))
3636                            } else {
3637                                Ok(JValue::Null)
3638                            }
3639                        } else {
3640                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null))
3641                        }
3642                    }
3643                    JValue::Array(arr) => {
3644                        // Array mapping: extract field from each element
3645                        // Optimized: use references to access fields without cloning entire objects
3646                        // Check first element for tuple-ness (tuples are all-or-nothing)
3647                        let has_tuples = arr.first().is_some_and(|item| {
3648                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
3649                        });
3650
3651                        if !has_tuples {
3652                            // Fast path: no tuples, just direct field lookups
3653                            let mut result = Vec::with_capacity(arr.len());
3654                            for item in arr.iter() {
3655                                if let JValue::Object(obj) = item {
3656                                    if let Some(val) = obj.get(field_name) {
3657                                        if !val.is_null() {
3658                                            match val {
3659                                                JValue::Array(arr_val) => {
3660                                                    result.extend(arr_val.iter().cloned());
3661                                                }
3662                                                other => result.push(other.clone()),
3663                                            }
3664                                        }
3665                                    }
3666                                } else if let JValue::Array(inner_arr) = item {
3667                                    let nested_result = self.evaluate_path(
3668                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
3669                                        &JValue::Array(inner_arr.clone()),
3670                                    )?;
3671                                    match nested_result {
3672                                        JValue::Array(nested) => {
3673                                            result.extend(nested.iter().cloned());
3674                                        }
3675                                        JValue::Null => {}
3676                                        other => result.push(other),
3677                                    }
3678                                }
3679                            }
3680
3681                            if result.is_empty() {
3682                                Ok(JValue::Null)
3683                            } else if result.len() == 1 {
3684                                Ok(result.into_iter().next().unwrap())
3685                            } else {
3686                                Ok(JValue::array(result))
3687                            }
3688                        } else {
3689                            // Tuple path: per-element tuple handling
3690                            let mut result = Vec::new();
3691                            for item in arr.iter() {
3692                                match item {
3693                                    JValue::Object(obj) => {
3694                                        let is_tuple =
3695                                            obj.get("__tuple__") == Some(&JValue::Bool(true));
3696
3697                                        if is_tuple {
3698                                            let inner = match obj.get("@") {
3699                                                Some(JValue::Object(inner)) => inner,
3700                                                _ => continue,
3701                                            };
3702
3703                                            if let Some(val) = inner.get(field_name) {
3704                                                if !val.is_null() {
3705                                                    // Build tuple wrapper - only clone bindings when needed
3706                                                    let wrap = |v: JValue| -> JValue {
3707                                                        let mut wrapper = IndexMap::new();
3708                                                        wrapper.insert("@".to_string(), v);
3709                                                        wrapper.insert(
3710                                                            "__tuple__".to_string(),
3711                                                            JValue::Bool(true),
3712                                                        );
3713                                                        for (k, v) in obj.iter() {
3714                                                            if k.starts_with('$') {
3715                                                                wrapper
3716                                                                    .insert(k.clone(), v.clone());
3717                                                            }
3718                                                        }
3719                                                        JValue::object(wrapper)
3720                                                    };
3721
3722                                                    match val {
3723                                                        JValue::Array(arr_val) => {
3724                                                            for item in arr_val.iter() {
3725                                                                result.push(wrap(item.clone()));
3726                                                            }
3727                                                        }
3728                                                        other => result.push(wrap(other.clone())),
3729                                                    }
3730                                                }
3731                                            }
3732                                        } else {
3733                                            // Non-tuple: access field directly by reference, only clone the field value
3734                                            if let Some(val) = obj.get(field_name) {
3735                                                if !val.is_null() {
3736                                                    match val {
3737                                                        JValue::Array(arr_val) => {
3738                                                            for item in arr_val.iter() {
3739                                                                result.push(item.clone());
3740                                                            }
3741                                                        }
3742                                                        other => result.push(other.clone()),
3743                                                    }
3744                                                }
3745                                            }
3746                                        }
3747                                    }
3748                                    JValue::Array(inner_arr) => {
3749                                        // Recursively map over nested array
3750                                        let nested_result = self.evaluate_path(
3751                                            &[PathStep::new(AstNode::Name(field_name.clone()))],
3752                                            &JValue::Array(inner_arr.clone()),
3753                                        )?;
3754                                        // Add nested result to our results
3755                                        match nested_result {
3756                                            JValue::Array(nested) => {
3757                                                // Flatten nested arrays from recursive mapping
3758                                                result.extend(nested.iter().cloned());
3759                                            }
3760                                            JValue::Null => {} // Skip nulls from nested arrays
3761                                            other => result.push(other),
3762                                        }
3763                                    }
3764                                    _ => {} // Skip non-object items
3765                                }
3766                            }
3767
3768                            // Return array result
3769                            // JSONata singleton unwrapping: if we have exactly one result,
3770                            // unwrap it (even if it's an array)
3771                            if result.is_empty() {
3772                                Ok(JValue::Null)
3773                            } else if result.len() == 1 {
3774                                Ok(result.into_iter().next().unwrap())
3775                            } else {
3776                                Ok(JValue::array(result))
3777                            }
3778                        } // end else (tuple path)
3779                    }
3780                    _ => Ok(JValue::Null),
3781                };
3782            }
3783        }
3784
3785        // Fast path: 2-step $variable.field with no stages
3786        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
3787        if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
3788            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
3789                (&steps[0].node, &steps[1].node)
3790            {
3791                if !var_name.is_empty() {
3792                    if let Some(value) = self.context.lookup(var_name) {
3793                        match value {
3794                            JValue::Object(obj) => {
3795                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
3796                            }
3797                            JValue::Array(arr) => {
3798                                // Map field extraction over array (same as single-step Name on Array)
3799                                let mut result = Vec::with_capacity(arr.len());
3800                                for item in arr.iter() {
3801                                    if let JValue::Object(obj) = item {
3802                                        if let Some(val) = obj.get(field_name) {
3803                                            if !val.is_null() {
3804                                                match val {
3805                                                    JValue::Array(inner) => {
3806                                                        result.extend(inner.iter().cloned());
3807                                                    }
3808                                                    other => result.push(other.clone()),
3809                                                }
3810                                            }
3811                                        }
3812                                    }
3813                                }
3814                                return match result.len() {
3815                                    0 => Ok(JValue::Null),
3816                                    1 => Ok(result.pop().unwrap()),
3817                                    _ => Ok(JValue::array(result)),
3818                                };
3819                            }
3820                            _ => {} // Fall through to general path evaluation
3821                        }
3822                    }
3823                }
3824            }
3825        }
3826
3827        // Track whether we did array mapping (for singleton unwrapping)
3828        let mut did_array_mapping = false;
3829
3830        // For the first step, work with a reference
3831        let mut current: JValue = match &steps[0].node {
3832            AstNode::Wildcard => {
3833                // Wildcard as first step
3834                match data {
3835                    JValue::Object(obj) => {
3836                        let mut result = Vec::new();
3837                        for value in obj.values() {
3838                            // Flatten arrays into the result
3839                            match value {
3840                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3841                                _ => result.push(value.clone()),
3842                            }
3843                        }
3844                        JValue::array(result)
3845                    }
3846                    JValue::Array(arr) => JValue::Array(arr.clone()),
3847                    _ => JValue::Null,
3848                }
3849            }
3850            AstNode::Descendant => {
3851                // Descendant as first step
3852                let descendants = self.collect_descendants(data);
3853                JValue::array(descendants)
3854            }
3855            AstNode::ParentVariable(name) => {
3856                // Parent variable as first step
3857                let parent_data = self.context.get_parent().ok_or_else(|| {
3858                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3859                })?;
3860
3861                if name.is_empty() {
3862                    // $$ alone returns parent context
3863                    parent_data.clone()
3864                } else {
3865                    // $$field accesses field on parent
3866                    match parent_data {
3867                        JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
3868                        _ => JValue::Null,
3869                    }
3870                }
3871            }
3872            AstNode::Name(field_name) => {
3873                // Field/property access - get the stages for this step
3874                let stages = &steps[0].stages;
3875
3876                match data {
3877                    JValue::Object(obj) => {
3878                        let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
3879                        // Apply any stages to the extracted value
3880                        if !stages.is_empty() {
3881                            self.apply_stages(val, stages)?
3882                        } else {
3883                            val
3884                        }
3885                    }
3886                    JValue::Array(arr) => {
3887                        // Array mapping: extract field from each element and apply stages
3888                        let mut result = Vec::new();
3889                        for item in arr.iter() {
3890                            match item {
3891                                JValue::Object(obj) => {
3892                                    let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
3893                                    if !val.is_null() {
3894                                        if !stages.is_empty() {
3895                                            // Apply stages to the extracted value
3896                                            let processed_val = self.apply_stages(val, stages)?;
3897                                            // Stages always return an array (or null); extend results
3898                                            match processed_val {
3899                                                JValue::Array(arr) => {
3900                                                    result.extend(arr.iter().cloned())
3901                                                }
3902                                                JValue::Null => {} // Skip nulls from stage application
3903                                                other => result.push(other), // Shouldn't happen, but handle it
3904                                            }
3905                                        } else {
3906                                            // No stages: flatten arrays, push scalars
3907                                            match val {
3908                                                JValue::Array(arr) => {
3909                                                    result.extend(arr.iter().cloned())
3910                                                }
3911                                                other => result.push(other),
3912                                            }
3913                                        }
3914                                    }
3915                                }
3916                                JValue::Array(inner_arr) => {
3917                                    // Recursively map over nested array
3918                                    let nested_result = self.evaluate_path(
3919                                        &[steps[0].clone()],
3920                                        &JValue::Array(inner_arr.clone()),
3921                                    )?;
3922                                    match nested_result {
3923                                        JValue::Array(nested) => {
3924                                            result.extend(nested.iter().cloned())
3925                                        }
3926                                        JValue::Null => {} // Skip nulls from nested arrays
3927                                        other => result.push(other),
3928                                    }
3929                                }
3930                                _ => {} // Skip non-object items
3931                            }
3932                        }
3933                        JValue::array(result)
3934                    }
3935                    JValue::Null => JValue::Null,
3936                    // Accessing field on non-object returns undefined (not an error)
3937                    _ => JValue::Undefined,
3938                }
3939            }
3940            AstNode::String(string_literal) => {
3941                // String literal in path context - evaluate as literal and apply stages
3942                // This handles cases like "Red"[true] where "Red" is a literal, not a field access
3943                let stages = &steps[0].stages;
3944                let val = JValue::string(string_literal.clone());
3945
3946                if !stages.is_empty() {
3947                    // Apply stages (predicates) to the string literal
3948                    let result = self.apply_stages(val, stages)?;
3949                    // Unwrap single-element arrays back to scalar
3950                    // (string literals with predicates should return scalar or null, not arrays)
3951                    match result {
3952                        JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
3953                        JValue::Array(arr) if arr.is_empty() => JValue::Null,
3954                        other => other,
3955                    }
3956                } else {
3957                    val
3958                }
3959            }
3960            AstNode::Predicate(pred_expr) => {
3961                // Predicate as first step
3962                self.evaluate_predicate(data, pred_expr)?
3963            }
3964            AstNode::IndexBind { .. } => {
3965                // Index binding as first step - evaluate the IndexBind to create tuple array
3966                self.evaluate_internal(&steps[0].node, data)?
3967            }
3968            _ => {
3969                // Complex first step - evaluate it
3970                self.evaluate_path_step(&steps[0].node, data, data)?
3971            }
3972        };
3973
3974        // Process remaining steps
3975        for step in steps[1..].iter() {
3976            // Early return if current is null/undefined - no point continuing
3977            // This handles cases like `blah.{}` where blah doesn't exist
3978            if current.is_null() || current.is_undefined() {
3979                return Ok(JValue::Null);
3980            }
3981
3982            // Check if current is a tuple array - if so, we need to bind tuple variables
3983            // to context so they're available in nested expressions (like predicates)
3984            let is_tuple_array = if let JValue::Array(arr) = &current {
3985                arr.first().is_some_and(|first| {
3986                    if let JValue::Object(obj) = first {
3987                        obj.get("__tuple__") == Some(&JValue::Bool(true))
3988                    } else {
3989                        false
3990                    }
3991                })
3992            } else {
3993                false
3994            };
3995
3996            // For tuple arrays with certain step types, we need special handling to bind
3997            // tuple variables to context so they're available in nested expressions.
3998            // This is needed for:
3999            // - Object constructors: {"label": $$.items[$i]} needs $i in context
4000            // - Function applications: .($$.items[$i]) needs $i in context
4001            // - Variable lookups: .$i needs to find the tuple binding
4002            //
4003            // Steps like Name (field access) already have proper tuple handling in their
4004            // specific cases, so we don't intercept those here.
4005            let needs_tuple_context_binding = is_tuple_array
4006                && matches!(
4007                    &step.node,
4008                    AstNode::Object(_) | AstNode::FunctionApplication(_) | AstNode::Variable(_)
4009                );
4010
4011            if needs_tuple_context_binding {
4012                if let JValue::Array(arr) = &current {
4013                    let mut results = Vec::new();
4014
4015                    for tuple in arr.iter() {
4016                        if let JValue::Object(tuple_obj) = tuple {
4017                            // Extract tuple bindings (variables starting with $)
4018                            let bindings: Vec<(String, JValue)> = tuple_obj
4019                                .iter()
4020                                .filter(|(k, _)| k.starts_with('$') && k.len() > 1) // $i, $j, etc.
4021                                .map(|(k, v)| (k[1..].to_string(), v.clone())) // Remove $ prefix for context binding
4022                                .collect();
4023
4024                            // Save current bindings
4025                            let saved_bindings: Vec<(String, Option<JValue>)> = bindings
4026                                .iter()
4027                                .map(|(name, _)| (name.clone(), self.context.lookup(name).cloned()))
4028                                .collect();
4029
4030                            // Bind tuple variables to context
4031                            for (name, value) in &bindings {
4032                                self.context.bind(name.clone(), value.clone());
4033                            }
4034
4035                            // Get the actual value from the tuple (@ field)
4036                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4037
4038                            // Evaluate the step
4039                            let step_result = match &step.node {
4040                                AstNode::Variable(_) => {
4041                                    // Variable lookup - check context (which now has bindings)
4042                                    self.evaluate_internal(&step.node, tuple)?
4043                                }
4044                                AstNode::Object(_) | AstNode::FunctionApplication(_) => {
4045                                    // Object constructor or function application - evaluate on actual data
4046                                    self.evaluate_internal(&step.node, &actual_data)?
4047                                }
4048                                _ => unreachable!(), // We only match specific types above
4049                            };
4050
4051                            // Restore previous bindings
4052                            for (name, saved_value) in &saved_bindings {
4053                                if let Some(value) = saved_value {
4054                                    self.context.bind(name.clone(), value.clone());
4055                                } else {
4056                                    self.context.unbind(name);
4057                                }
4058                            }
4059
4060                            // Collect result
4061                            if !step_result.is_null() && !step_result.is_undefined() {
4062                                // For object constructors, collect results directly
4063                                // For other steps, handle arrays
4064                                if matches!(&step.node, AstNode::Object(_)) {
4065                                    results.push(step_result);
4066                                } else if matches!(step_result, JValue::Array(_)) {
4067                                    if let JValue::Array(arr) = step_result {
4068                                        results.extend(arr.iter().cloned());
4069                                    }
4070                                } else {
4071                                    results.push(step_result);
4072                                }
4073                            }
4074                        }
4075                    }
4076
4077                    current = JValue::array(results);
4078                    continue; // Skip the regular step processing
4079                }
4080            }
4081
4082            current = match &step.node {
4083                AstNode::Wildcard => {
4084                    // Wildcard in path
4085                    let stages = &step.stages;
4086                    let wildcard_result = match &current {
4087                        JValue::Object(obj) => {
4088                            let mut result = Vec::new();
4089                            for value in obj.values() {
4090                                // Flatten arrays into the result
4091                                match value {
4092                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4093                                    _ => result.push(value.clone()),
4094                                }
4095                            }
4096                            JValue::array(result)
4097                        }
4098                        JValue::Array(arr) => {
4099                            // Map wildcard over array
4100                            let mut all_values = Vec::new();
4101                            for item in arr.iter() {
4102                                match item {
4103                                    JValue::Object(obj) => {
4104                                        for value in obj.values() {
4105                                            // Flatten arrays
4106                                            match value {
4107                                                JValue::Array(arr) => {
4108                                                    all_values.extend(arr.iter().cloned())
4109                                                }
4110                                                _ => all_values.push(value.clone()),
4111                                            }
4112                                        }
4113                                    }
4114                                    JValue::Array(inner) => {
4115                                        all_values.extend(inner.iter().cloned());
4116                                    }
4117                                    _ => {}
4118                                }
4119                            }
4120                            JValue::array(all_values)
4121                        }
4122                        _ => JValue::Null,
4123                    };
4124
4125                    // Apply stages (predicates) if present
4126                    if !stages.is_empty() {
4127                        self.apply_stages(wildcard_result, stages)?
4128                    } else {
4129                        wildcard_result
4130                    }
4131                }
4132                AstNode::Descendant => {
4133                    // Descendant in path
4134                    match &current {
4135                        JValue::Array(arr) => {
4136                            // Collect descendants from all array elements
4137                            let mut all_descendants = Vec::new();
4138                            for item in arr.iter() {
4139                                all_descendants.extend(self.collect_descendants(item));
4140                            }
4141                            JValue::array(all_descendants)
4142                        }
4143                        _ => {
4144                            // Collect descendants from current value
4145                            let descendants = self.collect_descendants(&current);
4146                            JValue::array(descendants)
4147                        }
4148                    }
4149                }
4150                AstNode::Name(field_name) => {
4151                    // Navigate into object field or map over array, applying stages
4152                    let stages = &step.stages;
4153
4154                    match &current {
4155                        JValue::Object(obj) => {
4156                            // Single object field extraction - NOT array mapping
4157                            // This resets did_array_mapping because we're extracting from
4158                            // a single value, not mapping over an array. The field's value
4159                            // (even if it's an array) should be preserved as-is.
4160                            did_array_mapping = false;
4161                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Null);
4162                            // Apply stages if present
4163                            if !stages.is_empty() {
4164                                self.apply_stages(val, stages)?
4165                            } else {
4166                                val
4167                            }
4168                        }
4169                        JValue::Array(arr) => {
4170                            // Array mapping: extract field from each element and apply stages
4171                            did_array_mapping = true; // Track that we did array mapping
4172
4173                            // Fast path: if no elements are tuples and no stages,
4174                            // skip all tuple checking overhead (common case for products.price etc.)
4175                            // Tuples are all-or-nothing (created by index binding #$i),
4176                            // so checking only the first element is sufficient.
4177                            let has_tuples = arr.first().is_some_and(|item| {
4178                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4179                            });
4180
4181                            if !has_tuples && stages.is_empty() {
4182                                let mut result = Vec::with_capacity(arr.len());
4183                                for item in arr.iter() {
4184                                    match item {
4185                                        JValue::Object(obj) => {
4186                                            if let Some(val) = obj.get(field_name) {
4187                                                if !val.is_null() {
4188                                                    match val {
4189                                                        JValue::Array(arr_val) => {
4190                                                            result.extend(arr_val.iter().cloned())
4191                                                        }
4192                                                        other => result.push(other.clone()),
4193                                                    }
4194                                                }
4195                                            }
4196                                        }
4197                                        JValue::Array(_) => {
4198                                            let nested_result =
4199                                                self.evaluate_path(&[step.clone()], item)?;
4200                                            match nested_result {
4201                                                JValue::Array(nested) => {
4202                                                    result.extend(nested.iter().cloned())
4203                                                }
4204                                                JValue::Null => {}
4205                                                other => result.push(other),
4206                                            }
4207                                        }
4208                                        _ => {}
4209                                    }
4210                                }
4211                                JValue::array(result)
4212                            } else {
4213                                // Full path with tuple support and stages
4214                                let mut result = Vec::new();
4215
4216                                for item in arr.iter() {
4217                                    match item {
4218                                        JValue::Object(obj) => {
4219                                            // Check if this is a tuple stream element
4220                                            let (actual_obj, tuple_bindings) = if obj
4221                                                .get("__tuple__")
4222                                                == Some(&JValue::Bool(true))
4223                                            {
4224                                                // This is a tuple - extract '@' value and preserve bindings
4225                                                if let Some(JValue::Object(inner)) = obj.get("@") {
4226                                                    // Collect index bindings (variables starting with $)
4227                                                    let bindings: Vec<(String, JValue)> = obj
4228                                                        .iter()
4229                                                        .filter(|(k, _)| k.starts_with('$'))
4230                                                        .map(|(k, v)| (k.clone(), v.clone()))
4231                                                        .collect();
4232                                                    (inner.clone(), Some(bindings))
4233                                                } else {
4234                                                    continue; // Invalid tuple
4235                                                }
4236                                            } else {
4237                                                (obj.clone(), None)
4238                                            };
4239
4240                                            let val = actual_obj
4241                                                .get(field_name)
4242                                                .cloned()
4243                                                .unwrap_or(JValue::Null);
4244
4245                                            if !val.is_null() {
4246                                                // Helper to wrap value in tuple if we have bindings
4247                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
4248                                                    if let Some(b) = bindings {
4249                                                        let mut wrapper = IndexMap::new();
4250                                                        wrapper.insert("@".to_string(), v);
4251                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4252                                                        for (k, val) in b {
4253                                                            wrapper.insert(k.clone(), val.clone());
4254                                                        }
4255                                                        JValue::object(wrapper)
4256                                                    } else {
4257                                                        v
4258                                                    }
4259                                                };
4260
4261                                                if !stages.is_empty() {
4262                                                    // Apply stages to the extracted value
4263                                                    let processed_val =
4264                                                        self.apply_stages(val, stages)?;
4265                                                    // Stages always return an array (or null); extend results
4266                                                    match processed_val {
4267                                                        JValue::Array(arr) => {
4268                                                            for item in arr.iter() {
4269                                                                result.push(wrap_in_tuple(
4270                                                                    item.clone(),
4271                                                                    &tuple_bindings,
4272                                                                ));
4273                                                            }
4274                                                        }
4275                                                        JValue::Null => {} // Skip nulls from stage application
4276                                                        other => result.push(wrap_in_tuple(
4277                                                            other,
4278                                                            &tuple_bindings,
4279                                                        )),
4280                                                    }
4281                                                } else {
4282                                                    // No stages: flatten arrays, push scalars
4283                                                    // But preserve tuple bindings!
4284                                                    match val {
4285                                                        JValue::Array(arr) => {
4286                                                            for item in arr.iter() {
4287                                                                result.push(wrap_in_tuple(
4288                                                                    item.clone(),
4289                                                                    &tuple_bindings,
4290                                                                ));
4291                                                            }
4292                                                        }
4293                                                        other => result.push(wrap_in_tuple(
4294                                                            other,
4295                                                            &tuple_bindings,
4296                                                        )),
4297                                                    }
4298                                                }
4299                                            }
4300                                        }
4301                                        JValue::Array(_) => {
4302                                            // Recursively map over nested array
4303                                            let nested_result =
4304                                                self.evaluate_path(&[step.clone()], item)?;
4305                                            match nested_result {
4306                                                JValue::Array(nested) => {
4307                                                    result.extend(nested.iter().cloned())
4308                                                }
4309                                                JValue::Null => {}
4310                                                other => result.push(other),
4311                                            }
4312                                        }
4313                                        _ => {}
4314                                    }
4315                                }
4316
4317                                JValue::array(result)
4318                            }
4319                        }
4320                        JValue::Null => JValue::Null,
4321                        // Accessing field on non-object returns undefined (not an error)
4322                        _ => JValue::Undefined,
4323                    }
4324                }
4325                AstNode::String(string_literal) => {
4326                    // String literal as a path step - evaluate as literal and apply stages
4327                    let stages = &step.stages;
4328                    let val = JValue::string(string_literal.clone());
4329
4330                    if !stages.is_empty() {
4331                        // Apply stages (predicates) to the string literal
4332                        let result = self.apply_stages(val, stages)?;
4333                        // Unwrap single-element arrays back to scalar
4334                        match result {
4335                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4336                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4337                            other => other,
4338                        }
4339                    } else {
4340                        val
4341                    }
4342                }
4343                AstNode::Predicate(pred_expr) => {
4344                    // Predicate in path - filter or index into current value
4345                    self.evaluate_predicate(&current, pred_expr)?
4346                }
4347                AstNode::ArrayGroup(elements) => {
4348                    // Array grouping: map expression over array but keep results grouped
4349                    // .[expr] means evaluate expr for each array element
4350                    match &current {
4351                        JValue::Array(arr) => {
4352                            let mut result = Vec::new();
4353                            for item in arr.iter() {
4354                                // For each array item, evaluate all elements and collect results
4355                                let mut group_values = Vec::new();
4356                                for element in elements {
4357                                    let value = self.evaluate_internal(element, item)?;
4358                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
4359                                    // This ensures [[expr]] produces properly nested arrays
4360                                    let should_preserve_array = matches!(
4361                                        element,
4362                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
4363                                    );
4364
4365                                    if should_preserve_array {
4366                                        // Keep the array as a single element to preserve nesting
4367                                        group_values.push(value);
4368                                    } else {
4369                                        // Flatten the value into group_values
4370                                        match value {
4371                                            JValue::Array(arr) => {
4372                                                group_values.extend(arr.iter().cloned())
4373                                            }
4374                                            other => group_values.push(other),
4375                                        }
4376                                    }
4377                                }
4378                                // Each array element gets its own sub-array with all values
4379                                result.push(JValue::array(group_values));
4380                            }
4381                            JValue::array(result)
4382                        }
4383                        _ => {
4384                            // For non-arrays, just evaluate the array constructor normally
4385                            let mut result = Vec::new();
4386                            for element in elements {
4387                                let value = self.evaluate_internal(element, &current)?;
4388                                result.push(value);
4389                            }
4390                            JValue::array(result)
4391                        }
4392                    }
4393                }
4394                AstNode::FunctionApplication(expr) => {
4395                    // Function application: map expr over the current value
4396                    // .(expr) means evaluate expr for each element, with $ bound to that element
4397                    // Null/undefined results are filtered out
4398                    match &current {
4399                        JValue::Array(arr) => {
4400                            // Produce the mapped result (compiled fast path or tree-walker fallback).
4401                            // Do NOT return early — singleton unwrapping is applied by the outer
4402                            // path evaluation code after all steps are processed.
4403                            let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
4404                            {
4405                                let shape = arr.first().and_then(build_shape_cache);
4406                                let mut result = Vec::with_capacity(arr.len());
4407                                for item in arr.iter() {
4408                                    let value = if let Some(ref s) = shape {
4409                                        eval_compiled_shaped(&compiled, item, None, s)?
4410                                    } else {
4411                                        eval_compiled(&compiled, item, None)?
4412                                    };
4413                                    if !value.is_null() && !value.is_undefined() {
4414                                        result.push(value);
4415                                    }
4416                                }
4417                                result
4418                            } else {
4419                                let mut result = Vec::new();
4420                                for item in arr.iter() {
4421                                    // Save the current $ binding
4422                                    let saved_dollar = self.context.lookup("$").cloned();
4423
4424                                    // Bind $ to the current item
4425                                    self.context.bind("$".to_string(), item.clone());
4426
4427                                    // Evaluate the expression in the context of this item
4428                                    let value = self.evaluate_internal(expr, item)?;
4429
4430                                    // Restore the previous $ binding
4431                                    if let Some(saved) = saved_dollar {
4432                                        self.context.bind("$".to_string(), saved);
4433                                    } else {
4434                                        self.context.unbind("$");
4435                                    }
4436
4437                                    // Only include non-null/undefined values
4438                                    if !value.is_null() && !value.is_undefined() {
4439                                        result.push(value);
4440                                    }
4441                                }
4442                                result
4443                            };
4444                            // Don't do singleton unwrapping here - let the path result
4445                            // handling deal with it, which respects has_explicit_array_keep
4446                            JValue::array(mapped)
4447                        }
4448                        _ => {
4449                            // For non-arrays, bind $ and evaluate
4450                            let saved_dollar = self.context.lookup("$").cloned();
4451                            self.context.bind("$".to_string(), current.clone());
4452
4453                            let value = self.evaluate_internal(expr, &current)?;
4454
4455                            if let Some(saved) = saved_dollar {
4456                                self.context.bind("$".to_string(), saved);
4457                            } else {
4458                                self.context.unbind("$");
4459                            }
4460
4461                            value
4462                        }
4463                    }
4464                }
4465                AstNode::Sort { terms, .. } => {
4466                    // Sort as a path step - sort 'current' by the terms
4467                    self.evaluate_sort(&current, terms)?
4468                }
4469                AstNode::IndexBind { variable, .. } => {
4470                    // Index binding as a path step - creates tuple stream from current
4471                    // This wraps each element with an index binding
4472                    match &current {
4473                        JValue::Array(arr) => {
4474                            let mut result = Vec::new();
4475                            for (idx, item) in arr.iter().enumerate() {
4476                                let mut wrapper = IndexMap::new();
4477                                wrapper.insert("@".to_string(), item.clone());
4478                                wrapper
4479                                    .insert(format!("${}", variable), JValue::Number(idx as f64));
4480                                wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4481                                result.push(JValue::object(wrapper));
4482                            }
4483                            JValue::array(result)
4484                        }
4485                        other => {
4486                            // Single value: wrap with index 0
4487                            let mut wrapper = IndexMap::new();
4488                            wrapper.insert("@".to_string(), other.clone());
4489                            wrapper.insert(format!("${}", variable), JValue::from(0i64));
4490                            wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4491                            JValue::object(wrapper)
4492                        }
4493                    }
4494                }
4495                // Handle complex path steps (e.g., computed properties, object construction)
4496                _ => self.evaluate_path_step(&step.node, &current, data)?,
4497            };
4498        }
4499
4500        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
4501        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
4502
4503        // Check for explicit array-keeping operations
4504        // Empty predicate [] can be represented as:
4505        // 1. Predicate(Boolean(true)) as a path step node
4506        // 2. Stage::Filter(Boolean(true)) as a stage
4507        let has_explicit_array_keep = steps.iter().any(|step| {
4508            // Check if the step itself is an empty predicate
4509            if let AstNode::Predicate(pred) = &step.node {
4510                if matches!(**pred, AstNode::Boolean(true)) {
4511                    return true;
4512                }
4513            }
4514            // Check if any stage is an empty predicate
4515            step.stages.iter().any(|stage| {
4516                let crate::ast::Stage::Filter(pred) = stage;
4517                matches!(**pred, AstNode::Boolean(true))
4518            })
4519        });
4520
4521        // Unwrap when:
4522        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
4523        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
4524        //    Note: did_array_mapping is reset to false when extracting from a single object,
4525        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
4526        // BUT NOT when there's an explicit array-keeping operation
4527        //
4528        // Important: We DON'T unwrap just because original data was an array - what matters is
4529        // whether the final extraction was from an array mapping context or a single object.
4530        let should_unwrap = !has_explicit_array_keep
4531            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
4532
4533        let result = match &current {
4534            // Empty arrays become null/undefined
4535            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4536            // Unwrap singleton arrays when appropriate
4537            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
4538            // Keep arrays otherwise
4539            _ => current,
4540        };
4541
4542        Ok(result)
4543    }
4544
4545    /// Helper to evaluate a complex path step
4546    fn evaluate_path_step(
4547        &mut self,
4548        step: &AstNode,
4549        current: &JValue,
4550        original_data: &JValue,
4551    ) -> Result<JValue, EvaluatorError> {
4552        // Special case: array mapping with object construction
4553        // e.g., items.{"name": name, "price": price}
4554        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
4555            match (current, step) {
4556                (JValue::Array(arr), AstNode::Object(pairs)) => {
4557                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
4558                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
4559                        let shape = arr.first().and_then(build_shape_cache);
4560                        let mut mapped = Vec::with_capacity(arr.len());
4561                        for item in arr.iter() {
4562                            let result = if let Some(ref s) = shape {
4563                                eval_compiled_shaped(&compiled, item, None, s)?
4564                            } else {
4565                                eval_compiled(&compiled, item, None)?
4566                            };
4567                            if !result.is_undefined() {
4568                                mapped.push(result);
4569                            }
4570                        }
4571                        return Ok(JValue::array(mapped));
4572                    }
4573                    // Fallback: full AST evaluation per element
4574                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
4575                        .iter()
4576                        .map(|item| self.evaluate_internal(step, item))
4577                        .collect();
4578                    Ok(JValue::array(mapped?))
4579                }
4580                _ => unreachable!(),
4581            }
4582        } else {
4583            // Special case: array.$ should map $ over the array, returning each element
4584            // e.g., [1, 2, 3].$ returns [1, 2, 3]
4585            if let AstNode::Variable(name) = step {
4586                if name.is_empty() {
4587                    // Bare $ - map over array if current is an array
4588                    if let JValue::Array(arr) = current {
4589                        // Map $ over each element - $ refers to each element in turn
4590                        return Ok(JValue::Array(arr.clone()));
4591                    } else {
4592                        // For non-arrays, $ refers to the current value
4593                        return Ok(current.clone());
4594                    }
4595                }
4596            }
4597
4598            // Special case: Variable access on tuple arrays (from index binding #$var)
4599            // When current is a tuple array, we need to evaluate the variable against each tuple
4600            // so that tuple bindings ($i, etc.) can be found
4601            if matches!(step, AstNode::Variable(_)) {
4602                if let JValue::Array(arr) = current {
4603                    // Check if this is a tuple array
4604                    let is_tuple_array = arr.first().is_some_and(|first| {
4605                        if let JValue::Object(obj) = first {
4606                            obj.get("__tuple__") == Some(&JValue::Bool(true))
4607                        } else {
4608                            false
4609                        }
4610                    });
4611
4612                    if is_tuple_array {
4613                        // Map the variable lookup over each tuple
4614                        let mut results = Vec::new();
4615                        for tuple in arr.iter() {
4616                            // Evaluate the variable in the context of this tuple
4617                            // This allows tuple bindings ($i, etc.) to be found
4618                            let val = self.evaluate_internal(step, tuple)?;
4619                            if !val.is_null() && !val.is_undefined() {
4620                                results.push(val);
4621                            }
4622                        }
4623                        return Ok(JValue::array(results));
4624                    }
4625                }
4626            }
4627
4628            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
4629            // rather than being used to index/access the current value
4630            // e.g., items[price > 50] where [price > 50] is a filter operation
4631            // or $x.price where $x is a variable binding
4632            // or $$.field where $$ is the parent context
4633            // or [0..9] where it's an array constructor
4634            // or $^(field) where it's a sort operator
4635            // or (expr).field where (expr) is a block that evaluates to a value
4636            if matches!(
4637                step,
4638                AstNode::Binary { .. }
4639                    | AstNode::Function { .. }
4640                    | AstNode::Variable(_)
4641                    | AstNode::ParentVariable(_)
4642                    | AstNode::Array(_)
4643                    | AstNode::Object(_)
4644                    | AstNode::Sort { .. }
4645                    | AstNode::Block(_)
4646            ) {
4647                // Evaluate the step in the context of original_data and return the result directly
4648                return self.evaluate_internal(step, original_data);
4649            }
4650
4651            // Standard path step evaluation for indexing/accessing current value
4652            let step_value = self.evaluate_internal(step, original_data)?;
4653            Ok(match (current, &step_value) {
4654                (JValue::Object(obj), JValue::String(key)) => {
4655                    obj.get(&**key).cloned().unwrap_or(JValue::Null)
4656                }
4657                (JValue::Array(arr), JValue::Number(n)) => {
4658                    let index = *n as i64;
4659                    let len = arr.len() as i64;
4660
4661                    // Handle negative indexing (offset from end)
4662                    let actual_idx = if index < 0 { len + index } else { index };
4663
4664                    if actual_idx < 0 || actual_idx >= len {
4665                        JValue::Null
4666                    } else {
4667                        arr[actual_idx as usize].clone()
4668                    }
4669                }
4670                _ => JValue::Null,
4671            })
4672        }
4673    }
4674
4675    /// Evaluate a binary operation
4676    fn evaluate_binary_op(
4677        &mut self,
4678        op: crate::ast::BinaryOp,
4679        lhs: &AstNode,
4680        rhs: &AstNode,
4681        data: &JValue,
4682    ) -> Result<JValue, EvaluatorError> {
4683        use crate::ast::BinaryOp;
4684
4685        // Special handling for coalescing operator (??)
4686        // Returns right side if left is undefined (produces no value)
4687        // Note: literal null is a value, so it's NOT replaced
4688        if op == BinaryOp::Coalesce {
4689            // Try to evaluate the left side
4690            return match self.evaluate_internal(lhs, data) {
4691                Ok(value) => {
4692                    // Successfully evaluated to a value (even if it's null)
4693                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
4694                    if matches!(lhs, AstNode::Null) {
4695                        Ok(value)
4696                    }
4697                    // For paths and variables, null means undefined - use RHS
4698                    else if value.is_null()
4699                        && (matches!(lhs, AstNode::Path { .. })
4700                            || matches!(lhs, AstNode::String(_))
4701                            || matches!(lhs, AstNode::Variable(_)))
4702                    {
4703                        self.evaluate_internal(rhs, data)
4704                    } else {
4705                        Ok(value)
4706                    }
4707                }
4708                Err(_) => {
4709                    // Evaluation failed (e.g., undefined variable) - use RHS
4710                    self.evaluate_internal(rhs, data)
4711                }
4712            };
4713        }
4714
4715        // Special handling for default operator (?:)
4716        // Returns right side if left is falsy or a non-value (like a function)
4717        if op == BinaryOp::Default {
4718            let left = self.evaluate_internal(lhs, data)?;
4719            if self.is_truthy_for_default(&left) {
4720                return Ok(left);
4721            }
4722            return self.evaluate_internal(rhs, data);
4723        }
4724
4725        // Special handling for chain/pipe operator (~>)
4726        // Pipes the LHS result to the RHS function as the first argument
4727        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
4728        if op == BinaryOp::ChainPipe {
4729            // Handle regex on RHS - treat as $match(lhs, regex)
4730            if let AstNode::Regex { pattern, flags } = rhs {
4731                // Evaluate LHS
4732                let lhs_value = self.evaluate_internal(lhs, data)?;
4733                // Do regex match inline
4734                return match lhs_value {
4735                    JValue::String(s) => {
4736                        // Build the regex
4737                        let case_insensitive = flags.contains('i');
4738                        let regex_pattern = if case_insensitive {
4739                            format!("(?i){}", pattern)
4740                        } else {
4741                            pattern.clone()
4742                        };
4743                        match regex::Regex::new(&regex_pattern) {
4744                            Ok(re) => {
4745                                if let Some(m) = re.find(&s) {
4746                                    // Return match object
4747                                    let mut result = IndexMap::new();
4748                                    result.insert(
4749                                        "match".to_string(),
4750                                        JValue::string(m.as_str().to_string()),
4751                                    );
4752                                    result.insert(
4753                                        "start".to_string(),
4754                                        JValue::Number(m.start() as f64),
4755                                    );
4756                                    result
4757                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
4758
4759                                    // Capture groups
4760                                    let mut groups = Vec::new();
4761                                    for cap in re.captures_iter(&s).take(1) {
4762                                        for i in 1..cap.len() {
4763                                            if let Some(c) = cap.get(i) {
4764                                                groups.push(JValue::string(c.as_str().to_string()));
4765                                            }
4766                                        }
4767                                    }
4768                                    if !groups.is_empty() {
4769                                        result.insert("groups".to_string(), JValue::array(groups));
4770                                    }
4771
4772                                    Ok(JValue::object(result))
4773                                } else {
4774                                    Ok(JValue::Null)
4775                                }
4776                            }
4777                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
4778                                "Invalid regex: {}",
4779                                e
4780                            ))),
4781                        }
4782                    }
4783                    JValue::Null => Ok(JValue::Null),
4784                    _ => Err(EvaluatorError::TypeError(
4785                        "Left side of ~> /regex/ must be a string".to_string(),
4786                    )),
4787                };
4788            }
4789
4790            // Early check: if LHS evaluates to undefined, return undefined
4791            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
4792            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
4793            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
4794                return Ok(JValue::Undefined);
4795            }
4796
4797            // Handle different RHS types
4798            match rhs {
4799                AstNode::Function {
4800                    name,
4801                    args,
4802                    is_builtin,
4803                } => {
4804                    // RHS is a function call
4805                    // Check if the function call has placeholder arguments (partial application)
4806                    let has_placeholder =
4807                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
4808
4809                    if has_placeholder {
4810                        // Partial application: replace the first placeholder with LHS value
4811                        let lhs_value = self.evaluate_internal(lhs, data)?;
4812                        let mut filled_args = Vec::new();
4813                        let mut lhs_used = false;
4814
4815                        for arg in args.iter() {
4816                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
4817                                // Replace first placeholder with evaluated LHS
4818                                // We need to create a temporary binding to pass the value
4819                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
4820                                self.context.bind(temp_name.clone(), lhs_value.clone());
4821                                filled_args.push(AstNode::Variable(temp_name));
4822                                lhs_used = true;
4823                            } else {
4824                                filled_args.push(arg.clone());
4825                            }
4826                        }
4827
4828                        // Evaluate the function with filled args
4829                        let result =
4830                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
4831
4832                        // Clean up temp bindings
4833                        for (i, arg) in args.iter().enumerate() {
4834                            if matches!(arg, AstNode::Placeholder) {
4835                                self.context.unbind(&format!("__pipe_arg_{}", i));
4836                            }
4837                        }
4838
4839                        // Unwrap singleton results from chain operator
4840                        return result.map(|v| self.unwrap_singleton(v));
4841                    } else {
4842                        // No placeholders: build args list with LHS as first argument
4843                        let mut all_args = vec![lhs.clone()];
4844                        all_args.extend_from_slice(args);
4845                        // Unwrap singleton results from chain operator
4846                        return self
4847                            .evaluate_function_call(name, &all_args, *is_builtin, data)
4848                            .map(|v| self.unwrap_singleton(v));
4849                    }
4850                }
4851                AstNode::Variable(var_name) => {
4852                    // RHS is a function reference (no parens)
4853                    // e.g., $average($tempReadings) ~> $round
4854                    let all_args = vec![lhs.clone()];
4855                    // Unwrap singleton results from chain operator
4856                    return self
4857                        .evaluate_function_call(var_name, &all_args, true, data)
4858                        .map(|v| self.unwrap_singleton(v));
4859                }
4860                AstNode::Binary {
4861                    op: BinaryOp::ChainPipe,
4862                    ..
4863                } => {
4864                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
4865                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
4866                    let lhs_value = self.evaluate_internal(lhs, data)?;
4867                    return self.evaluate_internal(rhs, &lhs_value);
4868                }
4869                AstNode::Transform { .. } => {
4870                    // RHS is a transform - invoke it with LHS as input
4871                    // Evaluate LHS first
4872                    let lhs_value = self.evaluate_internal(lhs, data)?;
4873
4874                    // Bind $ to the LHS value, then evaluate the transform
4875                    let saved_binding = self.context.lookup("$").cloned();
4876                    self.context.bind("$".to_string(), lhs_value.clone());
4877
4878                    let result = self.evaluate_internal(rhs, data);
4879
4880                    // Restore $ binding
4881                    if let Some(saved) = saved_binding {
4882                        self.context.bind("$".to_string(), saved);
4883                    } else {
4884                        self.context.unbind("$");
4885                    }
4886
4887                    // Unwrap singleton results from chain operator
4888                    return result.map(|v| self.unwrap_singleton(v));
4889                }
4890                AstNode::Lambda {
4891                    params,
4892                    body,
4893                    signature,
4894                    thunk,
4895                } => {
4896                    // RHS is a lambda - invoke it with LHS as argument
4897                    let lhs_value = self.evaluate_internal(lhs, data)?;
4898                    // Unwrap singleton results from chain operator
4899                    return self
4900                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
4901                        .map(|v| self.unwrap_singleton(v));
4902                }
4903                AstNode::Path { steps } => {
4904                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
4905                    // If the first step is a function call, we need to add LHS as first argument
4906                    if let Some(first_step) = steps.first() {
4907                        match &first_step.node {
4908                            AstNode::Function {
4909                                name,
4910                                args,
4911                                is_builtin,
4912                            } => {
4913                                // Prepend LHS to the function arguments
4914                                let mut all_args = vec![lhs.clone()];
4915                                all_args.extend_from_slice(args);
4916
4917                                // Call the function
4918                                let mut result = self.evaluate_function_call(
4919                                    name,
4920                                    &all_args,
4921                                    *is_builtin,
4922                                    data,
4923                                )?;
4924
4925                                // Apply stages from the first step (e.g., predicates)
4926                                for stage in &first_step.stages {
4927                                    match stage {
4928                                        Stage::Filter(filter_expr) => {
4929                                            result = self.evaluate_predicate_as_stage(
4930                                                &result,
4931                                                filter_expr,
4932                                            )?;
4933                                        }
4934                                    }
4935                                }
4936
4937                                // Apply remaining path steps if any
4938                                if steps.len() > 1 {
4939                                    let remaining_path = AstNode::Path {
4940                                        steps: steps[1..].to_vec(),
4941                                    };
4942                                    result = self.evaluate_internal(&remaining_path, &result)?;
4943                                }
4944
4945                                // Unwrap singleton results from chain operator, unless there are stages
4946                                // Stages (like predicates) indicate we want to preserve array structure
4947                                if !first_step.stages.is_empty() || steps.len() > 1 {
4948                                    return Ok(result);
4949                                } else {
4950                                    return Ok(self.unwrap_singleton(result));
4951                                }
4952                            }
4953                            AstNode::Variable(var_name) => {
4954                                // Variable that should resolve to a function
4955                                let all_args = vec![lhs.clone()];
4956                                let mut result =
4957                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
4958
4959                                // Apply stages from the first step
4960                                for stage in &first_step.stages {
4961                                    match stage {
4962                                        Stage::Filter(filter_expr) => {
4963                                            result = self.evaluate_predicate_as_stage(
4964                                                &result,
4965                                                filter_expr,
4966                                            )?;
4967                                        }
4968                                    }
4969                                }
4970
4971                                // Apply remaining path steps if any
4972                                if steps.len() > 1 {
4973                                    let remaining_path = AstNode::Path {
4974                                        steps: steps[1..].to_vec(),
4975                                    };
4976                                    result = self.evaluate_internal(&remaining_path, &result)?;
4977                                }
4978
4979                                // Unwrap singleton results from chain operator, unless there are stages
4980                                // Stages (like predicates) indicate we want to preserve array structure
4981                                if !first_step.stages.is_empty() || steps.len() > 1 {
4982                                    return Ok(result);
4983                                } else {
4984                                    return Ok(self.unwrap_singleton(result));
4985                                }
4986                            }
4987                            _ => {
4988                                // Other path types - just evaluate normally with LHS as context
4989                                let lhs_value = self.evaluate_internal(lhs, data)?;
4990                                return self
4991                                    .evaluate_internal(rhs, &lhs_value)
4992                                    .map(|v| self.unwrap_singleton(v));
4993                            }
4994                        }
4995                    }
4996
4997                    // Empty path? Shouldn't happen, but handle it
4998                    let lhs_value = self.evaluate_internal(lhs, data)?;
4999                    return self
5000                        .evaluate_internal(rhs, &lhs_value)
5001                        .map(|v| self.unwrap_singleton(v));
5002                }
5003                _ => {
5004                    return Err(EvaluatorError::TypeError(
5005                        "Right side of ~> must be a function call or function reference"
5006                            .to_string(),
5007                    ));
5008                }
5009            }
5010        }
5011
5012        // Special handling for variable binding (:=)
5013        if op == BinaryOp::ColonEqual {
5014            // Extract variable name from LHS
5015            let var_name = match lhs {
5016                AstNode::Variable(name) => name.clone(),
5017                _ => {
5018                    return Err(EvaluatorError::TypeError(
5019                        "Left side of := must be a variable".to_string(),
5020                    ))
5021                }
5022            };
5023
5024            // Check if RHS is a lambda - store it specially
5025            if let AstNode::Lambda {
5026                params,
5027                body,
5028                signature,
5029                thunk,
5030            } = rhs
5031            {
5032                // Store the lambda AST for later invocation
5033                // Capture only the free variables referenced by the lambda body
5034                let captured_env = self.capture_environment_for(body, params);
5035                let compiled_body = if !thunk {
5036                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
5037                    try_compile_expr_with_allowed_vars(body, &var_refs)
5038                } else {
5039                    None
5040                };
5041                let stored_lambda = StoredLambda {
5042                    params: params.clone(),
5043                    body: (**body).clone(),
5044                    compiled_body,
5045                    signature: signature.clone(),
5046                    captured_env,
5047                    captured_data: Some(data.clone()),
5048                    thunk: *thunk,
5049                };
5050                let lambda_params = stored_lambda.params.clone();
5051                let lambda_sig = stored_lambda.signature.clone();
5052                self.context.bind_lambda(var_name.clone(), stored_lambda);
5053
5054                // Return a lambda marker value (include _lambda_id so it can be found later)
5055                let lambda_repr = JValue::lambda(
5056                    var_name.as_str(),
5057                    lambda_params,
5058                    Some(var_name.clone()),
5059                    lambda_sig,
5060                );
5061                return Ok(lambda_repr);
5062            }
5063
5064            // Check if RHS is a pure function composition (ChainPipe between function references)
5065            // e.g., $uppertrim := $trim ~> $uppercase
5066            // This creates a lambda that composes the functions.
5067            // But NOT for data ~> function, which should be evaluated immediately.
5068            // e.g., $result := data ~> $map($fn) should evaluate the pipe
5069            if let AstNode::Binary {
5070                op: BinaryOp::ChainPipe,
5071                lhs: chain_lhs,
5072                rhs: chain_rhs,
5073            } = rhs
5074            {
5075                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
5076                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
5077                let is_function_composition = match chain_lhs.as_ref() {
5078                    // LHS is a function reference like $trim or $sum
5079                    AstNode::Variable(name)
5080                        if self.is_builtin_function(name)
5081                            || self.context.lookup_lambda(name).is_some() =>
5082                    {
5083                        true
5084                    }
5085                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
5086                    AstNode::Binary {
5087                        op: BinaryOp::ChainPipe,
5088                        ..
5089                    } => true,
5090                    // A function call with placeholder creates a partial application
5091                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
5092                    AstNode::Function { args, .. }
5093                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
5094                    {
5095                        true
5096                    }
5097                    // Anything else (data, function calls, arrays, etc.) is not pure composition
5098                    _ => false,
5099                };
5100
5101                if is_function_composition {
5102                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
5103                    // The original chain is $trim ~> $uppercase (left-associative)
5104                    // We want to create: ($ ~> $trim) ~> $uppercase
5105                    let param_name = "$".to_string();
5106
5107                    // First create $ ~> $trim
5108                    let first_pipe = AstNode::Binary {
5109                        op: BinaryOp::ChainPipe,
5110                        lhs: Box::new(AstNode::Variable(param_name.clone())),
5111                        rhs: chain_lhs.clone(),
5112                    };
5113
5114                    // Then wrap with ~> $uppercase (or the rest of the chain)
5115                    let composed_body = AstNode::Binary {
5116                        op: BinaryOp::ChainPipe,
5117                        lhs: Box::new(first_pipe),
5118                        rhs: chain_rhs.clone(),
5119                    };
5120
5121                    let stored_lambda = StoredLambda {
5122                        params: vec![param_name],
5123                        body: composed_body,
5124                        compiled_body: None, // ChainPipe body is not compilable
5125                        signature: None,
5126                        captured_env: self.capture_current_environment(),
5127                        captured_data: Some(data.clone()),
5128                        thunk: false,
5129                    };
5130                    self.context.bind_lambda(var_name.clone(), stored_lambda);
5131
5132                    // Return a lambda marker value (include _lambda_id for later lookup)
5133                    let lambda_repr = JValue::lambda(
5134                        var_name.as_str(),
5135                        vec!["$".to_string()],
5136                        Some(var_name.clone()),
5137                        None::<String>,
5138                    );
5139                    return Ok(lambda_repr);
5140                }
5141                // If not function composition, fall through to normal evaluation below
5142            }
5143
5144            // Evaluate the RHS
5145            let value = self.evaluate_internal(rhs, data)?;
5146
5147            // If the value is a lambda, copy the stored lambda to the new variable name
5148            if let Some(stored) = self.lookup_lambda_from_value(&value) {
5149                self.context.bind_lambda(var_name.clone(), stored);
5150            }
5151
5152            // Bind even if undefined (null) so inner scopes can shadow outer variables
5153            self.context.bind(var_name, value.clone());
5154            return Ok(value);
5155        }
5156
5157        // Special handling for 'In' operator - check for array filtering
5158        // Must evaluate lhs first to determine if this is array filtering
5159        if op == BinaryOp::In {
5160            let left = self.evaluate_internal(lhs, data)?;
5161
5162            // Check if this is array filtering: array[predicate]
5163            if matches!(left, JValue::Array(_)) {
5164                // Try evaluating rhs in current context to see if it's a simple index
5165                let right_result = self.evaluate_internal(rhs, data);
5166
5167                if let Ok(JValue::Number(_)) = right_result {
5168                    // Simple numeric index: array[n]
5169                    return self.array_index(&left, &right_result.unwrap());
5170                } else {
5171                    // This is array filtering: array[predicate]
5172                    // Evaluate the predicate for each array item
5173                    return self.array_filter(lhs, rhs, &left, data);
5174                }
5175            }
5176        }
5177
5178        // Special handling for logical operators (short-circuit evaluation)
5179        if op == BinaryOp::And {
5180            let left = self.evaluate_internal(lhs, data)?;
5181            if !self.is_truthy(&left) {
5182                // Short-circuit: if left is falsy, return false without evaluating right
5183                return Ok(JValue::Bool(false));
5184            }
5185            let right = self.evaluate_internal(rhs, data)?;
5186            return Ok(JValue::Bool(self.is_truthy(&right)));
5187        }
5188
5189        if op == BinaryOp::Or {
5190            let left = self.evaluate_internal(lhs, data)?;
5191            if self.is_truthy(&left) {
5192                // Short-circuit: if left is truthy, return true without evaluating right
5193                return Ok(JValue::Bool(true));
5194            }
5195            let right = self.evaluate_internal(rhs, data)?;
5196            return Ok(JValue::Bool(self.is_truthy(&right)));
5197        }
5198
5199        // Check if operands are explicit null literals (vs undefined from variables)
5200        let left_is_explicit_null = matches!(lhs, AstNode::Null);
5201        let right_is_explicit_null = matches!(rhs, AstNode::Null);
5202
5203        // Standard evaluation: evaluate both operands
5204        let left = self.evaluate_internal(lhs, data)?;
5205        let right = self.evaluate_internal(rhs, data)?;
5206
5207        match op {
5208            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
5209            BinaryOp::Subtract => {
5210                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
5211            }
5212            BinaryOp::Multiply => {
5213                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
5214            }
5215            BinaryOp::Divide => {
5216                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
5217            }
5218            BinaryOp::Modulo => {
5219                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
5220            }
5221
5222            BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
5223            BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
5224            BinaryOp::LessThan => {
5225                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
5226            }
5227            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
5228                &left,
5229                &right,
5230                left_is_explicit_null,
5231                right_is_explicit_null,
5232            ),
5233            BinaryOp::GreaterThan => {
5234                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
5235            }
5236            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
5237                &left,
5238                &right,
5239                left_is_explicit_null,
5240                right_is_explicit_null,
5241            ),
5242
5243            // And/Or handled above with short-circuit evaluation
5244            BinaryOp::And | BinaryOp::Or => unreachable!(),
5245
5246            BinaryOp::Concatenate => self.concatenate(&left, &right),
5247            BinaryOp::Range => self.range(&left, &right),
5248            BinaryOp::In => self.in_operator(&left, &right),
5249
5250            // These operators are all handled as special cases earlier in evaluate_binary_op
5251            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
5252                unreachable!()
5253            }
5254        }
5255    }
5256
5257    /// Evaluate a unary operation
5258    fn evaluate_unary_op(
5259        &mut self,
5260        op: crate::ast::UnaryOp,
5261        operand: &AstNode,
5262        data: &JValue,
5263    ) -> Result<JValue, EvaluatorError> {
5264        use crate::ast::UnaryOp;
5265
5266        let value = self.evaluate_internal(operand, data)?;
5267
5268        match op {
5269            UnaryOp::Negate => match value {
5270                // undefined returns undefined
5271                JValue::Null => Ok(JValue::Null),
5272                JValue::Number(n) => Ok(JValue::Number(-n)),
5273                _ => Err(EvaluatorError::TypeError(
5274                    "D1002: Cannot negate non-number value".to_string(),
5275                )),
5276            },
5277            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
5278        }
5279    }
5280
5281    /// Try to fuse an aggregate function call with its Path argument.
5282    /// Handles patterns like:
5283    /// - $sum(arr.field) → iterate arr, extract field, accumulate
5284    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
5285    ///
5286    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
5287    fn try_fused_aggregate(
5288        &mut self,
5289        name: &str,
5290        arg: &AstNode,
5291        data: &JValue,
5292    ) -> Result<Option<JValue>, EvaluatorError> {
5293        // Only applies to numeric aggregates
5294        if !matches!(name, "sum" | "max" | "min" | "average") {
5295            return Ok(None);
5296        }
5297
5298        // Argument must be a Path
5299        let AstNode::Path { steps } = arg else {
5300            return Ok(None);
5301        };
5302
5303        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
5304        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
5305        if steps.len() != 2 {
5306            return Ok(None);
5307        }
5308
5309        // Last step must be a simple Name (the field to extract)
5310        let field_step = &steps[1];
5311        if !field_step.stages.is_empty() {
5312            return Ok(None);
5313        }
5314        let AstNode::Name(extract_field) = &field_step.node else {
5315            return Ok(None);
5316        };
5317
5318        // First step: Name with optional filter stage
5319        let arr_step = &steps[0];
5320        let AstNode::Name(arr_name) = &arr_step.node else {
5321            return Ok(None);
5322        };
5323
5324        // Get the source array from data
5325        let arr = match data {
5326            JValue::Object(obj) => match obj.get(arr_name) {
5327                Some(JValue::Array(arr)) => arr,
5328                _ => return Ok(None),
5329            },
5330            _ => return Ok(None),
5331        };
5332
5333        // Check for filter stage — try CompiledExpr for the predicate
5334        let filter_compiled = match arr_step.stages.as_slice() {
5335            [] => None,
5336            [Stage::Filter(pred)] => try_compile_expr(pred),
5337            _ => return Ok(None),
5338        };
5339        // If filter stage exists but wasn't compilable, bail out
5340        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
5341            return Ok(None);
5342        }
5343
5344        // Build shape cache for the array
5345        let shape = arr.first().and_then(build_shape_cache);
5346
5347        // Fused iteration: filter (optional) + extract + aggregate
5348        let mut total = 0.0f64;
5349        let mut count = 0usize;
5350        let mut max_val = f64::NEG_INFINITY;
5351        let mut min_val = f64::INFINITY;
5352        let mut has_any = false;
5353
5354        for item in arr.iter() {
5355            // Apply compiled filter if present
5356            if let Some(ref compiled) = filter_compiled {
5357                let result = if let Some(ref s) = shape {
5358                    eval_compiled_shaped(compiled, item, None, s)?
5359                } else {
5360                    eval_compiled(compiled, item, None)?
5361                };
5362                if !compiled_is_truthy(&result) {
5363                    continue;
5364                }
5365            }
5366
5367            // Extract field value
5368            let val = match item {
5369                JValue::Object(obj) => match obj.get(extract_field) {
5370                    Some(JValue::Number(n)) => *n,
5371                    Some(_) | None => continue, // Skip non-numeric / missing
5372                },
5373                _ => continue,
5374            };
5375
5376            has_any = true;
5377            match name {
5378                "sum" => total += val,
5379                "max" => max_val = max_val.max(val),
5380                "min" => min_val = min_val.min(val),
5381                "average" => {
5382                    total += val;
5383                    count += 1;
5384                }
5385                _ => unreachable!(),
5386            }
5387        }
5388
5389        if !has_any {
5390            return Ok(Some(match name {
5391                "sum" => JValue::from(0i64),
5392                "average" | "max" | "min" => JValue::Null,
5393                _ => unreachable!(),
5394            }));
5395        }
5396
5397        Ok(Some(match name {
5398            "sum" => JValue::Number(total),
5399            "max" => JValue::Number(max_val),
5400            "min" => JValue::Number(min_val),
5401            "average" => JValue::Number(total / count as f64),
5402            _ => unreachable!(),
5403        }))
5404    }
5405
5406    /// Evaluate a function call
5407    fn evaluate_function_call(
5408        &mut self,
5409        name: &str,
5410        args: &[AstNode],
5411        is_builtin: bool,
5412        data: &JValue,
5413    ) -> Result<JValue, EvaluatorError> {
5414        use crate::functions;
5415
5416        // Check for partial application (any argument is a Placeholder)
5417        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
5418        if has_placeholder {
5419            return self.create_partial_application(name, args, is_builtin, data);
5420        }
5421
5422        // FIRST check if this variable holds a function value (lambda or builtin reference)
5423        // This is critical for:
5424        // 1. Allowing function parameters to shadow stored lambdas
5425        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
5426        // 2. Calling built-in functions passed as parameters
5427        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
5428        if let Some(value) = self.context.lookup(name).cloned() {
5429            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
5430                let mut evaluated_args = Vec::with_capacity(args.len());
5431                for arg in args {
5432                    evaluated_args.push(self.evaluate_internal(arg, data)?);
5433                }
5434                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
5435            }
5436            if let JValue::Builtin { name: builtin_name } = &value {
5437                // This is a built-in function reference (e.g., $f bound to $sum)
5438                let mut evaluated_args = Vec::with_capacity(args.len());
5439                for arg in args {
5440                    evaluated_args.push(self.evaluate_internal(arg, data)?);
5441                }
5442                return self.call_builtin_with_values(builtin_name, &evaluated_args);
5443            }
5444        }
5445
5446        // THEN check if this is a stored lambda (user-defined function by name)
5447        // This only applies if not shadowed by a binding above
5448        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
5449            let mut evaluated_args = Vec::with_capacity(args.len());
5450            for arg in args {
5451                evaluated_args.push(self.evaluate_internal(arg, data)?);
5452            }
5453            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
5454        }
5455
5456        // If the function was called without $ prefix and it's not a stored lambda,
5457        // it's an error (unknown function without $ prefix)
5458        if !is_builtin && name != "__lambda__" {
5459            return Err(EvaluatorError::ReferenceError(format!(
5460                "Unknown function: {}",
5461                name
5462            )));
5463        }
5464
5465        // Special handling for $exists function
5466        // It needs to know if the argument is explicit null vs undefined
5467        if name == "exists" && args.len() == 1 {
5468            let arg = &args[0];
5469
5470            // Check if it's an explicit null literal
5471            if matches!(arg, AstNode::Null) {
5472                return Ok(JValue::Bool(true)); // Explicit null exists
5473            }
5474
5475            // Check if it's a function reference
5476            if let AstNode::Variable(var_name) = arg {
5477                if self.is_builtin_function(var_name) {
5478                    return Ok(JValue::Bool(true)); // Built-in function exists
5479                }
5480
5481                // Check if it's a stored lambda
5482                if self.context.lookup_lambda(var_name).is_some() {
5483                    return Ok(JValue::Bool(true)); // Lambda exists
5484                }
5485
5486                // Check if the variable is defined
5487                if let Some(val) = self.context.lookup(var_name) {
5488                    // A variable bound to the undefined marker doesn't "exist"
5489                    if val.is_undefined() {
5490                        return Ok(JValue::Bool(false));
5491                    }
5492                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
5493                } else {
5494                    return Ok(JValue::Bool(false)); // Variable is undefined
5495                }
5496            }
5497
5498            // For other expressions, evaluate and check if non-null/non-undefined
5499            let value = self.evaluate_internal(arg, data)?;
5500            return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
5501        }
5502
5503        // Check if any arguments are undefined variables or undefined paths
5504        // Functions like $not() should return undefined when given undefined values
5505        for arg in args {
5506            // Check for undefined variable (e.g., $undefined_var)
5507            if let AstNode::Variable(var_name) = arg {
5508                // Skip built-in function names - they're function references, not undefined variables
5509                if !var_name.is_empty()
5510                    && !self.is_builtin_function(var_name)
5511                    && self.context.lookup(var_name).is_none()
5512                {
5513                    // Undefined variable - for functions that should propagate undefined
5514                    if propagates_undefined(name) {
5515                        return Ok(JValue::Null); // Return undefined
5516                    }
5517                }
5518            }
5519            // Check for simple field name (e.g., blah) that evaluates to undefined
5520            if let AstNode::Name(field_name) = arg {
5521                let field_exists =
5522                    matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
5523                if !field_exists && propagates_undefined(name) {
5524                    return Ok(JValue::Null);
5525                }
5526            }
5527            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
5528            // Field accesses are represented as AstNode::Path. String literals should never
5529            // be checked for undefined propagation.
5530            // Check for Path expressions that evaluate to undefined
5531            if let AstNode::Path { steps } = arg {
5532                // For paths that evaluate to null, we need to determine if it's because:
5533                // 1. A field doesn't exist (undefined) - should propagate as undefined
5534                // 2. A field exists with value null - should throw T0410
5535                //
5536                // We can distinguish these by checking if the path is accessing a field
5537                // that doesn't exist on an object vs one that has an explicit null value.
5538                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
5539                    // Path evaluated to null - now check if it's truly undefined
5540                    // For single-step paths, check if the field exists
5541                    if steps.len() == 1 {
5542                        // Get field name - could be Name (identifier) or String (quoted)
5543                        let field_name = match &steps[0].node {
5544                            AstNode::Name(n) => Some(n.as_str()),
5545                            AstNode::String(s) => Some(s.as_str()),
5546                            _ => None,
5547                        };
5548                        if let Some(field) = field_name {
5549                            match data {
5550                                JValue::Object(obj) => {
5551                                    if !obj.contains_key(field) {
5552                                        // Field doesn't exist - return undefined
5553                                        if propagates_undefined(name) {
5554                                            return Ok(JValue::Null);
5555                                        }
5556                                    }
5557                                    // Field exists with value null - continue to throw T0410
5558                                }
5559                                JValue::Null => {
5560                                    // Trying to access field on null data - return undefined
5561                                    if propagates_undefined(name) {
5562                                        return Ok(JValue::Null);
5563                                    }
5564                                }
5565                                _ => {}
5566                            }
5567                        }
5568                    }
5569                    // For multi-step paths, check if any intermediate step failed
5570                    else if steps.len() > 1 {
5571                        // Evaluate each step to find where it breaks
5572                        let mut current = data;
5573                        let mut failed_due_to_missing_field = false;
5574
5575                        for (i, step) in steps.iter().enumerate() {
5576                            if let AstNode::Name(field_name) = &step.node {
5577                                match current {
5578                                    JValue::Object(obj) => {
5579                                        if let Some(val) = obj.get(field_name) {
5580                                            current = val;
5581                                        } else {
5582                                            // Field doesn't exist
5583                                            failed_due_to_missing_field = true;
5584                                            break;
5585                                        }
5586                                    }
5587                                    JValue::Array(_) => {
5588                                        // Array access - evaluate normally
5589                                        break;
5590                                    }
5591                                    JValue::Null => {
5592                                        // Hit null in the middle of the path
5593                                        if i > 0 {
5594                                            // Previous field had null value - not undefined
5595                                            failed_due_to_missing_field = false;
5596                                        }
5597                                        break;
5598                                    }
5599                                    _ => break,
5600                                }
5601                            }
5602                        }
5603
5604                        if failed_due_to_missing_field && propagates_undefined(name) {
5605                            return Ok(JValue::Null);
5606                        }
5607                    }
5608                }
5609            }
5610        }
5611
5612        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
5613        // try to fuse filter+extract+aggregate into a single pass.
5614        if args.len() == 1 {
5615            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
5616                return Ok(result);
5617            }
5618        }
5619
5620        let mut evaluated_args = Vec::with_capacity(args.len());
5621        for arg in args {
5622            evaluated_args.push(self.evaluate_internal(arg, data)?);
5623        }
5624
5625        // JSONata feature: when a function is called with no arguments but expects
5626        // at least one, use the current context value (data) as the implicit first argument
5627        // This also applies when functions expecting N arguments receive N-1 arguments,
5628        // in which case the context value becomes the first argument
5629        let context_functions_zero_arg = ["string", "number", "boolean", "uppercase", "lowercase"];
5630        let context_functions_missing_first = [
5631            "substringBefore",
5632            "substringAfter",
5633            "contains",
5634            "split",
5635            "replace",
5636        ];
5637
5638        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
5639            // Use the current context value as the implicit argument
5640            evaluated_args.push(data.clone());
5641        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
5642            // These functions expect 2+ arguments, but received 1
5643            // Only insert context if it's a compatible type (string for string functions)
5644            // Otherwise, let the function throw T0411 for wrong argument count
5645            if matches!(data, JValue::String(_)) {
5646                evaluated_args.insert(0, data.clone());
5647            }
5648        }
5649
5650        // Special handling for $string() with no explicit arguments
5651        // After context insertion, check if the argument is null (undefined context)
5652        if name == "string"
5653            && args.is_empty()
5654            && !evaluated_args.is_empty()
5655            && evaluated_args[0].is_null()
5656        {
5657            // Context was null/undefined, so return undefined
5658            return Ok(JValue::Null);
5659        }
5660
5661        match name {
5662            "string" => {
5663                if evaluated_args.len() > 2 {
5664                    return Err(EvaluatorError::EvaluationError(
5665                        "string() takes at most 2 arguments".to_string(),
5666                    ));
5667                }
5668
5669                let prettify = if evaluated_args.len() == 2 {
5670                    match &evaluated_args[1] {
5671                        JValue::Bool(b) => Some(*b),
5672                        _ => {
5673                            return Err(EvaluatorError::TypeError(
5674                                "string() prettify parameter must be a boolean".to_string(),
5675                            ))
5676                        }
5677                    }
5678                } else {
5679                    None
5680                };
5681
5682                Ok(functions::string::string(&evaluated_args[0], prettify)?)
5683            }
5684            "length" => {
5685                if evaluated_args.len() != 1 {
5686                    return Err(EvaluatorError::EvaluationError(
5687                        "length() requires exactly 1 argument".to_string(),
5688                    ));
5689                }
5690                match &evaluated_args[0] {
5691                    JValue::String(s) => Ok(functions::string::length(s)?),
5692                    _ => Err(EvaluatorError::TypeError(
5693                        "T0410: Argument 1 of function length does not match function signature"
5694                            .to_string(),
5695                    )),
5696                }
5697            }
5698            "uppercase" => {
5699                if evaluated_args.len() != 1 {
5700                    return Err(EvaluatorError::EvaluationError(
5701                        "uppercase() requires exactly 1 argument".to_string(),
5702                    ));
5703                }
5704                match &evaluated_args[0] {
5705                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
5706                    _ => Err(EvaluatorError::TypeError(
5707                        "T0410: Argument 1 of function uppercase does not match function signature"
5708                            .to_string(),
5709                    )),
5710                }
5711            }
5712            "lowercase" => {
5713                if evaluated_args.len() != 1 {
5714                    return Err(EvaluatorError::EvaluationError(
5715                        "lowercase() requires exactly 1 argument".to_string(),
5716                    ));
5717                }
5718                match &evaluated_args[0] {
5719                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
5720                    _ => Err(EvaluatorError::TypeError(
5721                        "T0410: Argument 1 of function lowercase does not match function signature"
5722                            .to_string(),
5723                    )),
5724                }
5725            }
5726            "number" => {
5727                if evaluated_args.is_empty() {
5728                    return Err(EvaluatorError::EvaluationError(
5729                        "number() requires at least 1 argument".to_string(),
5730                    ));
5731                }
5732                if evaluated_args.len() > 1 {
5733                    return Err(EvaluatorError::TypeError(
5734                        "T0410: Argument 2 of function number does not match function signature"
5735                            .to_string(),
5736                    ));
5737                }
5738                Ok(functions::numeric::number(&evaluated_args[0])?)
5739            }
5740            "sum" => {
5741                if evaluated_args.len() != 1 {
5742                    return Err(EvaluatorError::EvaluationError(
5743                        "sum() requires exactly 1 argument".to_string(),
5744                    ));
5745                }
5746                // Return undefined if argument is undefined
5747                if evaluated_args[0].is_undefined() {
5748                    return Ok(JValue::Undefined);
5749                }
5750                match &evaluated_args[0] {
5751                    JValue::Null => Ok(JValue::Null),
5752                    JValue::Array(arr) => {
5753                        // Use zero-clone iterator-based sum
5754                        Ok(aggregation::sum(arr)?)
5755                    }
5756                    // Non-array values: extract number directly
5757                    JValue::Number(n) => Ok(JValue::Number(*n)),
5758                    other => Ok(functions::numeric::sum(&[other.clone()])?),
5759                }
5760            }
5761            "count" => {
5762                if evaluated_args.len() != 1 {
5763                    return Err(EvaluatorError::EvaluationError(
5764                        "count() requires exactly 1 argument".to_string(),
5765                    ));
5766                }
5767                // Return 0 if argument is undefined
5768                if evaluated_args[0].is_undefined() {
5769                    return Ok(JValue::from(0i64));
5770                }
5771                match &evaluated_args[0] {
5772                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
5773                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
5774                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
5775                }
5776            }
5777            "substring" => {
5778                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
5779                    return Err(EvaluatorError::EvaluationError(
5780                        "substring() requires 2 or 3 arguments".to_string(),
5781                    ));
5782                }
5783                match (&evaluated_args[0], &evaluated_args[1]) {
5784                    (JValue::String(s), JValue::Number(start)) => {
5785                        let length = if evaluated_args.len() == 3 {
5786                            match &evaluated_args[2] {
5787                                JValue::Number(l) => Some(*l as i64),
5788                                _ => return Err(EvaluatorError::TypeError(
5789                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
5790                                )),
5791                            }
5792                        } else {
5793                            None
5794                        };
5795                        Ok(functions::string::substring(s, *start as i64, length)?)
5796                    }
5797                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5798                        "T0410: Argument 2 of function substring does not match function signature"
5799                            .to_string(),
5800                    )),
5801                    _ => Err(EvaluatorError::TypeError(
5802                        "T0410: Argument 1 of function substring does not match function signature"
5803                            .to_string(),
5804                    )),
5805                }
5806            }
5807            "substringBefore" => {
5808                if evaluated_args.len() != 2 {
5809                    return Err(EvaluatorError::TypeError(
5810                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
5811                    ));
5812                }
5813                match (&evaluated_args[0], &evaluated_args[1]) {
5814                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
5815                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5816                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
5817                    )),
5818                    _ => Err(EvaluatorError::TypeError(
5819                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
5820                    )),
5821                }
5822            }
5823            "substringAfter" => {
5824                if evaluated_args.len() != 2 {
5825                    return Err(EvaluatorError::TypeError(
5826                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
5827                    ));
5828                }
5829                match (&evaluated_args[0], &evaluated_args[1]) {
5830                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
5831                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
5832                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
5833                    )),
5834                    _ => Err(EvaluatorError::TypeError(
5835                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
5836                    )),
5837                }
5838            }
5839            "pad" => {
5840                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
5841                    return Err(EvaluatorError::EvaluationError(
5842                        "pad() requires 2 or 3 arguments".to_string(),
5843                    ));
5844                }
5845
5846                // First argument: string to pad
5847                let string = match &evaluated_args[0] {
5848                    JValue::String(s) => s.clone(),
5849                    JValue::Null => return Ok(JValue::Null),
5850                    _ => {
5851                        return Err(EvaluatorError::TypeError(
5852                            "pad() first argument must be a string".to_string(),
5853                        ))
5854                    }
5855                };
5856
5857                // Second argument: width (negative = left pad, positive = right pad)
5858                let width = match &evaluated_args.get(1) {
5859                    Some(JValue::Number(n)) => *n as i32,
5860                    _ => {
5861                        return Err(EvaluatorError::TypeError(
5862                            "pad() second argument must be a number".to_string(),
5863                        ))
5864                    }
5865                };
5866
5867                // Third argument: padding string (optional, defaults to space)
5868                let pad_string = match evaluated_args.get(2) {
5869                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
5870                    _ => Rc::from(" "),
5871                };
5872
5873                let abs_width = width.unsigned_abs() as usize;
5874                // Count Unicode characters (code points), not bytes
5875                let char_count = string.chars().count();
5876
5877                if char_count >= abs_width {
5878                    // String is already long enough
5879                    return Ok(JValue::string(string));
5880                }
5881
5882                let padding_needed = abs_width - char_count;
5883
5884                let pad_chars: Vec<char> = pad_string.chars().collect();
5885                let mut padding = String::with_capacity(padding_needed);
5886                for i in 0..padding_needed {
5887                    padding.push(pad_chars[i % pad_chars.len()]);
5888                }
5889
5890                let result = if width < 0 {
5891                    // Left pad (negative width)
5892                    format!("{}{}", padding, string)
5893                } else {
5894                    // Right pad (positive width)
5895                    format!("{}{}", string, padding)
5896                };
5897
5898                Ok(JValue::string(result))
5899            }
5900
5901            "trim" => {
5902                if evaluated_args.is_empty() {
5903                    return Ok(JValue::Null); // undefined
5904                }
5905                if evaluated_args.len() != 1 {
5906                    return Err(EvaluatorError::EvaluationError(
5907                        "trim() requires at most 1 argument".to_string(),
5908                    ));
5909                }
5910                match &evaluated_args[0] {
5911                    JValue::Null => Ok(JValue::Null),
5912                    JValue::String(s) => Ok(functions::string::trim(s)?),
5913                    _ => Err(EvaluatorError::TypeError(
5914                        "trim() requires a string argument".to_string(),
5915                    )),
5916                }
5917            }
5918            "contains" => {
5919                if evaluated_args.len() != 2 {
5920                    return Err(EvaluatorError::EvaluationError(
5921                        "contains() requires exactly 2 arguments".to_string(),
5922                    ));
5923                }
5924                if evaluated_args[0].is_null() {
5925                    return Ok(JValue::Null);
5926                }
5927                match &evaluated_args[0] {
5928                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
5929                    _ => Err(EvaluatorError::TypeError(
5930                        "contains() requires a string as the first argument".to_string(),
5931                    )),
5932                }
5933            }
5934            "split" => {
5935                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
5936                    return Err(EvaluatorError::EvaluationError(
5937                        "split() requires 2 or 3 arguments".to_string(),
5938                    ));
5939                }
5940                if evaluated_args[0].is_null() {
5941                    return Ok(JValue::Null);
5942                }
5943                match &evaluated_args[0] {
5944                    JValue::String(s) => {
5945                        let limit = if evaluated_args.len() == 3 {
5946                            match &evaluated_args[2] {
5947                                JValue::Number(n) => {
5948                                    let f = *n;
5949                                    // Negative limit is an error
5950                                    if f < 0.0 {
5951                                        return Err(EvaluatorError::EvaluationError(
5952                                            "D3020: Third argument of split function must be a positive number".to_string(),
5953                                        ));
5954                                    }
5955                                    // Floor the value for non-integer limits
5956                                    Some(f.floor() as usize)
5957                                }
5958                                _ => {
5959                                    return Err(EvaluatorError::TypeError(
5960                                        "split() limit must be a number".to_string(),
5961                                    ))
5962                                }
5963                            }
5964                        } else {
5965                            None
5966                        };
5967                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
5968                    }
5969                    _ => Err(EvaluatorError::TypeError(
5970                        "split() requires a string as the first argument".to_string(),
5971                    )),
5972                }
5973            }
5974            "join" => {
5975                // Special case: if first arg is undefined, return undefined
5976                // But if separator (2nd arg) is undefined, use empty string (default)
5977                if evaluated_args.is_empty() {
5978                    return Err(EvaluatorError::TypeError(
5979                        "T0410: Argument 1 of function $join does not match function signature"
5980                            .to_string(),
5981                    ));
5982                }
5983                if evaluated_args[0].is_null() {
5984                    return Ok(JValue::Null);
5985                }
5986
5987                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
5988                // The signature handles coercion and validation
5989                use crate::signature::Signature;
5990
5991                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
5992                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
5993                })?;
5994
5995                let coerced_args = match signature.validate_and_coerce(&evaluated_args) {
5996                    Ok(args) => args,
5997                    Err(crate::signature::SignatureError::UndefinedArgument) => {
5998                        // This can happen if the separator is undefined
5999                        // In that case, just validate the first arg and use default separator
6000                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
6001                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
6002                        })?;
6003
6004                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1]) {
6005                            Ok(args) => args,
6006                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
6007                                index,
6008                                expected,
6009                            }) => {
6010                                return Err(EvaluatorError::TypeError(format!(
6011                                    "T0412: Argument {} of function $join must be an array of {}",
6012                                    index, expected
6013                                )));
6014                            }
6015                            Err(e) => {
6016                                return Err(EvaluatorError::TypeError(format!(
6017                                    "Signature validation failed: {}",
6018                                    e
6019                                )));
6020                            }
6021                        }
6022                    }
6023                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
6024                        index,
6025                        expected,
6026                    }) => {
6027                        return Err(EvaluatorError::TypeError(
6028                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
6029                        ));
6030                    }
6031                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
6032                        index,
6033                        expected,
6034                    }) => {
6035                        return Err(EvaluatorError::TypeError(format!(
6036                            "T0412: Argument {} of function $join must be an array of {}",
6037                            index, expected
6038                        )));
6039                    }
6040                    Err(e) => {
6041                        return Err(EvaluatorError::TypeError(format!(
6042                            "Signature validation failed: {}",
6043                            e
6044                        )));
6045                    }
6046                };
6047
6048                // After coercion, first arg is guaranteed to be an array of strings
6049                match &coerced_args[0] {
6050                    JValue::Array(arr) => {
6051                        let separator = if coerced_args.len() == 2 {
6052                            match &coerced_args[1] {
6053                                JValue::String(s) => Some(&**s),
6054                                JValue::Null => None, // Undefined separator -> use empty string
6055                                _ => None,            // Signature should have validated this
6056                            }
6057                        } else {
6058                            None // No separator provided -> use empty string
6059                        };
6060                        Ok(functions::string::join(arr, separator)?)
6061                    }
6062                    JValue::Null => Ok(JValue::Null),
6063                    _ => unreachable!("Signature validation should ensure array type"),
6064                }
6065            }
6066            "replace" => {
6067                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
6068                    return Err(EvaluatorError::EvaluationError(
6069                        "replace() requires 3 or 4 arguments".to_string(),
6070                    ));
6071                }
6072                if evaluated_args[0].is_null() {
6073                    return Ok(JValue::Null);
6074                }
6075
6076                // Check if replacement (3rd arg) is a function/lambda
6077                let replacement_is_lambda = matches!(
6078                    evaluated_args[2],
6079                    JValue::Lambda { .. } | JValue::Builtin { .. }
6080                );
6081
6082                if replacement_is_lambda {
6083                    // Lambda replacement mode
6084                    return self.replace_with_lambda(
6085                        &evaluated_args[0],
6086                        &evaluated_args[1],
6087                        &evaluated_args[2],
6088                        if evaluated_args.len() == 4 {
6089                            Some(&evaluated_args[3])
6090                        } else {
6091                            None
6092                        },
6093                        data,
6094                    );
6095                }
6096
6097                // String replacement mode
6098                match (&evaluated_args[0], &evaluated_args[2]) {
6099                    (JValue::String(s), JValue::String(replacement)) => {
6100                        let limit = if evaluated_args.len() == 4 {
6101                            match &evaluated_args[3] {
6102                                JValue::Number(n) => {
6103                                    let lim_f64 = *n;
6104                                    if lim_f64 < 0.0 {
6105                                        return Err(EvaluatorError::EvaluationError(format!(
6106                                            "D3011: Limit must be non-negative, got {}",
6107                                            lim_f64
6108                                        )));
6109                                    }
6110                                    Some(lim_f64 as usize)
6111                                }
6112                                _ => {
6113                                    return Err(EvaluatorError::TypeError(
6114                                        "replace() limit must be a number".to_string(),
6115                                    ))
6116                                }
6117                            }
6118                        } else {
6119                            None
6120                        };
6121                        Ok(functions::string::replace(
6122                            s,
6123                            &evaluated_args[1],
6124                            replacement,
6125                            limit,
6126                        )?)
6127                    }
6128                    _ => Err(EvaluatorError::TypeError(
6129                        "replace() requires string arguments".to_string(),
6130                    )),
6131                }
6132            }
6133            "match" => {
6134                // $match(str, pattern [, limit])
6135                // Returns array of match objects for regex matches or custom matcher function
6136                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
6137                    return Err(EvaluatorError::EvaluationError(
6138                        "match() requires 1 to 3 arguments".to_string(),
6139                    ));
6140                }
6141                if evaluated_args[0].is_null() {
6142                    return Ok(JValue::Null);
6143                }
6144
6145                let s = match &evaluated_args[0] {
6146                    JValue::String(s) => s.clone(),
6147                    _ => {
6148                        return Err(EvaluatorError::TypeError(
6149                            "match() first argument must be a string".to_string(),
6150                        ))
6151                    }
6152                };
6153
6154                // Get optional limit
6155                let limit = if evaluated_args.len() == 3 {
6156                    match &evaluated_args[2] {
6157                        JValue::Number(n) => Some(*n as usize),
6158                        JValue::Null => None,
6159                        _ => {
6160                            return Err(EvaluatorError::TypeError(
6161                                "match() limit must be a number".to_string(),
6162                            ))
6163                        }
6164                    }
6165                } else {
6166                    None
6167                };
6168
6169                // Check if second argument is a custom matcher function (lambda)
6170                let pattern_value = evaluated_args.get(1);
6171                let is_custom_matcher = pattern_value.is_some_and(|val| {
6172                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
6173                });
6174
6175                if is_custom_matcher {
6176                    // Custom matcher function support
6177                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
6178                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
6179                }
6180
6181                // Get regex pattern from second argument
6182                let (pattern, flags) = match pattern_value {
6183                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
6184                        EvaluatorError::TypeError(
6185                            "match() second argument must be a regex pattern or matcher function"
6186                                .to_string(),
6187                        )
6188                    })?,
6189                    None => (".*".to_string(), "".to_string()),
6190                };
6191
6192                // Build regex
6193                let is_global = flags.contains('g');
6194                let regex_pattern = if flags.contains('i') {
6195                    format!("(?i){}", pattern)
6196                } else {
6197                    pattern.clone()
6198                };
6199
6200                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
6201                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
6202                })?;
6203
6204                let mut results = Vec::new();
6205                let mut count = 0;
6206
6207                for caps in re.captures_iter(&s) {
6208                    if let Some(lim) = limit {
6209                        if count >= lim {
6210                            break;
6211                        }
6212                    }
6213
6214                    let full_match = caps.get(0).unwrap();
6215                    let mut match_obj = IndexMap::new();
6216                    match_obj.insert(
6217                        "match".to_string(),
6218                        JValue::string(full_match.as_str().to_string()),
6219                    );
6220                    match_obj.insert(
6221                        "index".to_string(),
6222                        JValue::Number(full_match.start() as f64),
6223                    );
6224
6225                    // Collect capture groups
6226                    let mut groups: Vec<JValue> = Vec::new();
6227                    for i in 1..caps.len() {
6228                        if let Some(group) = caps.get(i) {
6229                            groups.push(JValue::string(group.as_str().to_string()));
6230                        } else {
6231                            groups.push(JValue::Null);
6232                        }
6233                    }
6234                    if !groups.is_empty() {
6235                        match_obj.insert("groups".to_string(), JValue::array(groups));
6236                    }
6237
6238                    results.push(JValue::object(match_obj));
6239                    count += 1;
6240
6241                    // If not global, only return first match
6242                    if !is_global {
6243                        break;
6244                    }
6245                }
6246
6247                if results.is_empty() {
6248                    Ok(JValue::Null)
6249                } else if results.len() == 1 && !is_global {
6250                    // Single match (non-global) returns the match object directly
6251                    Ok(results.into_iter().next().unwrap())
6252                } else {
6253                    Ok(JValue::array(results))
6254                }
6255            }
6256            "max" => {
6257                if evaluated_args.len() != 1 {
6258                    return Err(EvaluatorError::EvaluationError(
6259                        "max() requires exactly 1 argument".to_string(),
6260                    ));
6261                }
6262                // Check for undefined
6263                if evaluated_args[0].is_undefined() {
6264                    return Ok(JValue::Undefined);
6265                }
6266                match &evaluated_args[0] {
6267                    JValue::Null => Ok(JValue::Null),
6268                    JValue::Array(arr) => {
6269                        // Use zero-clone iterator-based max
6270                        Ok(aggregation::max(arr)?)
6271                    }
6272                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
6273                    _ => Err(EvaluatorError::TypeError(
6274                        "max() requires an array or number argument".to_string(),
6275                    )),
6276                }
6277            }
6278            "min" => {
6279                if evaluated_args.len() != 1 {
6280                    return Err(EvaluatorError::EvaluationError(
6281                        "min() requires exactly 1 argument".to_string(),
6282                    ));
6283                }
6284                // Check for undefined
6285                if evaluated_args[0].is_undefined() {
6286                    return Ok(JValue::Undefined);
6287                }
6288                match &evaluated_args[0] {
6289                    JValue::Null => Ok(JValue::Null),
6290                    JValue::Array(arr) => {
6291                        // Use zero-clone iterator-based min
6292                        Ok(aggregation::min(arr)?)
6293                    }
6294                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
6295                    _ => Err(EvaluatorError::TypeError(
6296                        "min() requires an array or number argument".to_string(),
6297                    )),
6298                }
6299            }
6300            "average" => {
6301                if evaluated_args.len() != 1 {
6302                    return Err(EvaluatorError::EvaluationError(
6303                        "average() requires exactly 1 argument".to_string(),
6304                    ));
6305                }
6306                // Return undefined if argument is undefined
6307                if evaluated_args[0].is_undefined() {
6308                    return Ok(JValue::Undefined);
6309                }
6310                match &evaluated_args[0] {
6311                    JValue::Null => Ok(JValue::Null),
6312                    JValue::Array(arr) => {
6313                        // Use zero-clone iterator-based average
6314                        Ok(aggregation::average(arr)?)
6315                    }
6316                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
6317                    _ => Err(EvaluatorError::TypeError(
6318                        "average() requires an array or number argument".to_string(),
6319                    )),
6320                }
6321            }
6322            "abs" => {
6323                if evaluated_args.len() != 1 {
6324                    return Err(EvaluatorError::EvaluationError(
6325                        "abs() requires exactly 1 argument".to_string(),
6326                    ));
6327                }
6328                match &evaluated_args[0] {
6329                    JValue::Null => Ok(JValue::Null),
6330                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
6331                    _ => Err(EvaluatorError::TypeError(
6332                        "abs() requires a number argument".to_string(),
6333                    )),
6334                }
6335            }
6336            "floor" => {
6337                if evaluated_args.len() != 1 {
6338                    return Err(EvaluatorError::EvaluationError(
6339                        "floor() requires exactly 1 argument".to_string(),
6340                    ));
6341                }
6342                match &evaluated_args[0] {
6343                    JValue::Null => Ok(JValue::Null),
6344                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
6345                    _ => Err(EvaluatorError::TypeError(
6346                        "floor() requires a number argument".to_string(),
6347                    )),
6348                }
6349            }
6350            "ceil" => {
6351                if evaluated_args.len() != 1 {
6352                    return Err(EvaluatorError::EvaluationError(
6353                        "ceil() requires exactly 1 argument".to_string(),
6354                    ));
6355                }
6356                match &evaluated_args[0] {
6357                    JValue::Null => Ok(JValue::Null),
6358                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
6359                    _ => Err(EvaluatorError::TypeError(
6360                        "ceil() requires a number argument".to_string(),
6361                    )),
6362                }
6363            }
6364            "round" => {
6365                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6366                    return Err(EvaluatorError::EvaluationError(
6367                        "round() requires 1 or 2 arguments".to_string(),
6368                    ));
6369                }
6370                match &evaluated_args[0] {
6371                    JValue::Null => Ok(JValue::Null),
6372                    JValue::Number(n) => {
6373                        let precision = if evaluated_args.len() == 2 {
6374                            match &evaluated_args[1] {
6375                                JValue::Number(p) => Some(*p as i32),
6376                                _ => {
6377                                    return Err(EvaluatorError::TypeError(
6378                                        "round() precision must be a number".to_string(),
6379                                    ))
6380                                }
6381                            }
6382                        } else {
6383                            None
6384                        };
6385                        Ok(functions::numeric::round(*n, precision)?)
6386                    }
6387                    _ => Err(EvaluatorError::TypeError(
6388                        "round() requires a number argument".to_string(),
6389                    )),
6390                }
6391            }
6392            "sqrt" => {
6393                if evaluated_args.len() != 1 {
6394                    return Err(EvaluatorError::EvaluationError(
6395                        "sqrt() requires exactly 1 argument".to_string(),
6396                    ));
6397                }
6398                match &evaluated_args[0] {
6399                    JValue::Null => Ok(JValue::Null),
6400                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
6401                    _ => Err(EvaluatorError::TypeError(
6402                        "sqrt() requires a number argument".to_string(),
6403                    )),
6404                }
6405            }
6406            "power" => {
6407                if evaluated_args.len() != 2 {
6408                    return Err(EvaluatorError::EvaluationError(
6409                        "power() requires exactly 2 arguments".to_string(),
6410                    ));
6411                }
6412                if evaluated_args[0].is_null() {
6413                    return Ok(JValue::Null);
6414                }
6415                match (&evaluated_args[0], &evaluated_args[1]) {
6416                    (JValue::Number(base), JValue::Number(exp)) => {
6417                        Ok(functions::numeric::power(*base, *exp)?)
6418                    }
6419                    _ => Err(EvaluatorError::TypeError(
6420                        "power() requires number arguments".to_string(),
6421                    )),
6422                }
6423            }
6424            "formatNumber" => {
6425                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6426                    return Err(EvaluatorError::EvaluationError(
6427                        "formatNumber() requires 2 or 3 arguments".to_string(),
6428                    ));
6429                }
6430                if evaluated_args[0].is_null() {
6431                    return Ok(JValue::Null);
6432                }
6433                match (&evaluated_args[0], &evaluated_args[1]) {
6434                    (JValue::Number(num), JValue::String(picture)) => {
6435                        let options = if evaluated_args.len() == 3 {
6436                            Some(&evaluated_args[2])
6437                        } else {
6438                            None
6439                        };
6440                        Ok(functions::numeric::format_number(*num, picture, options)?)
6441                    }
6442                    _ => Err(EvaluatorError::TypeError(
6443                        "formatNumber() requires a number and a string".to_string(),
6444                    )),
6445                }
6446            }
6447            "formatBase" => {
6448                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6449                    return Err(EvaluatorError::EvaluationError(
6450                        "formatBase() requires 1 or 2 arguments".to_string(),
6451                    ));
6452                }
6453                // Handle undefined input
6454                if evaluated_args[0].is_null() {
6455                    return Ok(JValue::Null);
6456                }
6457                match &evaluated_args[0] {
6458                    JValue::Number(num) => {
6459                        let radix = if evaluated_args.len() == 2 {
6460                            match &evaluated_args[1] {
6461                                JValue::Number(r) => Some(r.trunc() as i64),
6462                                _ => {
6463                                    return Err(EvaluatorError::TypeError(
6464                                        "formatBase() radix must be a number".to_string(),
6465                                    ))
6466                                }
6467                            }
6468                        } else {
6469                            None
6470                        };
6471                        Ok(functions::numeric::format_base(*num, radix)?)
6472                    }
6473                    _ => Err(EvaluatorError::TypeError(
6474                        "formatBase() requires a number".to_string(),
6475                    )),
6476                }
6477            }
6478            "append" => {
6479                if evaluated_args.len() != 2 {
6480                    return Err(EvaluatorError::EvaluationError(
6481                        "append() requires exactly 2 arguments".to_string(),
6482                    ));
6483                }
6484                // Handle null/undefined arguments
6485                let first = &evaluated_args[0];
6486                let second = &evaluated_args[1];
6487
6488                // If second arg is null, return first as-is (no change)
6489                if second.is_null() {
6490                    return Ok(first.clone());
6491                }
6492
6493                // If first arg is null, return second as-is (appending to nothing gives second)
6494                if first.is_null() {
6495                    return Ok(second.clone());
6496                }
6497
6498                // Convert both to arrays if needed, then append
6499                let arr = match first {
6500                    JValue::Array(a) => a.to_vec(),
6501                    other => vec![other.clone()], // Wrap non-array in array
6502                };
6503
6504                Ok(functions::array::append(&arr, second)?)
6505            }
6506            "reverse" => {
6507                if evaluated_args.len() != 1 {
6508                    return Err(EvaluatorError::EvaluationError(
6509                        "reverse() requires exactly 1 argument".to_string(),
6510                    ));
6511                }
6512                match &evaluated_args[0] {
6513                    JValue::Null => Ok(JValue::Null), // undefined returns undefined
6514                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
6515                    _ => Err(EvaluatorError::TypeError(
6516                        "reverse() requires an array argument".to_string(),
6517                    )),
6518                }
6519            }
6520            "shuffle" => {
6521                if evaluated_args.len() != 1 {
6522                    return Err(EvaluatorError::EvaluationError(
6523                        "shuffle() requires exactly 1 argument".to_string(),
6524                    ));
6525                }
6526                if evaluated_args[0].is_null() {
6527                    return Ok(JValue::Null);
6528                }
6529                match &evaluated_args[0] {
6530                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
6531                    _ => Err(EvaluatorError::TypeError(
6532                        "shuffle() requires an array argument".to_string(),
6533                    )),
6534                }
6535            }
6536
6537            "sift" => {
6538                // $sift(object, function) or $sift(function) - filter object by predicate
6539                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6540                    return Err(EvaluatorError::EvaluationError(
6541                        "sift() requires 1 or 2 arguments".to_string(),
6542                    ));
6543                }
6544
6545                // Determine which argument is the function
6546                let func_arg = if evaluated_args.len() == 1 {
6547                    &args[0]
6548                } else {
6549                    &args[1]
6550                };
6551
6552                // Detect how many parameters the callback expects
6553                let param_count = self.get_callback_param_count(func_arg);
6554
6555                // Helper function to sift a single object
6556                let sift_object = |evaluator: &mut Self,
6557                                   obj: &IndexMap<String, JValue>,
6558                                   func_node: &AstNode,
6559                                   context_data: &JValue,
6560                                   param_count: usize|
6561                 -> Result<JValue, EvaluatorError> {
6562                    // Only create the object value if callback uses 3 parameters
6563                    let obj_value = if param_count >= 3 {
6564                        Some(JValue::object(obj.clone()))
6565                    } else {
6566                        None
6567                    };
6568
6569                    let mut result = IndexMap::new();
6570                    for (key, value) in obj.iter() {
6571                        // Build argument list based on what callback expects
6572                        let call_args = match param_count {
6573                            1 => vec![value.clone()],
6574                            2 => vec![value.clone(), JValue::string(key.clone())],
6575                            _ => vec![
6576                                value.clone(),
6577                                JValue::string(key.clone()),
6578                                obj_value.as_ref().unwrap().clone(),
6579                            ],
6580                        };
6581
6582                        let pred_result =
6583                            evaluator.apply_function(func_node, &call_args, context_data)?;
6584                        if evaluator.is_truthy(&pred_result) {
6585                            result.insert(key.clone(), value.clone());
6586                        }
6587                    }
6588                    // Return undefined for empty results (will be filtered by function application)
6589                    if result.is_empty() {
6590                        Ok(JValue::Undefined)
6591                    } else {
6592                        Ok(JValue::object(result))
6593                    }
6594                };
6595
6596                // Handle partial application - if only 1 arg, use current context as object
6597                if evaluated_args.len() == 1 {
6598                    // $sift(function) - use current context data as object
6599                    match data {
6600                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
6601                        JValue::Array(arr) => {
6602                            // Map sift over each object in the array
6603                            let mut results = Vec::new();
6604                            for item in arr.iter() {
6605                                if let JValue::Object(o) = item {
6606                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
6607                                    // sift_object returns undefined for empty results
6608                                    if !sifted.is_undefined() {
6609                                        results.push(sifted);
6610                                    }
6611                                }
6612                            }
6613                            Ok(JValue::array(results))
6614                        }
6615                        JValue::Null => Ok(JValue::Null),
6616                        _ => Ok(JValue::Undefined),
6617                    }
6618                } else {
6619                    // $sift(object, function)
6620                    match &evaluated_args[0] {
6621                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
6622                        JValue::Null => Ok(JValue::Null),
6623                        _ => Err(EvaluatorError::TypeError(
6624                            "sift() first argument must be an object".to_string(),
6625                        )),
6626                    }
6627                }
6628            }
6629
6630            "zip" => {
6631                if evaluated_args.is_empty() {
6632                    return Err(EvaluatorError::EvaluationError(
6633                        "zip() requires at least 1 argument".to_string(),
6634                    ));
6635                }
6636
6637                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
6638                // If any argument is null/undefined, return empty array
6639                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
6640                for arg in &evaluated_args {
6641                    match arg {
6642                        JValue::Array(arr) => {
6643                            if arr.is_empty() {
6644                                // Empty array means result is empty
6645                                return Ok(JValue::array(vec![]));
6646                            }
6647                            arrays.push(arr.to_vec());
6648                        }
6649                        JValue::Null => {
6650                            // Null/undefined means result is empty
6651                            return Ok(JValue::array(vec![]));
6652                        }
6653                        other => {
6654                            // Wrap non-array values in single-element array
6655                            arrays.push(vec![other.clone()]);
6656                        }
6657                    }
6658                }
6659
6660                if arrays.is_empty() {
6661                    return Ok(JValue::array(vec![]));
6662                }
6663
6664                // Find the length of the shortest array
6665                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
6666
6667                // Zip the arrays together
6668                let mut result = Vec::with_capacity(min_len);
6669                for i in 0..min_len {
6670                    let mut tuple = Vec::with_capacity(arrays.len());
6671                    for array in &arrays {
6672                        tuple.push(array[i].clone());
6673                    }
6674                    result.push(JValue::array(tuple));
6675                }
6676
6677                Ok(JValue::array(result))
6678            }
6679
6680            "sort" => {
6681                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
6682                    return Err(EvaluatorError::EvaluationError(
6683                        "sort() requires 1 or 2 arguments".to_string(),
6684                    ));
6685                }
6686
6687                // Use pre-evaluated first argument (avoid double evaluation)
6688                let array_value = &evaluated_args[0];
6689
6690                // Handle undefined input
6691                if array_value.is_null() {
6692                    return Ok(JValue::Null);
6693                }
6694
6695                let mut arr = match array_value {
6696                    JValue::Array(arr) => arr.to_vec(),
6697                    other => vec![other.clone()],
6698                };
6699
6700                if args.len() == 2 {
6701                    // Sort using the comparator from raw args (need unevaluated lambda AST)
6702                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
6703                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
6704                    Ok(JValue::array(arr))
6705                } else {
6706                    // Default sort (no comparator)
6707                    Ok(functions::array::sort(&arr)?)
6708                }
6709            }
6710            "distinct" => {
6711                if evaluated_args.len() != 1 {
6712                    return Err(EvaluatorError::EvaluationError(
6713                        "distinct() requires exactly 1 argument".to_string(),
6714                    ));
6715                }
6716                match &evaluated_args[0] {
6717                    JValue::Array(arr) => Ok(functions::array::distinct(arr)?),
6718                    _ => Err(EvaluatorError::TypeError(
6719                        "distinct() requires an array argument".to_string(),
6720                    )),
6721                }
6722            }
6723            "exists" => {
6724                if evaluated_args.len() != 1 {
6725                    return Err(EvaluatorError::EvaluationError(
6726                        "exists() requires exactly 1 argument".to_string(),
6727                    ));
6728                }
6729                Ok(functions::array::exists(&evaluated_args[0])?)
6730            }
6731            "keys" => {
6732                if evaluated_args.len() != 1 {
6733                    return Err(EvaluatorError::EvaluationError(
6734                        "keys() requires exactly 1 argument".to_string(),
6735                    ));
6736                }
6737
6738                // Helper to unwrap single-element arrays
6739                let unwrap_single = |keys: Vec<JValue>| -> JValue {
6740                    if keys.len() == 1 {
6741                        keys.into_iter().next().unwrap()
6742                    } else {
6743                        JValue::array(keys)
6744                    }
6745                };
6746
6747                match &evaluated_args[0] {
6748                    JValue::Null => Ok(JValue::Null),
6749                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
6750                    JValue::Object(obj) => {
6751                        // Return undefined for empty objects
6752                        if obj.is_empty() {
6753                            Ok(JValue::Null)
6754                        } else {
6755                            let keys: Vec<JValue> =
6756                                obj.keys().map(|k| JValue::string(k.clone())).collect();
6757                            Ok(unwrap_single(keys))
6758                        }
6759                    }
6760                    JValue::Array(arr) => {
6761                        // For arrays, collect keys from all objects
6762                        let mut all_keys = Vec::new();
6763                        for item in arr.iter() {
6764                            // Skip lambda/builtin values
6765                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
6766                                continue;
6767                            }
6768                            if let JValue::Object(obj) = item {
6769                                for key in obj.keys() {
6770                                    if !all_keys.contains(&JValue::string(key.clone())) {
6771                                        all_keys.push(JValue::string(key.clone()));
6772                                    }
6773                                }
6774                            }
6775                        }
6776                        if all_keys.is_empty() {
6777                            Ok(JValue::Null)
6778                        } else {
6779                            Ok(unwrap_single(all_keys))
6780                        }
6781                    }
6782                    // Non-object types return undefined
6783                    _ => Ok(JValue::Null),
6784                }
6785            }
6786            "lookup" => {
6787                if evaluated_args.len() != 2 {
6788                    return Err(EvaluatorError::EvaluationError(
6789                        "lookup() requires exactly 2 arguments".to_string(),
6790                    ));
6791                }
6792                if evaluated_args[0].is_null() {
6793                    return Ok(JValue::Null);
6794                }
6795
6796                let key = match &evaluated_args[1] {
6797                    JValue::String(k) => &**k,
6798                    _ => {
6799                        return Err(EvaluatorError::TypeError(
6800                            "lookup() requires a string key".to_string(),
6801                        ))
6802                    }
6803                };
6804
6805                // Helper function to recursively lookup in values
6806                fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
6807                    match val {
6808                        JValue::Array(arr) => {
6809                            let mut results = Vec::new();
6810                            for item in arr.iter() {
6811                                let nested = lookup_recursive(item, key);
6812                                results.extend(nested.iter().cloned());
6813                            }
6814                            results
6815                        }
6816                        JValue::Object(obj) => {
6817                            if let Some(v) = obj.get(key) {
6818                                vec![v.clone()]
6819                            } else {
6820                                vec![]
6821                            }
6822                        }
6823                        _ => vec![],
6824                    }
6825                }
6826
6827                let results = lookup_recursive(&evaluated_args[0], key);
6828                if results.is_empty() {
6829                    Ok(JValue::Null)
6830                } else if results.len() == 1 {
6831                    Ok(results[0].clone())
6832                } else {
6833                    Ok(JValue::array(results))
6834                }
6835            }
6836            "spread" => {
6837                if evaluated_args.len() != 1 {
6838                    return Err(EvaluatorError::EvaluationError(
6839                        "spread() requires exactly 1 argument".to_string(),
6840                    ));
6841                }
6842                match &evaluated_args[0] {
6843                    JValue::Null => Ok(JValue::Null),
6844                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Undefined),
6845                    JValue::Object(obj) => Ok(functions::object::spread(obj)?),
6846                    JValue::Array(arr) => {
6847                        // Spread each object in the array
6848                        let mut result = Vec::new();
6849                        for item in arr.iter() {
6850                            match item {
6851                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
6852                                    // Skip lambdas in array
6853                                    continue;
6854                                }
6855                                JValue::Object(obj) => {
6856                                    let spread_result = functions::object::spread(obj)?;
6857                                    if let JValue::Array(spread_items) = spread_result {
6858                                        result.extend(spread_items.iter().cloned());
6859                                    } else {
6860                                        result.push(spread_result);
6861                                    }
6862                                }
6863                                // Non-objects in array are returned unchanged
6864                                other => result.push(other.clone()),
6865                            }
6866                        }
6867                        Ok(JValue::array(result))
6868                    }
6869                    // Non-objects are returned unchanged
6870                    other => Ok(other.clone()),
6871                }
6872            }
6873            "merge" => {
6874                if evaluated_args.is_empty() {
6875                    return Err(EvaluatorError::EvaluationError(
6876                        "merge() requires at least 1 argument".to_string(),
6877                    ));
6878                }
6879                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
6880                // vs multiple object arguments: $merge(obj1, obj2)
6881                if evaluated_args.len() == 1 {
6882                    match &evaluated_args[0] {
6883                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
6884                        JValue::Null => Ok(JValue::Null), // $merge(undefined) returns undefined
6885                        JValue::Object(_) => {
6886                            // Single object - just return it
6887                            Ok(evaluated_args[0].clone())
6888                        }
6889                        _ => Err(EvaluatorError::TypeError(
6890                            "merge() requires objects or an array of objects".to_string(),
6891                        )),
6892                    }
6893                } else {
6894                    Ok(functions::object::merge(&evaluated_args)?)
6895                }
6896            }
6897
6898            "map" => {
6899                if args.len() != 2 {
6900                    return Err(EvaluatorError::EvaluationError(
6901                        "map() requires exactly 2 arguments".to_string(),
6902                    ));
6903                }
6904
6905                // Evaluate the array argument
6906                let array = self.evaluate_internal(&args[0], data)?;
6907
6908                match array {
6909                    JValue::Array(arr) => {
6910                        // Detect how many parameters the callback expects
6911                        let param_count = self.get_callback_param_count(&args[1]);
6912
6913                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
6914                        if param_count == 1 {
6915                            if let AstNode::Lambda {
6916                                params,
6917                                body,
6918                                signature: None,
6919                                thunk: false,
6920                            } = &args[1]
6921                            {
6922                                let var_refs: Vec<&str> =
6923                                    params.iter().map(|s| s.as_str()).collect();
6924                                if let Some(compiled) =
6925                                    try_compile_expr_with_allowed_vars(body, &var_refs)
6926                                {
6927                                    let param_name = params[0].as_str();
6928                                    let mut result = Vec::with_capacity(arr.len());
6929                                    let mut vars = HashMap::new();
6930                                    for item in arr.iter() {
6931                                        vars.insert(param_name, item);
6932                                        let mapped = eval_compiled(&compiled, data, Some(&vars))?;
6933                                        if !mapped.is_undefined() {
6934                                            result.push(mapped);
6935                                        }
6936                                    }
6937                                    return Ok(JValue::array(result));
6938                                }
6939                            }
6940                            // Stored lambda variable fast path: $var with pre-compiled body
6941                            if let AstNode::Variable(var_name) = &args[1] {
6942                                if let Some(stored) = self.context.lookup_lambda(var_name) {
6943                                    if let Some(ref ce) = stored.compiled_body.clone() {
6944                                        let param_name = stored.params[0].clone();
6945                                        let captured_data = stored.captured_data.clone();
6946                                        let captured_env_clone = stored.captured_env.clone();
6947                                        let ce_clone = ce.clone();
6948                                        if !captured_env_clone.values().any(|v| {
6949                                            matches!(
6950                                                v,
6951                                                JValue::Lambda { .. } | JValue::Builtin { .. }
6952                                            )
6953                                        }) {
6954                                            let call_data = captured_data.as_ref().unwrap_or(data);
6955                                            let mut result = Vec::with_capacity(arr.len());
6956                                            let mut vars: HashMap<&str, &JValue> =
6957                                                captured_env_clone
6958                                                    .iter()
6959                                                    .map(|(k, v)| (k.as_str(), v))
6960                                                    .collect();
6961                                            for item in arr.iter() {
6962                                                vars.insert(param_name.as_str(), item);
6963                                                let mapped = eval_compiled(
6964                                                    &ce_clone,
6965                                                    call_data,
6966                                                    Some(&vars),
6967                                                )?;
6968                                                if !mapped.is_undefined() {
6969                                                    result.push(mapped);
6970                                                }
6971                                            }
6972                                            return Ok(JValue::array(result));
6973                                        }
6974                                    }
6975                                }
6976                            }
6977                        }
6978
6979                        // Only create the array value if callback uses 3 parameters
6980                        let arr_value = if param_count >= 3 {
6981                            Some(JValue::Array(arr.clone()))
6982                        } else {
6983                            None
6984                        };
6985
6986                        let mut result = Vec::with_capacity(arr.len());
6987                        for (index, item) in arr.iter().enumerate() {
6988                            // Build argument list based on what callback expects
6989                            let call_args = match param_count {
6990                                1 => vec![item.clone()],
6991                                2 => vec![item.clone(), JValue::Number(index as f64)],
6992                                _ => vec![
6993                                    item.clone(),
6994                                    JValue::Number(index as f64),
6995                                    arr_value.as_ref().unwrap().clone(),
6996                                ],
6997                            };
6998
6999                            let mapped = self.apply_function(&args[1], &call_args, data)?;
7000                            // Filter out undefined results but keep explicit null (JSONata map semantics)
7001                            // undefined comes from missing else clause, null is explicit
7002                            if !mapped.is_undefined() {
7003                                result.push(mapped);
7004                            }
7005                        }
7006                        Ok(JValue::array(result))
7007                    }
7008                    JValue::Null => Ok(JValue::Null),
7009                    _ => Err(EvaluatorError::TypeError(
7010                        "map() first argument must be an array".to_string(),
7011                    )),
7012                }
7013            }
7014
7015            "filter" => {
7016                if args.len() != 2 {
7017                    return Err(EvaluatorError::EvaluationError(
7018                        "filter() requires exactly 2 arguments".to_string(),
7019                    ));
7020                }
7021
7022                // Evaluate the array argument
7023                let array = self.evaluate_internal(&args[0], data)?;
7024
7025                // Handle undefined input - return undefined
7026                if array.is_undefined() {
7027                    return Ok(JValue::Undefined);
7028                }
7029
7030                // Handle null input
7031                if array.is_null() {
7032                    return Ok(JValue::Undefined);
7033                }
7034
7035                // Coerce non-array values to single-element arrays
7036                // Track if input was a single value to unwrap result appropriately
7037                // Use references to avoid upfront cloning of all elements
7038                let single_holder;
7039                let (items, was_single_value): (&[JValue], bool) = match &array {
7040                    JValue::Array(arr) => (arr.as_slice(), false),
7041                    _ => {
7042                        single_holder = [array];
7043                        (&single_holder[..], true)
7044                    }
7045                };
7046
7047                // Detect how many parameters the callback expects
7048                let param_count = self.get_callback_param_count(&args[1]);
7049
7050                // CompiledExpr fast path: direct lambda with 1 param, compilable body
7051                if param_count == 1 {
7052                    if let AstNode::Lambda {
7053                        params,
7054                        body,
7055                        signature: None,
7056                        thunk: false,
7057                    } = &args[1]
7058                    {
7059                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
7060                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
7061                        {
7062                            let param_name = params[0].as_str();
7063                            let mut result = Vec::with_capacity(items.len() / 2);
7064                            let mut vars = HashMap::new();
7065                            for item in items.iter() {
7066                                vars.insert(param_name, item);
7067                                let pred_result = eval_compiled(&compiled, data, Some(&vars))?;
7068                                if compiled_is_truthy(&pred_result) {
7069                                    result.push(item.clone());
7070                                }
7071                            }
7072                            if was_single_value {
7073                                if result.len() == 1 {
7074                                    return Ok(result.remove(0));
7075                                } else if result.is_empty() {
7076                                    return Ok(JValue::Undefined);
7077                                }
7078                            }
7079                            return Ok(JValue::array(result));
7080                        }
7081                    }
7082                    // Stored lambda variable fast path: $var with pre-compiled body
7083                    if let AstNode::Variable(var_name) = &args[1] {
7084                        if let Some(stored) = self.context.lookup_lambda(var_name) {
7085                            if let Some(ref ce) = stored.compiled_body.clone() {
7086                                let param_name = stored.params[0].clone();
7087                                let captured_data = stored.captured_data.clone();
7088                                let captured_env_clone = stored.captured_env.clone();
7089                                let ce_clone = ce.clone();
7090                                if !captured_env_clone.values().any(|v| {
7091                                    matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
7092                                }) {
7093                                    let call_data = captured_data.as_ref().unwrap_or(data);
7094                                    let mut result = Vec::with_capacity(items.len() / 2);
7095                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
7096                                        .iter()
7097                                        .map(|(k, v)| (k.as_str(), v))
7098                                        .collect();
7099                                    for item in items.iter() {
7100                                        vars.insert(param_name.as_str(), item);
7101                                        let pred_result =
7102                                            eval_compiled(&ce_clone, call_data, Some(&vars))?;
7103                                        if compiled_is_truthy(&pred_result) {
7104                                            result.push(item.clone());
7105                                        }
7106                                    }
7107                                    if was_single_value {
7108                                        if result.len() == 1 {
7109                                            return Ok(result.remove(0));
7110                                        } else if result.is_empty() {
7111                                            return Ok(JValue::Undefined);
7112                                        }
7113                                    }
7114                                    return Ok(JValue::array(result));
7115                                }
7116                            }
7117                        }
7118                    }
7119                }
7120
7121                // Only create the array value if callback uses 3 parameters
7122                let arr_value = if param_count >= 3 {
7123                    Some(JValue::array(items.to_vec()))
7124                } else {
7125                    None
7126                };
7127
7128                let mut result = Vec::with_capacity(items.len() / 2);
7129
7130                for (index, item) in items.iter().enumerate() {
7131                    // Build argument list based on what callback expects
7132                    let call_args = match param_count {
7133                        1 => vec![item.clone()],
7134                        2 => vec![item.clone(), JValue::Number(index as f64)],
7135                        _ => vec![
7136                            item.clone(),
7137                            JValue::Number(index as f64),
7138                            arr_value.as_ref().unwrap().clone(),
7139                        ],
7140                    };
7141
7142                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
7143                    if self.is_truthy(&predicate_result) {
7144                        result.push(item.clone());
7145                    }
7146                }
7147
7148                // If input was a single value, return the single matching item
7149                // (or undefined if no match)
7150                if was_single_value {
7151                    if result.len() == 1 {
7152                        return Ok(result.remove(0));
7153                    } else if result.is_empty() {
7154                        return Ok(JValue::Undefined);
7155                    }
7156                }
7157
7158                Ok(JValue::array(result))
7159            }
7160
7161            "reduce" => {
7162                if args.len() < 2 || args.len() > 3 {
7163                    return Err(EvaluatorError::EvaluationError(
7164                        "reduce() requires 2 or 3 arguments".to_string(),
7165                    ));
7166                }
7167
7168                // Check that the callback function has at least 2 parameters
7169                if let AstNode::Lambda { params, .. } = &args[1] {
7170                    if params.len() < 2 {
7171                        return Err(EvaluatorError::EvaluationError(
7172                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
7173                        ));
7174                    }
7175                } else if let AstNode::Function { name, .. } = &args[1] {
7176                    // For now, we can't validate built-in function signatures here
7177                    // But user-defined functions via lambda will be validated above
7178                    let _ = name; // avoid unused warning
7179                }
7180
7181                // Evaluate the array argument
7182                let array = self.evaluate_internal(&args[0], data)?;
7183
7184                // Convert single value to array (JSONata reduce accepts single values)
7185                // Use references to avoid upfront cloning of all elements
7186                let single_holder;
7187                let items: &[JValue] = match &array {
7188                    JValue::Array(arr) => arr.as_slice(),
7189                    JValue::Null => return Ok(JValue::Null),
7190                    _ => {
7191                        single_holder = [array];
7192                        &single_holder[..]
7193                    }
7194                };
7195
7196                if items.is_empty() {
7197                    // Return initial value if provided, otherwise null
7198                    return if args.len() == 3 {
7199                        self.evaluate_internal(&args[2], data)
7200                    } else {
7201                        Ok(JValue::Null)
7202                    };
7203                }
7204
7205                // Get initial accumulator
7206                let mut accumulator = if args.len() == 3 {
7207                    self.evaluate_internal(&args[2], data)?
7208                } else {
7209                    items[0].clone()
7210                };
7211
7212                let start_idx = if args.len() == 3 { 0 } else { 1 };
7213
7214                // Detect how many parameters the callback expects
7215                let param_count = self.get_callback_param_count(&args[1]);
7216
7217                // CompiledExpr fast path: direct lambda with 2 params, compilable body
7218                if param_count == 2 {
7219                    if let AstNode::Lambda {
7220                        params,
7221                        body,
7222                        signature: None,
7223                        thunk: false,
7224                    } = &args[1]
7225                    {
7226                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
7227                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
7228                        {
7229                            let acc_name = params[0].as_str();
7230                            let item_name = params[1].as_str();
7231                            for item in items[start_idx..].iter() {
7232                                let vars: HashMap<&str, &JValue> =
7233                                    HashMap::from([(acc_name, &accumulator), (item_name, item)]);
7234                                accumulator = eval_compiled(&compiled, data, Some(&vars))?;
7235                            }
7236                            return Ok(accumulator);
7237                        }
7238                    }
7239                    // Stored lambda variable fast path: $var with pre-compiled body
7240                    if let AstNode::Variable(var_name) = &args[1] {
7241                        if let Some(stored) = self.context.lookup_lambda(var_name) {
7242                            if stored.params.len() == 2 {
7243                                if let Some(ref ce) = stored.compiled_body.clone() {
7244                                    let acc_param = stored.params[0].clone();
7245                                    let item_param = stored.params[1].clone();
7246                                    let captured_data = stored.captured_data.clone();
7247                                    let captured_env_clone = stored.captured_env.clone();
7248                                    let ce_clone = ce.clone();
7249                                    if !captured_env_clone.values().any(|v| {
7250                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
7251                                    }) {
7252                                        let call_data = captured_data.as_ref().unwrap_or(data);
7253                                        for item in items[start_idx..].iter() {
7254                                            let mut vars: HashMap<&str, &JValue> =
7255                                                captured_env_clone
7256                                                    .iter()
7257                                                    .map(|(k, v)| (k.as_str(), v))
7258                                                    .collect();
7259                                            vars.insert(acc_param.as_str(), &accumulator);
7260                                            vars.insert(item_param.as_str(), item);
7261                                            // Evaluate and drop vars before assigning accumulator
7262                                            // to satisfy borrow checker (vars borrows accumulator)
7263                                            let new_acc =
7264                                                eval_compiled(&ce_clone, call_data, Some(&vars))?;
7265                                            drop(vars);
7266                                            accumulator = new_acc;
7267                                        }
7268                                        return Ok(accumulator);
7269                                    }
7270                                }
7271                            }
7272                        }
7273                    }
7274                }
7275
7276                // Only create the array value if callback uses 4 parameters
7277                let arr_value = if param_count >= 4 {
7278                    Some(JValue::array(items.to_vec()))
7279                } else {
7280                    None
7281                };
7282
7283                // Apply function to each element
7284                for (idx, item) in items[start_idx..].iter().enumerate() {
7285                    // For reduce, the function receives (accumulator, value, index, array)
7286                    // Callbacks may use any subset of these parameters
7287                    let actual_idx = start_idx + idx;
7288
7289                    // Build argument list based on what callback expects
7290                    let call_args = match param_count {
7291                        2 => vec![accumulator.clone(), item.clone()],
7292                        3 => vec![
7293                            accumulator.clone(),
7294                            item.clone(),
7295                            JValue::Number(actual_idx as f64),
7296                        ],
7297                        _ => vec![
7298                            accumulator.clone(),
7299                            item.clone(),
7300                            JValue::Number(actual_idx as f64),
7301                            arr_value.as_ref().unwrap().clone(),
7302                        ],
7303                    };
7304
7305                    accumulator = self.apply_function(&args[1], &call_args, data)?;
7306                }
7307
7308                Ok(accumulator)
7309            }
7310
7311            "single" => {
7312                if args.is_empty() || args.len() > 2 {
7313                    return Err(EvaluatorError::EvaluationError(
7314                        "single() requires 1 or 2 arguments".to_string(),
7315                    ));
7316                }
7317
7318                // Evaluate the array argument
7319                let array = self.evaluate_internal(&args[0], data)?;
7320
7321                // Convert to array (wrap single values)
7322                let arr = match array {
7323                    JValue::Array(arr) => arr.to_vec(),
7324                    JValue::Null => return Ok(JValue::Null),
7325                    other => vec![other],
7326                };
7327
7328                if args.len() == 1 {
7329                    // No predicate - array must have exactly 1 element
7330                    match arr.len() {
7331                        0 => Err(EvaluatorError::EvaluationError(
7332                            "single() argument is empty".to_string(),
7333                        )),
7334                        1 => Ok(arr.into_iter().next().unwrap()),
7335                        count => Err(EvaluatorError::EvaluationError(format!(
7336                            "single() argument has {} values (expected exactly 1)",
7337                            count
7338                        ))),
7339                    }
7340                } else {
7341                    // With predicate - find exactly 1 matching element
7342                    let arr_value = JValue::array(arr.clone());
7343                    let mut matches = Vec::new();
7344                    for (index, item) in arr.into_iter().enumerate() {
7345                        // Apply predicate function with (item, index, array)
7346                        let predicate_result = self.apply_function(
7347                            &args[1],
7348                            &[
7349                                item.clone(),
7350                                JValue::Number(index as f64),
7351                                arr_value.clone(),
7352                            ],
7353                            data,
7354                        )?;
7355                        if self.is_truthy(&predicate_result) {
7356                            matches.push(item);
7357                        }
7358                    }
7359
7360                    match matches.len() {
7361                        0 => Err(EvaluatorError::EvaluationError(
7362                            "single() predicate matches no values".to_string(),
7363                        )),
7364                        1 => Ok(matches.into_iter().next().unwrap()),
7365                        count => Err(EvaluatorError::EvaluationError(format!(
7366                            "single() predicate matches {} values (expected exactly 1)",
7367                            count
7368                        ))),
7369                    }
7370                }
7371            }
7372
7373            "each" => {
7374                // $each(object, function) - iterate over object, applying function to each value/key pair
7375                // Returns an array of the function results
7376                if args.is_empty() || args.len() > 2 {
7377                    return Err(EvaluatorError::EvaluationError(
7378                        "each() requires 1 or 2 arguments".to_string(),
7379                    ));
7380                }
7381
7382                // Determine which argument is the object and which is the function
7383                let (obj_value, func_arg) = if args.len() == 1 {
7384                    // Single argument: use current data as object
7385                    (data.clone(), &args[0])
7386                } else {
7387                    // Two arguments: first is object, second is function
7388                    (self.evaluate_internal(&args[0], data)?, &args[1])
7389                };
7390
7391                // Detect how many parameters the callback expects
7392                let param_count = self.get_callback_param_count(func_arg);
7393
7394                match obj_value {
7395                    JValue::Object(obj) => {
7396                        let mut result = Vec::new();
7397                        for (key, value) in obj.iter() {
7398                            // Build argument list based on what callback expects
7399                            // The callback receives the value as the first argument and key as second
7400                            let call_args = match param_count {
7401                                1 => vec![value.clone()],
7402                                _ => vec![value.clone(), JValue::string(key.clone())],
7403                            };
7404
7405                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
7406                            // Skip undefined results (similar to map behavior)
7407                            if !fn_result.is_null() && !fn_result.is_undefined() {
7408                                result.push(fn_result);
7409                            }
7410                        }
7411                        Ok(JValue::array(result))
7412                    }
7413                    JValue::Null => Ok(JValue::Null),
7414                    _ => Err(EvaluatorError::TypeError(
7415                        "each() first argument must be an object".to_string(),
7416                    )),
7417                }
7418            }
7419
7420            "not" => {
7421                if evaluated_args.len() != 1 {
7422                    return Err(EvaluatorError::EvaluationError(
7423                        "not() requires exactly 1 argument".to_string(),
7424                    ));
7425                }
7426                // $not(x) returns the logical negation of x
7427                // null is falsy, so $not(null) = true
7428                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
7429            }
7430            "boolean" => {
7431                if evaluated_args.len() != 1 {
7432                    return Err(EvaluatorError::EvaluationError(
7433                        "boolean() requires exactly 1 argument".to_string(),
7434                    ));
7435                }
7436                Ok(functions::boolean::boolean(&evaluated_args[0])?)
7437            }
7438            "type" => {
7439                if evaluated_args.len() != 1 {
7440                    return Err(EvaluatorError::EvaluationError(
7441                        "type() requires exactly 1 argument".to_string(),
7442                    ));
7443                }
7444                // Return type string
7445                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
7446                // We use a special marker object to distinguish undefined from null
7447                match &evaluated_args[0] {
7448                    JValue::Null => Ok(JValue::string("null")),
7449                    JValue::Bool(_) => Ok(JValue::string("boolean")),
7450                    JValue::Number(_) => Ok(JValue::string("number")),
7451                    JValue::String(_) => Ok(JValue::string("string")),
7452                    JValue::Array(_) => Ok(JValue::string("array")),
7453                    JValue::Object(_) => Ok(JValue::string("object")),
7454                    JValue::Undefined => Ok(JValue::Undefined),
7455                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
7456                        Ok(JValue::string("function"))
7457                    }
7458                    JValue::Regex { .. } => Ok(JValue::string("regex")),
7459                }
7460            }
7461
7462            "base64encode" => {
7463                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
7464                    return Ok(JValue::Null);
7465                }
7466                if evaluated_args.len() != 1 {
7467                    return Err(EvaluatorError::EvaluationError(
7468                        "base64encode() requires exactly 1 argument".to_string(),
7469                    ));
7470                }
7471                match &evaluated_args[0] {
7472                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
7473                    _ => Err(EvaluatorError::TypeError(
7474                        "base64encode() requires a string argument".to_string(),
7475                    )),
7476                }
7477            }
7478            "base64decode" => {
7479                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
7480                    return Ok(JValue::Null);
7481                }
7482                if evaluated_args.len() != 1 {
7483                    return Err(EvaluatorError::EvaluationError(
7484                        "base64decode() requires exactly 1 argument".to_string(),
7485                    ));
7486                }
7487                match &evaluated_args[0] {
7488                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
7489                    _ => Err(EvaluatorError::TypeError(
7490                        "base64decode() requires a string argument".to_string(),
7491                    )),
7492                }
7493            }
7494            "encodeUrlComponent" => {
7495                if evaluated_args.len() != 1 {
7496                    return Err(EvaluatorError::EvaluationError(
7497                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
7498                    ));
7499                }
7500                if evaluated_args[0].is_null() {
7501                    return Ok(JValue::Null);
7502                }
7503                match &evaluated_args[0] {
7504                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
7505                    _ => Err(EvaluatorError::TypeError(
7506                        "encodeUrlComponent() requires a string argument".to_string(),
7507                    )),
7508                }
7509            }
7510            "decodeUrlComponent" => {
7511                if evaluated_args.len() != 1 {
7512                    return Err(EvaluatorError::EvaluationError(
7513                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
7514                    ));
7515                }
7516                if evaluated_args[0].is_null() {
7517                    return Ok(JValue::Null);
7518                }
7519                match &evaluated_args[0] {
7520                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
7521                    _ => Err(EvaluatorError::TypeError(
7522                        "decodeUrlComponent() requires a string argument".to_string(),
7523                    )),
7524                }
7525            }
7526            "encodeUrl" => {
7527                if evaluated_args.len() != 1 {
7528                    return Err(EvaluatorError::EvaluationError(
7529                        "encodeUrl() requires exactly 1 argument".to_string(),
7530                    ));
7531                }
7532                if evaluated_args[0].is_null() {
7533                    return Ok(JValue::Null);
7534                }
7535                match &evaluated_args[0] {
7536                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
7537                    _ => Err(EvaluatorError::TypeError(
7538                        "encodeUrl() requires a string argument".to_string(),
7539                    )),
7540                }
7541            }
7542            "decodeUrl" => {
7543                if evaluated_args.len() != 1 {
7544                    return Err(EvaluatorError::EvaluationError(
7545                        "decodeUrl() requires exactly 1 argument".to_string(),
7546                    ));
7547                }
7548                if evaluated_args[0].is_null() {
7549                    return Ok(JValue::Null);
7550                }
7551                match &evaluated_args[0] {
7552                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
7553                    _ => Err(EvaluatorError::TypeError(
7554                        "decodeUrl() requires a string argument".to_string(),
7555                    )),
7556                }
7557            }
7558
7559            "error" => {
7560                // $error(message) - throw error with custom message
7561                if evaluated_args.is_empty() {
7562                    // No message provided
7563                    return Err(EvaluatorError::EvaluationError(
7564                        "D3137: $error() function evaluated".to_string(),
7565                    ));
7566                }
7567
7568                match &evaluated_args[0] {
7569                    JValue::String(s) => {
7570                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
7571                    }
7572                    _ => Err(EvaluatorError::TypeError(
7573                        "T0410: Argument 1 of function error does not match function signature"
7574                            .to_string(),
7575                    )),
7576                }
7577            }
7578            "assert" => {
7579                // $assert(condition, message) - throw error if condition is false
7580                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7581                    return Err(EvaluatorError::EvaluationError(
7582                        "assert() requires 1 or 2 arguments".to_string(),
7583                    ));
7584                }
7585
7586                // First argument must be a boolean
7587                let condition = match &evaluated_args[0] {
7588                    JValue::Bool(b) => *b,
7589                    _ => {
7590                        return Err(EvaluatorError::TypeError(
7591                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
7592                        ));
7593                    }
7594                };
7595
7596                if !condition {
7597                    let message = if evaluated_args.len() == 2 {
7598                        match &evaluated_args[1] {
7599                            JValue::String(s) => s.clone(),
7600                            _ => Rc::from("$assert() statement failed"),
7601                        }
7602                    } else {
7603                        Rc::from("$assert() statement failed")
7604                    };
7605                    return Err(EvaluatorError::EvaluationError(format!(
7606                        "D3141: {}",
7607                        message
7608                    )));
7609                }
7610
7611                Ok(JValue::Null)
7612            }
7613
7614            "eval" => {
7615                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
7616                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7617                    return Err(EvaluatorError::EvaluationError(
7618                        "T0410: Argument 1 of function $eval must be a string".to_string(),
7619                    ));
7620                }
7621
7622                // If the first argument is null/undefined, return undefined
7623                if evaluated_args[0].is_null() {
7624                    return Ok(JValue::Null);
7625                }
7626
7627                // First argument must be a string expression
7628                let expr_str = match &evaluated_args[0] {
7629                    JValue::String(s) => &**s,
7630                    _ => {
7631                        return Err(EvaluatorError::EvaluationError(
7632                            "T0410: Argument 1 of function $eval must be a string".to_string(),
7633                        ));
7634                    }
7635                };
7636
7637                // Parse the expression
7638                let parsed_ast = match parser::parse(expr_str) {
7639                    Ok(ast) => ast,
7640                    Err(e) => {
7641                        // D3120 is the error code for parse errors in $eval
7642                        return Err(EvaluatorError::EvaluationError(format!(
7643                            "D3120: The expression passed to $eval cannot be parsed: {}",
7644                            e
7645                        )));
7646                    }
7647                };
7648
7649                // Determine the context to use for evaluation
7650                let eval_context = if evaluated_args.len() == 2 {
7651                    &evaluated_args[1]
7652                } else {
7653                    data
7654                };
7655
7656                // Evaluate the parsed expression
7657                match self.evaluate_internal(&parsed_ast, eval_context) {
7658                    Ok(result) => Ok(result),
7659                    Err(e) => {
7660                        // D3121 is the error code for evaluation errors in $eval
7661                        let err_msg = e.to_string();
7662                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
7663                            Err(EvaluatorError::EvaluationError(format!(
7664                                "D3121: {}",
7665                                err_msg
7666                            )))
7667                        } else {
7668                            Err(e)
7669                        }
7670                    }
7671                }
7672            }
7673
7674            "now" => {
7675                if !evaluated_args.is_empty() {
7676                    return Err(EvaluatorError::EvaluationError(
7677                        "now() takes no arguments".to_string(),
7678                    ));
7679                }
7680                Ok(crate::datetime::now())
7681            }
7682
7683            "millis" => {
7684                if !evaluated_args.is_empty() {
7685                    return Err(EvaluatorError::EvaluationError(
7686                        "millis() takes no arguments".to_string(),
7687                    ));
7688                }
7689                Ok(crate::datetime::millis())
7690            }
7691
7692            "toMillis" => {
7693                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7694                    return Err(EvaluatorError::EvaluationError(
7695                        "toMillis() requires 1 or 2 arguments".to_string(),
7696                    ));
7697                }
7698
7699                match &evaluated_args[0] {
7700                    JValue::String(s) => {
7701                        // Optional second argument is a picture string for custom parsing
7702                        if evaluated_args.len() == 2 {
7703                            match &evaluated_args[1] {
7704                                JValue::String(picture) => {
7705                                    // Use custom picture format parsing
7706                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
7707                                }
7708                                JValue::Null => Ok(JValue::Null),
7709                                _ => Err(EvaluatorError::TypeError(
7710                                    "toMillis() second argument must be a string".to_string(),
7711                                )),
7712                            }
7713                        } else {
7714                            // Use ISO 8601 partial date parsing
7715                            Ok(crate::datetime::to_millis(s)?)
7716                        }
7717                    }
7718                    JValue::Null => Ok(JValue::Null),
7719                    _ => Err(EvaluatorError::TypeError(
7720                        "toMillis() requires a string argument".to_string(),
7721                    )),
7722                }
7723            }
7724
7725            "fromMillis" => {
7726                if evaluated_args.len() != 1 {
7727                    return Err(EvaluatorError::EvaluationError(
7728                        "fromMillis() requires exactly 1 argument".to_string(),
7729                    ));
7730                }
7731
7732                match &evaluated_args[0] {
7733                    JValue::Number(n) => {
7734                        let millis = (if n.fract() == 0.0 {
7735                            Ok(*n as i64)
7736                        } else {
7737                            Err(())
7738                        })
7739                        .map_err(|_| {
7740                            EvaluatorError::TypeError(
7741                                "fromMillis() requires an integer".to_string(),
7742                            )
7743                        })?;
7744                        Ok(crate::datetime::from_millis(millis)?)
7745                    }
7746                    JValue::Null => Ok(JValue::Null),
7747                    _ => Err(EvaluatorError::TypeError(
7748                        "fromMillis() requires a number argument".to_string(),
7749                    )),
7750                }
7751            }
7752
7753            _ => Err(EvaluatorError::ReferenceError(format!(
7754                "Unknown function: {}",
7755                name
7756            ))),
7757        }
7758    }
7759
7760    /// Apply a function (lambda or expression) to values
7761    ///
7762    /// This handles both:
7763    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
7764    /// 2. Simple expressions: price * 2 - evaluates with values as context
7765    fn apply_function(
7766        &mut self,
7767        func_node: &AstNode,
7768        values: &[JValue],
7769        data: &JValue,
7770    ) -> Result<JValue, EvaluatorError> {
7771        match func_node {
7772            AstNode::Lambda {
7773                params,
7774                body,
7775                signature,
7776                thunk,
7777            } => {
7778                // Direct lambda - invoke it
7779                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
7780            }
7781            AstNode::Function {
7782                name,
7783                args,
7784                is_builtin,
7785            } => {
7786                // Function call - check if it has placeholders (partial application)
7787                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
7788
7789                if has_placeholder {
7790                    // This is a partial application - evaluate it to get the lambda value
7791                    let partial_lambda =
7792                        self.create_partial_application(name, args, *is_builtin, data)?;
7793
7794                    // Now invoke the partial lambda with the provided values
7795                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
7796                        return self.invoke_stored_lambda(&stored, values, data);
7797                    }
7798                    Err(EvaluatorError::EvaluationError(
7799                        "Failed to apply partial application".to_string(),
7800                    ))
7801                } else {
7802                    // Regular function call without placeholders
7803                    // Evaluate it and apply if it returns a function
7804                    let result = self.evaluate_internal(func_node, data)?;
7805
7806                    // Check if result is a lambda value
7807                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
7808                        return self.invoke_stored_lambda(&stored, values, data);
7809                    }
7810
7811                    // Otherwise just return the result
7812                    Ok(result)
7813                }
7814            }
7815            AstNode::Variable(var_name) => {
7816                // Check if this variable holds a stored lambda
7817                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
7818                    self.invoke_stored_lambda(&stored_lambda, values, data)
7819                } else if let Some(value) = self.context.lookup(var_name).cloned() {
7820                    // Check if this variable holds a lambda value
7821                    // This handles lambdas passed as bound arguments in partial applications
7822                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
7823                        return self.invoke_stored_lambda(&stored, values, data);
7824                    }
7825                    // Regular variable value - evaluate with first value as context
7826                    if values.is_empty() {
7827                        self.evaluate_internal(func_node, data)
7828                    } else {
7829                        self.evaluate_internal(func_node, &values[0])
7830                    }
7831                } else if self.is_builtin_function(var_name) {
7832                    // This is a built-in function reference (e.g., $string, $number)
7833                    // Call it directly with the provided values (already evaluated)
7834                    self.call_builtin_with_values(var_name, values)
7835                } else {
7836                    // Unknown variable - evaluate with first value as context
7837                    if values.is_empty() {
7838                        self.evaluate_internal(func_node, data)
7839                    } else {
7840                        self.evaluate_internal(func_node, &values[0])
7841                    }
7842                }
7843            }
7844            _ => {
7845                // For non-lambda expressions, evaluate with first value as context
7846                if values.is_empty() {
7847                    self.evaluate_internal(func_node, data)
7848                } else {
7849                    self.evaluate_internal(func_node, &values[0])
7850                }
7851            }
7852        }
7853    }
7854
7855    /// Execute a transform operator on the bound $ value
7856    fn execute_transform(
7857        &mut self,
7858        location: &AstNode,
7859        update: &AstNode,
7860        delete: Option<&AstNode>,
7861        _original_data: &JValue,
7862    ) -> Result<JValue, EvaluatorError> {
7863        // Get the input value from $ binding
7864        let input = self
7865            .context
7866            .lookup("$")
7867            .ok_or_else(|| {
7868                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
7869            })?
7870            .clone();
7871
7872        // Evaluate location expression on the input to get objects to transform
7873        let located_objects = self.evaluate_internal(location, &input)?;
7874
7875        // Collect target objects into a vector for comparison
7876        let targets: Vec<JValue> = match located_objects {
7877            JValue::Array(arr) => arr.to_vec(),
7878            JValue::Object(_) => vec![located_objects],
7879            JValue::Null => Vec::new(),
7880            other => vec![other],
7881        };
7882
7883        // Validate update parameter - must be an object constructor
7884        // We need to check this before evaluation in case of errors
7885        // For now, we'll validate after evaluation in the transform helper
7886
7887        // Parse delete field names if provided
7888        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
7889            let delete_val = self.evaluate_internal(delete_node, &input)?;
7890            match delete_val {
7891                JValue::Array(arr) => arr
7892                    .iter()
7893                    .filter_map(|v| match v {
7894                        JValue::String(s) => Some(s.to_string()),
7895                        _ => None,
7896                    })
7897                    .collect(),
7898                JValue::String(s) => vec![s.to_string()],
7899                JValue::Null => Vec::new(), // Undefined variable is treated as no deletion
7900                _ => {
7901                    // Delete parameter must be an array of strings or a string
7902                    return Err(EvaluatorError::EvaluationError(
7903                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
7904                    ));
7905                }
7906            }
7907        } else {
7908            Vec::new()
7909        };
7910
7911        // Recursive helper to apply transformation throughout the structure
7912        fn apply_transform_deep(
7913            evaluator: &mut Evaluator,
7914            value: &JValue,
7915            targets: &[JValue],
7916            update: &AstNode,
7917            delete_fields: &[String],
7918        ) -> Result<JValue, EvaluatorError> {
7919            // Check if this value is one of the targets to transform
7920            // Use JValue's PartialEq for semantic equality comparison
7921            if targets.iter().any(|t| t == value) {
7922                // Transform this object
7923                if let JValue::Object(map_rc) = value.clone() {
7924                    let mut map = (*map_rc).clone();
7925                    let update_val = evaluator.evaluate_internal(update, value)?;
7926                    // Validate that update evaluates to an object or null (undefined)
7927                    match update_val {
7928                        JValue::Object(update_map) => {
7929                            for (key, val) in update_map.iter() {
7930                                map.insert(key.clone(), val.clone());
7931                            }
7932                        }
7933                        JValue::Null => {
7934                            // Null/undefined means no updates, just continue to deletions
7935                        }
7936                        _ => {
7937                            return Err(EvaluatorError::EvaluationError(
7938                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
7939                            ));
7940                        }
7941                    }
7942                    for field in delete_fields {
7943                        map.shift_remove(field);
7944                    }
7945                    return Ok(JValue::object(map));
7946                }
7947                return Ok(value.clone());
7948            }
7949
7950            // Otherwise, recursively process children to find and transform targets
7951            match value {
7952                JValue::Object(map) => {
7953                    let mut new_map = IndexMap::new();
7954                    for (k, v) in map.iter() {
7955                        new_map.insert(
7956                            k.clone(),
7957                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
7958                        );
7959                    }
7960                    Ok(JValue::object(new_map))
7961                }
7962                JValue::Array(arr) => {
7963                    let mut new_arr = Vec::new();
7964                    for item in arr.iter() {
7965                        new_arr.push(apply_transform_deep(
7966                            evaluator,
7967                            item,
7968                            targets,
7969                            update,
7970                            delete_fields,
7971                        )?);
7972                    }
7973                    Ok(JValue::array(new_arr))
7974                }
7975                _ => Ok(value.clone()),
7976            }
7977        }
7978
7979        // Apply transformation recursively starting from input
7980        apply_transform_deep(self, &input, &targets, update, &delete_fields)
7981    }
7982
7983    /// Helper to invoke a lambda with given parameters
7984    fn invoke_lambda(
7985        &mut self,
7986        params: &[String],
7987        body: &AstNode,
7988        signature: Option<&String>,
7989        values: &[JValue],
7990        data: &JValue,
7991        thunk: bool,
7992    ) -> Result<JValue, EvaluatorError> {
7993        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
7994    }
7995
7996    /// Invoke a lambda with optional captured environment (for closures)
7997    fn invoke_lambda_with_env(
7998        &mut self,
7999        params: &[String],
8000        body: &AstNode,
8001        signature: Option<&String>,
8002        values: &[JValue],
8003        data: &JValue,
8004        captured_env: Option<&HashMap<String, JValue>>,
8005        captured_data: Option<&JValue>,
8006        thunk: bool,
8007    ) -> Result<JValue, EvaluatorError> {
8008        // If this is a thunk (has tail calls), use TCO trampoline
8009        if thunk {
8010            let stored = StoredLambda {
8011                params: params.to_vec(),
8012                body: body.clone(),
8013                compiled_body: None, // Thunks use TCO, not the compiled fast path
8014                signature: signature.cloned(),
8015                captured_env: captured_env.cloned().unwrap_or_default(),
8016                captured_data: captured_data.cloned(),
8017                thunk,
8018            };
8019            return self.invoke_lambda_with_tco(&stored, values, data);
8020        }
8021
8022        // Validate signature if present, and get coerced arguments
8023        // Push a new scope for this lambda invocation
8024        self.context.push_scope();
8025
8026        // First apply captured environment (for closures)
8027        if let Some(env) = captured_env {
8028            for (name, value) in env {
8029                self.context.bind(name.clone(), value.clone());
8030            }
8031        }
8032
8033        if let Some(sig_str) = signature {
8034            // Validate and coerce arguments with signature
8035            let coerced_values = match crate::signature::Signature::parse(sig_str) {
8036                Ok(sig) => match sig.validate_and_coerce(values) {
8037                    Ok(coerced) => coerced,
8038                    Err(e) => {
8039                        self.context.pop_scope();
8040                        match e {
8041                            crate::signature::SignatureError::UndefinedArgument => {
8042                                return Ok(JValue::Null);
8043                            }
8044                            crate::signature::SignatureError::ArgumentTypeMismatch {
8045                                index,
8046                                expected,
8047                            } => {
8048                                return Err(EvaluatorError::TypeError(
8049                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
8050                                    ));
8051                            }
8052                            crate::signature::SignatureError::ArrayTypeMismatch {
8053                                index,
8054                                expected,
8055                            } => {
8056                                return Err(EvaluatorError::TypeError(format!(
8057                                    "T0412: Argument {} of function must be an array of {}",
8058                                    index, expected
8059                                )));
8060                            }
8061                            _ => {
8062                                return Err(EvaluatorError::TypeError(format!(
8063                                    "Signature validation failed: {}",
8064                                    e
8065                                )));
8066                            }
8067                        }
8068                    }
8069                },
8070                Err(e) => {
8071                    self.context.pop_scope();
8072                    return Err(EvaluatorError::EvaluationError(format!(
8073                        "Invalid signature: {}",
8074                        e
8075                    )));
8076                }
8077            };
8078            // Bind coerced values to params
8079            for (i, param) in params.iter().enumerate() {
8080                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
8081                self.context.bind(param.clone(), value);
8082            }
8083        } else {
8084            // No signature - bind directly from values slice (no allocation)
8085            for (i, param) in params.iter().enumerate() {
8086                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
8087                self.context.bind(param.clone(), value);
8088            }
8089        }
8090
8091        // Check if this is a partial application (body is a special marker string)
8092        if let AstNode::String(body_str) = body {
8093            if body_str.starts_with("__partial_call:") {
8094                // Parse the partial call info
8095                let parts: Vec<&str> = body_str.split(':').collect();
8096                if parts.len() >= 4 {
8097                    let func_name = parts[1];
8098                    let is_builtin = parts[2] == "true";
8099                    let total_args: usize = parts[3].parse().unwrap_or(0);
8100
8101                    // Get placeholder positions from captured env
8102                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
8103                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
8104                            positions
8105                                .iter()
8106                                .filter_map(|v| v.as_f64().map(|n| n as usize))
8107                                .collect()
8108                        } else {
8109                            vec![]
8110                        }
8111                    } else {
8112                        vec![]
8113                    };
8114
8115                    // Reconstruct the full argument list
8116                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
8117
8118                    // Fill in bound arguments from captured environment
8119                    if let Some(env) = captured_env {
8120                        for (key, value) in env {
8121                            if key.starts_with("__bound_arg_") {
8122                                if let Ok(pos) = key[12..].parse::<usize>() {
8123                                    if pos < total_args {
8124                                        full_args[pos] = value.clone();
8125                                    }
8126                                }
8127                            }
8128                        }
8129                    }
8130
8131                    // Fill in placeholder positions with provided values
8132                    for (i, &pos) in placeholder_positions.iter().enumerate() {
8133                        if pos < total_args {
8134                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
8135                            full_args[pos] = value;
8136                        }
8137                    }
8138
8139                    // Pop lambda scope, then push a new scope for temp args
8140                    self.context.pop_scope();
8141                    self.context.push_scope();
8142
8143                    // Build AST nodes for the function call arguments
8144                    let mut temp_args: Vec<AstNode> = Vec::new();
8145                    for (i, value) in full_args.iter().enumerate() {
8146                        let temp_name = format!("__temp_arg_{}", i);
8147                        self.context.bind(temp_name.clone(), value.clone());
8148                        temp_args.push(AstNode::Variable(temp_name));
8149                    }
8150
8151                    // Call the original function
8152                    let result =
8153                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
8154
8155                    // Pop temp scope
8156                    self.context.pop_scope();
8157
8158                    return result;
8159                }
8160            }
8161        }
8162
8163        // Evaluate lambda body (normal case)
8164        // Use captured_data for lexical scoping if available, otherwise use call-site data
8165        let body_data = captured_data.unwrap_or(data);
8166        let result = self.evaluate_internal(body, body_data)?;
8167
8168        // Pop lambda scope, preserving any lambdas referenced by the return value
8169        // Fast path: scalar results can never contain lambda references
8170        let is_scalar = matches!(
8171            &result,
8172            JValue::Number(_)
8173                | JValue::Bool(_)
8174                | JValue::String(_)
8175                | JValue::Null
8176                | JValue::Undefined
8177        );
8178        if is_scalar {
8179            self.context.pop_scope();
8180        } else {
8181            let lambdas_to_keep = self.extract_lambda_ids(&result);
8182            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
8183        }
8184
8185        Ok(result)
8186    }
8187
8188    /// Invoke a lambda with tail call optimization using a trampoline
8189    /// This method uses an iterative loop to handle tail-recursive calls without
8190    /// growing the stack, enabling deep recursion for tail-recursive functions.
8191    fn invoke_lambda_with_tco(
8192        &mut self,
8193        stored_lambda: &StoredLambda,
8194        initial_args: &[JValue],
8195        data: &JValue,
8196    ) -> Result<JValue, EvaluatorError> {
8197        let mut current_lambda = stored_lambda.clone();
8198        let mut current_args = initial_args.to_vec();
8199        let mut current_data = data.clone();
8200
8201        // Maximum number of tail call iterations to prevent infinite loops
8202        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
8203        const MAX_TCO_ITERATIONS: usize = 100_000;
8204        let mut iterations = 0;
8205
8206        // Push a persistent scope for the TCO trampoline loop.
8207        // This scope persists across all iterations so that lambdas defined
8208        // in one iteration (like recursive $iter) remain available in subsequent ones.
8209        self.context.push_scope();
8210
8211        // Trampoline loop - keeps evaluating until we get a final value
8212        let result = loop {
8213            iterations += 1;
8214            if iterations > MAX_TCO_ITERATIONS {
8215                self.context.pop_scope();
8216                return Err(EvaluatorError::EvaluationError(
8217                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
8218                ));
8219            }
8220
8221            // Evaluate the lambda body within the persistent scope
8222            let result =
8223                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
8224
8225            match result {
8226                LambdaResult::JValue(v) => break v,
8227                LambdaResult::TailCall { lambda, args, data } => {
8228                    // Continue with the tail call - no stack growth
8229                    current_lambda = *lambda;
8230                    current_args = args;
8231                    current_data = data;
8232                }
8233            }
8234        };
8235
8236        // Pop the persistent TCO scope, preserving lambdas referenced by the result
8237        let lambdas_to_keep = self.extract_lambda_ids(&result);
8238        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
8239
8240        Ok(result)
8241    }
8242
8243    /// Evaluate a lambda body, detecting tail calls for TCO
8244    /// Returns either a final value or a tail call continuation.
8245    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
8246    /// manages the persistent scope for the trampoline loop.
8247    fn invoke_lambda_body_for_tco(
8248        &mut self,
8249        lambda: &StoredLambda,
8250        values: &[JValue],
8251        data: &JValue,
8252    ) -> Result<LambdaResult, EvaluatorError> {
8253        // Validate signature if present
8254        let coerced_values = if let Some(sig_str) = &lambda.signature {
8255            match crate::signature::Signature::parse(sig_str) {
8256                Ok(sig) => match sig.validate_and_coerce(values) {
8257                    Ok(coerced) => coerced,
8258                    Err(e) => match e {
8259                        crate::signature::SignatureError::UndefinedArgument => {
8260                            return Ok(LambdaResult::JValue(JValue::Null));
8261                        }
8262                        crate::signature::SignatureError::ArgumentTypeMismatch {
8263                            index,
8264                            expected,
8265                        } => {
8266                            return Err(EvaluatorError::TypeError(
8267                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
8268                                    ));
8269                        }
8270                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
8271                            return Err(EvaluatorError::TypeError(format!(
8272                                "T0412: Argument {} of function must be an array of {}",
8273                                index, expected
8274                            )));
8275                        }
8276                        _ => {
8277                            return Err(EvaluatorError::TypeError(format!(
8278                                "Signature validation failed: {}",
8279                                e
8280                            )));
8281                        }
8282                    },
8283                },
8284                Err(e) => {
8285                    return Err(EvaluatorError::EvaluationError(format!(
8286                        "Invalid signature: {}",
8287                        e
8288                    )));
8289                }
8290            }
8291        } else {
8292            values.to_vec()
8293        };
8294
8295        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
8296        // Apply captured environment
8297        for (name, value) in &lambda.captured_env {
8298            self.context.bind(name.clone(), value.clone());
8299        }
8300
8301        // Bind parameters
8302        for (i, param) in lambda.params.iter().enumerate() {
8303            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
8304            self.context.bind(param.clone(), value);
8305        }
8306
8307        // Evaluate the body with tail call detection
8308        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
8309        self.evaluate_for_tco(&lambda.body, body_data)
8310    }
8311
8312    /// Evaluate an expression for TCO, detecting tail calls
8313    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
8314    fn evaluate_for_tco(
8315        &mut self,
8316        node: &AstNode,
8317        data: &JValue,
8318    ) -> Result<LambdaResult, EvaluatorError> {
8319        match node {
8320            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
8321            AstNode::Conditional {
8322                condition,
8323                then_branch,
8324                else_branch,
8325            } => {
8326                let cond_value = self.evaluate_internal(condition, data)?;
8327                let is_truthy = self.is_truthy(&cond_value);
8328
8329                if is_truthy {
8330                    self.evaluate_for_tco(then_branch, data)
8331                } else if let Some(else_expr) = else_branch {
8332                    self.evaluate_for_tco(else_expr, data)
8333                } else {
8334                    Ok(LambdaResult::JValue(JValue::Null))
8335                }
8336            }
8337
8338            // Block: evaluate all but last normally, last for TCO
8339            AstNode::Block(exprs) => {
8340                if exprs.is_empty() {
8341                    return Ok(LambdaResult::JValue(JValue::Null));
8342                }
8343
8344                // Evaluate all expressions except the last
8345                let mut result = JValue::Null;
8346                for (i, expr) in exprs.iter().enumerate() {
8347                    if i == exprs.len() - 1 {
8348                        // Last expression - evaluate for TCO
8349                        return self.evaluate_for_tco(expr, data);
8350                    } else {
8351                        result = self.evaluate_internal(expr, data)?;
8352                    }
8353                }
8354                Ok(LambdaResult::JValue(result))
8355            }
8356
8357            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
8358            AstNode::Binary {
8359                op: BinaryOp::ColonEqual,
8360                lhs,
8361                rhs,
8362            } => {
8363                // This is var := value; get the variable name
8364                let var_name = match lhs.as_ref() {
8365                    AstNode::Variable(name) => name.clone(),
8366                    _ => {
8367                        // Not a simple variable binding, evaluate normally
8368                        let result = self.evaluate_internal(node, data)?;
8369                        return Ok(LambdaResult::JValue(result));
8370                    }
8371                };
8372
8373                // Check if RHS is a lambda - store it specially
8374                if let AstNode::Lambda {
8375                    params,
8376                    body,
8377                    signature,
8378                    thunk,
8379                } = rhs.as_ref()
8380                {
8381                    let captured_env = self.capture_environment_for(body, params);
8382                    let compiled_body = if !thunk {
8383                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8384                        try_compile_expr_with_allowed_vars(body, &var_refs)
8385                    } else {
8386                        None
8387                    };
8388                    let stored_lambda = StoredLambda {
8389                        params: params.clone(),
8390                        body: (**body).clone(),
8391                        compiled_body,
8392                        signature: signature.clone(),
8393                        captured_env,
8394                        captured_data: Some(data.clone()),
8395                        thunk: *thunk,
8396                    };
8397                    self.context.bind_lambda(var_name, stored_lambda);
8398                    let lambda_repr =
8399                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
8400                    return Ok(LambdaResult::JValue(lambda_repr));
8401                }
8402
8403                // Evaluate the RHS
8404                let value = self.evaluate_internal(rhs, data)?;
8405                self.context.bind(var_name, value.clone());
8406                Ok(LambdaResult::JValue(value))
8407            }
8408
8409            // Function call - this is where TCO happens
8410            AstNode::Function { name, args, .. } => {
8411                // Check if this is a call to a stored lambda (user function)
8412                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
8413                    if stored_lambda.thunk {
8414                        let mut evaluated_args = Vec::with_capacity(args.len());
8415                        for arg in args {
8416                            evaluated_args.push(self.evaluate_internal(arg, data)?);
8417                        }
8418                        return Ok(LambdaResult::TailCall {
8419                            lambda: Box::new(stored_lambda),
8420                            args: evaluated_args,
8421                            data: data.clone(),
8422                        });
8423                    }
8424                }
8425                // Not a thunk lambda - evaluate normally
8426                let result = self.evaluate_internal(node, data)?;
8427                Ok(LambdaResult::JValue(result))
8428            }
8429
8430            // Call node (calling a lambda value)
8431            AstNode::Call { procedure, args } => {
8432                // Evaluate the procedure to get the callable
8433                let callable = self.evaluate_internal(procedure, data)?;
8434
8435                // Check if it's a lambda with TCO
8436                if let JValue::Lambda { lambda_id, .. } = &callable {
8437                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
8438                        if stored_lambda.thunk {
8439                            let mut evaluated_args = Vec::with_capacity(args.len());
8440                            for arg in args {
8441                                evaluated_args.push(self.evaluate_internal(arg, data)?);
8442                            }
8443                            return Ok(LambdaResult::TailCall {
8444                                lambda: Box::new(stored_lambda),
8445                                args: evaluated_args,
8446                                data: data.clone(),
8447                            });
8448                        }
8449                    }
8450                }
8451                // Not a thunk - evaluate normally
8452                let result = self.evaluate_internal(node, data)?;
8453                Ok(LambdaResult::JValue(result))
8454            }
8455
8456            // Variable reference that might be a function call
8457            // This handles cases like $f($x) where $f is referenced by name
8458            AstNode::Variable(_) => {
8459                let result = self.evaluate_internal(node, data)?;
8460                Ok(LambdaResult::JValue(result))
8461            }
8462
8463            // Any other expression - evaluate normally
8464            _ => {
8465                let result = self.evaluate_internal(node, data)?;
8466                Ok(LambdaResult::JValue(result))
8467            }
8468        }
8469    }
8470
8471    /// Match with custom matcher function
8472    ///
8473    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
8474    /// The matcher function is called with the string and returns:
8475    /// { match: string, start: number, end: number, groups: [], next: function }
8476    /// The next function is called repeatedly to get subsequent matches
8477    fn match_with_custom_matcher(
8478        &mut self,
8479        str_value: &str,
8480        matcher_node: &AstNode,
8481        limit: Option<usize>,
8482        data: &JValue,
8483    ) -> Result<JValue, EvaluatorError> {
8484        let mut results = Vec::new();
8485        let mut count = 0;
8486
8487        // Call the matcher function with the string
8488        let str_val = JValue::string(str_value.to_string());
8489        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
8490
8491        // Iterate through matches following the 'next' chain
8492        while !current_match.is_undefined() && !current_match.is_null() {
8493            // Check limit
8494            if let Some(lim) = limit {
8495                if count >= lim {
8496                    break;
8497                }
8498            }
8499
8500            // Extract match information from the result object
8501            if let JValue::Object(ref match_obj) = current_match {
8502                // Validate that this is a proper match object
8503                let has_match = match_obj.contains_key("match");
8504                let has_start = match_obj.contains_key("start");
8505                let has_end = match_obj.contains_key("end");
8506                let has_groups = match_obj.contains_key("groups");
8507                let has_next = match_obj.contains_key("next");
8508
8509                if !has_match && !has_start && !has_end && !has_groups && !has_next {
8510                    // Invalid matcher result - T1010 error
8511                    return Err(EvaluatorError::EvaluationError(
8512                        "T1010: The matcher function did not return the correct object structure"
8513                            .to_string(),
8514                    ));
8515                }
8516
8517                // Build the result match object (match, index, groups)
8518                let mut result_obj = IndexMap::new();
8519
8520                if let Some(match_val) = match_obj.get("match") {
8521                    result_obj.insert("match".to_string(), match_val.clone());
8522                }
8523
8524                if let Some(start_val) = match_obj.get("start") {
8525                    result_obj.insert("index".to_string(), start_val.clone());
8526                }
8527
8528                if let Some(groups_val) = match_obj.get("groups") {
8529                    result_obj.insert("groups".to_string(), groups_val.clone());
8530                }
8531
8532                results.push(JValue::object(result_obj));
8533                count += 1;
8534
8535                // Get the next match by calling the 'next' function
8536                if let Some(next_func) = match_obj.get("next") {
8537                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
8538                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
8539                        continue;
8540                    }
8541                }
8542
8543                // No next function or couldn't call it - stop iteration
8544                break;
8545            } else {
8546                // Not a valid match object
8547                break;
8548            }
8549        }
8550
8551        // Return results
8552        if results.is_empty() {
8553            Ok(JValue::Undefined)
8554        } else {
8555            Ok(JValue::array(results))
8556        }
8557    }
8558
8559    /// Replace with lambda/function callback
8560    ///
8561    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
8562    /// The function receives a match object with: match, start, end, groups
8563    fn replace_with_lambda(
8564        &mut self,
8565        str_value: &JValue,
8566        pattern_value: &JValue,
8567        lambda_value: &JValue,
8568        limit_value: Option<&JValue>,
8569        data: &JValue,
8570    ) -> Result<JValue, EvaluatorError> {
8571        // Extract string
8572        let s = match str_value {
8573            JValue::String(s) => &**s,
8574            _ => {
8575                return Err(EvaluatorError::TypeError(
8576                    "replace() requires string arguments".to_string(),
8577                ))
8578            }
8579        };
8580
8581        // Extract regex pattern
8582        let (pattern, flags) =
8583            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
8584                EvaluatorError::TypeError(
8585                    "replace() pattern must be a regex when using lambda replacement".to_string(),
8586                )
8587            })?;
8588
8589        // Build regex
8590        let re = crate::functions::string::build_regex(&pattern, &flags)?;
8591
8592        // Parse limit
8593        let limit = if let Some(lim_val) = limit_value {
8594            match lim_val {
8595                JValue::Number(n) => {
8596                    let lim_f64 = *n;
8597                    if lim_f64 < 0.0 {
8598                        return Err(EvaluatorError::EvaluationError(format!(
8599                            "D3011: Limit must be non-negative, got {}",
8600                            lim_f64
8601                        )));
8602                    }
8603                    Some(lim_f64 as usize)
8604                }
8605                _ => {
8606                    return Err(EvaluatorError::TypeError(
8607                        "replace() limit must be a number".to_string(),
8608                    ))
8609                }
8610            }
8611        } else {
8612            None
8613        };
8614
8615        // Iterate through matches and replace using lambda
8616        let mut result = String::new();
8617        let mut last_end = 0;
8618        let mut count = 0;
8619
8620        for cap in re.captures_iter(s) {
8621            // Check limit
8622            if let Some(lim) = limit {
8623                if count >= lim {
8624                    break;
8625                }
8626            }
8627
8628            let m = cap.get(0).unwrap();
8629            let match_start = m.start();
8630            let match_end = m.end();
8631            let match_str = m.as_str();
8632
8633            // Add text before match
8634            result.push_str(&s[last_end..match_start]);
8635
8636            // Build match object
8637            let groups: Vec<JValue> = (1..cap.len())
8638                .map(|i| {
8639                    cap.get(i)
8640                        .map(|m| JValue::string(m.as_str().to_string()))
8641                        .unwrap_or(JValue::Null)
8642                })
8643                .collect();
8644
8645            let mut match_map = IndexMap::new();
8646            match_map.insert("match".to_string(), JValue::string(match_str));
8647            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
8648            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
8649            match_map.insert("groups".to_string(), JValue::array(groups));
8650            let match_obj = JValue::object(match_map);
8651
8652            // Invoke lambda with match object
8653            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
8654                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
8655            })?;
8656            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
8657            let replacement_str = match lambda_result {
8658                JValue::String(s) => s,
8659                _ => {
8660                    return Err(EvaluatorError::TypeError(format!(
8661                        "D3012: Replacement function must return a string, got {:?}",
8662                        lambda_result
8663                    )))
8664                }
8665            };
8666
8667            // Add replacement
8668            result.push_str(&replacement_str);
8669
8670            last_end = match_end;
8671            count += 1;
8672        }
8673
8674        // Add remaining text after last match
8675        result.push_str(&s[last_end..]);
8676
8677        Ok(JValue::string(result))
8678    }
8679
8680    /// Capture the current environment bindings for closure support
8681    fn capture_current_environment(&self) -> HashMap<String, JValue> {
8682        self.context.all_bindings()
8683    }
8684
8685    /// Capture only the variables referenced by a lambda body (selective capture).
8686    /// This avoids cloning the entire environment when only a few variables are needed.
8687    fn capture_environment_for(
8688        &self,
8689        body: &AstNode,
8690        params: &[String],
8691    ) -> HashMap<String, JValue> {
8692        let free_vars = Self::collect_free_variables(body, params);
8693        if free_vars.is_empty() {
8694            return HashMap::new();
8695        }
8696        let mut result = HashMap::new();
8697        for var_name in &free_vars {
8698            if let Some(value) = self.context.lookup(var_name) {
8699                result.insert(var_name.clone(), value.clone());
8700            }
8701        }
8702        result
8703    }
8704
8705    /// Collect all free variables in an AST node that are not bound by the given params.
8706    /// A "free variable" is one that is referenced but not defined within the expression.
8707    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
8708        let mut free_vars = HashSet::new();
8709        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
8710        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
8711        free_vars
8712    }
8713
8714    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
8715        match node {
8716            AstNode::Variable(name) => {
8717                if !bound.contains(name.as_str()) {
8718                    free.insert(name.clone());
8719                }
8720            }
8721            AstNode::Function { name, args, .. } => {
8722                // Function name references a variable (e.g., $f(...))
8723                if !bound.contains(name.as_str()) {
8724                    free.insert(name.clone());
8725                }
8726                for arg in args {
8727                    Self::collect_free_vars_walk(arg, bound, free);
8728                }
8729            }
8730            AstNode::Lambda { params, body, .. } => {
8731                // Inner lambda introduces new bindings
8732                let mut inner_bound = bound.clone();
8733                for p in params {
8734                    inner_bound.insert(p.as_str());
8735                }
8736                Self::collect_free_vars_walk(body, &inner_bound, free);
8737            }
8738            AstNode::Binary { op, lhs, rhs } => {
8739                Self::collect_free_vars_walk(lhs, bound, free);
8740                Self::collect_free_vars_walk(rhs, bound, free);
8741                // For ColonEqual, note: the binding is visible after this expr in blocks,
8742                // but block handling takes care of that separately
8743                let _ = op;
8744            }
8745            AstNode::Unary { operand, .. } => {
8746                Self::collect_free_vars_walk(operand, bound, free);
8747            }
8748            AstNode::Path { steps } => {
8749                for step in steps {
8750                    Self::collect_free_vars_walk(&step.node, bound, free);
8751                    for stage in &step.stages {
8752                        match stage {
8753                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
8754                        }
8755                    }
8756                }
8757            }
8758            AstNode::Call { procedure, args } => {
8759                Self::collect_free_vars_walk(procedure, bound, free);
8760                for arg in args {
8761                    Self::collect_free_vars_walk(arg, bound, free);
8762                }
8763            }
8764            AstNode::Conditional {
8765                condition,
8766                then_branch,
8767                else_branch,
8768            } => {
8769                Self::collect_free_vars_walk(condition, bound, free);
8770                Self::collect_free_vars_walk(then_branch, bound, free);
8771                if let Some(else_expr) = else_branch {
8772                    Self::collect_free_vars_walk(else_expr, bound, free);
8773                }
8774            }
8775            AstNode::Block(exprs) => {
8776                let mut block_bound = bound.clone();
8777                for expr in exprs {
8778                    Self::collect_free_vars_walk(expr, &block_bound, free);
8779                    // Bindings introduced via := become bound for subsequent expressions
8780                    if let AstNode::Binary {
8781                        op: BinaryOp::ColonEqual,
8782                        lhs,
8783                        ..
8784                    } = expr
8785                    {
8786                        if let AstNode::Variable(var_name) = lhs.as_ref() {
8787                            block_bound.insert(var_name.as_str());
8788                        }
8789                    }
8790                }
8791            }
8792            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
8793                for expr in exprs {
8794                    Self::collect_free_vars_walk(expr, bound, free);
8795                }
8796            }
8797            AstNode::Object(pairs) => {
8798                for (key, value) in pairs {
8799                    Self::collect_free_vars_walk(key, bound, free);
8800                    Self::collect_free_vars_walk(value, bound, free);
8801                }
8802            }
8803            AstNode::ObjectTransform { input, pattern } => {
8804                Self::collect_free_vars_walk(input, bound, free);
8805                for (key, value) in pattern {
8806                    Self::collect_free_vars_walk(key, bound, free);
8807                    Self::collect_free_vars_walk(value, bound, free);
8808                }
8809            }
8810            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
8811                Self::collect_free_vars_walk(expr, bound, free);
8812            }
8813            AstNode::Sort { input, terms } => {
8814                Self::collect_free_vars_walk(input, bound, free);
8815                for (expr, _) in terms {
8816                    Self::collect_free_vars_walk(expr, bound, free);
8817                }
8818            }
8819            AstNode::IndexBind { input, .. } => {
8820                Self::collect_free_vars_walk(input, bound, free);
8821            }
8822            AstNode::Transform {
8823                location,
8824                update,
8825                delete,
8826            } => {
8827                Self::collect_free_vars_walk(location, bound, free);
8828                Self::collect_free_vars_walk(update, bound, free);
8829                if let Some(del) = delete {
8830                    Self::collect_free_vars_walk(del, bound, free);
8831                }
8832            }
8833            // Leaf nodes with no variable references
8834            AstNode::String(_)
8835            | AstNode::Name(_)
8836            | AstNode::Number(_)
8837            | AstNode::Boolean(_)
8838            | AstNode::Null
8839            | AstNode::Undefined
8840            | AstNode::Placeholder
8841            | AstNode::Regex { .. }
8842            | AstNode::Wildcard
8843            | AstNode::Descendant
8844            | AstNode::ParentVariable(_) => {}
8845        }
8846    }
8847
8848    /// Check if a name refers to a built-in function
8849    fn is_builtin_function(&self, name: &str) -> bool {
8850        matches!(
8851            name,
8852            // String functions
8853            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
8854            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
8855            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
8856            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
8857
8858            // Numeric functions
8859            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
8860            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
8861
8862            // Aggregation functions
8863            "sum" | "max" | "min" | "average" |
8864
8865            // Boolean/logic functions
8866            "boolean" | "not" | "exists" |
8867
8868            // Array functions
8869            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
8870
8871            // Object functions
8872            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
8873
8874            // Higher-order functions
8875            "map" | "filter" | "reduce" | "singletonArray" |
8876
8877            // Date/time functions
8878            "now" | "millis" | "fromMillis" | "toMillis"
8879        )
8880    }
8881
8882    /// Call a built-in function directly with pre-evaluated Values
8883    /// This is used when passing built-in functions to higher-order functions like $map
8884    fn call_builtin_with_values(
8885        &mut self,
8886        name: &str,
8887        values: &[JValue],
8888    ) -> Result<JValue, EvaluatorError> {
8889        use crate::functions;
8890
8891        if values.is_empty() {
8892            return Err(EvaluatorError::EvaluationError(format!(
8893                "{}() requires at least 1 argument",
8894                name
8895            )));
8896        }
8897
8898        let arg = &values[0];
8899
8900        match name {
8901            "string" => Ok(functions::string::string(arg, None)?),
8902            "number" => Ok(functions::numeric::number(arg)?),
8903            "boolean" => Ok(functions::boolean::boolean(arg)?),
8904            "not" => {
8905                let b = functions::boolean::boolean(arg)?;
8906                match b {
8907                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
8908                    _ => Err(EvaluatorError::TypeError(
8909                        "not() requires a boolean".to_string(),
8910                    )),
8911                }
8912            }
8913            "exists" => Ok(JValue::Bool(!arg.is_null())),
8914            "abs" => match arg {
8915                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
8916                _ => Err(EvaluatorError::TypeError(
8917                    "abs() requires a number argument".to_string(),
8918                )),
8919            },
8920            "floor" => match arg {
8921                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
8922                _ => Err(EvaluatorError::TypeError(
8923                    "floor() requires a number argument".to_string(),
8924                )),
8925            },
8926            "ceil" => match arg {
8927                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
8928                _ => Err(EvaluatorError::TypeError(
8929                    "ceil() requires a number argument".to_string(),
8930                )),
8931            },
8932            "round" => match arg {
8933                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
8934                _ => Err(EvaluatorError::TypeError(
8935                    "round() requires a number argument".to_string(),
8936                )),
8937            },
8938            "sqrt" => match arg {
8939                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
8940                _ => Err(EvaluatorError::TypeError(
8941                    "sqrt() requires a number argument".to_string(),
8942                )),
8943            },
8944            "uppercase" => match arg {
8945                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
8946                JValue::Null => Ok(JValue::Null),
8947                _ => Err(EvaluatorError::TypeError(
8948                    "uppercase() requires a string argument".to_string(),
8949                )),
8950            },
8951            "lowercase" => match arg {
8952                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
8953                JValue::Null => Ok(JValue::Null),
8954                _ => Err(EvaluatorError::TypeError(
8955                    "lowercase() requires a string argument".to_string(),
8956                )),
8957            },
8958            "trim" => match arg {
8959                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
8960                JValue::Null => Ok(JValue::Null),
8961                _ => Err(EvaluatorError::TypeError(
8962                    "trim() requires a string argument".to_string(),
8963                )),
8964            },
8965            "length" => match arg {
8966                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
8967                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
8968                JValue::Null => Ok(JValue::Null),
8969                _ => Err(EvaluatorError::TypeError(
8970                    "length() requires a string or array argument".to_string(),
8971                )),
8972            },
8973            "sum" => match arg {
8974                JValue::Array(arr) => {
8975                    let mut total = 0.0;
8976                    for item in arr.iter() {
8977                        match item {
8978                            JValue::Number(n) => {
8979                                total += *n;
8980                            }
8981                            _ => {
8982                                return Err(EvaluatorError::TypeError(
8983                                    "sum() requires all array elements to be numbers".to_string(),
8984                                ));
8985                            }
8986                        }
8987                    }
8988                    Ok(JValue::Number(total))
8989                }
8990                JValue::Number(n) => Ok(JValue::Number(*n)),
8991                JValue::Null => Ok(JValue::Null),
8992                _ => Err(EvaluatorError::TypeError(
8993                    "sum() requires an array of numbers".to_string(),
8994                )),
8995            },
8996            "count" => {
8997                match arg {
8998                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
8999                    JValue::Null => Ok(JValue::Number(0.0)),
9000                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
9001                }
9002            }
9003            "max" => match arg {
9004                JValue::Array(arr) => {
9005                    let mut max_val: Option<f64> = None;
9006                    for item in arr.iter() {
9007                        if let JValue::Number(n) = item {
9008                            let f = *n;
9009                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
9010                        }
9011                    }
9012                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
9013                }
9014                JValue::Number(n) => Ok(JValue::Number(*n)),
9015                JValue::Null => Ok(JValue::Null),
9016                _ => Err(EvaluatorError::TypeError(
9017                    "max() requires an array of numbers".to_string(),
9018                )),
9019            },
9020            "min" => match arg {
9021                JValue::Array(arr) => {
9022                    let mut min_val: Option<f64> = None;
9023                    for item in arr.iter() {
9024                        if let JValue::Number(n) = item {
9025                            let f = *n;
9026                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
9027                        }
9028                    }
9029                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
9030                }
9031                JValue::Number(n) => Ok(JValue::Number(*n)),
9032                JValue::Null => Ok(JValue::Null),
9033                _ => Err(EvaluatorError::TypeError(
9034                    "min() requires an array of numbers".to_string(),
9035                )),
9036            },
9037            "average" => match arg {
9038                JValue::Array(arr) => {
9039                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
9040                    if nums.is_empty() {
9041                        Ok(JValue::Null)
9042                    } else {
9043                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
9044                        Ok(JValue::Number(avg))
9045                    }
9046                }
9047                JValue::Number(n) => Ok(JValue::Number(*n)),
9048                JValue::Null => Ok(JValue::Null),
9049                _ => Err(EvaluatorError::TypeError(
9050                    "average() requires an array of numbers".to_string(),
9051                )),
9052            },
9053            "append" => {
9054                // append(array1, array2) - append second array to first
9055                if values.len() < 2 {
9056                    return Err(EvaluatorError::EvaluationError(
9057                        "append() requires 2 arguments".to_string(),
9058                    ));
9059                }
9060                let first = &values[0];
9061                let second = &values[1];
9062
9063                // Convert first to array if needed
9064                let mut result = match first {
9065                    JValue::Array(arr) => arr.to_vec(),
9066                    JValue::Null => vec![],
9067                    other => vec![other.clone()],
9068                };
9069
9070                // Append second (flatten if array)
9071                match second {
9072                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
9073                    JValue::Null => {}
9074                    other => result.push(other.clone()),
9075                }
9076
9077                Ok(JValue::array(result))
9078            }
9079            "reverse" => match arg {
9080                JValue::Array(arr) => {
9081                    let mut reversed = arr.to_vec();
9082                    reversed.reverse();
9083                    Ok(JValue::array(reversed))
9084                }
9085                JValue::Null => Ok(JValue::Null),
9086                _ => Err(EvaluatorError::TypeError(
9087                    "reverse() requires an array".to_string(),
9088                )),
9089            },
9090            "keys" => match arg {
9091                JValue::Object(obj) => {
9092                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
9093                    Ok(JValue::array(keys))
9094                }
9095                JValue::Null => Ok(JValue::Null),
9096                _ => Err(EvaluatorError::TypeError(
9097                    "keys() requires an object".to_string(),
9098                )),
9099            },
9100
9101            // Add more functions as needed
9102            _ => Err(EvaluatorError::ReferenceError(format!(
9103                "Built-in function {} cannot be called with values directly",
9104                name
9105            ))),
9106        }
9107    }
9108
9109    /// Collect all descendant values recursively
9110    fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
9111        let mut descendants = Vec::new();
9112
9113        match value {
9114            JValue::Null => {
9115                // Null has no descendants, return empty
9116                return descendants;
9117            }
9118            JValue::Object(obj) => {
9119                // Include the current object
9120                descendants.push(value.clone());
9121
9122                for val in obj.values() {
9123                    // Recursively collect descendants
9124                    descendants.extend(self.collect_descendants(val));
9125                }
9126            }
9127            JValue::Array(arr) => {
9128                // DO NOT include the array itself - only recurse into elements
9129                // This matches JavaScript behavior: arrays are traversed but not collected
9130                for val in arr.iter() {
9131                    // Recursively collect descendants
9132                    descendants.extend(self.collect_descendants(val));
9133                }
9134            }
9135            _ => {
9136                // For primitives (string, number, boolean), just include the value itself
9137                descendants.push(value.clone());
9138            }
9139        }
9140
9141        descendants
9142    }
9143
9144    /// Evaluate a predicate (array filter or index)
9145    fn evaluate_predicate(
9146        &mut self,
9147        current: &JValue,
9148        predicate: &AstNode,
9149    ) -> Result<JValue, EvaluatorError> {
9150        // Special case: empty brackets [] (represented as Boolean(true))
9151        // This forces the value to be wrapped in an array
9152        if matches!(predicate, AstNode::Boolean(true)) {
9153            return match current {
9154                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
9155                JValue::Null => Ok(JValue::Null),
9156                other => Ok(JValue::array(vec![other.clone()])),
9157            };
9158        }
9159
9160        match current {
9161            JValue::Array(_arr) => {
9162                // Standalone predicates do simple array operations (no mapping over sub-arrays)
9163
9164                // First, try to evaluate predicate as a simple number (array index)
9165                if let AstNode::Number(n) = predicate {
9166                    // Direct array indexing
9167                    return self.array_index(current, &JValue::Number(*n));
9168                }
9169
9170                // Fast path: if predicate is definitely a filter expression (comparison/logical),
9171                // skip speculative numeric evaluation and go directly to filter logic
9172                if Self::is_filter_predicate(predicate) {
9173                    // Try CompiledExpr fast path
9174                    if let Some(compiled) = try_compile_expr(predicate) {
9175                        let shape = _arr.first().and_then(build_shape_cache);
9176                        let mut filtered = Vec::with_capacity(_arr.len());
9177                        for item in _arr.iter() {
9178                            let result = if let Some(ref s) = shape {
9179                                eval_compiled_shaped(&compiled, item, None, s)?
9180                            } else {
9181                                eval_compiled(&compiled, item, None)?
9182                            };
9183                            if compiled_is_truthy(&result) {
9184                                filtered.push(item.clone());
9185                            }
9186                        }
9187                        return Ok(JValue::array(filtered));
9188                    }
9189                    // Fallback: full AST evaluation per element
9190                    let mut filtered = Vec::new();
9191                    for item in _arr.iter() {
9192                        let item_result = self.evaluate_internal(predicate, item)?;
9193                        if self.is_truthy(&item_result) {
9194                            filtered.push(item.clone());
9195                        }
9196                    }
9197                    return Ok(JValue::array(filtered));
9198                }
9199
9200                // Try to evaluate the predicate to see if it's a numeric index
9201                // If evaluation succeeds and yields a number, use it as an index
9202                // If evaluation fails (e.g., comparison error), treat as filter
9203                match self.evaluate_internal(predicate, current) {
9204                    Ok(JValue::Number(_)) => {
9205                        // It's a numeric index
9206                        let pred_result = self.evaluate_internal(predicate, current)?;
9207                        return self.array_index(current, &pred_result);
9208                    }
9209                    Ok(JValue::Array(indices)) => {
9210                        // Multiple array selectors [[indices]]
9211                        // Check if array contains any non-numeric values
9212                        let has_non_numeric =
9213                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
9214
9215                        if has_non_numeric {
9216                            // If array contains non-numeric values, return entire array
9217                            return Ok(current.clone());
9218                        }
9219
9220                        // Collect numeric indices, handling negative indices
9221                        let arr_len = _arr.len() as i64;
9222                        let mut resolved_indices: Vec<i64> = indices
9223                            .iter()
9224                            .filter_map(|v| {
9225                                if let JValue::Number(n) = v {
9226                                    let idx = *n as i64;
9227                                    // Resolve negative indices
9228                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
9229                                    // Only include valid indices
9230                                    if actual_idx >= 0 && actual_idx < arr_len {
9231                                        Some(actual_idx)
9232                                    } else {
9233                                        None
9234                                    }
9235                                } else {
9236                                    None
9237                                }
9238                            })
9239                            .collect();
9240
9241                        // Sort and deduplicate indices
9242                        resolved_indices.sort();
9243                        resolved_indices.dedup();
9244
9245                        // Select elements at each sorted index
9246                        let result: Vec<JValue> = resolved_indices
9247                            .iter()
9248                            .map(|&idx| _arr[idx as usize].clone())
9249                            .collect();
9250
9251                        return Ok(JValue::array(result));
9252                    }
9253                    Ok(_) => {
9254                        // Evaluated successfully but not a number - might be a filter
9255                        // Fall through to filter logic
9256                    }
9257                    Err(_) => {
9258                        // Evaluation failed - it's likely a filter expression
9259                        // Fall through to filter logic
9260                    }
9261                }
9262
9263                // Try CompiledExpr fast path for filter expressions
9264                if let Some(compiled) = try_compile_expr(predicate) {
9265                    let shape = _arr.first().and_then(build_shape_cache);
9266                    let mut filtered = Vec::with_capacity(_arr.len());
9267                    for item in _arr.iter() {
9268                        let result = if let Some(ref s) = shape {
9269                            eval_compiled_shaped(&compiled, item, None, s)?
9270                        } else {
9271                            eval_compiled(&compiled, item, None)?
9272                        };
9273                        if compiled_is_truthy(&result) {
9274                            filtered.push(item.clone());
9275                        }
9276                    }
9277                    return Ok(JValue::array(filtered));
9278                }
9279
9280                // It's a filter expression - evaluate the predicate for each array element
9281                let mut filtered = Vec::new();
9282                for item in _arr.iter() {
9283                    let item_result = self.evaluate_internal(predicate, item)?;
9284
9285                    // If result is truthy, include this item
9286                    if self.is_truthy(&item_result) {
9287                        filtered.push(item.clone());
9288                    }
9289                }
9290
9291                Ok(JValue::array(filtered))
9292            }
9293            JValue::Object(obj) => {
9294                // For objects, predicate can be either:
9295                // 1. A string - property access (computed property name)
9296                // 2. A boolean expression - filter (return object if truthy)
9297                let pred_result = self.evaluate_internal(predicate, current)?;
9298
9299                // If it's a string, use it as a key for property access
9300                if let JValue::String(key) = &pred_result {
9301                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
9302                }
9303
9304                // Otherwise, treat as a filter expression
9305                // If the predicate is truthy, return the object; otherwise return undefined
9306                if self.is_truthy(&pred_result) {
9307                    Ok(current.clone())
9308                } else {
9309                    Ok(JValue::Undefined)
9310                }
9311            }
9312            _ => {
9313                // For primitive values (string, number, boolean):
9314                // In JSONata, scalars are treated as single-element arrays when indexed.
9315                // So value[0] returns value, value[1] returns undefined.
9316
9317                // First check if predicate is a numeric literal
9318                if let AstNode::Number(n) = predicate {
9319                    // For scalars, index 0 or -1 returns the value, others return undefined
9320                    let idx = n.floor() as i64;
9321                    if idx == 0 || idx == -1 {
9322                        return Ok(current.clone());
9323                    } else {
9324                        return Ok(JValue::Undefined);
9325                    }
9326                }
9327
9328                // Try to evaluate the predicate to see if it's a numeric index
9329                let pred_result = self.evaluate_internal(predicate, current)?;
9330
9331                if let JValue::Number(n) = &pred_result {
9332                    // It's a numeric index - treat scalar as single-element array
9333                    let idx = n.floor() as i64;
9334                    if idx == 0 || idx == -1 {
9335                        return Ok(current.clone());
9336                    } else {
9337                        return Ok(JValue::Undefined);
9338                    }
9339                }
9340
9341                // For non-numeric predicates, treat as a filter:
9342                // value[true] returns value, value[false] returns undefined
9343                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
9344                if self.is_truthy(&pred_result) {
9345                    Ok(current.clone())
9346                } else {
9347                    // Return undefined (not null) so $map can filter it out
9348                    Ok(JValue::Undefined)
9349                }
9350            }
9351        }
9352    }
9353
9354    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
9355    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
9356    fn evaluate_sort_term(
9357        &mut self,
9358        term_expr: &AstNode,
9359        element: &JValue,
9360    ) -> Result<JValue, EvaluatorError> {
9361        // For tuples (from index binding), extract the actual value from @ field
9362        let actual_element = if let JValue::Object(obj) = element {
9363            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
9364                obj.get("@").cloned().unwrap_or(JValue::Null)
9365            } else {
9366                element.clone()
9367            }
9368        } else {
9369            element.clone()
9370        };
9371
9372        // For simple field access (Path with single Name step), check if field exists
9373        if let AstNode::Path { steps } = term_expr {
9374            if steps.len() == 1 && steps[0].stages.is_empty() {
9375                if let AstNode::Name(field_name) = &steps[0].node {
9376                    // Check if the field exists in the element
9377                    if let JValue::Object(obj) = &actual_element {
9378                        return match obj.get(field_name) {
9379                            Some(val) => Ok(val.clone()),  // Field exists (may be null)
9380                            None => Ok(JValue::Undefined), // Field is missing
9381                        };
9382                    } else {
9383                        // Not an object - return undefined
9384                        return Ok(JValue::Undefined);
9385                    }
9386                }
9387            }
9388        }
9389
9390        // For complex expressions, evaluate normally against the actual element
9391        // but with the full tuple as the data context (so index bindings are accessible)
9392        let result = self.evaluate_internal(term_expr, element)?;
9393
9394        // If the result is null from a complex expression, we can't easily tell if it's
9395        // "missing field" or "explicit null". For now, treat null results as undefined
9396        // to maintain compatibility with existing tests.
9397        // TODO: For full JS compatibility, would need deeper analysis of the expression
9398        if result.is_null() {
9399            return Ok(JValue::Undefined);
9400        }
9401
9402        Ok(result)
9403    }
9404
9405    /// Evaluate sort operator
9406    fn evaluate_sort(
9407        &mut self,
9408        data: &JValue,
9409        terms: &[(AstNode, bool)],
9410    ) -> Result<JValue, EvaluatorError> {
9411        // If data is null, return null
9412        if data.is_null() {
9413            return Ok(JValue::Null);
9414        }
9415
9416        // If data is not an array, return it as-is (can't sort a single value)
9417        let array = match data {
9418            JValue::Array(arr) => arr.clone(),
9419            other => return Ok(other.clone()),
9420        };
9421
9422        // If empty array, return as-is
9423        if array.is_empty() {
9424            return Ok(JValue::Array(array));
9425        }
9426
9427        // Evaluate sort keys for each element
9428        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
9429
9430        for (idx, element) in array.iter().enumerate() {
9431            let mut sort_keys = Vec::new();
9432
9433            // Evaluate each sort term with $ bound to the element
9434            for (term_expr, _ascending) in terms {
9435                // Save current $ binding
9436                let saved_dollar = self.context.lookup("$").cloned();
9437
9438                // Bind $ to current element
9439                self.context.bind("$".to_string(), element.clone());
9440
9441                // Evaluate the sort expression, distinguishing missing fields from explicit null
9442                let sort_value = self.evaluate_sort_term(term_expr, element)?;
9443
9444                // Restore $ binding
9445                if let Some(val) = saved_dollar {
9446                    self.context.bind("$".to_string(), val);
9447                } else {
9448                    self.context.unbind("$");
9449                }
9450
9451                sort_keys.push(sort_value);
9452            }
9453
9454            indexed_array.push((idx, sort_keys));
9455        }
9456
9457        // Validate that all sort keys are comparable (same type, or undefined)
9458        // Undefined values (missing fields) are allowed and sort to the end
9459        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
9460        for term_idx in 0..terms.len() {
9461            let mut first_valid_type: Option<&str> = None;
9462
9463            for (_idx, sort_keys) in &indexed_array {
9464                let sort_value = &sort_keys[term_idx];
9465
9466                // Skip undefined markers (missing fields) - these are allowed and sort to end
9467                if sort_value.is_undefined() {
9468                    continue;
9469                }
9470
9471                // Get the type name for this value
9472                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
9473                let value_type = match sort_value {
9474                    JValue::Number(_) => "number",
9475                    JValue::String(_) => "string",
9476                    JValue::Bool(_) => "boolean",
9477                    JValue::Array(_) => "array",
9478                    JValue::Object(_) => "object", // This catches non-undefined objects
9479                    JValue::Null => "null",        // Explicit null from data
9480                    _ => "unknown",
9481                };
9482
9483                // Check that sort keys are only numbers or strings
9484                // Null, boolean, array, and object types are not valid for sorting
9485                if value_type != "number" && value_type != "string" {
9486                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
9487                }
9488
9489                // Check if this matches the first valid type we saw
9490                if let Some(first_type) = first_valid_type {
9491                    if first_type != value_type {
9492                        return Err(EvaluatorError::TypeError(format!(
9493                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
9494                            first_type, value_type
9495                        )));
9496                    }
9497                } else {
9498                    first_valid_type = Some(value_type);
9499                }
9500            }
9501        }
9502
9503        // Sort the indexed array
9504        indexed_array.sort_by(|a, b| {
9505            // Compare sort keys in order
9506            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
9507                let left = &a.1[i];
9508                let right = &b.1[i];
9509
9510                let cmp = self.compare_values(left, right);
9511
9512                if cmp != std::cmp::Ordering::Equal {
9513                    return if *ascending { cmp } else { cmp.reverse() };
9514                }
9515            }
9516
9517            // If all keys are equal, maintain original order (stable sort)
9518            a.0.cmp(&b.0)
9519        });
9520
9521        // Extract sorted elements
9522        let sorted: Vec<JValue> = indexed_array
9523            .iter()
9524            .map(|(idx, _)| array[*idx].clone())
9525            .collect();
9526
9527        Ok(JValue::array(sorted))
9528    }
9529
9530    /// Compare two values for sorting (JSONata semantics)
9531    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
9532        // Handle undefined markers first - they sort to the end
9533        let left_undef = left.is_undefined();
9534        let right_undef = right.is_undefined();
9535
9536        if left_undef && right_undef {
9537            return Ordering::Equal;
9538        }
9539        if left_undef {
9540            return Ordering::Greater; // Undefined sorts last
9541        }
9542        if right_undef {
9543            return Ordering::Less;
9544        }
9545
9546        match (left, right) {
9547            // Nulls also sort last (explicit null in data)
9548            (JValue::Null, JValue::Null) => Ordering::Equal,
9549            (JValue::Null, _) => Ordering::Greater,
9550            (_, JValue::Null) => Ordering::Less,
9551
9552            // Numbers
9553            (JValue::Number(a), JValue::Number(b)) => {
9554                let a_f64 = *a;
9555                let b_f64 = *b;
9556                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
9557            }
9558
9559            // Strings
9560            (JValue::String(a), JValue::String(b)) => a.cmp(b),
9561
9562            // Booleans
9563            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
9564
9565            // Arrays (lexicographic comparison)
9566            (JValue::Array(a), JValue::Array(b)) => {
9567                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
9568                    let cmp = self.compare_values(a_elem, b_elem);
9569                    if cmp != Ordering::Equal {
9570                        return cmp;
9571                    }
9572                }
9573                a.len().cmp(&b.len())
9574            }
9575
9576            // Different types: use type ordering
9577            // null < bool < number < string < array < object
9578            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
9579            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
9580            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
9581            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
9582
9583            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
9584            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
9585            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
9586            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
9587
9588            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
9589            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
9590            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
9591            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
9592
9593            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
9594            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
9595            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
9596            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
9597
9598            (JValue::Object(_), _) => Ordering::Greater,
9599            _ => Ordering::Equal,
9600        }
9601    }
9602
9603    /// Check if a value is truthy (JSONata semantics).
9604    fn is_truthy(&self, value: &JValue) -> bool {
9605        match value {
9606            JValue::Null | JValue::Undefined => false,
9607            JValue::Bool(b) => *b,
9608            JValue::Number(n) => *n != 0.0,
9609            JValue::String(s) => !s.is_empty(),
9610            JValue::Array(arr) => !arr.is_empty(),
9611            JValue::Object(obj) => !obj.is_empty(),
9612            _ => false,
9613        }
9614    }
9615
9616    /// Check if a value is truthy for the default operator (?:)
9617    /// This has special semantics:
9618    /// - Lambda/function objects are not values, so they're falsy
9619    /// - Arrays containing only falsy elements are falsy
9620    /// - Otherwise, use standard truthiness
9621    fn is_truthy_for_default(&self, value: &JValue) -> bool {
9622        match value {
9623            // Lambda/function values are not data values, so they're falsy
9624            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
9625            // Arrays need special handling - check if all elements are falsy
9626            JValue::Array(arr) => {
9627                if arr.is_empty() {
9628                    return false;
9629                }
9630                // Array is truthy only if it contains at least one truthy element
9631                arr.iter().any(|elem| self.is_truthy(elem))
9632            }
9633            // For all other types, use standard truthiness
9634            _ => self.is_truthy(value),
9635        }
9636    }
9637
9638    /// Unwrap singleton arrays to scalar values
9639    /// This is used when no explicit array-keeping operation (like []) was used
9640    fn unwrap_singleton(&self, value: JValue) -> JValue {
9641        match value {
9642            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
9643            _ => value,
9644        }
9645    }
9646
9647    /// Extract lambda IDs from a value (used for closure preservation)
9648    /// Finds any lambda_id references in the value so they can be preserved
9649    /// when exiting a block scope
9650    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
9651        // Fast path: scalars can never contain lambda references
9652        match value {
9653            JValue::Number(_)
9654            | JValue::Bool(_)
9655            | JValue::String(_)
9656            | JValue::Null
9657            | JValue::Undefined
9658            | JValue::Regex { .. }
9659            | JValue::Builtin { .. } => return Vec::new(),
9660            _ => {}
9661        }
9662        let mut ids = Vec::new();
9663        self.collect_lambda_ids(value, &mut ids);
9664        ids
9665    }
9666
9667    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
9668        match value {
9669            JValue::Lambda { lambda_id, .. } => {
9670                let id_str = lambda_id.to_string();
9671                if !ids.contains(&id_str) {
9672                    ids.push(id_str);
9673                    // Transitively follow the stored lambda's captured_env
9674                    // to find all referenced lambdas. This is critical for
9675                    // closures like the Y-combinator where returned lambdas
9676                    // capture other lambdas in their environment.
9677                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
9678                        let env_values: Vec<JValue> =
9679                            stored.captured_env.values().cloned().collect();
9680                        for env_value in &env_values {
9681                            self.collect_lambda_ids(env_value, ids);
9682                        }
9683                    }
9684                }
9685            }
9686            JValue::Object(map) => {
9687                // Recurse into object values
9688                for v in map.values() {
9689                    self.collect_lambda_ids(v, ids);
9690                }
9691            }
9692            JValue::Array(arr) => {
9693                // Recurse into array elements
9694                for v in arr.iter() {
9695                    self.collect_lambda_ids(v, ids);
9696                }
9697            }
9698            _ => {}
9699        }
9700    }
9701
9702    /// Equality comparison (JSONata semantics)
9703    fn equals(&self, left: &JValue, right: &JValue) -> bool {
9704        crate::functions::array::values_equal(left, right)
9705    }
9706
9707    /// Addition
9708    fn add(
9709        &self,
9710        left: &JValue,
9711        right: &JValue,
9712        left_is_explicit_null: bool,
9713        right_is_explicit_null: bool,
9714    ) -> Result<JValue, EvaluatorError> {
9715        match (left, right) {
9716            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
9717            // Explicit null literal with number -> T2002 error
9718            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
9719                Err(EvaluatorError::TypeError(
9720                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
9721                ))
9722            }
9723            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
9724                Err(EvaluatorError::TypeError(
9725                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
9726                ))
9727            }
9728            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
9729                Err(EvaluatorError::TypeError(
9730                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
9731                ))
9732            }
9733            // Undefined variable (null) with number -> undefined result
9734            (JValue::Null, JValue::Number(_)) | (JValue::Number(_), JValue::Null) => {
9735                Ok(JValue::Null)
9736            }
9737            // Boolean with anything (including undefined) -> T2001 error
9738            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
9739                "T2001: The left side of the '+' operator must evaluate to a number or a string"
9740                    .to_string(),
9741            )),
9742            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
9743                "T2001: The right side of the '+' operator must evaluate to a number or a string"
9744                    .to_string(),
9745            )),
9746            // Undefined with undefined -> undefined
9747            (JValue::Null, JValue::Null) => Ok(JValue::Null),
9748            _ => Err(EvaluatorError::TypeError(format!(
9749                "Cannot add {:?} and {:?}",
9750                left, right
9751            ))),
9752        }
9753    }
9754
9755    /// Subtraction
9756    fn subtract(
9757        &self,
9758        left: &JValue,
9759        right: &JValue,
9760        left_is_explicit_null: bool,
9761        right_is_explicit_null: bool,
9762    ) -> Result<JValue, EvaluatorError> {
9763        match (left, right) {
9764            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
9765            // Explicit null literal -> error
9766            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9767                "T2002: The left side of the - operator must evaluate to a number".to_string(),
9768            )),
9769            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9770                "T2002: The right side of the - operator must evaluate to a number".to_string(),
9771            )),
9772            // Undefined variables -> undefined result
9773            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9774            _ => Err(EvaluatorError::TypeError(format!(
9775                "Cannot subtract {:?} and {:?}",
9776                left, right
9777            ))),
9778        }
9779    }
9780
9781    /// Multiplication
9782    fn multiply(
9783        &self,
9784        left: &JValue,
9785        right: &JValue,
9786        left_is_explicit_null: bool,
9787        right_is_explicit_null: bool,
9788    ) -> Result<JValue, EvaluatorError> {
9789        match (left, right) {
9790            (JValue::Number(a), JValue::Number(b)) => {
9791                let result = *a * *b;
9792                // Check for overflow to Infinity
9793                if result.is_infinite() {
9794                    return Err(EvaluatorError::EvaluationError(
9795                        "D1001: Number out of range".to_string(),
9796                    ));
9797                }
9798                Ok(JValue::Number(result))
9799            }
9800            // Explicit null literal -> error
9801            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9802                "T2002: The left side of the * operator must evaluate to a number".to_string(),
9803            )),
9804            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9805                "T2002: The right side of the * operator must evaluate to a number".to_string(),
9806            )),
9807            // Undefined variables -> undefined result
9808            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9809            _ => Err(EvaluatorError::TypeError(format!(
9810                "Cannot multiply {:?} and {:?}",
9811                left, right
9812            ))),
9813        }
9814    }
9815
9816    /// Division
9817    fn divide(
9818        &self,
9819        left: &JValue,
9820        right: &JValue,
9821        left_is_explicit_null: bool,
9822        right_is_explicit_null: bool,
9823    ) -> Result<JValue, EvaluatorError> {
9824        match (left, right) {
9825            (JValue::Number(a), JValue::Number(b)) => {
9826                let denominator = *b;
9827                if denominator == 0.0 {
9828                    return Err(EvaluatorError::EvaluationError(
9829                        "Division by zero".to_string(),
9830                    ));
9831                }
9832                Ok(JValue::Number(*a / denominator))
9833            }
9834            // Explicit null literal -> error
9835            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9836                "T2002: The left side of the / operator must evaluate to a number".to_string(),
9837            )),
9838            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9839                "T2002: The right side of the / operator must evaluate to a number".to_string(),
9840            )),
9841            // Undefined variables -> undefined result
9842            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9843            _ => Err(EvaluatorError::TypeError(format!(
9844                "Cannot divide {:?} and {:?}",
9845                left, right
9846            ))),
9847        }
9848    }
9849
9850    /// Modulo
9851    fn modulo(
9852        &self,
9853        left: &JValue,
9854        right: &JValue,
9855        left_is_explicit_null: bool,
9856        right_is_explicit_null: bool,
9857    ) -> Result<JValue, EvaluatorError> {
9858        match (left, right) {
9859            (JValue::Number(a), JValue::Number(b)) => {
9860                let denominator = *b;
9861                if denominator == 0.0 {
9862                    return Err(EvaluatorError::EvaluationError(
9863                        "Division by zero".to_string(),
9864                    ));
9865                }
9866                Ok(JValue::Number(*a % denominator))
9867            }
9868            // Explicit null literal -> error
9869            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
9870                "T2002: The left side of the % operator must evaluate to a number".to_string(),
9871            )),
9872            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
9873                "T2002: The right side of the % operator must evaluate to a number".to_string(),
9874            )),
9875            // Undefined variables -> undefined result
9876            (JValue::Null, _) | (_, JValue::Null) => Ok(JValue::Null),
9877            _ => Err(EvaluatorError::TypeError(format!(
9878                "Cannot compute modulo of {:?} and {:?}",
9879                left, right
9880            ))),
9881        }
9882    }
9883
9884    /// Get human-readable type name for error messages
9885    fn type_name(value: &JValue) -> &'static str {
9886        match value {
9887            JValue::Null => "null",
9888            JValue::Bool(_) => "boolean",
9889            JValue::Number(_) => "number",
9890            JValue::String(_) => "string",
9891            JValue::Array(_) => "array",
9892            JValue::Object(_) => "object",
9893            _ => "unknown",
9894        }
9895    }
9896
9897    /// Ordered comparison with null/type checking shared across <, <=, >, >=
9898    ///
9899    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
9900    /// `compare_strs` receives (left_str, right_str) for string operands.
9901    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
9902    fn ordered_compare(
9903        &self,
9904        left: &JValue,
9905        right: &JValue,
9906        left_is_explicit_null: bool,
9907        right_is_explicit_null: bool,
9908        op_symbol: &str,
9909        compare_nums: fn(f64, f64) -> bool,
9910        compare_strs: fn(&str, &str) -> bool,
9911    ) -> Result<JValue, EvaluatorError> {
9912        match (left, right) {
9913            (JValue::Number(a), JValue::Number(b)) => {
9914                Ok(JValue::Bool(compare_nums(*a, *b)))
9915            }
9916            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
9917            // Both null/undefined -> return undefined
9918            (JValue::Null, JValue::Null) => Ok(JValue::Null),
9919            // Explicit null literal with any type (except null) -> T2010 error
9920            (JValue::Null, _) if left_is_explicit_null => {
9921                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9922            }
9923            (_, JValue::Null) if right_is_explicit_null => {
9924                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9925            }
9926            // Boolean with undefined -> T2010 error
9927            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
9928                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
9929            }
9930            // Number or String with undefined (not explicit null) -> undefined result
9931            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
9932            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
9933                Ok(JValue::Null)
9934            }
9935            // String vs Number -> T2009
9936            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
9937                Err(EvaluatorError::EvaluationError(format!(
9938                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
9939                    op_symbol
9940                )))
9941            }
9942            // Boolean comparisons -> T2010
9943            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
9944                Err(EvaluatorError::EvaluationError(format!(
9945                    "T2010: Cannot compare {} and {}",
9946                    Self::type_name(left), Self::type_name(right)
9947                )))
9948            }
9949            // Other type mismatches
9950            _ => Err(EvaluatorError::EvaluationError(format!(
9951                "T2010: Cannot compare {} and {}",
9952                Self::type_name(left), Self::type_name(right)
9953            ))),
9954        }
9955    }
9956
9957    /// Less than comparison
9958    fn less_than(
9959        &self,
9960        left: &JValue,
9961        right: &JValue,
9962        left_is_explicit_null: bool,
9963        right_is_explicit_null: bool,
9964    ) -> Result<JValue, EvaluatorError> {
9965        self.ordered_compare(
9966            left,
9967            right,
9968            left_is_explicit_null,
9969            right_is_explicit_null,
9970            "<",
9971            |a, b| a < b,
9972            |a, b| a < b,
9973        )
9974    }
9975
9976    /// Less than or equal comparison
9977    fn less_than_or_equal(
9978        &self,
9979        left: &JValue,
9980        right: &JValue,
9981        left_is_explicit_null: bool,
9982        right_is_explicit_null: bool,
9983    ) -> Result<JValue, EvaluatorError> {
9984        self.ordered_compare(
9985            left,
9986            right,
9987            left_is_explicit_null,
9988            right_is_explicit_null,
9989            "<=",
9990            |a, b| a <= b,
9991            |a, b| a <= b,
9992        )
9993    }
9994
9995    /// Greater than comparison
9996    fn greater_than(
9997        &self,
9998        left: &JValue,
9999        right: &JValue,
10000        left_is_explicit_null: bool,
10001        right_is_explicit_null: bool,
10002    ) -> Result<JValue, EvaluatorError> {
10003        self.ordered_compare(
10004            left,
10005            right,
10006            left_is_explicit_null,
10007            right_is_explicit_null,
10008            ">",
10009            |a, b| a > b,
10010            |a, b| a > b,
10011        )
10012    }
10013
10014    /// Greater than or equal comparison
10015    fn greater_than_or_equal(
10016        &self,
10017        left: &JValue,
10018        right: &JValue,
10019        left_is_explicit_null: bool,
10020        right_is_explicit_null: bool,
10021    ) -> Result<JValue, EvaluatorError> {
10022        self.ordered_compare(
10023            left,
10024            right,
10025            left_is_explicit_null,
10026            right_is_explicit_null,
10027            ">=",
10028            |a, b| a >= b,
10029            |a, b| a >= b,
10030        )
10031    }
10032
10033    /// Convert a value to a string for concatenation
10034    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
10035        match value {
10036            JValue::String(s) => Ok(s.to_string()),
10037            JValue::Null => Ok(String::new()),
10038            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
10039                match crate::functions::string::string(value, None) {
10040                    Ok(JValue::String(s)) => Ok(s.to_string()),
10041                    Ok(JValue::Null) => Ok(String::new()),
10042                    _ => Err(EvaluatorError::TypeError(
10043                        "Cannot concatenate complex types".to_string(),
10044                    )),
10045                }
10046            }
10047            _ => Ok(String::new()),
10048        }
10049    }
10050
10051    /// String concatenation
10052    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
10053        let left_str = Self::value_to_concat_string(left)?;
10054        let right_str = Self::value_to_concat_string(right)?;
10055        Ok(JValue::string(format!("{}{}", left_str, right_str)))
10056    }
10057
10058    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
10059    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
10060        // Check left operand is a number or null
10061        let start_f64 = match left {
10062            JValue::Number(n) => Some(*n),
10063            JValue::Null => None,
10064            _ => {
10065                return Err(EvaluatorError::EvaluationError(
10066                    "T2003: Left operand of range operator must be a number".to_string(),
10067                ));
10068            }
10069        };
10070
10071        // Check left operand is an integer (if it's a number)
10072        if let Some(val) = start_f64 {
10073            if val.fract() != 0.0 {
10074                return Err(EvaluatorError::EvaluationError(
10075                    "T2003: Left operand of range operator must be an integer".to_string(),
10076                ));
10077            }
10078        }
10079
10080        // Check right operand is a number or null
10081        let end_f64 = match right {
10082            JValue::Number(n) => Some(*n),
10083            JValue::Null => None,
10084            _ => {
10085                return Err(EvaluatorError::EvaluationError(
10086                    "T2004: Right operand of range operator must be a number".to_string(),
10087                ));
10088            }
10089        };
10090
10091        // Check right operand is an integer (if it's a number)
10092        if let Some(val) = end_f64 {
10093            if val.fract() != 0.0 {
10094                return Err(EvaluatorError::EvaluationError(
10095                    "T2004: Right operand of range operator must be an integer".to_string(),
10096                ));
10097            }
10098        }
10099
10100        // If either operand is null, return empty array
10101        if start_f64.is_none() || end_f64.is_none() {
10102            return Ok(JValue::array(vec![]));
10103        }
10104
10105        let start = start_f64.unwrap() as i64;
10106        let end = end_f64.unwrap() as i64;
10107
10108        // Check range size limit (10 million elements max)
10109        let size = if start <= end {
10110            (end - start + 1) as usize
10111        } else {
10112            0
10113        };
10114        if size > 10_000_000 {
10115            return Err(EvaluatorError::EvaluationError(
10116                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
10117            ));
10118        }
10119
10120        let mut result = Vec::with_capacity(size);
10121        if start <= end {
10122            for i in start..=end {
10123                result.push(JValue::Number(i as f64));
10124            }
10125        }
10126        // Note: if start > end, return empty array (not reversed)
10127        Ok(JValue::array(result))
10128    }
10129
10130    /// In operator (checks if left is in right array/object)
10131    /// Array indexing: array[index]
10132    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
10133        match (array, index) {
10134            (JValue::Array(arr), JValue::Number(n)) => {
10135                let idx = *n as i64;
10136                let len = arr.len() as i64;
10137
10138                // Handle negative indexing (offset from end)
10139                let actual_idx = if idx < 0 { len + idx } else { idx };
10140
10141                if actual_idx < 0 || actual_idx >= len {
10142                    Ok(JValue::Null)
10143                } else {
10144                    Ok(arr[actual_idx as usize].clone())
10145                }
10146            }
10147            _ => Err(EvaluatorError::TypeError(
10148                "Array indexing requires array and number".to_string(),
10149            )),
10150        }
10151    }
10152
10153    /// Array filtering: array[predicate]
10154    /// Evaluates the predicate for each item in the array and returns items where predicate is true
10155    fn array_filter(
10156        &mut self,
10157        _lhs_node: &AstNode,
10158        rhs_node: &AstNode,
10159        array: &JValue,
10160        _original_data: &JValue,
10161    ) -> Result<JValue, EvaluatorError> {
10162        match array {
10163            JValue::Array(arr) => {
10164                // Pre-allocate with estimated capacity (assume ~50% will match)
10165                let mut filtered = Vec::with_capacity(arr.len() / 2);
10166
10167                for item in arr.iter() {
10168                    // Evaluate the predicate in the context of this array item
10169                    // The item becomes the new "current context" ($)
10170                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
10171
10172                    // Check if the predicate is truthy
10173                    if self.is_truthy(&predicate_result) {
10174                        filtered.push(item.clone());
10175                    }
10176                }
10177
10178                Ok(JValue::array(filtered))
10179            }
10180            _ => Err(EvaluatorError::TypeError(
10181                "Array filtering requires an array".to_string(),
10182            )),
10183        }
10184    }
10185
10186    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
10187        // If either side is undefined/null, return false (not an error)
10188        // This matches JavaScript behavior
10189        if left.is_null() || right.is_null() {
10190            return Ok(JValue::Bool(false));
10191        }
10192
10193        match right {
10194            JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
10195            JValue::Object(obj) => {
10196                if let JValue::String(key) = left {
10197                    Ok(JValue::Bool(obj.contains_key(&**key)))
10198                } else {
10199                    Ok(JValue::Bool(false))
10200                }
10201            }
10202            // If right side is not an array or object (e.g., string, number),
10203            // wrap it in an array for comparison
10204            other => Ok(JValue::Bool(self.equals(left, other))),
10205        }
10206    }
10207
10208    /// Create a partially applied function from a function call with placeholder arguments
10209    /// This evaluates non-placeholder arguments and creates a new lambda that takes
10210    /// the placeholder positions as parameters.
10211    fn create_partial_application(
10212        &mut self,
10213        name: &str,
10214        args: &[AstNode],
10215        is_builtin: bool,
10216        data: &JValue,
10217    ) -> Result<JValue, EvaluatorError> {
10218        // First, look up the function to ensure it exists
10219        let is_lambda = self.context.lookup_lambda(name).is_some()
10220            || (self
10221                .context
10222                .lookup(name)
10223                .map(|v| matches!(v, JValue::Lambda { .. }))
10224                .unwrap_or(false));
10225
10226        // Built-in functions must be called with $ prefix for partial application
10227        // Without $, it's an error (T1007) suggesting the user forgot the $
10228        if !is_lambda && !is_builtin {
10229            // Check if it's a built-in function called without $
10230            if self.is_builtin_function(name) {
10231                return Err(EvaluatorError::EvaluationError(format!(
10232                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
10233                    name
10234                )));
10235            }
10236            return Err(EvaluatorError::EvaluationError(
10237                "T1008: Attempted to partially apply a non-function".to_string(),
10238            ));
10239        }
10240
10241        // Evaluate non-placeholder arguments and track placeholder positions
10242        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
10243        let mut placeholder_positions: Vec<usize> = Vec::new();
10244
10245        for (i, arg) in args.iter().enumerate() {
10246            if matches!(arg, AstNode::Placeholder) {
10247                placeholder_positions.push(i);
10248            } else {
10249                let value = self.evaluate_internal(arg, data)?;
10250                bound_args.push((i, value));
10251            }
10252        }
10253
10254        // Generate parameter names for each placeholder
10255        let param_names: Vec<String> = placeholder_positions
10256            .iter()
10257            .enumerate()
10258            .map(|(i, _)| format!("__p{}", i))
10259            .collect();
10260
10261        // Store the partial application info as a special lambda
10262        // When invoked, it will call the original function with bound + placeholder args
10263        let partial_id = format!(
10264            "__partial_{}_{}_{}",
10265            name,
10266            placeholder_positions.len(),
10267            bound_args.len()
10268        );
10269
10270        // Create a stored lambda that represents this partial application
10271        // The body is a marker that we'll interpret specially during invocation
10272        let stored_lambda = StoredLambda {
10273            params: param_names.clone(),
10274            body: AstNode::String(format!(
10275                "__partial_call:{}:{}:{}",
10276                name,
10277                is_builtin,
10278                args.len()
10279            )),
10280            compiled_body: None, // Partial application uses a special body marker
10281            signature: None,
10282            captured_env: {
10283                let mut env = self.capture_current_environment();
10284                // Store the bound arguments in the captured environment
10285                for (pos, value) in &bound_args {
10286                    env.insert(format!("__bound_arg_{}", pos), value.clone());
10287                }
10288                // Store placeholder positions
10289                env.insert(
10290                    "__placeholder_positions".to_string(),
10291                    JValue::array(
10292                        placeholder_positions
10293                            .iter()
10294                            .map(|p| JValue::Number(*p as f64))
10295                            .collect::<Vec<_>>(),
10296                    ),
10297                );
10298                // Store total argument count
10299                env.insert(
10300                    "__total_args".to_string(),
10301                    JValue::Number(args.len() as f64),
10302                );
10303                env
10304            },
10305            captured_data: Some(data.clone()),
10306            thunk: false,
10307        };
10308
10309        self.context.bind_lambda(partial_id.clone(), stored_lambda);
10310
10311        // Return a lambda object that can be invoked
10312        let lambda_obj = JValue::lambda(
10313            partial_id.as_str(),
10314            param_names,
10315            Some(name.to_string()),
10316            None::<String>,
10317        );
10318
10319        Ok(lambda_obj)
10320    }
10321}
10322
10323impl Default for Evaluator {
10324    fn default() -> Self {
10325        Self::new()
10326    }
10327}
10328
10329#[cfg(test)]
10330mod tests {
10331    use super::*;
10332    use crate::ast::{BinaryOp, UnaryOp};
10333
10334    #[test]
10335    fn test_evaluate_literals() {
10336        let mut evaluator = Evaluator::new();
10337        let data = JValue::Null;
10338
10339        // String literal
10340        let result = evaluator
10341            .evaluate(&AstNode::string("hello"), &data)
10342            .unwrap();
10343        assert_eq!(result, JValue::string("hello"));
10344
10345        // Number literal
10346        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
10347        assert_eq!(result, JValue::from(42i64));
10348
10349        // Boolean literal
10350        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
10351        assert_eq!(result, JValue::Bool(true));
10352
10353        // Null literal
10354        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
10355        assert_eq!(result, JValue::Null);
10356    }
10357
10358    #[test]
10359    fn test_evaluate_variables() {
10360        let mut evaluator = Evaluator::new();
10361        let data = JValue::Null;
10362
10363        // Bind a variable
10364        evaluator
10365            .context
10366            .bind("x".to_string(), JValue::from(100i64));
10367
10368        // Look up the variable
10369        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
10370        assert_eq!(result, JValue::from(100i64));
10371
10372        // Undefined variable returns null (undefined in JSONata semantics)
10373        let result = evaluator
10374            .evaluate(&AstNode::variable("undefined"), &data)
10375            .unwrap();
10376        assert_eq!(result, JValue::Null);
10377    }
10378
10379    #[test]
10380    fn test_evaluate_path() {
10381        let mut evaluator = Evaluator::new();
10382        let data = JValue::from(serde_json::json!({
10383            "foo": {
10384                "bar": {
10385                    "baz": 42
10386                }
10387            }
10388        }));
10389        // Simple path
10390        let path = AstNode::Path {
10391            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
10392        };
10393        let result = evaluator.evaluate(&path, &data).unwrap();
10394        assert_eq!(
10395            result,
10396            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
10397        );
10398
10399        // Nested path
10400        let path = AstNode::Path {
10401            steps: vec![
10402                PathStep::new(AstNode::Name("foo".to_string())),
10403                PathStep::new(AstNode::Name("bar".to_string())),
10404                PathStep::new(AstNode::Name("baz".to_string())),
10405            ],
10406        };
10407        let result = evaluator.evaluate(&path, &data).unwrap();
10408        assert_eq!(result, JValue::from(42i64));
10409
10410        // Missing path returns null
10411        let path = AstNode::Path {
10412            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
10413        };
10414        let result = evaluator.evaluate(&path, &data).unwrap();
10415        assert_eq!(result, JValue::Null);
10416    }
10417
10418    #[test]
10419    fn test_arithmetic_operations() {
10420        let mut evaluator = Evaluator::new();
10421        let data = JValue::Null;
10422
10423        // Addition
10424        let expr = AstNode::Binary {
10425            op: BinaryOp::Add,
10426            lhs: Box::new(AstNode::number(10.0)),
10427            rhs: Box::new(AstNode::number(5.0)),
10428        };
10429        let result = evaluator.evaluate(&expr, &data).unwrap();
10430        assert_eq!(result, JValue::Number(15.0));
10431
10432        // Subtraction
10433        let expr = AstNode::Binary {
10434            op: BinaryOp::Subtract,
10435            lhs: Box::new(AstNode::number(10.0)),
10436            rhs: Box::new(AstNode::number(5.0)),
10437        };
10438        let result = evaluator.evaluate(&expr, &data).unwrap();
10439        assert_eq!(result, JValue::Number(5.0));
10440
10441        // Multiplication
10442        let expr = AstNode::Binary {
10443            op: BinaryOp::Multiply,
10444            lhs: Box::new(AstNode::number(10.0)),
10445            rhs: Box::new(AstNode::number(5.0)),
10446        };
10447        let result = evaluator.evaluate(&expr, &data).unwrap();
10448        assert_eq!(result, JValue::Number(50.0));
10449
10450        // Division
10451        let expr = AstNode::Binary {
10452            op: BinaryOp::Divide,
10453            lhs: Box::new(AstNode::number(10.0)),
10454            rhs: Box::new(AstNode::number(5.0)),
10455        };
10456        let result = evaluator.evaluate(&expr, &data).unwrap();
10457        assert_eq!(result, JValue::Number(2.0));
10458
10459        // Modulo
10460        let expr = AstNode::Binary {
10461            op: BinaryOp::Modulo,
10462            lhs: Box::new(AstNode::number(10.0)),
10463            rhs: Box::new(AstNode::number(3.0)),
10464        };
10465        let result = evaluator.evaluate(&expr, &data).unwrap();
10466        assert_eq!(result, JValue::Number(1.0));
10467    }
10468
10469    #[test]
10470    fn test_division_by_zero() {
10471        let mut evaluator = Evaluator::new();
10472        let data = JValue::Null;
10473
10474        let expr = AstNode::Binary {
10475            op: BinaryOp::Divide,
10476            lhs: Box::new(AstNode::number(10.0)),
10477            rhs: Box::new(AstNode::number(0.0)),
10478        };
10479        let result = evaluator.evaluate(&expr, &data);
10480        assert!(result.is_err());
10481    }
10482
10483    #[test]
10484    fn test_comparison_operations() {
10485        let mut evaluator = Evaluator::new();
10486        let data = JValue::Null;
10487
10488        // Equal
10489        let expr = AstNode::Binary {
10490            op: BinaryOp::Equal,
10491            lhs: Box::new(AstNode::number(5.0)),
10492            rhs: Box::new(AstNode::number(5.0)),
10493        };
10494        assert_eq!(
10495            evaluator.evaluate(&expr, &data).unwrap(),
10496            JValue::Bool(true)
10497        );
10498
10499        // Not equal
10500        let expr = AstNode::Binary {
10501            op: BinaryOp::NotEqual,
10502            lhs: Box::new(AstNode::number(5.0)),
10503            rhs: Box::new(AstNode::number(3.0)),
10504        };
10505        assert_eq!(
10506            evaluator.evaluate(&expr, &data).unwrap(),
10507            JValue::Bool(true)
10508        );
10509
10510        // Less than
10511        let expr = AstNode::Binary {
10512            op: BinaryOp::LessThan,
10513            lhs: Box::new(AstNode::number(3.0)),
10514            rhs: Box::new(AstNode::number(5.0)),
10515        };
10516        assert_eq!(
10517            evaluator.evaluate(&expr, &data).unwrap(),
10518            JValue::Bool(true)
10519        );
10520
10521        // Greater than
10522        let expr = AstNode::Binary {
10523            op: BinaryOp::GreaterThan,
10524            lhs: Box::new(AstNode::number(5.0)),
10525            rhs: Box::new(AstNode::number(3.0)),
10526        };
10527        assert_eq!(
10528            evaluator.evaluate(&expr, &data).unwrap(),
10529            JValue::Bool(true)
10530        );
10531    }
10532
10533    #[test]
10534    fn test_logical_operations() {
10535        let mut evaluator = Evaluator::new();
10536        let data = JValue::Null;
10537
10538        // And - both true
10539        let expr = AstNode::Binary {
10540            op: BinaryOp::And,
10541            lhs: Box::new(AstNode::boolean(true)),
10542            rhs: Box::new(AstNode::boolean(true)),
10543        };
10544        assert_eq!(
10545            evaluator.evaluate(&expr, &data).unwrap(),
10546            JValue::Bool(true)
10547        );
10548
10549        // And - first false
10550        let expr = AstNode::Binary {
10551            op: BinaryOp::And,
10552            lhs: Box::new(AstNode::boolean(false)),
10553            rhs: Box::new(AstNode::boolean(true)),
10554        };
10555        assert_eq!(
10556            evaluator.evaluate(&expr, &data).unwrap(),
10557            JValue::Bool(false)
10558        );
10559
10560        // Or - first true
10561        let expr = AstNode::Binary {
10562            op: BinaryOp::Or,
10563            lhs: Box::new(AstNode::boolean(true)),
10564            rhs: Box::new(AstNode::boolean(false)),
10565        };
10566        assert_eq!(
10567            evaluator.evaluate(&expr, &data).unwrap(),
10568            JValue::Bool(true)
10569        );
10570
10571        // Or - both false
10572        let expr = AstNode::Binary {
10573            op: BinaryOp::Or,
10574            lhs: Box::new(AstNode::boolean(false)),
10575            rhs: Box::new(AstNode::boolean(false)),
10576        };
10577        assert_eq!(
10578            evaluator.evaluate(&expr, &data).unwrap(),
10579            JValue::Bool(false)
10580        );
10581    }
10582
10583    #[test]
10584    fn test_string_concatenation() {
10585        let mut evaluator = Evaluator::new();
10586        let data = JValue::Null;
10587
10588        let expr = AstNode::Binary {
10589            op: BinaryOp::Concatenate,
10590            lhs: Box::new(AstNode::string("Hello")),
10591            rhs: Box::new(AstNode::string(" World")),
10592        };
10593        let result = evaluator.evaluate(&expr, &data).unwrap();
10594        assert_eq!(result, JValue::string("Hello World"));
10595    }
10596
10597    #[test]
10598    fn test_range_operator() {
10599        let mut evaluator = Evaluator::new();
10600        let data = JValue::Null;
10601
10602        // Forward range
10603        let expr = AstNode::Binary {
10604            op: BinaryOp::Range,
10605            lhs: Box::new(AstNode::number(1.0)),
10606            rhs: Box::new(AstNode::number(5.0)),
10607        };
10608        let result = evaluator.evaluate(&expr, &data).unwrap();
10609        assert_eq!(
10610            result,
10611            JValue::array(vec![
10612                JValue::Number(1.0),
10613                JValue::Number(2.0),
10614                JValue::Number(3.0),
10615                JValue::Number(4.0),
10616                JValue::Number(5.0)
10617            ])
10618        );
10619
10620        // Backward range (start > end) returns empty array
10621        let expr = AstNode::Binary {
10622            op: BinaryOp::Range,
10623            lhs: Box::new(AstNode::number(5.0)),
10624            rhs: Box::new(AstNode::number(1.0)),
10625        };
10626        let result = evaluator.evaluate(&expr, &data).unwrap();
10627        assert_eq!(result, JValue::array(vec![]));
10628    }
10629
10630    #[test]
10631    fn test_in_operator() {
10632        let mut evaluator = Evaluator::new();
10633        let data = JValue::Null;
10634
10635        // In array
10636        let expr = AstNode::Binary {
10637            op: BinaryOp::In,
10638            lhs: Box::new(AstNode::number(3.0)),
10639            rhs: Box::new(AstNode::Array(vec![
10640                AstNode::number(1.0),
10641                AstNode::number(2.0),
10642                AstNode::number(3.0),
10643            ])),
10644        };
10645        let result = evaluator.evaluate(&expr, &data).unwrap();
10646        assert_eq!(result, JValue::Bool(true));
10647
10648        // Not in array
10649        let expr = AstNode::Binary {
10650            op: BinaryOp::In,
10651            lhs: Box::new(AstNode::number(5.0)),
10652            rhs: Box::new(AstNode::Array(vec![
10653                AstNode::number(1.0),
10654                AstNode::number(2.0),
10655                AstNode::number(3.0),
10656            ])),
10657        };
10658        let result = evaluator.evaluate(&expr, &data).unwrap();
10659        assert_eq!(result, JValue::Bool(false));
10660    }
10661
10662    #[test]
10663    fn test_unary_operations() {
10664        let mut evaluator = Evaluator::new();
10665        let data = JValue::Null;
10666
10667        // Negation
10668        let expr = AstNode::Unary {
10669            op: UnaryOp::Negate,
10670            operand: Box::new(AstNode::number(5.0)),
10671        };
10672        let result = evaluator.evaluate(&expr, &data).unwrap();
10673        assert_eq!(result, JValue::Number(-5.0));
10674
10675        // Not
10676        let expr = AstNode::Unary {
10677            op: UnaryOp::Not,
10678            operand: Box::new(AstNode::boolean(true)),
10679        };
10680        let result = evaluator.evaluate(&expr, &data).unwrap();
10681        assert_eq!(result, JValue::Bool(false));
10682    }
10683
10684    #[test]
10685    fn test_array_construction() {
10686        let mut evaluator = Evaluator::new();
10687        let data = JValue::Null;
10688
10689        let expr = AstNode::Array(vec![
10690            AstNode::number(1.0),
10691            AstNode::number(2.0),
10692            AstNode::number(3.0),
10693        ]);
10694        let result = evaluator.evaluate(&expr, &data).unwrap();
10695        // Whole number literals are preserved as integers
10696        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
10697    }
10698
10699    #[test]
10700    fn test_object_construction() {
10701        let mut evaluator = Evaluator::new();
10702        let data = JValue::Null;
10703
10704        let expr = AstNode::Object(vec![
10705            (AstNode::string("name"), AstNode::string("Alice")),
10706            (AstNode::string("age"), AstNode::number(30.0)),
10707        ]);
10708        let result = evaluator.evaluate(&expr, &data).unwrap();
10709        // Whole number literals are preserved as integers
10710        let mut expected = IndexMap::new();
10711        expected.insert("name".to_string(), JValue::string("Alice"));
10712        expected.insert("age".to_string(), JValue::Number(30.0));
10713        assert_eq!(result, JValue::object(expected));
10714    }
10715
10716    #[test]
10717    fn test_conditional() {
10718        let mut evaluator = Evaluator::new();
10719        let data = JValue::Null;
10720
10721        // True condition
10722        let expr = AstNode::Conditional {
10723            condition: Box::new(AstNode::boolean(true)),
10724            then_branch: Box::new(AstNode::string("yes")),
10725            else_branch: Some(Box::new(AstNode::string("no"))),
10726        };
10727        let result = evaluator.evaluate(&expr, &data).unwrap();
10728        assert_eq!(result, JValue::string("yes"));
10729
10730        // False condition
10731        let expr = AstNode::Conditional {
10732            condition: Box::new(AstNode::boolean(false)),
10733            then_branch: Box::new(AstNode::string("yes")),
10734            else_branch: Some(Box::new(AstNode::string("no"))),
10735        };
10736        let result = evaluator.evaluate(&expr, &data).unwrap();
10737        assert_eq!(result, JValue::string("no"));
10738
10739        // No else branch returns undefined (not null)
10740        let expr = AstNode::Conditional {
10741            condition: Box::new(AstNode::boolean(false)),
10742            then_branch: Box::new(AstNode::string("yes")),
10743            else_branch: None,
10744        };
10745        let result = evaluator.evaluate(&expr, &data).unwrap();
10746        assert_eq!(result, JValue::Undefined);
10747    }
10748
10749    #[test]
10750    fn test_block_expression() {
10751        let mut evaluator = Evaluator::new();
10752        let data = JValue::Null;
10753
10754        let expr = AstNode::Block(vec![
10755            AstNode::number(1.0),
10756            AstNode::number(2.0),
10757            AstNode::number(3.0),
10758        ]);
10759        let result = evaluator.evaluate(&expr, &data).unwrap();
10760        // Block returns the last expression; whole numbers are preserved as integers
10761        assert_eq!(result, JValue::from(3i64));
10762    }
10763
10764    #[test]
10765    fn test_function_calls() {
10766        let mut evaluator = Evaluator::new();
10767        let data = JValue::Null;
10768
10769        // uppercase function
10770        let expr = AstNode::Function {
10771            name: "uppercase".to_string(),
10772            args: vec![AstNode::string("hello")],
10773            is_builtin: true,
10774        };
10775        let result = evaluator.evaluate(&expr, &data).unwrap();
10776        assert_eq!(result, JValue::string("HELLO"));
10777
10778        // lowercase function
10779        let expr = AstNode::Function {
10780            name: "lowercase".to_string(),
10781            args: vec![AstNode::string("HELLO")],
10782            is_builtin: true,
10783        };
10784        let result = evaluator.evaluate(&expr, &data).unwrap();
10785        assert_eq!(result, JValue::string("hello"));
10786
10787        // length function
10788        let expr = AstNode::Function {
10789            name: "length".to_string(),
10790            args: vec![AstNode::string("hello")],
10791            is_builtin: true,
10792        };
10793        let result = evaluator.evaluate(&expr, &data).unwrap();
10794        assert_eq!(result, JValue::from(5i64));
10795
10796        // sum function
10797        let expr = AstNode::Function {
10798            name: "sum".to_string(),
10799            args: vec![AstNode::Array(vec![
10800                AstNode::number(1.0),
10801                AstNode::number(2.0),
10802                AstNode::number(3.0),
10803            ])],
10804            is_builtin: true,
10805        };
10806        let result = evaluator.evaluate(&expr, &data).unwrap();
10807        assert_eq!(result, JValue::Number(6.0));
10808
10809        // count function
10810        let expr = AstNode::Function {
10811            name: "count".to_string(),
10812            args: vec![AstNode::Array(vec![
10813                AstNode::number(1.0),
10814                AstNode::number(2.0),
10815                AstNode::number(3.0),
10816            ])],
10817            is_builtin: true,
10818        };
10819        let result = evaluator.evaluate(&expr, &data).unwrap();
10820        assert_eq!(result, JValue::from(3i64));
10821    }
10822
10823    #[test]
10824    fn test_complex_nested_data() {
10825        let mut evaluator = Evaluator::new();
10826        let data = JValue::from(serde_json::json!({
10827            "users": [
10828                {"name": "Alice", "age": 30},
10829                {"name": "Bob", "age": 25},
10830                {"name": "Charlie", "age": 35}
10831            ],
10832            "metadata": {
10833                "total": 3,
10834                "version": "1.0"
10835            }
10836        }));
10837        // Access nested field
10838        let path = AstNode::Path {
10839            steps: vec![
10840                PathStep::new(AstNode::Name("metadata".to_string())),
10841                PathStep::new(AstNode::Name("version".to_string())),
10842            ],
10843        };
10844        let result = evaluator.evaluate(&path, &data).unwrap();
10845        assert_eq!(result, JValue::string("1.0"));
10846    }
10847
10848    #[test]
10849    fn test_error_handling() {
10850        let mut evaluator = Evaluator::new();
10851        let data = JValue::Null;
10852
10853        // Type error: adding string and number
10854        let expr = AstNode::Binary {
10855            op: BinaryOp::Add,
10856            lhs: Box::new(AstNode::string("hello")),
10857            rhs: Box::new(AstNode::number(5.0)),
10858        };
10859        let result = evaluator.evaluate(&expr, &data);
10860        assert!(result.is_err());
10861
10862        // Reference error: undefined function
10863        let expr = AstNode::Function {
10864            name: "undefined_function".to_string(),
10865            args: vec![],
10866            is_builtin: false,
10867        };
10868        let result = evaluator.evaluate(&expr, &data);
10869        assert!(result.is_err());
10870    }
10871
10872    #[test]
10873    fn test_truthiness() {
10874        let evaluator = Evaluator::new();
10875
10876        assert!(!evaluator.is_truthy(&JValue::Null));
10877        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
10878        assert!(evaluator.is_truthy(&JValue::Bool(true)));
10879        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
10880        assert!(evaluator.is_truthy(&JValue::from(1i64)));
10881        assert!(!evaluator.is_truthy(&JValue::string("")));
10882        assert!(evaluator.is_truthy(&JValue::string("hello")));
10883        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
10884        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
10885    }
10886
10887    #[test]
10888    fn test_integration_with_parser() {
10889        use crate::parser::parse;
10890
10891        let mut evaluator = Evaluator::new();
10892        let data = JValue::from(serde_json::json!({
10893            "price": 10,
10894            "quantity": 5
10895        }));
10896        // Test simple path
10897        let ast = parse("price").unwrap();
10898        let result = evaluator.evaluate(&ast, &data).unwrap();
10899        assert_eq!(result, JValue::from(10i64));
10900
10901        // Test arithmetic
10902        let ast = parse("price * quantity").unwrap();
10903        let result = evaluator.evaluate(&ast, &data).unwrap();
10904        // Note: Arithmetic operations produce f64 results in JSON
10905        assert_eq!(result, JValue::Number(50.0));
10906
10907        // Test comparison
10908        let ast = parse("price > 5").unwrap();
10909        let result = evaluator.evaluate(&ast, &data).unwrap();
10910        assert_eq!(result, JValue::Bool(true));
10911    }
10912
10913    #[test]
10914    fn test_evaluate_dollar_function_uppercase() {
10915        use crate::parser::parse;
10916
10917        let mut evaluator = Evaluator::new();
10918        let ast = parse(r#"$uppercase("hello")"#).unwrap();
10919        let empty = JValue::object(IndexMap::new());
10920        let result = evaluator.evaluate(&ast, &empty).unwrap();
10921        assert_eq!(result, JValue::string("HELLO"));
10922    }
10923
10924    #[test]
10925    fn test_evaluate_dollar_function_sum() {
10926        use crate::parser::parse;
10927
10928        let mut evaluator = Evaluator::new();
10929        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
10930        let empty = JValue::object(IndexMap::new());
10931        let result = evaluator.evaluate(&ast, &empty).unwrap();
10932        assert_eq!(result, JValue::Number(15.0));
10933    }
10934
10935    #[test]
10936    fn test_evaluate_nested_dollar_functions() {
10937        use crate::parser::parse;
10938
10939        let mut evaluator = Evaluator::new();
10940        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
10941        let empty = JValue::object(IndexMap::new());
10942        let result = evaluator.evaluate(&ast, &empty).unwrap();
10943        // length() returns an integer, not a float
10944        assert_eq!(result, JValue::Number(5.0));
10945    }
10946
10947    #[test]
10948    fn test_array_mapping() {
10949        use crate::parser::parse;
10950
10951        let mut evaluator = Evaluator::new();
10952        let data: JValue = serde_json::from_str(
10953            r#"{
10954            "products": [
10955                {"id": 1, "name": "Laptop", "price": 999.99},
10956                {"id": 2, "name": "Mouse", "price": 29.99},
10957                {"id": 3, "name": "Keyboard", "price": 79.99}
10958            ]
10959        }"#,
10960        )
10961        .map(|v: serde_json::Value| JValue::from(v))
10962        .unwrap();
10963
10964        // Test mapping over array to extract field
10965        let ast = parse("products.name").unwrap();
10966        let result = evaluator.evaluate(&ast, &data).unwrap();
10967        assert_eq!(
10968            result,
10969            JValue::array(vec![
10970                JValue::string("Laptop"),
10971                JValue::string("Mouse"),
10972                JValue::string("Keyboard")
10973            ])
10974        );
10975
10976        // Test mapping over array to extract prices
10977        let ast = parse("products.price").unwrap();
10978        let result = evaluator.evaluate(&ast, &data).unwrap();
10979        assert_eq!(
10980            result,
10981            JValue::array(vec![
10982                JValue::Number(999.99),
10983                JValue::Number(29.99),
10984                JValue::Number(79.99)
10985            ])
10986        );
10987
10988        // Test with $sum function on mapped array
10989        let ast = parse("$sum(products.price)").unwrap();
10990        let result = evaluator.evaluate(&ast, &data).unwrap();
10991        assert_eq!(result, JValue::Number(1109.97));
10992    }
10993
10994    #[test]
10995    fn test_empty_brackets() {
10996        use crate::parser::parse;
10997
10998        let mut evaluator = Evaluator::new();
10999
11000        // Test empty brackets on simple value - should wrap in array
11001        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
11002        let ast = parse("foo[]").unwrap();
11003        let result = evaluator.evaluate(&ast, &data).unwrap();
11004        assert_eq!(
11005            result,
11006            JValue::array(vec![JValue::string("bar")]),
11007            "Empty brackets should wrap value in array"
11008        );
11009
11010        // Test empty brackets on array - should return array as-is
11011        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
11012        let ast2 = parse("arr[]").unwrap();
11013        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
11014        assert_eq!(
11015            result2,
11016            JValue::array(vec![
11017                JValue::Number(1.0),
11018                JValue::Number(2.0),
11019                JValue::Number(3.0)
11020            ]),
11021            "Empty brackets should preserve array"
11022        );
11023    }
11024}