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};
11use std::time::Instant;
12
13use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
14use crate::parser;
15use crate::value::JValue;
16use indexmap::IndexMap;
17use std::rc::Rc;
18use thiserror::Error;
19
20/// Specialized sort comparator for `$l.field op $r.field` patterns.
21/// Bypasses the full AST evaluator for simple field-based sort comparisons.
22///
23/// In JSONata `$sort`, the comparator returns true when `$l` should come AFTER `$r`.
24/// `$l.field > $r.field` swaps when left > right, producing ascending order.
25/// `$l.field < $r.field` swaps when left < right, producing descending order.
26struct SpecializedSortComparator {
27    field: String,
28    descending: bool,
29}
30
31/// Pre-extracted sort key for the Schwartzian transform in specialized sorting.
32enum SortKey {
33    Num(f64),
34    Str(Rc<str>),
35    None,
36}
37
38fn compare_sort_keys(a: &SortKey, b: &SortKey, descending: bool) -> Ordering {
39    let ord = match (a, b) {
40        (SortKey::Num(x), SortKey::Num(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
41        (SortKey::Str(x), SortKey::Str(y)) => (**x).cmp(&**y),
42        (SortKey::None, SortKey::None) => Ordering::Equal,
43        (SortKey::None, _) => Ordering::Greater,
44        (_, SortKey::None) => Ordering::Less,
45        // Mixed types: maintain original order
46        _ => Ordering::Equal,
47    };
48    if descending {
49        ord.reverse()
50    } else {
51        ord
52    }
53}
54
55/// Try to extract a specialized sort comparator from a lambda AST node.
56/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
57fn try_specialize_sort_comparator(
58    body: &AstNode,
59    left_param: &str,
60    right_param: &str,
61) -> Option<SpecializedSortComparator> {
62    let AstNode::Binary { op, lhs, rhs } = body else {
63        return None;
64    };
65
66    // Returns true if op means "swap when left > right" (ascending order).
67    let is_ascending = |op: &BinaryOp| -> Option<bool> {
68        match op {
69            BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
70            BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
71            _ => None,
72        }
73    };
74
75    // Extract field name from a `$param.field` path with no stages.
76    let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
77        let AstNode::Path { steps } = node else {
78            return None;
79        };
80        if steps.len() != 2 {
81            return None;
82        }
83        let AstNode::Variable(var) = &steps[0].node else {
84            return None;
85        };
86        if var != param {
87            return None;
88        }
89        let AstNode::Name(field) = &steps[1].node else {
90            return None;
91        };
92        if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() {
93            return None;
94        }
95        Some(field.clone())
96    };
97
98    // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
99    for flipped in [false, true] {
100        let (lhs_param, rhs_param) = if flipped {
101            (right_param, left_param)
102        } else {
103            (left_param, right_param)
104        };
105        if let (Some(lhs_field), Some(rhs_field)) = (
106            extract_var_field(lhs, lhs_param),
107            extract_var_field(rhs, rhs_param),
108        ) {
109            if lhs_field == rhs_field {
110                let descending = match op {
111                    // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
112                    // Flipped `$r.f - $l.f` → positive when r > l → descending.
113                    BinaryOp::Subtract => flipped,
114                    // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
115                    _ => {
116                        let ascending = is_ascending(op)?;
117                        if flipped {
118                            ascending
119                        } else {
120                            !ascending
121                        }
122                    }
123                };
124                return Some(SpecializedSortComparator {
125                    field: lhs_field,
126                    descending,
127                });
128            }
129        }
130    }
131    None
132}
133
134// ──────────────────────────────────────────────────────────────────────────────
135// CompiledExpr — unified compiled expression framework
136// ──────────────────────────────────────────────────────────────────────────────
137//
138// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
139// can represent arbitrary simple expressions without AST walking.  Evaluated in
140// a tight loop with no recursion tracking, no scope management, and no AstNode
141// pattern matching.
142
143/// Shape cache: maps field names to their positional index in an IndexMap.
144/// When all objects in an array share the same key ordering (extremely common
145/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
146/// instead of O(1)-amortized hash lookups.
147type ShapeCache = HashMap<String, usize>;
148
149/// Build a shape cache from the first object in an array.
150/// Returns None if the data is not an object.
151fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
152    match first_element {
153        JValue::Object(obj) => {
154            let mut cache = HashMap::with_capacity(obj.len());
155            for (idx, (key, _)) in obj.iter().enumerate() {
156                cache.insert(key.clone(), idx);
157            }
158            Some(cache)
159        }
160        _ => None,
161    }
162}
163
164/// Comparison operator for compiled expressions.
165#[derive(Debug, Clone, Copy)]
166pub(crate) enum CompiledCmp {
167    Eq,
168    Ne,
169    Lt,
170    Le,
171    Gt,
172    Ge,
173}
174
175/// Arithmetic operator for compiled expressions.
176#[derive(Debug, Clone, Copy)]
177pub(crate) enum CompiledArithOp {
178    Add,
179    Sub,
180    Mul,
181    Div,
182    Mod,
183}
184
185/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
186///
187/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
188/// expression-compile time (once), then `eval_compiled()` evaluates it per
189/// element in O(expression-size) with no heap allocation in the hot path.
190#[derive(Clone, Debug)]
191pub(crate) enum CompiledExpr {
192    // ── Leaves ──────────────────────────────────────────────────────────
193    /// A literal value known at compile time.
194    Literal(JValue),
195    /// Explicit `null` literal from `AstNode::Null`.
196    /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
197    /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
198    ExplicitNull,
199    /// Single-level field lookup on the current object: `obj.get("field")`.
200    FieldLookup(String),
201    /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
202    NestedFieldLookup(String, String),
203    /// Variable lookup from enclosing scope (e.g. `$var`).
204    /// Resolved at eval time via a provided variable map.
205    VariableLookup(String),
206
207    // ── Comparison ──────────────────────────────────────────────────────
208    Compare {
209        op: CompiledCmp,
210        lhs: Box<CompiledExpr>,
211        rhs: Box<CompiledExpr>,
212    },
213
214    // ── Arithmetic ──────────────────────────────────────────────────────
215    Arithmetic {
216        op: CompiledArithOp,
217        lhs: Box<CompiledExpr>,
218        rhs: Box<CompiledExpr>,
219    },
220
221    // ── String ──────────────────────────────────────────────────────────
222    Concat(Box<CompiledExpr>, Box<CompiledExpr>),
223
224    // ── Logical ─────────────────────────────────────────────────────────
225    And(Box<CompiledExpr>, Box<CompiledExpr>),
226    Or(Box<CompiledExpr>, Box<CompiledExpr>),
227    Not(Box<CompiledExpr>),
228    /// Negation of a numeric value.
229    Negate(Box<CompiledExpr>),
230
231    // ── Conditional ─────────────────────────────────────────────────────
232    Conditional {
233        condition: Box<CompiledExpr>,
234        then_expr: Box<CompiledExpr>,
235        else_expr: Option<Box<CompiledExpr>>,
236    },
237
238    // ── Compound ────────────────────────────────────────────────────────
239    /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
240    ObjectConstruct(Vec<(String, CompiledExpr)>),
241    /// Array construction: `[expr1, expr2, ...]`
242    ///
243    /// Each element carries a `bool` flag: `true` means the element originated
244    /// from an explicit `AstNode::Array` constructor and must be kept nested even
245    /// if it evaluates to an array. `false` means the element's array value is
246    /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
247    /// Undefined values are always skipped.
248    ArrayConstruct(Vec<(CompiledExpr, bool)>),
249
250    // ── Phase 2 extensions ──────────────────────────────────────────────
251    /// Named variable lookup from context scope (any `$name` not in lambda params).
252    /// Compiled when a named variable is encountered and no allowed_vars list is
253    /// provided (top-level compilation). At runtime, returns the value from the vars
254    /// map (lambda params or captured env), or Undefined if not present.
255    #[allow(dead_code)]
256    ContextVar(String),
257
258    /// Multi-step field path with optional per-step filters: `a.b[pred].c`
259    /// Applies implicit array-mapping semantics at each step.
260    FieldPath(Vec<CompiledStep>),
261
262    /// Call a pure, side-effect-free builtin with compiled arguments.
263    /// Only builtins in COMPILABLE_BUILTINS are allowed here.
264    BuiltinCall {
265        name: &'static str,
266        args: Vec<CompiledExpr>,
267    },
268
269    /// Sequential block: evaluate all expressions, return last value.
270    Block(Vec<CompiledExpr>),
271
272    /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
273    Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
274
275    // ── Higher-order functions with inline lambdas ───────────────────────
276    /// `$map(array, function($v [, $i]) { body })` — compiled when the second
277    /// argument is an inline lambda literal (not a stored variable).
278    /// `params` holds the lambda parameter names (without `$`), 1 or 2 elements.
279    MapCall {
280        array: Box<CompiledExpr>,
281        params: Vec<String>,
282        body: Box<CompiledExpr>,
283    },
284    /// `$filter(array, function($v [, $i]) { body })` — compiled when the second
285    /// argument is an inline lambda literal.
286    FilterCall {
287        array: Box<CompiledExpr>,
288        params: Vec<String>,
289        body: Box<CompiledExpr>,
290    },
291    /// `$reduce(array, function($acc, $v) { body } [, initial])` — compiled when the
292    /// second argument is an inline lambda literal with exactly 2 parameters.
293    ReduceCall {
294        array: Box<CompiledExpr>,
295        params: Vec<String>,
296        body: Box<CompiledExpr>,
297        initial: Option<Box<CompiledExpr>>,
298    },
299}
300
301/// One step in a compiled `FieldPath`.
302#[derive(Clone, Debug)]
303pub(crate) struct CompiledStep {
304    /// Field name to look up at this step.
305    pub field: String,
306    /// Optional predicate filter compiled from a `Stage::Filter` stage.
307    pub filter: Option<CompiledExpr>,
308}
309
310/// Try to compile an AstNode subtree into a CompiledExpr.
311/// Returns None for anything that requires full AST evaluation (lambda calls,
312/// function calls with side effects, complex paths, etc.).
313pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
314    reject_if_too_large_to_pool(try_compile_expr_inner(node, None)?)
315}
316
317/// Like `try_compile_expr` but additionally allows the specified variable names
318/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
319/// parameters are known and will be provided via the `vars` map at eval time.
320pub(crate) fn try_compile_expr_with_allowed_vars(
321    node: &AstNode,
322    allowed_vars: &[&str],
323) -> Option<CompiledExpr> {
324    reject_if_too_large_to_pool(try_compile_expr_inner(node, Some(allowed_vars))?)
325}
326
327/// Reject (fall back to the tree-walker) a fully-built `CompiledExpr` tree
328/// whose node count could overflow one of `BytecodeCompiler`'s `u16`-indexed
329/// pools (`const_pool` / `string_pool` / `fallback_exprs` / `sub_programs`).
330///
331/// `BytecodeCompiler::compile` is otherwise infallible (`fn compile(&CompiledExpr)
332/// -> BytecodeProgram`, called from `src/compiler.rs`'s own recursive filter-predicate
333/// compilation, `src/vm.rs`, and twice from `src/lib.rs`) - changing its signature to
334/// return `Option`/`Result` so its 4 pool-interning helpers could "abort gracefully"
335/// mid-compilation would ripple to all of those call sites for a bug class that is
336/// already astronomically impractical to trigger (it requires tens of thousands of
337/// distinct string/field-name/literal constants, or that many nested fallback
338/// sub-expressions, inside one compiled expression). Guarding here instead - before
339/// `BytecodeCompiler::compile` is ever invoked - avoids that ripple entirely, matching
340/// the same "compiler declines, tree-walker (which has no such limit) handles it"
341/// architecture used by the `AstNode::Array`/`AstNode::Block` arity guards above.
342fn reject_if_too_large_to_pool(compiled: CompiledExpr) -> Option<CompiledExpr> {
343    if compiled_expr_node_count_exceeds(&compiled, u16::MAX as usize) {
344        None
345    } else {
346        Some(compiled)
347    }
348}
349
350/// Conservative upper bound check: does `expr`'s node count exceed `limit`?
351///
352/// Each of `BytecodeCompiler`'s 4 pools gains at most one entry per countable
353/// unit processed here (fewer, after interning dedup), so the total count
354/// this walk produces is a safe upper bound for every pool's occupancy -
355/// including a pool populated by a *nested*, independently pooled
356/// `BytecodeCompiler` instance (e.g. a `FieldPath` step's filter predicate,
357/// recompiled into its own `BytecodeProgram` in `compiler.rs`'s `FieldPath`
358/// arm), since that nested instance can only ever process a strict subset of
359/// this tree's nodes. Over-counting (e.g. including a filter predicate's
360/// nodes in the outer count even though it is compiled by a separate
361/// `BytecodeCompiler` with its own pools) is safe: it only means falling back
362/// to the tree-walker slightly earlier than strictly necessary.
363///
364/// "Countable unit" is *not* simply "one `CompiledExpr` node": a
365/// `CompiledExpr::FieldPath`'s individual `CompiledStep`s are not
366/// `CompiledExpr` nodes themselves, yet `compiler.rs`'s `FieldPath` arm
367/// interns *every* step's field name into `string_pool` regardless of
368/// whether that step carries a filter predicate. So this walk counts each
369/// `PathStep` as its own unit (in addition to still recursing into any
370/// filter expression the step may have) - a `FieldPath` with N no-filter
371/// steps contributes N units here, matching the N `string_pool` entries it
372/// costs in `compiler.rs`, not zero.
373///
374/// Uses a shared decrementing budget so pathologically large trees bail out
375/// immediately instead of always walking to completion.
376fn compiled_expr_node_count_exceeds(expr: &CompiledExpr, limit: usize) -> bool {
377    fn walk(expr: &CompiledExpr, budget: &mut usize) -> bool {
378        if *budget == 0 {
379            return true;
380        }
381        *budget -= 1;
382        match expr {
383            // ── Leaves: no children ──────────────────────────────────
384            CompiledExpr::Literal(_)
385            | CompiledExpr::ExplicitNull
386            | CompiledExpr::FieldLookup(_)
387            | CompiledExpr::NestedFieldLookup(_, _)
388            | CompiledExpr::VariableLookup(_)
389            | CompiledExpr::ContextVar(_) => false,
390
391            // ── Binary ────────────────────────────────────────────────
392            CompiledExpr::Compare { lhs, rhs, .. }
393            | CompiledExpr::Arithmetic { lhs, rhs, .. }
394            | CompiledExpr::Concat(lhs, rhs)
395            | CompiledExpr::And(lhs, rhs)
396            | CompiledExpr::Or(lhs, rhs)
397            | CompiledExpr::Coalesce(lhs, rhs) => walk(lhs, budget) || walk(rhs, budget),
398
399            // ── Unary ─────────────────────────────────────────────────
400            CompiledExpr::Not(inner) | CompiledExpr::Negate(inner) => walk(inner, budget),
401
402            // ── Conditional ───────────────────────────────────────────
403            CompiledExpr::Conditional {
404                condition,
405                then_expr,
406                else_expr,
407            } => {
408                walk(condition, budget)
409                    || walk(then_expr, budget)
410                    || else_expr.as_ref().is_some_and(|e| walk(e, budget))
411            }
412
413            // ── Compound ──────────────────────────────────────────────
414            CompiledExpr::ObjectConstruct(pairs) => pairs.iter().any(|(_, v)| walk(v, budget)),
415            CompiledExpr::ArrayConstruct(elems) => elems.iter().any(|(e, _)| walk(e, budget)),
416            CompiledExpr::FieldPath(steps) => {
417                for step in steps.iter() {
418                    // Each step interns its field name into `string_pool` in
419                    // `compiler.rs` regardless of whether it has a filter -
420                    // count the step itself, not just its optional filter.
421                    if *budget == 0 {
422                        return true;
423                    }
424                    *budget -= 1;
425                    if let Some(filter) = step.filter.as_ref() {
426                        if walk(filter, budget) {
427                            return true;
428                        }
429                    }
430                }
431                false
432            }
433            CompiledExpr::BuiltinCall { args, .. } => args.iter().any(|a| walk(a, budget)),
434            CompiledExpr::Block(exprs) => exprs.iter().any(|e| walk(e, budget)),
435
436            // ── Higher-order functions ────────────────────────────────
437            CompiledExpr::MapCall { array, body, .. }
438            | CompiledExpr::FilterCall { array, body, .. } => {
439                walk(array, budget) || walk(body, budget)
440            }
441            CompiledExpr::ReduceCall {
442                array,
443                body,
444                initial,
445                ..
446            } => {
447                walk(array, budget)
448                    || walk(body, budget)
449                    || initial.as_ref().is_some_and(|i| walk(i, budget))
450            }
451        }
452    }
453    let mut budget = limit;
454    walk(expr, &mut budget)
455}
456
457fn try_compile_expr_inner(node: &AstNode, allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
458    match node {
459        // ── Literals ────────────────────────────────────────────────────
460        AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
461        AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
462        AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
463        AstNode::Null => Some(CompiledExpr::ExplicitNull),
464
465        // ── Field access ────────────────────────────────────────────────
466        AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
467
468        // ── Variable lookup ─────────────────────────────────────────────
469        // $ (empty name) always refers to the current element.
470        // Named variables: in HOF mode (allowed_vars=Some), only compile if the
471        // variable is in the allowed set (lambda params supplied via vars map).
472        // In top-level mode (allowed_vars=None), compile unknown variables as
473        // ContextVar — they return Undefined at runtime when no bindings are passed.
474        AstNode::Variable(var) if var.is_empty() => Some(CompiledExpr::VariableLookup(var.clone())),
475        AstNode::Variable(var) => {
476            if let Some(allowed) = allowed_vars {
477                // HOF mode: only compile if the variable is a known lambda param.
478                if allowed.contains(&var.as_str()) {
479                    return Some(CompiledExpr::VariableLookup(var.clone()));
480                }
481            }
482            // Named variables require Context for correct lookup (scope stack, builtins
483            // registry). The compiled fast path passes ctx=None, so fall back to the
484            // tree-walker for all non-empty variable references.
485            None
486        }
487
488        // ── Path expressions ────────────────────────────────────────────
489        AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
490
491        // ── Binary operations ───────────────────────────────────────────
492        AstNode::Binary { op, lhs, rhs } => {
493            let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
494            let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
495            match op {
496                // Comparison
497                BinaryOp::Equal => Some(CompiledExpr::Compare {
498                    op: CompiledCmp::Eq,
499                    lhs: Box::new(compiled_lhs),
500                    rhs: Box::new(compiled_rhs),
501                }),
502                BinaryOp::NotEqual => Some(CompiledExpr::Compare {
503                    op: CompiledCmp::Ne,
504                    lhs: Box::new(compiled_lhs),
505                    rhs: Box::new(compiled_rhs),
506                }),
507                BinaryOp::LessThan => Some(CompiledExpr::Compare {
508                    op: CompiledCmp::Lt,
509                    lhs: Box::new(compiled_lhs),
510                    rhs: Box::new(compiled_rhs),
511                }),
512                BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
513                    op: CompiledCmp::Le,
514                    lhs: Box::new(compiled_lhs),
515                    rhs: Box::new(compiled_rhs),
516                }),
517                BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
518                    op: CompiledCmp::Gt,
519                    lhs: Box::new(compiled_lhs),
520                    rhs: Box::new(compiled_rhs),
521                }),
522                BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
523                    op: CompiledCmp::Ge,
524                    lhs: Box::new(compiled_lhs),
525                    rhs: Box::new(compiled_rhs),
526                }),
527                // Arithmetic
528                BinaryOp::Add => Some(CompiledExpr::Arithmetic {
529                    op: CompiledArithOp::Add,
530                    lhs: Box::new(compiled_lhs),
531                    rhs: Box::new(compiled_rhs),
532                }),
533                BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
534                    op: CompiledArithOp::Sub,
535                    lhs: Box::new(compiled_lhs),
536                    rhs: Box::new(compiled_rhs),
537                }),
538                BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
539                    op: CompiledArithOp::Mul,
540                    lhs: Box::new(compiled_lhs),
541                    rhs: Box::new(compiled_rhs),
542                }),
543                BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
544                    op: CompiledArithOp::Div,
545                    lhs: Box::new(compiled_lhs),
546                    rhs: Box::new(compiled_rhs),
547                }),
548                BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
549                    op: CompiledArithOp::Mod,
550                    lhs: Box::new(compiled_lhs),
551                    rhs: Box::new(compiled_rhs),
552                }),
553                // Logical
554                BinaryOp::And => Some(CompiledExpr::And(
555                    Box::new(compiled_lhs),
556                    Box::new(compiled_rhs),
557                )),
558                BinaryOp::Or => Some(CompiledExpr::Or(
559                    Box::new(compiled_lhs),
560                    Box::new(compiled_rhs),
561                )),
562                // String concat
563                BinaryOp::Concatenate => Some(CompiledExpr::Concat(
564                    Box::new(compiled_lhs),
565                    Box::new(compiled_rhs),
566                )),
567                // Coalesce: return lhs if defined/non-null, else rhs
568                BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
569                    Box::new(compiled_lhs),
570                    Box::new(compiled_rhs),
571                )),
572                // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
573                _ => None,
574            }
575        }
576
577        // ── Unary operations ────────────────────────────────────────────
578        AstNode::Unary { op, operand } => {
579            let compiled = try_compile_expr_inner(operand, allowed_vars)?;
580            match op {
581                crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
582                crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
583            }
584        }
585
586        // ── Conditional ─────────────────────────────────────────────────
587        AstNode::Conditional {
588            condition,
589            then_branch,
590            else_branch,
591        } => {
592            let cond = try_compile_expr_inner(condition, allowed_vars)?;
593            let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
594            let else_e = match else_branch {
595                Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
596                None => None,
597            };
598            Some(CompiledExpr::Conditional {
599                condition: Box::new(cond),
600                then_expr: Box::new(then_e),
601                else_expr: else_e,
602            })
603        }
604
605        // ── Object construction ─────────────────────────────────────────
606        AstNode::Object(pairs) => {
607            // Instr::MakeObject's operand is a u16 element count - bail out
608            // to the (always-correct, no-limit) tree-walker rather than
609            // silently truncating via CompiledExpr::ObjectConstruct here.
610            if pairs.len() > u16::MAX as usize {
611                return None;
612            }
613            let mut fields = Vec::with_capacity(pairs.len());
614            for (key_node, val_node) in pairs {
615                // Key must be a string literal
616                let key = match key_node {
617                    AstNode::String(s) => s.clone(),
618                    _ => return None,
619                };
620                let val = try_compile_expr_inner(val_node, allowed_vars)?;
621                fields.push((key, val));
622            }
623            Some(CompiledExpr::ObjectConstruct(fields))
624        }
625
626        // ── Array construction ──────────────────────────────────────────
627        AstNode::Array(elems) => {
628            // Instr::MakeArray's operand is a u16 element count - bail out
629            // to the (always-correct, no-limit) tree-walker rather than
630            // silently truncating via CompiledExpr::ArrayConstruct here.
631            if elems.len() > u16::MAX as usize {
632                return None;
633            }
634            let mut compiled = Vec::with_capacity(elems.len());
635            for elem in elems {
636                // Tag whether the element itself is an array constructor: if so, its
637                // array value must be kept nested rather than flattened (tree-walker parity).
638                let is_nested = matches!(elem, AstNode::Array(_));
639                compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
640            }
641            Some(CompiledExpr::ArrayConstruct(compiled))
642        }
643
644        // ── Block (sequential evaluation) ───────────────────────────────
645        AstNode::Block(exprs) if !exprs.is_empty() => {
646            // Instr::BlockEnd's operand is a u16 element count - bail out to
647            // the (always-correct, no-limit) tree-walker rather than silently
648            // truncating via CompiledExpr::Block here. Same pattern as the
649            // AstNode::Array guard above.
650            if exprs.len() > u16::MAX as usize {
651                return None;
652            }
653            let compiled: Option<Vec<CompiledExpr>> = exprs
654                .iter()
655                .map(|e| try_compile_expr_inner(e, allowed_vars))
656                .collect();
657            compiled.map(CompiledExpr::Block)
658        }
659
660        // ── Pure builtin function calls ──────────────────────────────────
661        AstNode::Function {
662            name,
663            args,
664            is_builtin: true,
665        } => {
666            if is_compilable_builtin(name) {
667                // Arity guard: if the call site passes more args than the builtin accepts,
668                // fall back to the tree-walker so it can raise the correct T0410 error.
669                if let Some(max) = compilable_builtin_max_args(name) {
670                    if args.len() > max {
671                        return None;
672                    }
673                }
674                // Instr::CallBuiltin's arg_count operand is a u8 - this is NOT
675                // already covered by the max-args guard above for variadic
676                // builtins like "merge" (compilable_builtin_max_args returns
677                // None, i.e. unbounded), so a call site with > 255 arguments
678                // would otherwise silently truncate `args.len() as u8`. Bail
679                // out to the tree-walker rather than truncate.
680                if args.len() > u8::MAX as usize {
681                    return None;
682                }
683                let compiled_args: Option<Vec<CompiledExpr>> = args
684                    .iter()
685                    .map(|a| try_compile_expr_inner(a, allowed_vars))
686                    .collect();
687                compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
688                    name: static_builtin_name(name),
689                    args: cargs,
690                })
691            } else {
692                try_compile_hof_expr(name, args, allowed_vars)
693            }
694        }
695
696        // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
697        _ => None,
698    }
699}
700
701/// Extract an inline lambda's params and body from an AST node, returning `None` if the
702/// node is not a simple lambda (i.e. has a signature or is a TCO thunk).
703fn extract_inline_lambda(node: &AstNode) -> Option<(&Vec<String>, &AstNode)> {
704    match node {
705        AstNode::Lambda {
706            params,
707            body,
708            signature: None,
709            thunk: false,
710        } => Some((params, body)),
711        _ => None,
712    }
713}
714
715/// Compile the array argument + lambda body for a HOF call, returning `None` if either
716/// fails to compile. The lambda params are added to the allowed-vars set so the body
717/// can reference them.
718fn compile_hof_array_and_body(
719    array_node: &AstNode,
720    params: &[String],
721    body: &AstNode,
722    allowed_vars: Option<&[&str]>,
723) -> Option<(Box<CompiledExpr>, Box<CompiledExpr>)> {
724    let array = try_compile_expr_inner(array_node, allowed_vars)?;
725    let param_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
726    let compiled_body = try_compile_expr_inner(body, Some(&param_refs))?;
727    Some((Box::new(array), Box::new(compiled_body)))
728}
729
730/// Try to compile a higher-order function call (`$map`, `$filter`, `$reduce`) when the
731/// callback argument is an inline lambda literal with a compilable body.
732///
733/// Returns `None` when:
734/// - The callback is not an inline lambda (e.g. a stored variable `$f`) — fall back so
735///   the tree-walker can look up the lambda at runtime.
736/// - The lambda has a signature or is a TCO thunk — semantics require the full evaluator.
737/// - The lambda body is not fully compilable — fall back transparently.
738/// - Param count is outside the supported range (see per-function constraints below).
739fn try_compile_hof_expr(
740    name: &str,
741    args: &[AstNode],
742    allowed_vars: Option<&[&str]>,
743) -> Option<CompiledExpr> {
744    match name {
745        "map" | "filter" => {
746            if args.len() != 2 {
747                return None;
748            }
749            let (params, body) = extract_inline_lambda(&args[1])?;
750            if params.is_empty() || params.len() > 2 {
751                return None;
752            }
753            let (array, compiled_body) =
754                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
755            if name == "map" {
756                Some(CompiledExpr::MapCall {
757                    array,
758                    params: params.clone(),
759                    body: compiled_body,
760                })
761            } else {
762                Some(CompiledExpr::FilterCall {
763                    array,
764                    params: params.clone(),
765                    body: compiled_body,
766                })
767            }
768        }
769        "reduce" => {
770            if args.len() < 2 || args.len() > 3 {
771                return None;
772            }
773            let (params, body) = extract_inline_lambda(&args[1])?;
774            if params.len() != 2 {
775                return None;
776            }
777            let (array, compiled_body) =
778                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
779            let initial = if args.len() == 3 {
780                Some(Box::new(try_compile_expr_inner(&args[2], allowed_vars)?))
781            } else {
782                None
783            };
784            Some(CompiledExpr::ReduceCall {
785                array,
786                params: params.clone(),
787                body: compiled_body,
788                initial,
789            })
790        }
791        _ => None,
792    }
793}
794
795/// Returns true if the named builtin is pure (no side effects, no context dependency)
796/// and can be safely compiled into a BuiltinCall.
797fn is_compilable_builtin(name: &str) -> bool {
798    matches!(
799        name,
800        "string"
801            | "length"
802            | "substring"
803            | "substringBefore"
804            | "substringAfter"
805            | "uppercase"
806            | "lowercase"
807            | "trim"
808            | "contains"
809            | "split"
810            | "join"
811            | "number"
812            | "floor"
813            | "ceil"
814            | "round"
815            | "abs"
816            | "sqrt"
817            | "sum"
818            | "max"
819            | "min"
820            | "average"
821            | "count"
822            | "boolean"
823            | "not"
824            | "keys"
825            | "append"
826            | "reverse"
827            | "distinct"
828            | "merge"
829    )
830}
831
832/// Maximum number of explicit arguments accepted by each compilable builtin.
833/// Returns `None` for variadic functions with no fixed upper bound.
834/// Used at compile time to fall back to the tree-walker for over-arity calls
835/// (which the tree-walker turns into the correct T0410/T0411 type errors).
836fn compilable_builtin_max_args(name: &str) -> Option<usize> {
837    match name {
838        "string" => Some(2),
839        "length" | "uppercase" | "lowercase" | "trim" => Some(1),
840        "substring" | "split" => Some(3),
841        "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
842        "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
843        "sum" | "max" | "min" | "average" | "count" => Some(1),
844        "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
845        "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
846        _ => None,
847    }
848}
849
850/// Return the `&'static str` for a known compilable builtin name.
851/// SAFETY: only called after `is_compilable_builtin` returns true.
852fn static_builtin_name(name: &str) -> &'static str {
853    match name {
854        "string" => "string",
855        "length" => "length",
856        "substring" => "substring",
857        "substringBefore" => "substringBefore",
858        "substringAfter" => "substringAfter",
859        "uppercase" => "uppercase",
860        "lowercase" => "lowercase",
861        "trim" => "trim",
862        "contains" => "contains",
863        "split" => "split",
864        "join" => "join",
865        "number" => "number",
866        "floor" => "floor",
867        "ceil" => "ceil",
868        "round" => "round",
869        "abs" => "abs",
870        "sqrt" => "sqrt",
871        "sum" => "sum",
872        "max" => "max",
873        "min" => "min",
874        "average" => "average",
875        "count" => "count",
876        "boolean" => "boolean",
877        "not" => "not",
878        "keys" => "keys",
879        "append" => "append",
880        "reverse" => "reverse",
881        "distinct" => "distinct",
882        "merge" => "merge",
883        _ => unreachable!("Not a compilable builtin: {}", name),
884    }
885}
886
887/// Evaluate a compiled expression against a single element.
888///
889/// `data` is the current element (typically an object from an array).
890/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
891///
892/// This is the tight inner loop — no recursion tracking, no scope push/pop,
893/// no AstNode pattern matching.
894#[inline(always)]
895pub(crate) fn eval_compiled(
896    expr: &CompiledExpr,
897    data: &JValue,
898    vars: Option<&HashMap<&str, &JValue>>,
899    options: &EvaluatorOptions,
900    start_time: Option<Instant>,
901) -> Result<JValue, EvaluatorError> {
902    eval_compiled_inner(expr, data, vars, None, None, options, start_time)
903}
904
905/// Like `eval_compiled` but with an optional shape cache for O(1) positional
906/// field access. The shape cache maps field names to their index in the object's
907/// internal Vec, enabling `get_index()` instead of hash lookups.
908#[inline(always)]
909fn eval_compiled_shaped(
910    expr: &CompiledExpr,
911    data: &JValue,
912    vars: Option<&HashMap<&str, &JValue>>,
913    shape: &ShapeCache,
914    options: &EvaluatorOptions,
915    start_time: Option<Instant>,
916) -> Result<JValue, EvaluatorError> {
917    eval_compiled_inner(expr, data, vars, None, Some(shape), options, start_time)
918}
919
920/// Clone the outer variable bindings into a new HashMap with the given capacity hint.
921/// Used by HOF eval arms to create per-iteration variable scopes that merge outer vars
922/// with lambda parameters.
923#[inline]
924fn clone_outer_vars<'a>(
925    vars: Option<&HashMap<&'a str, &'a JValue>>,
926    capacity: usize,
927) -> HashMap<&'a str, &'a JValue> {
928    vars.map(|v| v.iter().map(|(&k, v)| (k, *v)).collect())
929        .unwrap_or_else(|| HashMap::with_capacity(capacity))
930}
931
932fn eval_compiled_inner(
933    expr: &CompiledExpr,
934    data: &JValue,
935    vars: Option<&HashMap<&str, &JValue>>,
936    ctx: Option<&Context>,
937    shape: Option<&ShapeCache>,
938    options: &EvaluatorOptions,
939    start_time: Option<Instant>,
940) -> Result<JValue, EvaluatorError> {
941    // Single, structurally-unbypassable D1012 checkpoint for the entire compiled fast
942    // path. Every route into compiled evaluation -- the VM's EvalFallback, this task's
943    // MapCall/FilterCall/ReduceCall loop bodies, invoke_stored_lambda's compiled fast
944    // path, evaluate_function_call's inline $map/$filter fast-path loops, and any future
945    // caller -- funnels through this one function (both eval_compiled and
946    // eval_compiled_shaped delegate here), so checking once at entry covers all of them
947    // without having to enumerate call sites. Deliberately timeout-only, no depth check:
948    // self-recursive lambdas cannot compile to CompiledExpr, so genuine recursion always
949    // routes through evaluate_internal's own (already guarded) recursion-depth counter.
950    check_loop_timeout(options, start_time)?;
951    match expr {
952        // ── Leaves ──────────────────────────────────────────────────────
953        CompiledExpr::Literal(v) => Ok(v.clone()),
954
955        // ExplicitNull evaluates to Null, but is flagged at compile-time for
956        // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
957        CompiledExpr::ExplicitNull => Ok(JValue::Null),
958
959        CompiledExpr::FieldLookup(field) => match data {
960            JValue::Object(obj) => {
961                // Shape-accelerated: use positional index if available
962                if let Some(shape) = shape {
963                    if let Some(&idx) = shape.get(field.as_str()) {
964                        return Ok(obj
965                            .get_index(idx)
966                            .map(|(_, v)| v.clone())
967                            .unwrap_or(JValue::Undefined));
968                    }
969                }
970                Ok(obj
971                    .get(field.as_str())
972                    .cloned()
973                    .unwrap_or(JValue::Undefined))
974            }
975            _ => Ok(JValue::Undefined),
976        },
977
978        CompiledExpr::NestedFieldLookup(outer, inner) => match data {
979            JValue::Object(obj) => {
980                // Shape-accelerated outer lookup
981                let outer_val = if let Some(shape) = shape {
982                    if let Some(&idx) = shape.get(outer.as_str()) {
983                        obj.get_index(idx).map(|(_, v)| v)
984                    } else {
985                        obj.get(outer.as_str())
986                    }
987                } else {
988                    obj.get(outer.as_str())
989                };
990                Ok(outer_val
991                    .and_then(|v| match v {
992                        JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
993                        _ => None,
994                    })
995                    .unwrap_or(JValue::Undefined))
996            }
997            _ => Ok(JValue::Undefined),
998        },
999
1000        CompiledExpr::VariableLookup(var) => {
1001            if let Some(vars) = vars {
1002                if let Some(val) = vars.get(var.as_str()) {
1003                    return Ok((*val).clone());
1004                }
1005            }
1006            // $ (empty var name) refers to the current data
1007            if var.is_empty() {
1008                return Ok(data.clone());
1009            }
1010            Ok(JValue::Undefined)
1011        }
1012
1013        // ── Comparison ──────────────────────────────────────────────────
1014        CompiledExpr::Compare { op, lhs, rhs } => {
1015            let lhs_explicit_null = is_compiled_explicit_null(lhs);
1016            let rhs_explicit_null = is_compiled_explicit_null(rhs);
1017            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1018            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1019            match op {
1020                CompiledCmp::Eq => Ok(JValue::Bool(crate::functions::array::values_equal(
1021                    &left, &right,
1022                ))),
1023                CompiledCmp::Ne => Ok(JValue::Bool(!crate::functions::array::values_equal(
1024                    &left, &right,
1025                ))),
1026                CompiledCmp::Lt => compiled_ordered_cmp(
1027                    &left,
1028                    &right,
1029                    lhs_explicit_null,
1030                    rhs_explicit_null,
1031                    |a, b| a < b,
1032                    |a, b| a < b,
1033                ),
1034                CompiledCmp::Le => compiled_ordered_cmp(
1035                    &left,
1036                    &right,
1037                    lhs_explicit_null,
1038                    rhs_explicit_null,
1039                    |a, b| a <= b,
1040                    |a, b| a <= b,
1041                ),
1042                CompiledCmp::Gt => compiled_ordered_cmp(
1043                    &left,
1044                    &right,
1045                    lhs_explicit_null,
1046                    rhs_explicit_null,
1047                    |a, b| a > b,
1048                    |a, b| a > b,
1049                ),
1050                CompiledCmp::Ge => compiled_ordered_cmp(
1051                    &left,
1052                    &right,
1053                    lhs_explicit_null,
1054                    rhs_explicit_null,
1055                    |a, b| a >= b,
1056                    |a, b| a >= b,
1057                ),
1058            }
1059        }
1060
1061        // ── Arithmetic ──────────────────────────────────────────────────
1062        CompiledExpr::Arithmetic { op, lhs, rhs } => {
1063            let lhs_explicit_null = is_compiled_explicit_null(lhs);
1064            let rhs_explicit_null = is_compiled_explicit_null(rhs);
1065            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1066            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1067            compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
1068        }
1069
1070        // ── String concat ───────────────────────────────────────────────
1071        CompiledExpr::Concat(lhs, rhs) => {
1072            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1073            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1074            let ls = compiled_to_concat_string(&left)?;
1075            let rs = compiled_to_concat_string(&right)?;
1076            Ok(JValue::string(format!("{}{}", ls, rs)))
1077        }
1078
1079        // ── Logical ─────────────────────────────────────────────────────
1080        CompiledExpr::And(lhs, rhs) => {
1081            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1082            if !compiled_is_truthy(&left) {
1083                return Ok(JValue::Bool(false));
1084            }
1085            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1086            Ok(JValue::Bool(compiled_is_truthy(&right)))
1087        }
1088        CompiledExpr::Or(lhs, rhs) => {
1089            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1090            if compiled_is_truthy(&left) {
1091                return Ok(JValue::Bool(true));
1092            }
1093            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1094            Ok(JValue::Bool(compiled_is_truthy(&right)))
1095        }
1096        CompiledExpr::Not(inner) => {
1097            let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1098            Ok(JValue::Bool(!compiled_is_truthy(&val)))
1099        }
1100        CompiledExpr::Negate(inner) => {
1101            let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1102            match val {
1103                JValue::Number(n) => Ok(JValue::Number(-n)),
1104                JValue::Null => Ok(JValue::Null),
1105                // Undefined operand propagates through unary minus, matching the tree-walker.
1106                v if v.is_undefined() => Ok(JValue::Undefined),
1107                _ => Err(EvaluatorError::TypeError(
1108                    "D1002: Cannot negate non-number value".to_string(),
1109                )),
1110            }
1111        }
1112
1113        // ── Conditional ─────────────────────────────────────────────────
1114        CompiledExpr::Conditional {
1115            condition,
1116            then_expr,
1117            else_expr,
1118        } => {
1119            let cond = eval_compiled_inner(condition, data, vars, ctx, shape, options, start_time)?;
1120            if compiled_is_truthy(&cond) {
1121                eval_compiled_inner(then_expr, data, vars, ctx, shape, options, start_time)
1122            } else if let Some(else_e) = else_expr {
1123                eval_compiled_inner(else_e, data, vars, ctx, shape, options, start_time)
1124            } else {
1125                Ok(JValue::Undefined)
1126            }
1127        }
1128
1129        // ── Object construction ─────────────────────────────────────────
1130        CompiledExpr::ObjectConstruct(fields) => {
1131            let mut result = IndexMap::with_capacity(fields.len());
1132            for (key, expr) in fields {
1133                let value = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1134                if !value.is_undefined() {
1135                    result.insert(key.clone(), value);
1136                }
1137            }
1138            Ok(JValue::object(result))
1139        }
1140
1141        // ── Array construction ──────────────────────────────────────────
1142        CompiledExpr::ArrayConstruct(elems) => {
1143            let mut result = Vec::new();
1144            for (elem_expr, is_nested) in elems {
1145                let value =
1146                    eval_compiled_inner(elem_expr, data, vars, ctx, shape, options, start_time)?;
1147                // Undefined values are excluded from array constructors (tree-walker parity)
1148                if value.is_undefined() {
1149                    continue;
1150                }
1151                if *is_nested {
1152                    // Explicit array constructor [...] — keep nested even if it's an array
1153                    result.push(value);
1154                } else if let JValue::Array(arr) = value {
1155                    // Non-constructor that evaluated to an array — flatten one level
1156                    result.extend(arr.iter().cloned());
1157                } else {
1158                    result.push(value);
1159                }
1160            }
1161            Ok(JValue::array(result))
1162        }
1163
1164        // ── Phase 2 new variants ─────────────────────────────────────────
1165
1166        // ContextVar: named variable lookup from context scope.
1167        // In top-level mode (ctx=None, no bindings), returns Undefined.
1168        // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
1169        // is only ever populated for top-level calls — always Undefined there.
1170        CompiledExpr::ContextVar(name) => {
1171            // Check vars map first (for lambda params that might shadow context)
1172            if let Some(vars) = vars {
1173                if let Some(val) = vars.get(name.as_str()) {
1174                    return Ok((*val).clone());
1175                }
1176            }
1177            // Then check context scope
1178            if let Some(ctx) = ctx {
1179                if let Some(val) = ctx.lookup(name) {
1180                    return Ok(val.clone());
1181                }
1182            }
1183            Ok(JValue::Undefined)
1184        }
1185
1186        // FieldPath: multi-step field access with implicit array mapping.
1187        CompiledExpr::FieldPath(steps) => {
1188            compiled_eval_field_path(steps, data, vars, ctx, shape, options, start_time)
1189        }
1190
1191        // BuiltinCall: evaluate all args, dispatch to pure builtin.
1192        CompiledExpr::BuiltinCall { name, args } => {
1193            let mut evaled_args = Vec::with_capacity(args.len());
1194            for arg in args.iter() {
1195                evaled_args.push(eval_compiled_inner(
1196                    arg, data, vars, ctx, shape, options, start_time,
1197                )?);
1198            }
1199            call_pure_builtin(name, &evaled_args, data, options)
1200        }
1201
1202        // Block: evaluate each expression in sequence, return the last value.
1203        CompiledExpr::Block(exprs) => {
1204            let mut result = JValue::Undefined;
1205            for expr in exprs.iter() {
1206                result = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1207            }
1208            Ok(result)
1209        }
1210
1211        // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1212        // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1213        CompiledExpr::Coalesce(lhs, rhs) => {
1214            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1215            if left.is_undefined() {
1216                eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)
1217            } else {
1218                Ok(left)
1219            }
1220        }
1221
1222        // ── Higher-order functions ─────────────────────────────────────────────
1223        //
1224        // These variants are emitted by try_compile_hof_expr when the HOF argument
1225        // is an inline lambda literal with a compilable body. Outer vars are merged
1226        // with the lambda params so that nested HOF can access variables from
1227        // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1228        CompiledExpr::MapCall {
1229            array,
1230            params,
1231            body,
1232        } => {
1233            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1234            let single_holder;
1235            let items: &[JValue] = match &arr_val {
1236                JValue::Array(a) => a.as_slice(),
1237                JValue::Undefined => return Ok(JValue::Undefined),
1238                other => {
1239                    single_holder = [other.clone()];
1240                    &single_holder[..]
1241                }
1242            };
1243            let mut result = Vec::with_capacity(items.len());
1244            let p0 = params.first().map(|s| s.as_str());
1245
1246            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1247                // 2-param lambda (element + index): build per-iteration because idx_val
1248                // is loop-local and cannot outlive the iteration.
1249                for (idx, item) in items.iter().enumerate() {
1250                    check_loop_timeout(options, start_time)?;
1251                    let idx_val = JValue::Number(idx as f64);
1252                    let mut call_vars = clone_outer_vars(vars, 2);
1253                    if let Some(p) = p0 {
1254                        call_vars.insert(p, item);
1255                    }
1256                    call_vars.insert(p1, &idx_val);
1257                    let mapped = eval_compiled_inner(
1258                        body,
1259                        data,
1260                        Some(&call_vars),
1261                        ctx,
1262                        shape,
1263                        options,
1264                        start_time,
1265                    )?;
1266                    if !mapped.is_undefined() {
1267                        result.push(mapped);
1268                    }
1269                }
1270            } else if let Some(p0) = p0 {
1271                // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1272                let mut call_vars = clone_outer_vars(vars, 1);
1273                for item in items.iter() {
1274                    check_loop_timeout(options, start_time)?;
1275                    call_vars.insert(p0, item);
1276                    let mapped = eval_compiled_inner(
1277                        body,
1278                        data,
1279                        Some(&call_vars),
1280                        ctx,
1281                        shape,
1282                        options,
1283                        start_time,
1284                    )?;
1285                    if !mapped.is_undefined() {
1286                        result.push(mapped);
1287                    }
1288                }
1289            }
1290            check_sequence_length(result.len(), options)?;
1291            Ok(if result.is_empty() {
1292                JValue::Undefined
1293            } else {
1294                JValue::array(result)
1295            })
1296        }
1297
1298        CompiledExpr::FilterCall {
1299            array,
1300            params,
1301            body,
1302        } => {
1303            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1304            if arr_val.is_undefined() || arr_val.is_null() {
1305                return Ok(JValue::Undefined);
1306            }
1307            let single_holder;
1308            let (items, was_single) = match &arr_val {
1309                JValue::Array(a) => (a.as_slice(), false),
1310                other => {
1311                    single_holder = [other.clone()];
1312                    (&single_holder[..], true)
1313                }
1314            };
1315            let mut result = Vec::with_capacity(items.len() / 2);
1316            let p0 = params.first().map(|s| s.as_str());
1317
1318            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1319                for (idx, item) in items.iter().enumerate() {
1320                    check_loop_timeout(options, start_time)?;
1321                    let idx_val = JValue::Number(idx as f64);
1322                    let mut call_vars = clone_outer_vars(vars, 2);
1323                    if let Some(p) = p0 {
1324                        call_vars.insert(p, item);
1325                    }
1326                    call_vars.insert(p1, &idx_val);
1327                    let pred = eval_compiled_inner(
1328                        body,
1329                        data,
1330                        Some(&call_vars),
1331                        ctx,
1332                        shape,
1333                        options,
1334                        start_time,
1335                    )?;
1336                    if compiled_is_truthy(&pred) {
1337                        result.push(item.clone());
1338                    }
1339                }
1340            } else if let Some(p0) = p0 {
1341                let mut call_vars = clone_outer_vars(vars, 1);
1342                for item in items.iter() {
1343                    check_loop_timeout(options, start_time)?;
1344                    call_vars.insert(p0, item);
1345                    let pred = eval_compiled_inner(
1346                        body,
1347                        data,
1348                        Some(&call_vars),
1349                        ctx,
1350                        shape,
1351                        options,
1352                        start_time,
1353                    )?;
1354                    if compiled_is_truthy(&pred) {
1355                        result.push(item.clone());
1356                    }
1357                }
1358            }
1359            if was_single {
1360                Ok(match result.len() {
1361                    0 => JValue::Undefined,
1362                    1 => {
1363                        check_sequence_length(1, options)?;
1364                        result.remove(0)
1365                    }
1366                    _ => {
1367                        check_sequence_length(result.len(), options)?;
1368                        JValue::array(result)
1369                    }
1370                })
1371            } else {
1372                check_sequence_length(result.len(), options)?;
1373                Ok(JValue::array(result))
1374            }
1375        }
1376
1377        CompiledExpr::ReduceCall {
1378            array,
1379            params,
1380            body,
1381            initial,
1382        } => {
1383            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1384            let single_holder;
1385            let items: &[JValue] = match &arr_val {
1386                JValue::Array(a) => a.as_slice(),
1387                JValue::Null => return Ok(JValue::Null),
1388                JValue::Undefined => return Ok(JValue::Undefined),
1389                other => {
1390                    single_holder = [other.clone()];
1391                    &single_holder[..]
1392                }
1393            };
1394            let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1395                let init_val =
1396                    eval_compiled_inner(init_expr, data, vars, ctx, shape, options, start_time)?;
1397                if items.is_empty() {
1398                    return Ok(init_val);
1399                }
1400                (0usize, init_val)
1401            } else {
1402                if items.is_empty() {
1403                    return Ok(JValue::Null);
1404                }
1405                (1, items[0].clone())
1406            };
1407            let acc_param = params[0].as_str();
1408            let item_param = params[1].as_str();
1409            for item in items[start_idx..].iter() {
1410                check_loop_timeout(options, start_time)?;
1411                // Per-iteration HashMap: &accumulator borrow must be released before we
1412                // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1413                let mut call_vars = clone_outer_vars(vars, 2);
1414                call_vars.insert(acc_param, &accumulator);
1415                call_vars.insert(item_param, item);
1416                let new_acc = eval_compiled_inner(
1417                    body,
1418                    data,
1419                    Some(&call_vars),
1420                    ctx,
1421                    shape,
1422                    options,
1423                    start_time,
1424                )?;
1425                drop(call_vars);
1426                accumulator = new_acc;
1427            }
1428            Ok(accumulator)
1429        }
1430    }
1431}
1432
1433/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1434#[inline]
1435pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1436    match value {
1437        JValue::Null | JValue::Undefined => false,
1438        JValue::Bool(b) => *b,
1439        JValue::Number(n) => *n != 0.0,
1440        JValue::String(s) => !s.is_empty(),
1441        JValue::Array(a) => !a.is_empty(),
1442        JValue::Object(o) => !o.is_empty(),
1443        _ => false,
1444    }
1445}
1446
1447/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1448/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1449#[inline]
1450fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1451    matches!(expr, CompiledExpr::ExplicitNull)
1452}
1453
1454/// Ordered comparison for compiled expressions.
1455/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1456#[inline]
1457pub(crate) fn compiled_ordered_cmp(
1458    left: &JValue,
1459    right: &JValue,
1460    left_is_explicit_null: bool,
1461    right_is_explicit_null: bool,
1462    cmp_num: fn(f64, f64) -> bool,
1463    cmp_str: fn(&str, &str) -> bool,
1464) -> Result<JValue, EvaluatorError> {
1465    match (left, right) {
1466        (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1467        (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1468        // Both null/undefined → undefined
1469        (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1470        (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1471        // Explicit null literal with any non-null type → T2010 error
1472        (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1473            "T2010: Type mismatch in comparison".to_string(),
1474        )),
1475        (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1476            "T2010: Type mismatch in comparison".to_string(),
1477        )),
1478        // Boolean with undefined → T2010 error
1479        (JValue::Bool(_), JValue::Null | JValue::Undefined)
1480        | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1481            EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1482        ),
1483        // Number or String with implicit undefined (missing field) → undefined result
1484        (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1485        | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1486            Ok(JValue::Null)
1487        }
1488        // Type mismatch (string vs number)
1489        (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1490            Err(EvaluatorError::EvaluationError(
1491                "T2009: The expressions on either side of operator must be of the same data type"
1492                    .to_string(),
1493            ))
1494        }
1495        _ => Err(EvaluatorError::EvaluationError(
1496            "T2010: Type mismatch in comparison".to_string(),
1497        )),
1498    }
1499}
1500
1501/// Arithmetic for compiled expressions.
1502/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1503#[inline]
1504pub(crate) fn compiled_arithmetic(
1505    op: CompiledArithOp,
1506    left: &JValue,
1507    right: &JValue,
1508    left_is_explicit_null: bool,
1509    right_is_explicit_null: bool,
1510) -> Result<JValue, EvaluatorError> {
1511    let op_sym = match op {
1512        CompiledArithOp::Add => "+",
1513        CompiledArithOp::Sub => "-",
1514        CompiledArithOp::Mul => "*",
1515        CompiledArithOp::Div => "/",
1516        CompiledArithOp::Mod => "%",
1517    };
1518    match (left, right) {
1519        (JValue::Number(a), JValue::Number(b)) => {
1520            let result = match op {
1521                CompiledArithOp::Add => *a + *b,
1522                CompiledArithOp::Sub => *a - *b,
1523                CompiledArithOp::Mul => {
1524                    let r = *a * *b;
1525                    if r.is_infinite() {
1526                        return Err(EvaluatorError::EvaluationError(
1527                            "D1001: Number out of range".to_string(),
1528                        ));
1529                    }
1530                    r
1531                }
1532                CompiledArithOp::Div => {
1533                    if *b == 0.0 {
1534                        return Err(EvaluatorError::EvaluationError(
1535                            "Division by zero".to_string(),
1536                        ));
1537                    }
1538                    *a / *b
1539                }
1540                CompiledArithOp::Mod => {
1541                    if *b == 0.0 {
1542                        return Err(EvaluatorError::EvaluationError(
1543                            "Division by zero".to_string(),
1544                        ));
1545                    }
1546                    *a % *b
1547                }
1548            };
1549            Ok(JValue::Number(result))
1550        }
1551        // Explicit null literal → T2002 error (matching tree-walker behavior)
1552        (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1553            Err(EvaluatorError::TypeError(format!(
1554                "T2002: The left side of the {} operator must evaluate to a number",
1555                op_sym
1556            )))
1557        }
1558        (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1559            Err(EvaluatorError::TypeError(format!(
1560                "T2002: The right side of the {} operator must evaluate to a number",
1561                op_sym
1562            )))
1563        }
1564        // Implicit undefined propagation (from missing field) → undefined result
1565        (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1566            Ok(JValue::Null)
1567        }
1568        _ => Err(EvaluatorError::TypeError(format!(
1569            "Cannot apply {} to {:?} and {:?}",
1570            op_sym, left, right
1571        ))),
1572    }
1573}
1574
1575/// Convert a value to string for concatenation in compiled expressions.
1576#[inline]
1577pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1578    match value {
1579        JValue::String(s) => Ok(s.to_string()),
1580        JValue::Null | JValue::Undefined => Ok(String::new()),
1581        JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1582            match crate::functions::string::string(value, None) {
1583                Ok(JValue::String(s)) => Ok(s.to_string()),
1584                Ok(JValue::Null) => Ok(String::new()),
1585                _ => Err(EvaluatorError::TypeError(
1586                    "Cannot concatenate complex types".to_string(),
1587                )),
1588            }
1589        }
1590        _ => Ok(String::new()),
1591    }
1592}
1593
1594/// Equality comparison for the bytecode VM.
1595#[inline]
1596pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> JValue {
1597    JValue::Bool(crate::functions::array::values_equal(lhs, rhs))
1598}
1599
1600/// String concatenation for the bytecode VM.
1601#[inline]
1602pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1603    let l = compiled_to_concat_string(&lhs)?;
1604    let r = compiled_to_concat_string(&rhs)?;
1605    Ok(JValue::string(l + &r))
1606}
1607
1608/// Entry point for the bytecode VM to call pure builtins by name.
1609#[inline]
1610pub(crate) fn call_pure_builtin_by_name(
1611    name: &str,
1612    args: &[JValue],
1613    data: &JValue,
1614    options: &EvaluatorOptions,
1615) -> Result<JValue, EvaluatorError> {
1616    call_pure_builtin(name, args, data, options)
1617}
1618
1619// ──────────────────────────────────────────────────────────────────────────────
1620// Phase 2: path compilation, builtin dispatch, and supporting helpers
1621// ──────────────────────────────────────────────────────────────────────────────
1622
1623/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1624///
1625/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1626/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1627fn try_compile_path(
1628    steps: &[crate::ast::PathStep],
1629    allowed_vars: Option<&[&str]>,
1630) -> Option<CompiledExpr> {
1631    use crate::ast::{AstNode, Stage};
1632
1633    if steps.is_empty() {
1634        return None;
1635    }
1636
1637    // Determine the start of the path:
1638    //   `$.field...`  → starts from current data (drop the leading `$` step)
1639    //   `$var.field`  → variable-prefixed paths: not compiled yet, fall back to tree-walker
1640    //   `field...`    → starts from current data
1641    let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1642        AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1643        AstNode::Variable(_) => return None,
1644        AstNode::Name(_) => steps,
1645        _ => return None,
1646    };
1647
1648    // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1649    // which represent index access in JSONata, not boolean filtering, and the
1650    // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1651    // to stay an array rather than filtering — the tree-walker's
1652    // `evaluate_predicate` special-cases it and the compiled path has no
1653    // equivalent, so bail out rather than silently treating it as `filter(true)`.
1654    let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1655        if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1656            return None;
1657        }
1658        try_compile_expr_inner(node, allowed_vars)
1659    };
1660
1661    // Compile each field step.
1662    // Handles:
1663    //   - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1664    //   - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1665    //     previous step's filter slot, since both encodings have identical runtime semantics.
1666    let mut compiled_steps = Vec::with_capacity(field_steps.len());
1667    for step in field_steps {
1668        // Tuple-stream steps (@ focus / # index / % parent binding) require the
1669        // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1670        // tuple handling). Never compile them to the flat bytecode field path,
1671        // which is unaware of the binding flags and would silently drop them.
1672        if step.focus.is_some()
1673            || step.index_var.is_some()
1674            || step.ancestor_label.is_some()
1675            || step.is_tuple
1676        {
1677            return None;
1678        }
1679        match &step.node {
1680            AstNode::Name(name) => {
1681                let filter = match step.stages.as_slice() {
1682                    [] => None,
1683                    [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1684                    _ => return None,
1685                };
1686                compiled_steps.push(CompiledStep {
1687                    field: name.clone(),
1688                    filter,
1689                });
1690            }
1691            AstNode::Predicate(filter_node) => {
1692                // Standalone predicate step — fold into the previous Name step's filter slot.
1693                if !step.stages.is_empty() {
1694                    return None;
1695                }
1696                let last = compiled_steps.last_mut()?;
1697                if last.filter.is_some() {
1698                    return None;
1699                }
1700                last.filter = Some(compile_filter(filter_node)?);
1701            }
1702            _ => return None,
1703        }
1704    }
1705
1706    if compiled_steps.is_empty() {
1707        // Bare `$` with no further field steps — current-data reference
1708        return Some(CompiledExpr::VariableLookup(String::new()));
1709    }
1710
1711    // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1712    // in HOF mode (allowed_vars=Some), where data is always a single Object element
1713    // from an array. In top-level mode (allowed_vars=None), data can itself be an
1714    // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1715    if allowed_vars.is_some() {
1716        if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1717            return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1718        }
1719        if compiled_steps.len() == 2
1720            && compiled_steps[0].filter.is_none()
1721            && compiled_steps[1].filter.is_none()
1722        {
1723            let outer = compiled_steps.remove(0).field;
1724            let inner = compiled_steps.remove(0).field;
1725            return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1726        }
1727    }
1728
1729    Some(CompiledExpr::FieldPath(compiled_steps))
1730}
1731
1732/// Evaluate a compiled `FieldPath` against `data`.
1733///
1734/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1735/// Filters are applied as predicates: truthy elements are kept.
1736///
1737/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1738/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1739/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1740fn compiled_eval_field_path(
1741    steps: &[CompiledStep],
1742    data: &JValue,
1743    vars: Option<&HashMap<&str, &JValue>>,
1744    ctx: Option<&Context>,
1745    shape: Option<&ShapeCache>,
1746    options: &EvaluatorOptions,
1747    start_time: Option<Instant>,
1748) -> Result<JValue, EvaluatorError> {
1749    let mut current = data.clone();
1750    // Track whether the most recent field step mapped over an array (like the tree-walker's
1751    // `did_array_mapping` flag). Filters also count as array operations.
1752    let mut did_array_mapping = false;
1753    for step in steps {
1754        // Determine if this step will do array mapping before we overwrite `current`
1755        let is_array = matches!(current, JValue::Array(_));
1756        // Field access with implicit array mapping
1757        current = compiled_field_step(&step.field, &current, options)?;
1758        if is_array {
1759            did_array_mapping = true;
1760        } else {
1761            // Extracting from a single object resets the flag (tree-walker parity)
1762            did_array_mapping = false;
1763        }
1764        // Apply filter if present (filter is an array operation — keep the flag set)
1765        if let Some(filter) = &step.filter {
1766            current =
1767                compiled_apply_filter(filter, &current, vars, ctx, shape, options, start_time)?;
1768            // Filter always implies we operated on an array
1769            did_array_mapping = true;
1770        }
1771    }
1772    // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1773    if did_array_mapping {
1774        Ok(match current {
1775            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1776            other => other,
1777        })
1778    } else {
1779        Ok(current)
1780    }
1781}
1782
1783/// Perform a single-field access with implicit array-mapping semantics.
1784///
1785/// - Object: look up `field`, return its value or Undefined
1786/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1787///   (this is a query-result sequence, so D2015 applies — mirrors `evaluate_path`'s
1788///   array-mapping check and `vm.rs`'s `get_field_cached`)
1789/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1790/// - Other: Undefined
1791fn compiled_field_step(
1792    field: &str,
1793    value: &JValue,
1794    options: &EvaluatorOptions,
1795) -> Result<JValue, EvaluatorError> {
1796    match value {
1797        JValue::Object(obj) => {
1798            // Check for tuple: extract from "@" inner object
1799            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1800                if let Some(JValue::Object(inner)) = obj.get("@") {
1801                    return Ok(inner.get(field).cloned().unwrap_or(JValue::Undefined));
1802                }
1803                return Ok(JValue::Undefined);
1804            }
1805            Ok(obj.get(field).cloned().unwrap_or(JValue::Undefined))
1806        }
1807        JValue::Array(arr) => {
1808            // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1809            let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1810                if let JValue::Object(obj) = v {
1811                    if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1812                        return build_shape_cache(v);
1813                    }
1814                }
1815                None
1816            });
1817            let mut result = Vec::new();
1818            for item in arr.iter() {
1819                let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1820                    // Tuple objects need the recursive path for "@" inner lookup.
1821                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1822                        compiled_field_step(field, item, options)?
1823                    } else if let Some(&pos) = sh.get(field) {
1824                        // Positional access with key verification: guards against heterogeneous
1825                        // schemas (objects where the same field is at a different index).
1826                        // On a mismatch, fall back to a regular hash lookup.
1827                        match obj.get_index(pos) {
1828                            Some((k, v)) if k.as_str() == field => v.clone(),
1829                            _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1830                        }
1831                    } else {
1832                        // Field not in the first object's schema — fall back to hash lookup
1833                        // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1834                        obj.get(field).cloned().unwrap_or(JValue::Undefined)
1835                    }
1836                } else {
1837                    compiled_field_step(field, item, options)?
1838                };
1839                match extracted {
1840                    JValue::Undefined => {}
1841                    JValue::Array(inner) => result.extend(inner.iter().cloned()),
1842                    other => result.push(other),
1843                }
1844            }
1845            check_sequence_length(result.len(), options)?;
1846            Ok(if result.is_empty() {
1847                JValue::Undefined
1848            } else {
1849                JValue::array(result)
1850            })
1851        }
1852        _ => Ok(JValue::Undefined),
1853    }
1854}
1855
1856/// Apply a compiled filter predicate to a value.
1857///
1858/// - Array: return elements for which the predicate is truthy
1859/// - Single value: return it if predicate is truthy, else Undefined
1860/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1861fn compiled_apply_filter(
1862    filter: &CompiledExpr,
1863    value: &JValue,
1864    vars: Option<&HashMap<&str, &JValue>>,
1865    ctx: Option<&Context>,
1866    shape: Option<&ShapeCache>,
1867    options: &EvaluatorOptions,
1868    start_time: Option<Instant>,
1869) -> Result<JValue, EvaluatorError> {
1870    match value {
1871        JValue::Array(arr) => {
1872            let mut result = Vec::new();
1873            // Auto-build shape cache from first element when not provided.
1874            // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1875            let local_shape: Option<ShapeCache> = if shape.is_none() {
1876                arr.first().and_then(build_shape_cache)
1877            } else {
1878                None
1879            };
1880            let effective_shape = shape.or(local_shape.as_ref());
1881            for item in arr.iter() {
1882                check_loop_timeout(options, start_time)?;
1883                let pred = eval_compiled_inner(
1884                    filter,
1885                    item,
1886                    vars,
1887                    ctx,
1888                    effective_shape,
1889                    options,
1890                    start_time,
1891                )?;
1892                if compiled_is_truthy(&pred) {
1893                    result.push(item.clone());
1894                }
1895            }
1896            if result.is_empty() {
1897                Ok(JValue::Undefined)
1898            } else if result.len() == 1 {
1899                check_sequence_length(1, options)?;
1900                Ok(result.remove(0))
1901            } else {
1902                check_sequence_length(result.len(), options)?;
1903                Ok(JValue::array(result))
1904            }
1905        }
1906        JValue::Undefined => Ok(JValue::Undefined),
1907        _ => {
1908            let pred = eval_compiled_inner(filter, value, vars, ctx, shape, options, start_time)?;
1909            if compiled_is_truthy(&pred) {
1910                Ok(value.clone())
1911            } else {
1912                Ok(JValue::Undefined)
1913            }
1914        }
1915    }
1916}
1917
1918/// Dispatch a pure builtin function call.
1919///
1920/// Replicates the tree-walker's evaluation for the subset of builtins in
1921/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1922/// `data` is the current context value for implicit-argument insertion.
1923fn call_pure_builtin(
1924    name: &str,
1925    args: &[JValue],
1926    data: &JValue,
1927    options: &EvaluatorOptions,
1928) -> Result<JValue, EvaluatorError> {
1929    use crate::functions;
1930
1931    // Apply implicit context insertion matching the tree-walker
1932    let args_storage: Vec<JValue>;
1933    let effective_args: &[JValue] = if args.is_empty() {
1934        match name {
1935            "string" => {
1936                // $string() with a null/undefined context returns undefined, not "null".
1937                // This mirrors the tree-walker's special case at the function-call site.
1938                if data.is_undefined() || data.is_null() {
1939                    return Ok(JValue::Undefined);
1940                }
1941                args_storage = vec![data.clone()];
1942                &args_storage
1943            }
1944            "number" | "boolean" | "uppercase" | "lowercase" => {
1945                args_storage = vec![data.clone()];
1946                &args_storage
1947            }
1948            _ => args,
1949        }
1950    } else if args.len() == 1 {
1951        match name {
1952            "substringBefore" | "substringAfter" | "contains" | "split" => {
1953                if matches!(data, JValue::String(_)) {
1954                    args_storage = std::iter::once(data.clone())
1955                        .chain(args.iter().cloned())
1956                        .collect();
1957                    &args_storage
1958                } else {
1959                    args
1960                }
1961            }
1962            _ => args,
1963        }
1964    } else {
1965        args
1966    };
1967
1968    // Apply undefined propagation: if the first effective argument is Undefined
1969    // and the function propagates undefined, return Undefined immediately.
1970    // This matches the tree-walker's `propagates_undefined` check.
1971    if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
1972        return Ok(JValue::Undefined);
1973    }
1974
1975    match name {
1976        // ── String functions ────────────────────────────────────────────
1977        "string" => {
1978            // Validate the optional prettify argument: must be a boolean.
1979            let prettify = match effective_args.get(1) {
1980                None => None,
1981                Some(JValue::Bool(b)) => Some(*b),
1982                Some(_) => {
1983                    return Err(EvaluatorError::TypeError(
1984                        "string() prettify parameter must be a boolean".to_string(),
1985                    ))
1986                }
1987            };
1988            let arg = effective_args.first().unwrap_or(&JValue::Null);
1989            Ok(functions::string::string(arg, prettify)?)
1990        }
1991        "length" => match effective_args.first() {
1992            Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1993            // Undefined input propagates (caught above by the undefined-propagation guard).
1994            Some(JValue::Undefined) => Ok(JValue::Undefined),
1995            // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1996            // so the test framework accepts it against any expected T-code).
1997            None => Err(EvaluatorError::EvaluationError(
1998                "length() requires exactly 1 argument".to_string(),
1999            )),
2000            // null and any other non-string type → T0410
2001            _ => Err(EvaluatorError::TypeError(
2002                "T0410: Argument 1 of function length does not match function signature"
2003                    .to_string(),
2004            )),
2005        },
2006        "uppercase" => match effective_args.first() {
2007            Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
2008            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2009            _ => Err(EvaluatorError::TypeError(
2010                "T0410: Argument 1 of function uppercase does not match function signature"
2011                    .to_string(),
2012            )),
2013        },
2014        "lowercase" => match effective_args.first() {
2015            Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
2016            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2017            _ => Err(EvaluatorError::TypeError(
2018                "T0410: Argument 1 of function lowercase does not match function signature"
2019                    .to_string(),
2020            )),
2021        },
2022        "trim" => match effective_args.first() {
2023            None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
2024            Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
2025            _ => Err(EvaluatorError::TypeError(
2026                "trim() requires a string argument".to_string(),
2027            )),
2028        },
2029        "substring" => {
2030            if effective_args.len() < 2 {
2031                return Err(EvaluatorError::EvaluationError(
2032                    "substring() requires at least 2 arguments".to_string(),
2033                ));
2034            }
2035            match (&effective_args[0], &effective_args[1]) {
2036                (JValue::String(s), JValue::Number(start)) => {
2037                    // Optional 3rd arg (length) must be a number if provided.
2038                    let length = match effective_args.get(2) {
2039                        None => None,
2040                        Some(JValue::Number(l)) => Some(*l as i64),
2041                        Some(_) => {
2042                            return Err(EvaluatorError::TypeError(
2043                                "T0410: Argument 3 of function substring does not match function signature"
2044                                    .to_string(),
2045                            ))
2046                        }
2047                    };
2048                    Ok(functions::string::substring(s, *start as i64, length)?)
2049                }
2050                _ => Err(EvaluatorError::TypeError(
2051                    "T0410: Argument 1 of function substring does not match function signature"
2052                        .to_string(),
2053                )),
2054            }
2055        }
2056        "substringBefore" => {
2057            if effective_args.len() != 2 {
2058                return Err(EvaluatorError::TypeError(
2059                    "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
2060                ));
2061            }
2062            match (&effective_args[0], &effective_args[1]) {
2063                (JValue::String(s), JValue::String(sep)) => {
2064                    Ok(functions::string::substring_before(s, sep)?)
2065                }
2066                // Undefined propagates; null is a type error.
2067                (JValue::Undefined, _) => Ok(JValue::Undefined),
2068                _ => Err(EvaluatorError::TypeError(
2069                    "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
2070                )),
2071            }
2072        }
2073        "substringAfter" => {
2074            if effective_args.len() != 2 {
2075                return Err(EvaluatorError::TypeError(
2076                    "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
2077                ));
2078            }
2079            match (&effective_args[0], &effective_args[1]) {
2080                (JValue::String(s), JValue::String(sep)) => {
2081                    Ok(functions::string::substring_after(s, sep)?)
2082                }
2083                // Undefined propagates; null is a type error.
2084                (JValue::Undefined, _) => Ok(JValue::Undefined),
2085                _ => Err(EvaluatorError::TypeError(
2086                    "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
2087                )),
2088            }
2089        }
2090        "contains" => {
2091            if effective_args.len() != 2 {
2092                return Err(EvaluatorError::EvaluationError(
2093                    "contains() requires exactly 2 arguments".to_string(),
2094                ));
2095            }
2096            match &effective_args[0] {
2097                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2098                JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
2099                _ => Err(EvaluatorError::TypeError(
2100                    "contains() requires a string as the first argument".to_string(),
2101                )),
2102            }
2103        }
2104        "split" => {
2105            if effective_args.len() < 2 {
2106                return Err(EvaluatorError::EvaluationError(
2107                    "split() requires at least 2 arguments".to_string(),
2108                ));
2109            }
2110            match &effective_args[0] {
2111                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2112                JValue::String(s) => {
2113                    // Validate the optional limit argument — must be a positive number.
2114                    let limit = match effective_args.get(2) {
2115                        None => None,
2116                        Some(JValue::Number(n)) => {
2117                            if *n < 0.0 {
2118                                return Err(EvaluatorError::EvaluationError(
2119                                    "D3020: Third argument of split function must be a positive number"
2120                                        .to_string(),
2121                                ));
2122                            }
2123                            Some(n.floor() as usize)
2124                        }
2125                        Some(_) => {
2126                            return Err(EvaluatorError::TypeError(
2127                                "split() limit must be a number".to_string(),
2128                            ))
2129                        }
2130                    };
2131                    Ok(functions::string::split(s, &effective_args[1], limit)?)
2132                }
2133                _ => Err(EvaluatorError::TypeError(
2134                    "split() requires a string as the first argument".to_string(),
2135                )),
2136            }
2137        }
2138        "join" => {
2139            if effective_args.is_empty() {
2140                return Err(EvaluatorError::TypeError(
2141                    "T0410: Argument 1 of function $join does not match function signature"
2142                        .to_string(),
2143                ));
2144            }
2145            match &effective_args[0] {
2146                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2147                // Signature: <a<s>s?:s> — first arg must be an array of strings.
2148                JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
2149                    Err(EvaluatorError::TypeError(
2150                        "T0412: Argument 1 of function $join must be an array of String"
2151                            .to_string(),
2152                    ))
2153                }
2154                JValue::Array(arr) => {
2155                    // All elements must be strings.
2156                    for item in arr.iter() {
2157                        if !matches!(item, JValue::String(_)) {
2158                            return Err(EvaluatorError::TypeError(
2159                                "T0412: Argument 1 of function $join must be an array of String"
2160                                    .to_string(),
2161                            ));
2162                        }
2163                    }
2164                    // Validate separator: must be a string if provided.
2165                    let separator = match effective_args.get(1) {
2166                        None | Some(JValue::Undefined) => None,
2167                        Some(JValue::String(s)) => Some(&**s),
2168                        Some(_) => {
2169                            return Err(EvaluatorError::TypeError(
2170                                "T0410: Argument 2 of function $join does not match function signature (expected String)"
2171                                    .to_string(),
2172                            ))
2173                        }
2174                    };
2175                    Ok(functions::string::join(arr, separator)?)
2176                }
2177                JValue::String(s) => Ok(JValue::String(s.clone())),
2178                _ => Err(EvaluatorError::TypeError(
2179                    "T0412: Argument 1 of function $join must be an array of String".to_string(),
2180                )),
2181            }
2182        }
2183
2184        // ── Numeric functions ───────────────────────────────────────────
2185        "number" => match effective_args.first() {
2186            Some(v) => Ok(functions::numeric::number(v)?),
2187            None => Err(EvaluatorError::EvaluationError(
2188                "number() requires at least 1 argument".to_string(),
2189            )),
2190        },
2191        "floor" => match effective_args.first() {
2192            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2193            Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
2194            _ => Err(EvaluatorError::TypeError(
2195                "floor() requires a number argument".to_string(),
2196            )),
2197        },
2198        "ceil" => match effective_args.first() {
2199            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2200            Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
2201            _ => Err(EvaluatorError::TypeError(
2202                "ceil() requires a number argument".to_string(),
2203            )),
2204        },
2205        "round" => match effective_args.first() {
2206            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2207            Some(JValue::Number(n)) => {
2208                let precision = effective_args.get(1).and_then(|v| {
2209                    if let JValue::Number(p) = v {
2210                        Some(*p as i32)
2211                    } else {
2212                        None
2213                    }
2214                });
2215                Ok(functions::numeric::round(*n, precision)?)
2216            }
2217            _ => Err(EvaluatorError::TypeError(
2218                "round() requires a number argument".to_string(),
2219            )),
2220        },
2221        "abs" => match effective_args.first() {
2222            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2223            Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
2224            _ => Err(EvaluatorError::TypeError(
2225                "abs() requires a number argument".to_string(),
2226            )),
2227        },
2228        "sqrt" => match effective_args.first() {
2229            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2230            Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
2231            _ => Err(EvaluatorError::TypeError(
2232                "sqrt() requires a number argument".to_string(),
2233            )),
2234        },
2235
2236        // ── Aggregation functions ───────────────────────────────────────
2237        "sum" => match effective_args.first() {
2238            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2239            None => Err(EvaluatorError::EvaluationError(
2240                "sum() requires exactly 1 argument".to_string(),
2241            )),
2242            Some(JValue::Null) => Ok(JValue::Null),
2243            Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
2244            Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
2245            Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
2246        },
2247        "max" => match effective_args.first() {
2248            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2249            Some(JValue::Null) | None => Ok(JValue::Null),
2250            Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
2251            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2252            _ => Err(EvaluatorError::TypeError(
2253                "max() requires an array or number argument".to_string(),
2254            )),
2255        },
2256        "min" => match effective_args.first() {
2257            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2258            Some(JValue::Null) | None => Ok(JValue::Null),
2259            Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
2260            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2261            _ => Err(EvaluatorError::TypeError(
2262                "min() requires an array or number argument".to_string(),
2263            )),
2264        },
2265        "average" => match effective_args.first() {
2266            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2267            Some(JValue::Null) | None => Ok(JValue::Null),
2268            Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2269            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2270            _ => Err(EvaluatorError::TypeError(
2271                "average() requires an array or number argument".to_string(),
2272            )),
2273        },
2274        "count" => match effective_args.first() {
2275            Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2276            Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2277            Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2278            _ => Ok(JValue::from(1i64)),
2279        },
2280
2281        // ── Boolean / logic ─────────────────────────────────────────────
2282        "boolean" => match effective_args.first() {
2283            Some(v) => Ok(functions::boolean::boolean(v)?),
2284            None => Err(EvaluatorError::EvaluationError(
2285                "boolean() requires 1 argument".to_string(),
2286            )),
2287        },
2288        "not" => match effective_args.first() {
2289            Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2290            None => Err(EvaluatorError::EvaluationError(
2291                "not() requires 1 argument".to_string(),
2292            )),
2293        },
2294
2295        // ── Array functions ─────────────────────────────────────────────
2296        "append" => {
2297            if effective_args.len() != 2 {
2298                return Err(EvaluatorError::EvaluationError(
2299                    "append() requires exactly 2 arguments".to_string(),
2300                ));
2301            }
2302            let first = &effective_args[0];
2303            let second = &effective_args[1];
2304            if matches!(second, JValue::Null | JValue::Undefined) {
2305                return Ok(first.clone());
2306            }
2307            if matches!(first, JValue::Null | JValue::Undefined) {
2308                return Ok(second.clone());
2309            }
2310            let arr = match first {
2311                JValue::Array(a) => a.to_vec(),
2312                other => vec![other.clone()],
2313            };
2314            let second_len = match second {
2315                JValue::Array(a) => a.len(),
2316                _ => 1,
2317            };
2318            check_sequence_length(arr.len() + second_len, options)?;
2319            Ok(functions::array::append(&arr, second)?)
2320        }
2321        "reverse" => match effective_args.first() {
2322            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2323            Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2324            _ => Err(EvaluatorError::TypeError(
2325                "reverse() requires an array argument".to_string(),
2326            )),
2327        },
2328        "distinct" => match effective_args.first() {
2329            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2330            Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2331            // Non-array input, and arrays of length <= 1, pass through unchanged
2332            // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2333            Some(other) => Ok(other.clone()),
2334        },
2335
2336        // ── Object functions ────────────────────────────────────────────
2337        "keys" => match effective_args.first() {
2338            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2339            Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2340            Some(JValue::Object(obj)) => {
2341                if obj.is_empty() {
2342                    Ok(JValue::Null)
2343                } else {
2344                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2345                    check_sequence_length(keys.len(), options)?;
2346                    if keys.len() == 1 {
2347                        Ok(keys.into_iter().next().unwrap())
2348                    } else {
2349                        Ok(JValue::array(keys))
2350                    }
2351                }
2352            }
2353            Some(JValue::Array(arr)) => {
2354                let mut all_keys: Vec<JValue> = Vec::new();
2355                for item in arr.iter() {
2356                    if let JValue::Object(obj) = item {
2357                        for key in obj.keys() {
2358                            let k = JValue::string(key.clone());
2359                            if !all_keys.contains(&k) {
2360                                all_keys.push(k);
2361                            }
2362                        }
2363                    }
2364                }
2365                if all_keys.is_empty() {
2366                    Ok(JValue::Null)
2367                } else if all_keys.len() == 1 {
2368                    Ok(all_keys.into_iter().next().unwrap())
2369                } else {
2370                    check_sequence_length(all_keys.len(), options)?;
2371                    Ok(JValue::array(all_keys))
2372                }
2373            }
2374            _ => Ok(JValue::Null),
2375        },
2376        "merge" => match effective_args.len() {
2377            0 => Err(EvaluatorError::EvaluationError(
2378                "merge() requires at least 1 argument".to_string(),
2379            )),
2380            1 => match &effective_args[0] {
2381                JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2382                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2383                JValue::Object(_) => Ok(effective_args[0].clone()),
2384                _ => Err(EvaluatorError::TypeError(
2385                    "merge() requires objects or an array of objects".to_string(),
2386                )),
2387            },
2388            _ => Ok(functions::object::merge(effective_args)?),
2389        },
2390
2391        _ => unreachable!(
2392            "call_pure_builtin called with non-compilable builtin: {}",
2393            name
2394        ),
2395    }
2396}
2397
2398// ──────────────────────────────────────────────────────────────────────────────
2399// End of CompiledExpr framework
2400// ──────────────────────────────────────────────────────────────────────────────
2401
2402/// Functions that propagate undefined (return undefined when given an undefined argument).
2403/// These functions should return null/undefined when their input path doesn't exist,
2404/// rather than throwing a type error.
2405const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2406    "not",
2407    "boolean",
2408    "length",
2409    "number",
2410    "uppercase",
2411    "lowercase",
2412    "substring",
2413    "substringBefore",
2414    "substringAfter",
2415    "string",
2416];
2417
2418/// Check whether a function propagates undefined values
2419fn propagates_undefined(name: &str) -> bool {
2420    UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2421}
2422
2423/// Iterator-based numeric aggregation helpers.
2424/// These avoid cloning values by iterating over references and extracting f64 values directly.
2425mod aggregation {
2426    use super::*;
2427
2428    /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2429    /// Returns Err if any non-numeric value is encountered.
2430    fn for_each_numeric(
2431        arr: &[JValue],
2432        func_name: &str,
2433        mut f: impl FnMut(f64),
2434    ) -> Result<(), EvaluatorError> {
2435        fn recurse(
2436            arr: &[JValue],
2437            func_name: &str,
2438            f: &mut dyn FnMut(f64),
2439        ) -> Result<(), EvaluatorError> {
2440            for value in arr.iter() {
2441                match value {
2442                    JValue::Array(inner) => recurse(inner, func_name, f)?,
2443                    JValue::Number(n) => {
2444                        f(*n);
2445                    }
2446                    _ => {
2447                        return Err(EvaluatorError::TypeError(format!(
2448                            "{}() requires all array elements to be numbers",
2449                            func_name
2450                        )));
2451                    }
2452                }
2453            }
2454            Ok(())
2455        }
2456        recurse(arr, func_name, &mut f)
2457    }
2458
2459    /// Count elements in a potentially nested array without cloning.
2460    fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2461        let mut count = 0usize;
2462        for_each_numeric(arr, func_name, |_| count += 1)?;
2463        Ok(count)
2464    }
2465
2466    pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2467        if arr.is_empty() {
2468            return Ok(JValue::from(0i64));
2469        }
2470        let mut total = 0.0f64;
2471        for_each_numeric(arr, "sum", |n| total += n)?;
2472        Ok(JValue::Number(total))
2473    }
2474
2475    pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2476        if arr.is_empty() {
2477            return Ok(JValue::Null);
2478        }
2479        let mut max_val = f64::NEG_INFINITY;
2480        for_each_numeric(arr, "max", |n| {
2481            if n > max_val {
2482                max_val = n;
2483            }
2484        })?;
2485        Ok(JValue::Number(max_val))
2486    }
2487
2488    pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2489        if arr.is_empty() {
2490            return Ok(JValue::Null);
2491        }
2492        let mut min_val = f64::INFINITY;
2493        for_each_numeric(arr, "min", |n| {
2494            if n < min_val {
2495                min_val = n;
2496            }
2497        })?;
2498        Ok(JValue::Number(min_val))
2499    }
2500
2501    pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2502        if arr.is_empty() {
2503            return Ok(JValue::Null);
2504        }
2505        let mut total = 0.0f64;
2506        let count = count_numeric(arr, "average")?;
2507        for_each_numeric(arr, "average", |n| total += n)?;
2508        Ok(JValue::Number(total / count as f64))
2509    }
2510}
2511
2512/// Evaluator errors
2513#[derive(Error, Debug)]
2514pub enum EvaluatorError {
2515    #[error("Type error: {0}")]
2516    TypeError(String),
2517
2518    #[error("Reference error: {0}")]
2519    ReferenceError(String),
2520
2521    #[error("Evaluation error: {0}")]
2522    EvaluationError(String),
2523}
2524
2525impl From<crate::functions::FunctionError> for EvaluatorError {
2526    fn from(e: crate::functions::FunctionError) -> Self {
2527        EvaluatorError::EvaluationError(e.to_string())
2528    }
2529}
2530
2531impl From<crate::datetime::DateTimeError> for EvaluatorError {
2532    fn from(e: crate::datetime::DateTimeError) -> Self {
2533        EvaluatorError::EvaluationError(e.to_string())
2534    }
2535}
2536
2537/// Result of evaluating a lambda body that may be a tail call
2538/// Used for trampoline-based tail call optimization
2539enum LambdaResult {
2540    /// Final value - evaluation is complete
2541    JValue(JValue),
2542    /// Tail call - need to continue with another lambda invocation
2543    TailCall {
2544        /// The lambda to call (boxed to reduce enum size)
2545        lambda: Box<StoredLambda>,
2546        /// Arguments for the call
2547        args: Vec<JValue>,
2548        /// Data context for the call
2549        data: JValue,
2550    },
2551}
2552
2553/// Lambda storage
2554/// Stores the AST of a lambda function along with its parameters, optional signature,
2555/// and captured environment for closures
2556#[derive(Clone, Debug)]
2557pub struct StoredLambda {
2558    pub params: Vec<String>,
2559    pub body: AstNode,
2560    /// Pre-compiled body for use in tight inner loops (HOF fast path).
2561    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2562    pub(crate) compiled_body: Option<CompiledExpr>,
2563    pub signature: Option<String>,
2564    /// Captured environment bindings for closures
2565    pub captured_env: HashMap<String, JValue>,
2566    /// Captured data context for lexical scoping of bare field names
2567    pub captured_data: Option<JValue>,
2568    /// Whether this lambda's body contains tail calls that can be optimized
2569    pub thunk: bool,
2570}
2571
2572/// A single scope in the scope stack
2573struct Scope {
2574    bindings: HashMap<String, JValue>,
2575    lambdas: HashMap<String, StoredLambda>,
2576}
2577
2578impl Scope {
2579    fn new() -> Self {
2580        Scope {
2581            bindings: HashMap::new(),
2582            lambdas: HashMap::new(),
2583        }
2584    }
2585}
2586
2587/// Evaluation context
2588///
2589/// Holds variable bindings and other state needed during evaluation.
2590/// Uses a scope stack for efficient push/pop instead of clone/restore.
2591pub struct Context {
2592    scope_stack: Vec<Scope>,
2593    parent_data: Option<JValue>,
2594}
2595
2596impl Context {
2597    pub fn new() -> Self {
2598        Context {
2599            scope_stack: vec![Scope::new()],
2600            parent_data: None,
2601        }
2602    }
2603
2604    /// Push a new scope onto the stack
2605    fn push_scope(&mut self) {
2606        self.scope_stack.push(Scope::new());
2607    }
2608
2609    /// Pop the top scope from the stack
2610    fn pop_scope(&mut self) {
2611        if self.scope_stack.len() > 1 {
2612            self.scope_stack.pop();
2613        }
2614    }
2615
2616    /// Pop scope but preserve specified lambdas by moving them to the current top scope
2617    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2618        if self.scope_stack.len() > 1 {
2619            let popped = self.scope_stack.pop().unwrap();
2620            if !lambda_ids.is_empty() {
2621                let top = self.scope_stack.last_mut().unwrap();
2622                for id in lambda_ids {
2623                    if let Some(stored) = popped.lambdas.get(id) {
2624                        top.lambdas.insert(id.clone(), stored.clone());
2625                    }
2626                }
2627            }
2628        }
2629    }
2630
2631    /// Clear all bindings and lambdas in the top scope without deallocating
2632    fn clear_current_scope(&mut self) {
2633        let top = self.scope_stack.last_mut().unwrap();
2634        top.bindings.clear();
2635        top.lambdas.clear();
2636    }
2637
2638    pub fn bind(&mut self, name: String, value: JValue) {
2639        self.scope_stack
2640            .last_mut()
2641            .unwrap()
2642            .bindings
2643            .insert(name, value);
2644    }
2645
2646    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2647        self.scope_stack
2648            .last_mut()
2649            .unwrap()
2650            .lambdas
2651            .insert(name, lambda);
2652    }
2653
2654    pub fn unbind(&mut self, name: &str) {
2655        // Remove from top scope only
2656        let top = self.scope_stack.last_mut().unwrap();
2657        top.bindings.remove(name);
2658        top.lambdas.remove(name);
2659    }
2660
2661    pub fn lookup(&self, name: &str) -> Option<&JValue> {
2662        // Walk scope stack from top to bottom
2663        for scope in self.scope_stack.iter().rev() {
2664            if let Some(value) = scope.bindings.get(name) {
2665                return Some(value);
2666            }
2667        }
2668        None
2669    }
2670
2671    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2672        // Walk scope stack from top to bottom
2673        for scope in self.scope_stack.iter().rev() {
2674            if let Some(lambda) = scope.lambdas.get(name) {
2675                return Some(lambda);
2676            }
2677        }
2678        None
2679    }
2680
2681    pub fn set_parent(&mut self, data: JValue) {
2682        self.parent_data = Some(data);
2683    }
2684
2685    pub fn get_parent(&self) -> Option<&JValue> {
2686        self.parent_data.as_ref()
2687    }
2688
2689    /// Collect all bindings across all scopes (for environment capture).
2690    /// Higher scopes shadow lower scopes.
2691    fn all_bindings(&self) -> HashMap<String, JValue> {
2692        let mut result = HashMap::new();
2693        for scope in &self.scope_stack {
2694            for (k, v) in &scope.bindings {
2695                result.insert(k.clone(), v.clone());
2696            }
2697        }
2698        result
2699    }
2700}
2701
2702impl Default for Context {
2703    fn default() -> Self {
2704        Self::new()
2705    }
2706}
2707
2708/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2709/// ...}`) from a value about to leave the evaluator.
2710///
2711/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2712/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2713/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2714/// path step consumes and re-wraps these as evaluation proceeds, but the
2715/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2716/// because the tuple-producing expression itself is the whole result (a bare
2717/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2718/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2719/// wrapper ends up embedded in a field value or array element rather than at
2720/// the top level. This recurses through both array elements and (non-tuple)
2721/// object field values so both shapes are cleaned up, not just a bare
2722/// top-level tuple array.
2723/// Merge a group of tuple wrappers into a single tuple, appending each key's
2724/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2725/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2726/// a key present in one tuple stays a scalar; a key present in several becomes an
2727/// array of the collected values (used by group-by value evaluation so a group
2728/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2729/// N collected focus values).
2730fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2731    fn append(acc: Option<JValue>, v: JValue) -> JValue {
2732        match acc {
2733            None => v,
2734            Some(a) => {
2735                let mut out: Vec<JValue> = match a {
2736                    JValue::Array(arr) => arr.iter().cloned().collect(),
2737                    other => vec![other],
2738                };
2739                match v {
2740                    JValue::Array(arr) => out.extend(arr.iter().cloned()),
2741                    other => out.push(other),
2742                }
2743                JValue::array(out)
2744            }
2745        }
2746    }
2747    let mut result: IndexMap<String, JValue> = IndexMap::new();
2748    for tuple in group {
2749        if let JValue::Object(obj) = tuple {
2750            for (k, v) in obj.iter() {
2751                if k == "__tuple__" {
2752                    result.insert(k.clone(), v.clone());
2753                    continue;
2754                }
2755                let merged = append(result.shift_remove(k), v.clone());
2756                result.insert(k.clone(), merged);
2757            }
2758        }
2759    }
2760    result
2761}
2762
2763fn unwrap_tuple_output(value: JValue) -> JValue {
2764    match value {
2765        JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2766            .get("@")
2767            .cloned()
2768            .map(unwrap_tuple_output)
2769            .unwrap_or(JValue::Undefined),
2770        JValue::Object(obj) => {
2771            let mut new_map = IndexMap::with_capacity(obj.len());
2772            for (k, v) in obj.iter() {
2773                new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2774            }
2775            JValue::object(new_map)
2776        }
2777        JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2778        other => other,
2779    }
2780}
2781
2782/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2783/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2784/// (if anything) was bound under that name beforehand. `restore` puts the
2785/// prior value back -- or removes the binding entirely if there wasn't
2786/// one -- rather than unconditionally unbinding, so a tuple key that
2787/// happens to share a name with a live outer `:=` binding in the same
2788/// scope frame doesn't get permanently deleted once the tuple-row
2789/// evaluation finishes.
2790struct TupleKeyBindings {
2791    saved: Vec<(String, Option<JValue>)>,
2792}
2793
2794impl TupleKeyBindings {
2795    /// True if `name` was one of the keys this guard bound (used by callers
2796    /// that need to know whether a given tuple key is already in scope
2797    /// before binding it a second time under a different role, e.g.
2798    /// `create_tuple_stream`'s ancestor-label handling).
2799    fn contains(&self, name: &str) -> bool {
2800        self.saved.iter().any(|(n, _)| n == name)
2801    }
2802
2803    fn restore(self, evaluator: &mut Evaluator) {
2804        for (name, prior) in self.saved {
2805            match prior {
2806                Some(value) => evaluator.context.bind(name, value),
2807                None => evaluator.context.unbind(&name),
2808            }
2809        }
2810    }
2811}
2812
2813/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
2814/// evaluator options. All fields default to `None` = unlimited (current behavior).
2815#[derive(Default, Clone, Debug)]
2816pub struct EvaluatorOptions {
2817    /// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
2818    pub timeout_ms: Option<u64>,
2819    /// Maximum AST-recursion stack depth. Exceeding it raises D1011 if this is the
2820    /// tighter of this value and the hardcoded native-stack safety ceiling (302);
2821    /// otherwise the hardcoded ceiling still raises U1001 (see GitHub issue #34).
2822    pub max_stack_depth: Option<usize>,
2823    /// Maximum length of a query-result sequence (map/filter/wildcard/descendants/
2824    /// keys/lookup/append/spread/each/range/path-mapping). Exceeding it raises D2015.
2825    /// Does NOT currently apply to literal array construction (`MakeArray`/
2826    /// `ArrayConstruct`) — NOTE this is a deliberate, temporary divergence from
2827    /// upstream, not a match: jsonata-js DOES cap flat/non-nested array literals
2828    /// (via `fn.append`'s `createSequence` hook in `evaluateUnary`'s `[` case).
2829    /// Deferred until the separate `MakeArray(u16)` truncation bug is fixed (see
2830    /// the design spec's "Sequence length → D2015" section).
2831    pub max_sequence_length: Option<usize>,
2832}
2833
2834/// Checks a constructed query-result sequence's length against the configured
2835/// `max_sequence_length` guardrail. Call this at sites that build a query-result
2836/// sequence (map/filter/wildcard/descendants/keys/lookup/append/spread/each/range/
2837/// path-mapping). NOT currently called at literal array construction (`[1,2,3]`) —
2838/// unlike upstream jsonata-js, which caps flat/non-nested literals too via
2839/// `fn.append`'s `createSequence()` hook. See `EvaluatorOptions::max_sequence_length`
2840/// doc comment above for why this is a deliberate, temporary gap.
2841pub(crate) fn check_sequence_length(
2842    len: usize,
2843    options: &EvaluatorOptions,
2844) -> Result<(), EvaluatorError> {
2845    if let Some(max) = options.max_sequence_length {
2846        if len > max {
2847            return Err(EvaluatorError::EvaluationError(format!(
2848                "D2015: The maximum sequence length of {} was exceeded.",
2849                max
2850            )));
2851        }
2852    }
2853    Ok(())
2854}
2855
2856/// Per-iteration D1012 check for loop-based compiled/VM constructs (map/filter/
2857/// reduce element loops, FilterByBytecode) that don't pass through
2858/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed.
2859#[inline]
2860pub(crate) fn check_loop_timeout(
2861    options: &EvaluatorOptions,
2862    start_time: Option<Instant>,
2863) -> Result<(), EvaluatorError> {
2864    if let Some(timeout_ms) = options.timeout_ms {
2865        if let Some(start) = start_time {
2866            if start.elapsed().as_millis() as u64 > timeout_ms {
2867                return Err(EvaluatorError::EvaluationError(format!(
2868                    "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
2869                    timeout_ms
2870                )));
2871            }
2872        }
2873    }
2874    Ok(())
2875}
2876
2877/// Evaluator for JSONata expressions
2878pub struct Evaluator {
2879    context: Context,
2880    recursion_depth: usize,
2881    max_recursion_depth: usize,
2882    /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
2883    /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
2884    /// using the AST node's pointer address (as before) collided whenever the same
2885    /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
2886    /// or other repeated recursion), aliasing unrelated closures that shared an id.
2887    next_lambda_id: u64,
2888    /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
2889    /// wrapper during this top-level `evaluate()` call. Reset at the start of
2890    /// `evaluate()` and checked at the end to decide whether the (recursive,
2891    /// O(result size)) tuple-unwrap pass is needed before returning to the
2892    /// caller — keeps the vast majority of evaluations, which never touch
2893    /// `%`/`@`/`#`, at zero added cost.
2894    tuple_stream_created: bool,
2895    /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
2896    /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
2897    /// by the two consumers that read those carried bindings directly from the
2898    /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
2899    /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
2900    /// evaluating its input path (key/value expressions read `$focus` off the
2901    /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
2902    /// of projecting each tuple's `@`.
2903    keep_tuple_stream: bool,
2904    options: EvaluatorOptions,
2905    /// Set in `evaluate()` (only when `options.timeout_ms` is configured) and
2906    /// checked in `evaluate_internal`'s per-node checkpoint for D1012.
2907    start_time: Option<Instant>,
2908}
2909
2910impl Evaluator {
2911    pub fn new() -> Self {
2912        Evaluator {
2913            context: Context::new(),
2914            recursion_depth: 0,
2915            // Limit recursion depth to prevent stack overflow
2916            // True TCO would allow deeper recursion but requires parser-level thunk marking
2917            max_recursion_depth: 302,
2918            next_lambda_id: 0,
2919            tuple_stream_created: false,
2920            keep_tuple_stream: false,
2921            options: EvaluatorOptions::default(),
2922            start_time: None,
2923        }
2924    }
2925
2926    pub fn with_context(context: Context) -> Self {
2927        Evaluator {
2928            context,
2929            recursion_depth: 0,
2930            max_recursion_depth: 302,
2931            next_lambda_id: 0,
2932            tuple_stream_created: false,
2933            keep_tuple_stream: false,
2934            options: EvaluatorOptions::default(),
2935            start_time: None,
2936        }
2937    }
2938
2939    /// Construct an `Evaluator` with guardrail options. `Evaluator::new()`/
2940    /// `with_context()` remain unchanged (unlimited options) for existing callers.
2941    pub fn with_options(context: Context, options: EvaluatorOptions) -> Self {
2942        Evaluator {
2943            context,
2944            recursion_depth: 0,
2945            max_recursion_depth: 302,
2946            next_lambda_id: 0,
2947            tuple_stream_created: false,
2948            keep_tuple_stream: false,
2949            options,
2950            start_time: None,
2951        }
2952    }
2953
2954    /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
2955    fn fresh_lambda_id(&mut self) -> u64 {
2956        let id = self.next_lambda_id;
2957        self.next_lambda_id += 1;
2958        id
2959    }
2960
2961    /// Invoke a stored lambda with its captured environment and data.
2962    /// This is the standard way to call a StoredLambda, handling the
2963    /// captured_env and captured_data extraction boilerplate.
2964    fn invoke_stored_lambda(
2965        &mut self,
2966        stored: &StoredLambda,
2967        args: &[JValue],
2968        data: &JValue,
2969    ) -> Result<JValue, EvaluatorError> {
2970        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2971        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2972        // and no captured lambda/builtin values (those require Context for runtime lookup).
2973        if let Some(ref ce) = stored.compiled_body {
2974            if stored.signature.is_none()
2975                && !stored.thunk
2976                && !stored
2977                    .captured_env
2978                    .values()
2979                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2980            {
2981                let call_data = stored.captured_data.as_ref().unwrap_or(data);
2982                let vars: HashMap<&str, &JValue> = stored
2983                    .params
2984                    .iter()
2985                    .zip(args.iter())
2986                    .map(|(p, v)| (p.as_str(), v))
2987                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2988                    .collect();
2989                return eval_compiled(ce, call_data, Some(&vars), &self.options, self.start_time);
2990            }
2991        }
2992
2993        let captured_env = if stored.captured_env.is_empty() {
2994            None
2995        } else {
2996            Some(&stored.captured_env)
2997        };
2998        let captured_data = stored.captured_data.as_ref();
2999        self.invoke_lambda_with_env(
3000            &stored.params,
3001            &stored.body,
3002            stored.signature.as_ref(),
3003            args,
3004            data,
3005            captured_env,
3006            captured_data,
3007            stored.thunk,
3008        )
3009    }
3010
3011    /// Look up a StoredLambda from a JValue that may be a lambda marker.
3012    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
3013    /// with a valid lambda_id that references a stored lambda.
3014    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
3015        if let JValue::Lambda { lambda_id, .. } = value {
3016            return self.context.lookup_lambda(lambda_id).cloned();
3017        }
3018        None
3019    }
3020
3021    /// Get the number of parameters a callback function expects by inspecting its AST.
3022    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
3023    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
3024    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
3025        match func_node {
3026            AstNode::Lambda { params, .. } => params.len(),
3027            AstNode::Variable(var_name) => {
3028                // Check if this variable holds a stored lambda
3029                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
3030                    return stored_lambda.params.len();
3031                }
3032                // Also check if it's a lambda value in bindings (e.g., from partial application)
3033                if let Some(value) = self.context.lookup(var_name) {
3034                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
3035                        return stored_lambda.params.len();
3036                    }
3037                }
3038                // Unknown, return max to be safe
3039                usize::MAX
3040            }
3041            AstNode::Function { .. } => {
3042                // For function references, we can't easily determine param count
3043                // Return max to be safe
3044                usize::MAX
3045            }
3046            _ => usize::MAX,
3047        }
3048    }
3049
3050    /// Specialized sort using pre-extracted keys (Schwartzian transform).
3051    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
3052    /// avoiding O(N log N) hash lookups during comparisons.
3053    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
3054        if arr.len() <= 1 {
3055            return;
3056        }
3057
3058        // Phase 1: Extract sort keys -- one IndexMap lookup per element
3059        let keys: Vec<SortKey> = arr
3060            .iter()
3061            .map(|item| match item {
3062                JValue::Object(obj) => match obj.get(&spec.field) {
3063                    Some(JValue::Number(n)) => SortKey::Num(*n),
3064                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
3065                    _ => SortKey::None,
3066                },
3067                _ => SortKey::None,
3068            })
3069            .collect();
3070
3071        // Phase 2: Build index permutation sorted by pre-extracted keys
3072        let mut perm: Vec<usize> = (0..arr.len()).collect();
3073        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
3074
3075        // Phase 3: Apply permutation in-place via cycle-following
3076        let mut placed = vec![false; arr.len()];
3077        for i in 0..arr.len() {
3078            if placed[i] || perm[i] == i {
3079                continue;
3080            }
3081            let mut j = i;
3082            loop {
3083                let target = perm[j];
3084                placed[j] = true;
3085                if target == i {
3086                    break;
3087                }
3088                arr.swap(j, target);
3089                j = target;
3090            }
3091        }
3092    }
3093
3094    /// Merge sort implementation using a comparator function.
3095    /// This replaces the O(n²) bubble sort for better performance on large arrays.
3096    /// The comparator returns true if the first element should come AFTER the second.
3097    fn merge_sort_with_comparator(
3098        &mut self,
3099        arr: &mut [JValue],
3100        comparator: &AstNode,
3101        data: &JValue,
3102    ) -> Result<(), EvaluatorError> {
3103        if arr.len() <= 1 {
3104            return Ok(());
3105        }
3106
3107        // Try specialized fast path for simple field comparisons like
3108        // function($l, $r) { $l.price > $r.price }
3109        if let AstNode::Lambda { params, body, .. } = comparator {
3110            if params.len() >= 2 {
3111                if let Some(spec) = try_specialize_sort_comparator(body, &params[0], &params[1]) {
3112                    Self::merge_sort_specialized(arr, &spec);
3113                    return Ok(());
3114                }
3115            }
3116        }
3117
3118        let mid = arr.len() / 2;
3119
3120        // Sort left half
3121        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
3122
3123        // Sort right half
3124        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
3125
3126        // Merge the sorted halves
3127        let mut temp = Vec::with_capacity(arr.len());
3128        let (left, right) = arr.split_at(mid);
3129
3130        let mut i = 0;
3131        let mut j = 0;
3132
3133        // For lambda comparators, use a reusable scope to avoid
3134        // push_scope/pop_scope per comparison (~n log n total comparisons)
3135        if let AstNode::Lambda { params, body, .. } = comparator {
3136            if params.len() >= 2 {
3137                // Pre-clone param names once outside the loop
3138                let param0 = params[0].clone();
3139                let param1 = params[1].clone();
3140                self.context.push_scope();
3141                while i < left.len() && j < right.len() {
3142                    // Reuse scope: clear and rebind instead of push/pop
3143                    self.context.clear_current_scope();
3144                    self.context.bind(param0.clone(), left[i].clone());
3145                    self.context.bind(param1.clone(), right[j].clone());
3146
3147                    let cmp_result = self.evaluate_internal(body, data)?;
3148
3149                    if self.is_truthy(&cmp_result) {
3150                        temp.push(right[j].clone());
3151                        j += 1;
3152                    } else {
3153                        temp.push(left[i].clone());
3154                        i += 1;
3155                    }
3156                }
3157                self.context.pop_scope();
3158            } else {
3159                // Unexpected param count - fall back to generic path
3160                while i < left.len() && j < right.len() {
3161                    let cmp_result = self.apply_function(
3162                        comparator,
3163                        &[left[i].clone(), right[j].clone()],
3164                        data,
3165                    )?;
3166                    if self.is_truthy(&cmp_result) {
3167                        temp.push(right[j].clone());
3168                        j += 1;
3169                    } else {
3170                        temp.push(left[i].clone());
3171                        i += 1;
3172                    }
3173                }
3174            }
3175        } else {
3176            // Non-lambda comparator: use generic apply_function path
3177            while i < left.len() && j < right.len() {
3178                let cmp_result =
3179                    self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
3180                if self.is_truthy(&cmp_result) {
3181                    temp.push(right[j].clone());
3182                    j += 1;
3183                } else {
3184                    temp.push(left[i].clone());
3185                    i += 1;
3186                }
3187            }
3188        }
3189
3190        // Copy remaining elements
3191        temp.extend_from_slice(&left[i..]);
3192        temp.extend_from_slice(&right[j..]);
3193
3194        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
3195        for (i, val) in temp.into_iter().enumerate() {
3196            arr[i] = val;
3197        }
3198
3199        Ok(())
3200    }
3201
3202    /// Evaluate an AST node against data
3203    ///
3204    /// This is the main entry point for evaluation. It sets up the parent context
3205    /// to be the root data if not already set.
3206    ///
3207    /// Also the single choke point for stripping any lingering tuple-stream
3208    /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
3209    /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
3210    /// tuple-stream representation (see `create_tuple_stream`), and without this
3211    /// a bare (or object/array-nested) tuple-producing expression would leak
3212    /// that internal representation into user-visible output instead of the
3213    /// plain value.
3214    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
3215        // Set parent context to root data if not already set
3216        if self.context.get_parent().is_none() {
3217            self.context.set_parent(data.clone());
3218        }
3219
3220        if self.options.timeout_ms.is_some() {
3221            self.start_time = Some(Instant::now());
3222        }
3223
3224        self.tuple_stream_created = false;
3225        let result = self.evaluate_internal(node, data)?;
3226        Ok(if self.tuple_stream_created {
3227            unwrap_tuple_output(result)
3228        } else {
3229            result
3230        })
3231    }
3232
3233    /// Fast evaluation for leaf nodes that don't need recursion tracking.
3234    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
3235    /// Returns None for anything requiring the full evaluator.
3236    #[inline(always)]
3237    fn evaluate_leaf(
3238        &mut self,
3239        node: &AstNode,
3240        data: &JValue,
3241    ) -> Option<Result<JValue, EvaluatorError>> {
3242        match node {
3243            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
3244            AstNode::Number(n) => {
3245                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3246                    Some(Ok(JValue::from(*n as i64)))
3247                } else {
3248                    Some(Ok(JValue::Number(*n)))
3249                }
3250            }
3251            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
3252            AstNode::Null => Some(Ok(JValue::Null)),
3253            AstNode::Undefined => Some(Ok(JValue::Undefined)),
3254            AstNode::Name(field_name) => match data {
3255                // Array mapping and other cases need full evaluator
3256                JValue::Object(obj) => Some(Ok(obj
3257                    .get(field_name)
3258                    .cloned()
3259                    .unwrap_or(JValue::Undefined))),
3260                _ => None,
3261            },
3262            AstNode::Variable(name) if !name.is_empty() => {
3263                // Simple variable lookup — only fast-path when no tuple data
3264                if let JValue::Object(obj) = data {
3265                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3266                        return None; // Tuple data needs full evaluator
3267                    }
3268                }
3269                // May be a lambda/builtin — needs full evaluator if None
3270                self.context.lookup(name).map(|value| Ok(value.clone()))
3271            }
3272            _ => None,
3273        }
3274    }
3275
3276    /// Internal evaluation method
3277    fn evaluate_internal(
3278        &mut self,
3279        node: &AstNode,
3280        data: &JValue,
3281    ) -> Result<JValue, EvaluatorError> {
3282        // Fast path for leaf nodes — skip recursion tracking overhead
3283        if let Some(result) = self.evaluate_leaf(node, data) {
3284            return result;
3285        }
3286
3287        // Check recursion depth to prevent stack overflow. `effective_limit` is
3288        // whichever is tighter: the user's `max_stack_depth` guardrail or the
3289        // hardcoded native-stack-safety ceiling (`max_recursion_depth`, always
3290        // 302, GitHub issue #34). The hardcoded ceiling is an always-on backstop
3291        // regardless of user options — only a user limit BELOW it can produce
3292        // D1011; hitting the hardcoded ceiling itself (no option set, or an
3293        // option set at/above 302) still produces U1001.
3294        self.recursion_depth += 1;
3295        let effective_limit = match self.options.max_stack_depth {
3296            Some(limit) => limit.min(self.max_recursion_depth),
3297            None => self.max_recursion_depth,
3298        };
3299        if self.recursion_depth > effective_limit {
3300            self.recursion_depth -= 1;
3301            return Err(EvaluatorError::EvaluationError(
3302                if effective_limit < self.max_recursion_depth {
3303                    "D1011: Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive".to_string()
3304                } else {
3305                    format!(
3306                        "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
3307                        effective_limit
3308                    )
3309                },
3310            ));
3311        }
3312
3313        // Check evaluation timeout (D1012). `start_time` is only set (in
3314        // `evaluate()`) when `options.timeout_ms` is configured, so this is a
3315        // single `is_none()` branch of overhead when no timeout is set.
3316        if let Some(timeout_ms) = self.options.timeout_ms {
3317            if let Some(start) = self.start_time {
3318                if start.elapsed().as_millis() as u64 > timeout_ms {
3319                    self.recursion_depth -= 1;
3320                    return Err(EvaluatorError::EvaluationError(format!(
3321                        "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3322                        timeout_ms
3323                    )));
3324                }
3325            }
3326        }
3327
3328        // The soft depth counter above is calibrated against a comfortably
3329        // large native stack. Hosts with a much smaller default thread stack
3330        // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
3331        // stack well before this counter trips, crashing the process instead
3332        // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
3333        // transparently swaps in a bigger stack segment when headroom is
3334        // low, so this stays a no-op cost on the common shallow path.
3335        const RED_ZONE: usize = 128 * 1024;
3336        const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
3337        let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
3338            self.evaluate_internal_impl(node, data)
3339        });
3340
3341        self.recursion_depth -= 1;
3342        result
3343    }
3344
3345    /// Internal evaluation implementation (separated to allow depth tracking)
3346    fn evaluate_internal_impl(
3347        &mut self,
3348        node: &AstNode,
3349        data: &JValue,
3350    ) -> Result<JValue, EvaluatorError> {
3351        match node {
3352            AstNode::String(s) => Ok(JValue::string(s.clone())),
3353
3354            // Name nodes represent field access on the current data
3355            AstNode::Name(field_name) => {
3356                match data {
3357                    JValue::Object(obj) => {
3358                        Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3359                    }
3360                    JValue::Array(arr) => {
3361                        // Map over array
3362                        let mut result = Vec::new();
3363                        for item in arr.iter() {
3364                            if let JValue::Object(obj) = item {
3365                                if let Some(val) = obj.get(field_name) {
3366                                    result.push(val.clone());
3367                                }
3368                            }
3369                        }
3370                        if result.is_empty() {
3371                            Ok(JValue::Undefined)
3372                        } else if result.len() == 1 {
3373                            Ok(result.into_iter().next().unwrap())
3374                        } else {
3375                            Ok(JValue::array(result))
3376                        }
3377                    }
3378                    _ => Ok(JValue::Undefined),
3379                }
3380            }
3381
3382            AstNode::Number(n) => {
3383                // Preserve integer-ness: if the number is a whole number, create an integer JValue
3384                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3385                    // It's a whole number that can be represented as i64
3386                    Ok(JValue::from(*n as i64))
3387                } else {
3388                    Ok(JValue::Number(*n))
3389                }
3390            }
3391            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3392            AstNode::Null => Ok(JValue::Null),
3393            AstNode::Undefined => Ok(JValue::Undefined),
3394            AstNode::Placeholder => {
3395                // Placeholders should only appear as function arguments
3396                // If we reach here, it's an error
3397                Err(EvaluatorError::EvaluationError(
3398                    "Placeholder '?' can only be used as a function argument".to_string(),
3399                ))
3400            }
3401            AstNode::Regex { pattern, flags } => {
3402                // Return a regex object as a special JSON value
3403                // This will be recognized by functions like $split, $match, $replace
3404                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3405            }
3406
3407            AstNode::Variable(name) => {
3408                // Special case: $ alone (empty name) refers to current context
3409                // First check if $ is bound in the context (for closures that captured $)
3410                // Otherwise, use the data parameter
3411                if name.is_empty() {
3412                    if let Some(value) = self.context.lookup("$") {
3413                        return Ok(value.clone());
3414                    }
3415                    // If data is a tuple, return the @ value
3416                    if let JValue::Object(obj) = data {
3417                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3418                            if let Some(inner) = obj.get("@") {
3419                                return Ok(inner.clone());
3420                            }
3421                        }
3422                    }
3423                    return Ok(data.clone());
3424                }
3425
3426                // Check variable bindings FIRST
3427                // This allows function parameters to shadow outer lambdas with the same name
3428                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3429                if let Some(value) = self.context.lookup(name) {
3430                    return Ok(value.clone());
3431                }
3432
3433                // Check tuple bindings in data (for index binding operator #$var)
3434                // When iterating over a tuple stream, $var can reference the bound index
3435                if let JValue::Object(obj) = data {
3436                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3437                        // Check for the variable in tuple bindings (stored as "$name")
3438                        let binding_key = format!("${}", name);
3439                        if let Some(binding_value) = obj.get(&binding_key) {
3440                            return Ok(binding_value.clone());
3441                        }
3442                    }
3443                }
3444
3445                // Then check if this is a stored lambda (user-defined functions)
3446                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3447                    // Return a lambda representation that can be passed to higher-order functions
3448                    // Include _lambda_id pointing to the stored lambda so it can be found
3449                    // when captured in closures
3450                    let lambda_repr = JValue::lambda(
3451                        name.as_str(),
3452                        stored_lambda.params.clone(),
3453                        Some(name.to_string()),
3454                        stored_lambda.signature.clone(),
3455                    );
3456                    return Ok(lambda_repr);
3457                }
3458
3459                // Check if this is a built-in function reference (only if not shadowed)
3460                if self.is_builtin_function(name) {
3461                    // Return a marker for built-in functions
3462                    // This allows built-in functions to be passed to higher-order functions
3463                    let builtin_repr = JValue::builtin(name.as_str());
3464                    return Ok(builtin_repr);
3465                }
3466
3467                // Undefined variable - return null (undefined in JSONata semantics)
3468                // This allows expressions like `$not(undefined_var)` to return undefined
3469                // and comparisons like `3 > $undefined` to return undefined
3470                Ok(JValue::Null)
3471            }
3472
3473            AstNode::ParentVariable(name) => {
3474                // Special case: $$ alone (empty name) refers to parent/root context
3475                if name.is_empty() {
3476                    return self.context.get_parent().cloned().ok_or_else(|| {
3477                        EvaluatorError::ReferenceError("Parent context not available".to_string())
3478                    });
3479                }
3480
3481                // For $$name, we need to evaluate name against parent context
3482                // This is similar to $.name but using parent data
3483                let parent_data = self.context.get_parent().ok_or_else(|| {
3484                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3485                })?;
3486
3487                // Access field on parent context
3488                match parent_data {
3489                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3490                    _ => Ok(JValue::Null),
3491                }
3492            }
3493
3494            AstNode::Path { steps } => self.evaluate_path(steps, data),
3495
3496            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3497
3498            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3499
3500            // Array constructor - JSONata semantics:
3501            AstNode::Array(elements) => {
3502                // - If element is itself an array constructor [...], keep it nested
3503                // - Otherwise, if element evaluates to an array, flatten it
3504                // - Undefined values are excluded
3505                let mut result = Vec::with_capacity(elements.len());
3506                for element in elements {
3507                    // Check if this element is itself an explicit array constructor
3508                    let is_array_constructor = matches!(element, AstNode::Array(_));
3509
3510                    let value = self.evaluate_internal(element, data)?;
3511
3512                    // Skip undefined values in array constructors
3513                    // Note: explicit null is preserved, only undefined (no value) is filtered
3514                    if value.is_undefined() {
3515                        continue;
3516                    }
3517
3518                    if is_array_constructor {
3519                        // Explicit array constructor - keep nested
3520                        result.push(value);
3521                    } else if let JValue::Array(arr) = value {
3522                        // Non-array-constructor that evaluated to array - flatten it
3523                        result.extend(arr.iter().cloned());
3524                    } else {
3525                        // Non-array value - add as-is
3526                        result.push(value);
3527                    }
3528                }
3529                Ok(JValue::array(result))
3530            }
3531
3532            AstNode::Object(pairs) => {
3533                let mut result = IndexMap::with_capacity(pairs.len());
3534
3535                // Check if all keys are string literals — can skip D1009 HashMap
3536                let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3537
3538                if all_literal_keys {
3539                    // Fast path: literal keys, no need for D1009 tracking
3540                    for (key_node, value_node) in pairs.iter() {
3541                        let key = match key_node {
3542                            AstNode::String(s) => s,
3543                            _ => unreachable!(),
3544                        };
3545                        let value = self.evaluate_internal(value_node, data)?;
3546                        if value.is_undefined() {
3547                            continue;
3548                        }
3549                        result.insert(key.clone(), value);
3550                    }
3551                } else {
3552                    let mut key_sources: HashMap<String, usize> = HashMap::new();
3553                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3554                        let key = match self.evaluate_internal(key_node, data)? {
3555                            JValue::String(s) => s,
3556                            JValue::Null => continue,
3557                            other => {
3558                                if other.is_undefined() {
3559                                    continue;
3560                                }
3561                                return Err(EvaluatorError::TypeError(format!(
3562                                    "Object key must be a string, got: {:?}",
3563                                    other
3564                                )));
3565                            }
3566                        };
3567
3568                        if let Some(&existing_idx) = key_sources.get(&*key) {
3569                            if existing_idx != pair_index {
3570                                return Err(EvaluatorError::EvaluationError(format!(
3571                                    "D1009: Multiple key expressions evaluate to same key: {}",
3572                                    key
3573                                )));
3574                            }
3575                        }
3576                        key_sources.insert(key.to_string(), pair_index);
3577
3578                        let value = self.evaluate_internal(value_node, data)?;
3579                        if value.is_undefined() {
3580                            continue;
3581                        }
3582                        result.insert(key.to_string(), value);
3583                    }
3584                }
3585                Ok(JValue::object(result))
3586            }
3587
3588            // Object transform: group items by key, then evaluate value once per group
3589            AstNode::ObjectTransform { input, pattern } => {
3590                // Evaluate the input expression. Keep tuple wrappers alive so the
3591                // group-by key/value expressions can read the carried `$focus`
3592                // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3593                let saved_keep = self.keep_tuple_stream;
3594                self.keep_tuple_stream = true;
3595                let input_value = self.evaluate_internal(input, data);
3596                self.keep_tuple_stream = saved_keep;
3597                let input_value = input_value?;
3598
3599                // If input is undefined, return undefined (not empty object)
3600                if input_value.is_undefined() {
3601                    return Ok(JValue::Undefined);
3602                }
3603
3604                // Handle array input - process each item
3605                let items: Vec<JValue> = match input_value {
3606                    JValue::Array(ref arr) => (**arr).clone(),
3607                    JValue::Null => return Ok(JValue::Null),
3608                    other => vec![other],
3609                };
3610
3611                // If array is empty, add undefined to enable literal JSON object generation
3612                let items = if items.is_empty() {
3613                    vec![JValue::Undefined]
3614                } else {
3615                    items
3616                };
3617
3618                // Grouping over a tuple stream ("reduce" mode, mirroring
3619                // jsonata-js evaluateGroupExpression): each item is a
3620                // `{@, $var, !label, __tuple__}` wrapper. The key/value
3621                // expressions are evaluated against the tuple's `@` value with the
3622                // carried focus/index/ancestor keys bound into scope (so
3623                // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3624                // `$e` AND resolves the relative `Phone` against the Contact `@`),
3625                // and grouped tuples are reduced (per-key values appended) before
3626                // the value expression sees them.
3627                let reduce = items.first().is_some_and(|it| {
3628                    matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3629                });
3630
3631                // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3632                // returns the saved prior values so they can be restored.
3633                let bind_tuple = |ev: &mut Self,
3634                                  tuple: &IndexMap<String, JValue>|
3635                 -> Vec<(String, Option<JValue>)> {
3636                    let mut saved = Vec::new();
3637                    for (k, v) in tuple.iter() {
3638                        let name = if let Some(n) = k.strip_prefix('$') {
3639                            if n.is_empty() {
3640                                continue;
3641                            } else {
3642                                n.to_string()
3643                            }
3644                        } else if k.starts_with('!') {
3645                            k.clone()
3646                        } else {
3647                            continue;
3648                        };
3649                        saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3650                        ev.context.bind(name, v.clone());
3651                    }
3652                    saved
3653                };
3654                let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3655                    for (name, old) in saved.into_iter().rev() {
3656                        match old {
3657                            Some(v) => ev.context.bind(name, v),
3658                            None => ev.context.unbind(&name),
3659                        }
3660                    }
3661                };
3662
3663                // Phase 1: Group items by key expression
3664                // groups maps key -> (grouped_data, expr_index)
3665                // When multiple items have same key, their data is appended together
3666                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3667
3668                // Save the current $ binding to restore later
3669                let saved_dollar = self.context.lookup("$").cloned();
3670
3671                for item in &items {
3672                    // In reduce mode evaluate the key against `@` with tuple keys
3673                    // bound; otherwise against the item itself.
3674                    let (key_data, tuple_saved) = match (reduce, item) {
3675                        (true, JValue::Object(o)) => {
3676                            let saved = bind_tuple(self, o);
3677                            (
3678                                o.get("@").cloned().unwrap_or(JValue::Undefined),
3679                                Some(saved),
3680                            )
3681                        }
3682                        _ => (item.clone(), None),
3683                    };
3684                    self.context.bind("$".to_string(), key_data.clone());
3685
3686                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3687                        // Evaluate key with current item as context
3688                        let key = match self.evaluate_internal(key_node, &key_data)? {
3689                            JValue::String(s) => s,
3690                            JValue::Null => continue, // Skip null keys
3691                            other => {
3692                                // Skip undefined keys
3693                                if other.is_undefined() {
3694                                    continue;
3695                                }
3696                                if let Some(saved) = tuple_saved {
3697                                    restore(self, saved);
3698                                }
3699                                return Err(EvaluatorError::TypeError(format!(
3700                                    "T1003: Object key must be a string, got: {:?}",
3701                                    other
3702                                )));
3703                            }
3704                        };
3705
3706                        // Group items by key
3707                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3708                            // Key already exists - check if from same expression index
3709                            if *existing_idx != pair_index {
3710                                if let Some(saved) = tuple_saved {
3711                                    restore(self, saved);
3712                                }
3713                                // D1009: multiple key expressions evaluate to same key
3714                                return Err(EvaluatorError::EvaluationError(format!(
3715                                    "D1009: Multiple key expressions evaluate to same key: {}",
3716                                    key
3717                                )));
3718                            }
3719                            // Append item to the group
3720                            existing_data.push(item.clone());
3721                        } else {
3722                            // New key - create new group
3723                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3724                        }
3725                    }
3726
3727                    if let Some(saved) = tuple_saved {
3728                        restore(self, saved);
3729                    }
3730                }
3731
3732                // Phase 2: Evaluate value expression for each group
3733                let mut result = IndexMap::new();
3734
3735                for (key, (grouped_data, expr_index)) in groups {
3736                    // Get the value expression for this group
3737                    let (_key_node, value_node) = &pattern[expr_index];
3738
3739                    if reduce {
3740                        // Reduce the grouped tuples into one (per-key values
3741                        // appended), mirroring jsonata-js reduceTupleStream, then
3742                        // evaluate the value against the merged `@` with the merged
3743                        // focus/index/ancestor keys bound.
3744                        let merged = reduce_tuple_stream(&grouped_data);
3745                        let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3746                        let mut tuple_no_at = merged.clone();
3747                        tuple_no_at.shift_remove("@");
3748                        let saved = bind_tuple(self, &tuple_no_at);
3749                        self.context.bind("$".to_string(), context.clone());
3750                        let value = self.evaluate_internal(value_node, &context);
3751                        restore(self, saved);
3752                        let value = value?;
3753                        if !value.is_undefined() {
3754                            result.insert(key, value);
3755                        }
3756                        continue;
3757                    }
3758
3759                    // Determine the context for value evaluation:
3760                    // - If single item, use that item directly
3761                    // - If multiple items, use the array of items
3762                    let context = if grouped_data.len() == 1 {
3763                        grouped_data.into_iter().next().unwrap()
3764                    } else {
3765                        JValue::array(grouped_data)
3766                    };
3767
3768                    // Bind $ to the context for value evaluation
3769                    self.context.bind("$".to_string(), context.clone());
3770
3771                    // Evaluate value expression with grouped context
3772                    let value = self.evaluate_internal(value_node, &context)?;
3773
3774                    // Skip undefined values
3775                    if !value.is_undefined() {
3776                        result.insert(key, value);
3777                    }
3778                }
3779
3780                // Restore the previous $ binding
3781                if let Some(saved) = saved_dollar {
3782                    self.context.bind("$".to_string(), saved);
3783                } else {
3784                    self.context.unbind("$");
3785                }
3786
3787                Ok(JValue::object(result))
3788            }
3789
3790            AstNode::Function {
3791                name,
3792                args,
3793                is_builtin,
3794            } => self.evaluate_function_call(name, args, *is_builtin, data),
3795
3796            // Call: invoke an arbitrary expression as a function
3797            // Used for IIFE patterns like (function($x){...})(5) or chained calls
3798            AstNode::Call { procedure, args } => {
3799                // Evaluate the procedure to get the callable value
3800                let callable = self.evaluate_internal(procedure, data)?;
3801
3802                // Check if it's a lambda value
3803                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3804                    let mut evaluated_args = Vec::with_capacity(args.len());
3805                    for arg in args.iter() {
3806                        evaluated_args.push(self.evaluate_internal(arg, data)?);
3807                    }
3808                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3809                }
3810
3811                // Not a callable value
3812                Err(EvaluatorError::TypeError(format!(
3813                    "Cannot call non-function value: {:?}",
3814                    callable
3815                )))
3816            }
3817
3818            AstNode::Conditional {
3819                condition,
3820                then_branch,
3821                else_branch,
3822            } => {
3823                let condition_value = self.evaluate_internal(condition, data)?;
3824                if self.is_truthy(&condition_value) {
3825                    self.evaluate_internal(then_branch, data)
3826                } else if let Some(else_branch) = else_branch {
3827                    self.evaluate_internal(else_branch, data)
3828                } else {
3829                    // No else branch - return undefined (not null)
3830                    // This allows $map to filter out results from conditionals without else
3831                    Ok(JValue::Undefined)
3832                }
3833            }
3834
3835            AstNode::Block(expressions) => {
3836                // Blocks create a new scope - push scope instead of clone/restore
3837                self.context.push_scope();
3838
3839                let mut result = JValue::Null;
3840                for expr in expressions {
3841                    result = self.evaluate_internal(expr, data)?;
3842                }
3843
3844                // Before popping, preserve any lambdas referenced by the result
3845                // This is essential for closures returned from blocks (IIFE pattern)
3846                let lambdas_to_keep = self.extract_lambda_ids(&result);
3847                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3848
3849                Ok(result)
3850            }
3851
3852            // Lambda: capture current environment for closure support
3853            AstNode::Lambda {
3854                params,
3855                body,
3856                signature,
3857                thunk,
3858            } => {
3859                let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
3860
3861                let compiled_body = if !thunk {
3862                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3863                    try_compile_expr_with_allowed_vars(body, &var_refs)
3864                } else {
3865                    None
3866                };
3867                let stored_lambda = StoredLambda {
3868                    params: params.clone(),
3869                    body: (**body).clone(),
3870                    compiled_body,
3871                    signature: signature.clone(),
3872                    captured_env: self.capture_environment_for(body, params),
3873                    captured_data: Some(data.clone()),
3874                    thunk: *thunk,
3875                };
3876                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3877
3878                let lambda_obj = JValue::lambda(
3879                    lambda_id.as_str(),
3880                    params.clone(),
3881                    None::<String>,
3882                    signature.clone(),
3883                );
3884
3885                Ok(lambda_obj)
3886            }
3887
3888            // Wildcard: collect all values from current object
3889            AstNode::Wildcard => {
3890                match data {
3891                    JValue::Object(obj) => {
3892                        let mut result = Vec::new();
3893                        for value in obj.values() {
3894                            // Flatten arrays into the result
3895                            match value {
3896                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3897                                _ => result.push(value.clone()),
3898                            }
3899                        }
3900                        check_sequence_length(result.len(), &self.options)?;
3901                        Ok(JValue::array(result))
3902                    }
3903                    JValue::Array(arr) => {
3904                        // For arrays, wildcard returns all elements
3905                        Ok(JValue::Array(arr.clone()))
3906                    }
3907                    _ => Ok(JValue::Null),
3908                }
3909            }
3910
3911            // Descendant: recursively traverse all nested values
3912            AstNode::Descendant => {
3913                let descendants = self.collect_descendants(data);
3914                if descendants.is_empty() {
3915                    Ok(JValue::Null) // No descendants means undefined
3916                } else {
3917                    check_sequence_length(descendants.len(), &self.options)?;
3918                    Ok(JValue::array(descendants))
3919                }
3920            }
3921
3922            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3923                "Predicate can only be used in path expressions".to_string(),
3924            )),
3925
3926            // Array grouping: same as Array but prevents flattening in path contexts
3927            AstNode::ArrayGroup(elements) => {
3928                let mut result = Vec::new();
3929                for element in elements {
3930                    let value = self.evaluate_internal(element, data)?;
3931                    result.push(value);
3932                }
3933                Ok(JValue::array(result))
3934            }
3935
3936            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3937                "Function application can only be used in path expressions".to_string(),
3938            )),
3939
3940            AstNode::Sort { input, terms } => {
3941                // Keep the input path's tuple wrappers so the sort terms can read
3942                // the carried `%`/`$focus`/`$index` bindings per element.
3943                let saved = self.keep_tuple_stream;
3944                self.keep_tuple_stream = true;
3945                let value = self.evaluate_internal(input, data);
3946                self.keep_tuple_stream = saved;
3947                self.evaluate_sort(&value?, terms)
3948            }
3949
3950            // Transform: |location|update[,delete]|
3951            AstNode::Transform {
3952                location,
3953                update,
3954                delete,
3955            } => {
3956                // Check if $ is bound (meaning we're being invoked as a lambda)
3957                if self.context.lookup("$").is_some() {
3958                    // Execute the transformation
3959                    self.execute_transform(location, update, delete.as_deref(), data)
3960                } else {
3961                    // Return a lambda representation
3962                    // The transform will be executed when the lambda is invoked
3963                    let transform_lambda = StoredLambda {
3964                        params: vec!["$".to_string()],
3965                        body: AstNode::Transform {
3966                            location: location.clone(),
3967                            update: update.clone(),
3968                            delete: delete.clone(),
3969                        },
3970                        compiled_body: None, // Transform is not a pure compilable expr
3971                        signature: None,
3972                        captured_env: HashMap::new(),
3973                        captured_data: None, // Transform takes $ as parameter
3974                        thunk: false,
3975                    };
3976
3977                    // Store with a generated unique name
3978                    let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
3979                    self.context.bind_lambda(lambda_name, transform_lambda);
3980
3981                    // Return lambda marker
3982                    Ok(JValue::string("<lambda>"))
3983                }
3984            }
3985
3986            // Parent-reference operator (%): ast_transform has already resolved
3987            // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
3988            // tuple step binds that label into scope (create_tuple_stream +
3989            // needs_tuple_context_binding), so resolving it is an ordinary scope
3990            // lookup, mirroring jsonata-js's
3991            // `case 'parent': result = environment.lookup(expr.slot.label);`.
3992            AstNode::Parent(label) => {
3993                if let Some(v) = self.context.lookup(label) {
3994                    return Ok(v.clone());
3995                }
3996                // Fall back to the tuple wrapper carried as `data`: a `%` used
3997                // inside a predicate/stage over a tuple stream -- e.g.
3998                // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
3999                // predicate is evaluated per tuple with the wrapper as data --
4000                // reads its ancestor from the tuple's `!label` key, which isn't
4001                // separately bound into scope here (mirrors AstNode::Variable's
4002                // tuple-binding fallback below).
4003                if let JValue::Object(obj) = data {
4004                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4005                        if let Some(v) = obj.get(label) {
4006                            return Ok(v.clone());
4007                        }
4008                    }
4009                }
4010                Ok(JValue::Undefined)
4011            }
4012        }
4013    }
4014
4015    /// Apply stages (filters/predicates) to a value during field extraction
4016    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
4017    /// This matches the JavaScript reference where stages apply to sequences
4018    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
4019        // Wrap non-arrays in an array for filtering (JSONata semantics)
4020        let mut result = match value {
4021            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
4022            JValue::Array(_) => value,
4023            other => JValue::array(vec![other]),
4024        };
4025
4026        for stage in stages {
4027            match stage {
4028                Stage::Filter(predicate_expr) => {
4029                    // When applying stages, use stage-specific predicate logic
4030                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
4031                }
4032                // Positional index stages are meaningful only over a tuple stream
4033                // (they set a variable to each tuple's position); they are applied
4034                // in `create_tuple_stream`, not on a plain value sequence here.
4035                Stage::Index(_) => {}
4036            }
4037        }
4038        Ok(result)
4039    }
4040
4041    /// Check if an AST node is definitely a filter expression (comparison/logical)
4042    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
4043    fn is_filter_predicate(predicate: &AstNode) -> bool {
4044        match predicate {
4045            AstNode::Binary { op, .. } => matches!(
4046                op,
4047                BinaryOp::GreaterThan
4048                    | BinaryOp::GreaterThanOrEqual
4049                    | BinaryOp::LessThan
4050                    | BinaryOp::LessThanOrEqual
4051                    | BinaryOp::Equal
4052                    | BinaryOp::NotEqual
4053                    | BinaryOp::And
4054                    | BinaryOp::Or
4055                    | BinaryOp::In
4056            ),
4057            AstNode::Unary {
4058                op: crate::ast::UnaryOp::Not,
4059                ..
4060            } => true,
4061            _ => false,
4062        }
4063    }
4064
4065    /// Evaluate a predicate as a stage during field extraction
4066    /// This has different semantics than standalone predicates:
4067    /// - Maps index operations over arrays of extracted values
4068    fn evaluate_predicate_as_stage(
4069        &mut self,
4070        current: &JValue,
4071        predicate: &AstNode,
4072    ) -> Result<JValue, EvaluatorError> {
4073        // Special case: empty brackets [] (represented as Boolean(true))
4074        if matches!(predicate, AstNode::Boolean(true)) {
4075            return match current {
4076                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
4077                JValue::Null => Ok(JValue::Null),
4078                other => Ok(JValue::array(vec![other.clone()])),
4079            };
4080        }
4081
4082        match current {
4083            JValue::Array(arr) => {
4084                // For stages: if we have an array of values (from field extraction),
4085                // apply the predicate to each value if appropriate
4086
4087                // Check if predicate is a numeric index
4088                if let AstNode::Number(n) = predicate {
4089                    // Check if this is an array of arrays (extracted array fields)
4090                    let is_array_of_arrays =
4091                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
4092
4093                    if !is_array_of_arrays {
4094                        // Simple values: just index normally
4095                        return self.array_index(current, &JValue::Number(*n));
4096                    }
4097
4098                    // Array of arrays: map index access over each extracted array
4099                    let mut result = Vec::new();
4100                    for item in arr.iter() {
4101                        match item {
4102                            JValue::Array(_) => {
4103                                let indexed = self.array_index(item, &JValue::Number(*n))?;
4104                                if !indexed.is_null() && !indexed.is_undefined() {
4105                                    result.push(indexed);
4106                                }
4107                            }
4108                            _ => {
4109                                if *n == 0.0 {
4110                                    result.push(item.clone());
4111                                }
4112                            }
4113                        }
4114                    }
4115                    return Ok(JValue::array(result));
4116                }
4117
4118                // Short-circuit: if predicate is definitely a comparison/logical expression,
4119                // skip speculative numeric evaluation and go directly to filter logic
4120                if Self::is_filter_predicate(predicate) {
4121                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
4122                    if let Some(compiled) = try_compile_expr(predicate) {
4123                        let shape = arr.first().and_then(build_shape_cache);
4124                        let mut filtered = Vec::with_capacity(arr.len());
4125                        for item in arr.iter() {
4126                            let result = if let Some(ref s) = shape {
4127                                eval_compiled_shaped(
4128                                    &compiled,
4129                                    item,
4130                                    None,
4131                                    s,
4132                                    &self.options,
4133                                    self.start_time,
4134                                )?
4135                            } else {
4136                                eval_compiled(
4137                                    &compiled,
4138                                    item,
4139                                    None,
4140                                    &self.options,
4141                                    self.start_time,
4142                                )?
4143                            };
4144                            if compiled_is_truthy(&result) {
4145                                filtered.push(item.clone());
4146                            }
4147                        }
4148                        return Ok(JValue::array(filtered));
4149                    }
4150                    // Fallback: full AST evaluation
4151                    let mut filtered = Vec::new();
4152                    for item in arr.iter() {
4153                        let item_result = self.evaluate_internal(predicate, item)?;
4154                        if self.is_truthy(&item_result) {
4155                            filtered.push(item.clone());
4156                        }
4157                    }
4158                    return Ok(JValue::array(filtered));
4159                }
4160
4161                // Try to evaluate the predicate to see if it's a numeric index or array of indices
4162                // If evaluation succeeds and yields a number, use it as an index
4163                // If it yields an array of numbers, use them as multiple indices
4164                // If evaluation fails (e.g., comparison error), treat as filter
4165                match self.evaluate_internal(predicate, current) {
4166                    Ok(JValue::Number(n)) => {
4167                        let n_val = n;
4168                        let is_array_of_arrays =
4169                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
4170
4171                        if !is_array_of_arrays {
4172                            let pred_result = JValue::Number(n_val);
4173                            return self.array_index(current, &pred_result);
4174                        }
4175
4176                        // Array of arrays: map index access
4177                        let mut result = Vec::new();
4178                        let pred_result = JValue::Number(n_val);
4179                        for item in arr.iter() {
4180                            match item {
4181                                JValue::Array(_) => {
4182                                    let indexed = self.array_index(item, &pred_result)?;
4183                                    if !indexed.is_null() && !indexed.is_undefined() {
4184                                        result.push(indexed);
4185                                    }
4186                                }
4187                                _ => {
4188                                    if n_val == 0.0 {
4189                                        result.push(item.clone());
4190                                    }
4191                                }
4192                            }
4193                        }
4194                        return Ok(JValue::array(result));
4195                    }
4196                    Ok(JValue::Array(indices)) => {
4197                        // Array of values - could be indices or filter results
4198                        // Check if all values are numeric
4199                        let has_non_numeric =
4200                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
4201
4202                        if has_non_numeric {
4203                            // Non-numeric values - treat as filter, fall through
4204                        } else {
4205                            // All numeric - use as indices
4206                            let arr_len = arr.len() as i64;
4207                            let mut resolved_indices: Vec<i64> = indices
4208                                .iter()
4209                                .filter_map(|v| {
4210                                    if let JValue::Number(n) = v {
4211                                        let idx = *n as i64;
4212                                        // Resolve negative indices
4213                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
4214                                        // Only include valid indices
4215                                        if actual_idx >= 0 && actual_idx < arr_len {
4216                                            Some(actual_idx)
4217                                        } else {
4218                                            None
4219                                        }
4220                                    } else {
4221                                        None
4222                                    }
4223                                })
4224                                .collect();
4225
4226                            // Sort and deduplicate indices
4227                            resolved_indices.sort();
4228                            resolved_indices.dedup();
4229
4230                            // Select elements at each sorted index
4231                            let result: Vec<JValue> = resolved_indices
4232                                .iter()
4233                                .map(|&idx| arr[idx as usize].clone())
4234                                .collect();
4235
4236                            return Ok(JValue::array(result));
4237                        }
4238                    }
4239                    Ok(_) => {
4240                        // Evaluated successfully but not a number or array - might be a filter
4241                        // Fall through to filter logic
4242                    }
4243                    Err(_) => {
4244                        // Evaluation failed - it's likely a filter expression
4245                        // Fall through to filter logic
4246                    }
4247                }
4248
4249                // It's a filter expression
4250                let mut filtered = Vec::new();
4251                for item in arr.iter() {
4252                    let item_result = self.evaluate_internal(predicate, item)?;
4253                    if self.is_truthy(&item_result) {
4254                        filtered.push(item.clone());
4255                    }
4256                }
4257                Ok(JValue::array(filtered))
4258            }
4259            JValue::Null => {
4260                // Null: return null
4261                Ok(JValue::Null)
4262            }
4263            other => {
4264                // Non-array values: treat as single-element conceptual array
4265                // For numeric predicates: index 0 returns the value, other indices return null
4266                // For boolean predicates: if truthy, return value; if falsy, return null
4267
4268                // Check if predicate is a numeric index
4269                if let AstNode::Number(n) = predicate {
4270                    // Index 0 returns the value, other indices return null
4271                    if *n == 0.0 {
4272                        return Ok(other.clone());
4273                    } else {
4274                        return Ok(JValue::Null);
4275                    }
4276                }
4277
4278                // Try to evaluate the predicate to see if it's a numeric index
4279                match self.evaluate_internal(predicate, other) {
4280                    Ok(JValue::Number(n)) => {
4281                        // Index 0 returns the value, other indices return null
4282                        if n == 0.0 {
4283                            Ok(other.clone())
4284                        } else {
4285                            Ok(JValue::Null)
4286                        }
4287                    }
4288                    Ok(pred_result) => {
4289                        // Boolean filter: return value if truthy, null if falsy
4290                        if self.is_truthy(&pred_result) {
4291                            Ok(other.clone())
4292                        } else {
4293                            Ok(JValue::Null)
4294                        }
4295                    }
4296                    Err(e) => Err(e),
4297                }
4298            }
4299        }
4300    }
4301
4302    /// Evaluate a path expression (e.g., foo.bar.baz)
4303    fn evaluate_path(
4304        &mut self,
4305        steps: &[PathStep],
4306        data: &JValue,
4307    ) -> Result<JValue, EvaluatorError> {
4308        // Avoid cloning by using references and only cloning when necessary
4309        if steps.is_empty() {
4310            return Ok(data.clone());
4311        }
4312
4313        // Fast path: single field access on object
4314        // This is a very common pattern, so optimize it.
4315        // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
4316        // creation handled below.
4317        if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
4318            if let AstNode::Name(field_name) = &steps[0].node {
4319                return match data {
4320                    JValue::Object(obj) => {
4321                        // Check if this is a tuple - extract '@' value
4322                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4323                            if let Some(JValue::Object(inner)) = obj.get("@") {
4324                                Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
4325                            } else {
4326                                Ok(JValue::Undefined)
4327                            }
4328                        } else {
4329                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
4330                        }
4331                    }
4332                    JValue::Array(arr) => {
4333                        // Array mapping: extract field from each element
4334                        // Optimized: use references to access fields without cloning entire objects
4335                        // Check first element for tuple-ness (tuples are all-or-nothing)
4336                        let has_tuples = arr.first().is_some_and(|item| {
4337                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4338                        });
4339
4340                        if !has_tuples {
4341                            // Fast path: no tuples, just direct field lookups
4342                            let mut result = Vec::with_capacity(arr.len());
4343                            for item in arr.iter() {
4344                                if let JValue::Object(obj) = item {
4345                                    if let Some(val) = obj.get(field_name) {
4346                                        if !val.is_null() {
4347                                            match val {
4348                                                JValue::Array(arr_val) => {
4349                                                    result.extend(arr_val.iter().cloned());
4350                                                }
4351                                                other => result.push(other.clone()),
4352                                            }
4353                                        }
4354                                    }
4355                                } else if let JValue::Array(inner_arr) = item {
4356                                    let nested_result = self.evaluate_path(
4357                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
4358                                        &JValue::Array(inner_arr.clone()),
4359                                    )?;
4360                                    match nested_result {
4361                                        JValue::Array(nested) => {
4362                                            result.extend(nested.iter().cloned());
4363                                        }
4364                                        JValue::Null => {}
4365                                        other => result.push(other),
4366                                    }
4367                                }
4368                            }
4369
4370                            if result.is_empty() {
4371                                Ok(JValue::Null)
4372                            } else if result.len() == 1 {
4373                                Ok(result.into_iter().next().unwrap())
4374                            } else {
4375                                check_sequence_length(result.len(), &self.options)?;
4376                                Ok(JValue::array(result))
4377                            }
4378                        } else {
4379                            // Tuple path: per-element tuple handling
4380                            let mut result = Vec::new();
4381                            for item in arr.iter() {
4382                                match item {
4383                                    JValue::Object(obj) => {
4384                                        let is_tuple =
4385                                            obj.get("__tuple__") == Some(&JValue::Bool(true));
4386
4387                                        if is_tuple {
4388                                            let inner = match obj.get("@") {
4389                                                Some(JValue::Object(inner)) => inner,
4390                                                _ => continue,
4391                                            };
4392
4393                                            if let Some(val) = inner.get(field_name) {
4394                                                if !val.is_null() {
4395                                                    // Build tuple wrapper - only clone bindings when needed
4396                                                    let wrap = |v: JValue| -> JValue {
4397                                                        let mut wrapper = IndexMap::new();
4398                                                        wrapper.insert("@".to_string(), v);
4399                                                        wrapper.insert(
4400                                                            "__tuple__".to_string(),
4401                                                            JValue::Bool(true),
4402                                                        );
4403                                                        for (k, v) in obj.iter() {
4404                                                            if k.starts_with('$') {
4405                                                                wrapper
4406                                                                    .insert(k.clone(), v.clone());
4407                                                            }
4408                                                        }
4409                                                        JValue::object(wrapper)
4410                                                    };
4411
4412                                                    match val {
4413                                                        JValue::Array(arr_val) => {
4414                                                            for item in arr_val.iter() {
4415                                                                result.push(wrap(item.clone()));
4416                                                            }
4417                                                        }
4418                                                        other => result.push(wrap(other.clone())),
4419                                                    }
4420                                                }
4421                                            }
4422                                        } else {
4423                                            // Non-tuple: access field directly by reference, only clone the field value
4424                                            if let Some(val) = obj.get(field_name) {
4425                                                if !val.is_null() {
4426                                                    match val {
4427                                                        JValue::Array(arr_val) => {
4428                                                            for item in arr_val.iter() {
4429                                                                result.push(item.clone());
4430                                                            }
4431                                                        }
4432                                                        other => result.push(other.clone()),
4433                                                    }
4434                                                }
4435                                            }
4436                                        }
4437                                    }
4438                                    JValue::Array(inner_arr) => {
4439                                        // Recursively map over nested array
4440                                        let nested_result = self.evaluate_path(
4441                                            &[PathStep::new(AstNode::Name(field_name.clone()))],
4442                                            &JValue::Array(inner_arr.clone()),
4443                                        )?;
4444                                        // Add nested result to our results
4445                                        match nested_result {
4446                                            JValue::Array(nested) => {
4447                                                // Flatten nested arrays from recursive mapping
4448                                                result.extend(nested.iter().cloned());
4449                                            }
4450                                            JValue::Null => {} // Skip nulls from nested arrays
4451                                            other => result.push(other),
4452                                        }
4453                                    }
4454                                    _ => {} // Skip non-object items
4455                                }
4456                            }
4457
4458                            // Return array result
4459                            // JSONata singleton unwrapping: if we have exactly one result,
4460                            // unwrap it (even if it's an array)
4461                            if result.is_empty() {
4462                                Ok(JValue::Null)
4463                            } else if result.len() == 1 {
4464                                Ok(result.into_iter().next().unwrap())
4465                            } else {
4466                                check_sequence_length(result.len(), &self.options)?;
4467                                Ok(JValue::array(result))
4468                            }
4469                        } // end else (tuple path)
4470                    }
4471                    _ => Ok(JValue::Undefined),
4472                };
4473            }
4474        }
4475
4476        // Fast path: 2-step $variable.field with no stages
4477        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4478        if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4479            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4480                (&steps[0].node, &steps[1].node)
4481            {
4482                if !var_name.is_empty() {
4483                    if let Some(value) = self.context.lookup(var_name) {
4484                        match value {
4485                            JValue::Object(obj) => {
4486                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4487                            }
4488                            JValue::Array(arr) => {
4489                                // Map field extraction over array (same as single-step Name on Array)
4490                                let mut result = Vec::with_capacity(arr.len());
4491                                for item in arr.iter() {
4492                                    if let JValue::Object(obj) = item {
4493                                        if let Some(val) = obj.get(field_name) {
4494                                            if !val.is_null() {
4495                                                match val {
4496                                                    JValue::Array(inner) => {
4497                                                        result.extend(inner.iter().cloned());
4498                                                    }
4499                                                    other => result.push(other.clone()),
4500                                                }
4501                                            }
4502                                        }
4503                                    }
4504                                }
4505                                return match result.len() {
4506                                    0 => Ok(JValue::Null),
4507                                    1 => Ok(result.pop().unwrap()),
4508                                    _ => {
4509                                        check_sequence_length(result.len(), &self.options)?;
4510                                        Ok(JValue::array(result))
4511                                    }
4512                                };
4513                            }
4514                            _ => {} // Fall through to general path evaluation
4515                        }
4516                    }
4517                }
4518            }
4519        }
4520
4521        // Track whether we did array mapping (for singleton unwrapping)
4522        let mut did_array_mapping = false;
4523
4524        // For the first step, work with a reference.
4525        // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4526        // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4527        // first path step where tupleBindings is undefined.
4528        let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4529            JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4530        } else {
4531            match &steps[0].node {
4532                AstNode::Wildcard => {
4533                    // Wildcard as first step
4534                    match data {
4535                        JValue::Object(obj) => {
4536                            let mut result = Vec::new();
4537                            for value in obj.values() {
4538                                // Flatten arrays into the result
4539                                match value {
4540                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4541                                    _ => result.push(value.clone()),
4542                                }
4543                            }
4544                            JValue::array(result)
4545                        }
4546                        JValue::Array(arr) => JValue::Array(arr.clone()),
4547                        _ => JValue::Null,
4548                    }
4549                }
4550                AstNode::Descendant => {
4551                    // Descendant as first step
4552                    let descendants = self.collect_descendants(data);
4553                    JValue::array(descendants)
4554                }
4555                AstNode::ParentVariable(name) => {
4556                    // Parent variable as first step
4557                    let parent_data = self.context.get_parent().ok_or_else(|| {
4558                        EvaluatorError::ReferenceError("Parent context not available".to_string())
4559                    })?;
4560
4561                    if name.is_empty() {
4562                        // $$ alone returns parent context
4563                        parent_data.clone()
4564                    } else {
4565                        // $$field accesses field on parent
4566                        match parent_data {
4567                            JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4568                            _ => JValue::Null,
4569                        }
4570                    }
4571                }
4572                AstNode::Name(field_name) => {
4573                    // Field/property access - get the stages for this step
4574                    let stages = &steps[0].stages;
4575
4576                    match data {
4577                        JValue::Object(obj) => {
4578                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4579                            // Apply any stages to the extracted value
4580                            if !stages.is_empty() {
4581                                self.apply_stages(val, stages)?
4582                            } else {
4583                                val
4584                            }
4585                        }
4586                        JValue::Array(arr) => {
4587                            // Array mapping: extract field from each element and apply stages
4588                            let mut result = Vec::new();
4589                            for item in arr.iter() {
4590                                match item {
4591                                    JValue::Object(obj) => {
4592                                        let val = obj
4593                                            .get(field_name)
4594                                            .cloned()
4595                                            .unwrap_or(JValue::Undefined);
4596                                        if !val.is_null() && !val.is_undefined() {
4597                                            if !stages.is_empty() {
4598                                                // Apply stages to the extracted value
4599                                                let processed_val =
4600                                                    self.apply_stages(val, stages)?;
4601                                                // Stages always return an array (or null); extend results
4602                                                match processed_val {
4603                                                    JValue::Array(arr) => {
4604                                                        result.extend(arr.iter().cloned())
4605                                                    }
4606                                                    JValue::Null => {} // Skip nulls from stage application
4607                                                    other => result.push(other), // Shouldn't happen, but handle it
4608                                                }
4609                                            } else {
4610                                                // No stages: flatten arrays, push scalars
4611                                                match val {
4612                                                    JValue::Array(arr) => {
4613                                                        result.extend(arr.iter().cloned())
4614                                                    }
4615                                                    other => result.push(other),
4616                                                }
4617                                            }
4618                                        }
4619                                    }
4620                                    JValue::Array(inner_arr) => {
4621                                        // Recursively map over nested array
4622                                        let nested_result = self.evaluate_path(
4623                                            &[steps[0].clone()],
4624                                            &JValue::Array(inner_arr.clone()),
4625                                        )?;
4626                                        match nested_result {
4627                                            JValue::Array(nested) => {
4628                                                result.extend(nested.iter().cloned())
4629                                            }
4630                                            JValue::Null => {} // Skip nulls from nested arrays
4631                                            other => result.push(other),
4632                                        }
4633                                    }
4634                                    _ => {} // Skip non-object items
4635                                }
4636                            }
4637                            JValue::array(result)
4638                        }
4639                        JValue::Null => JValue::Null,
4640                        // Accessing field on non-object returns undefined (not an error)
4641                        _ => JValue::Undefined,
4642                    }
4643                }
4644                AstNode::String(string_literal) => {
4645                    // String literal in path context - evaluate as literal and apply stages
4646                    // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4647                    let stages = &steps[0].stages;
4648                    let val = JValue::string(string_literal.clone());
4649
4650                    if !stages.is_empty() {
4651                        // Apply stages (predicates) to the string literal
4652                        let result = self.apply_stages(val, stages)?;
4653                        // Unwrap single-element arrays back to scalar
4654                        // (string literals with predicates should return scalar or null, not arrays)
4655                        match result {
4656                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4657                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4658                            other => other,
4659                        }
4660                    } else {
4661                        val
4662                    }
4663                }
4664                AstNode::Predicate(pred_expr) => {
4665                    // Predicate as first step
4666                    self.evaluate_predicate(data, pred_expr)?
4667                }
4668                _ => {
4669                    // Complex first step - evaluate it. When the step is
4670                    // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4671                    // whose `Product` is `%`-tagged, as in
4672                    // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4673                    // inner path's tuple wrappers so the following predicate/step
4674                    // can read the `!label` bindings.
4675                    let saved_keep = self.keep_tuple_stream;
4676                    if steps[0].is_tuple {
4677                        self.keep_tuple_stream = true;
4678                    }
4679                    let v = self.evaluate_path_step(&steps[0].node, data, data);
4680                    self.keep_tuple_stream = saved_keep;
4681                    v?
4682                }
4683            }
4684        };
4685
4686        // Process remaining steps
4687        for (step_idx, step) in steps[1..].iter().enumerate() {
4688            let is_last_step = step_idx == steps.len() - 2;
4689            // Early return if current is null/undefined - no point continuing
4690            // This handles cases like `blah.{}` where blah doesn't exist
4691            if current.is_null() {
4692                return Ok(JValue::Null);
4693            }
4694            if current.is_undefined() {
4695                return Ok(JValue::Undefined);
4696            }
4697
4698            // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4699            // a tuple stream, which selects a single tuple and unwraps it out of
4700            // the array) must stay a tuple stream so the following step keeps
4701            // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4702            // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4703            if let JValue::Object(o) = &current {
4704                if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4705                    current = JValue::array(vec![current.clone()]);
4706                    // The lone wrapper came from a singleton index selection, so
4707                    // the final result should unwrap back to a scalar (a following
4708                    // object step must not leave a spurious 1-element array).
4709                    did_array_mapping = true;
4710                }
4711            }
4712
4713            // Check if current is a tuple array - if so, we need to bind tuple variables
4714            // to context so they're available in nested expressions (like predicates)
4715            let is_tuple_array = if let JValue::Array(arr) = &current {
4716                arr.first().is_some_and(|first| {
4717                    if let JValue::Object(obj) = first {
4718                        obj.get("__tuple__") == Some(&JValue::Bool(true))
4719                    } else {
4720                        false
4721                    }
4722                })
4723            } else {
4724                false
4725            };
4726
4727            // Tuple-binding step (@ focus / # index / % parent): create/extend the
4728            // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4729            // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4730            // wrappers via the existing tuple-aware handling below.
4731            //
4732            // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
4733            // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
4734            // stream, but ONLY when it is consuming an existing tuple stream:
4735            // its ancestor label lives in those incoming tuples, so
4736            // create_tuple_stream's per-tuple frame binding is what lets
4737            // `evaluate_internal(Parent, ..)` resolve it (and any predicate
4738            // stage on the `%` step then resolves in the same frame). A `%`
4739            // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
4740            // predicate, whose input is plain data, not a tuple stream) must
4741            // NOT be routed here -- it's an ordinary scope lookup.
4742            let is_parent_step_over_tuple =
4743                matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
4744            if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
4745                current = JValue::array(self.create_tuple_stream(step, &current, false)?);
4746                continue;
4747            }
4748
4749            // For tuple arrays with certain step types, we need special handling to bind
4750            // tuple variables to context so they're available in nested expressions.
4751            // This is needed for:
4752            // - Object constructors: {"label": $$.items[$i]} needs $i in context
4753            // - Function applications: .($$.items[$i]) needs $i in context
4754            // - Variable lookups: .$i needs to find the tuple binding
4755            //
4756            // Steps like Name (field access) already have proper tuple handling in their
4757            // specific cases, so we don't intercept those here.
4758            let needs_tuple_context_binding = is_tuple_array
4759                && matches!(
4760                    &step.node,
4761                    AstNode::Object(_)
4762                        | AstNode::FunctionApplication(_)
4763                        | AstNode::Variable(_)
4764                        | AstNode::ArrayGroup(_)
4765                );
4766
4767            if needs_tuple_context_binding {
4768                if let JValue::Array(arr) = &current {
4769                    let mut results = Vec::new();
4770
4771                    for tuple in arr.iter() {
4772                        if let JValue::Object(tuple_obj) = tuple {
4773                            // Extract tuple bindings so nested expressions can see
4774                            // them: `$var` focus/index bindings (stored `$name`,
4775                            // bound as `name`) AND `!label` ancestor bindings for
4776                            // `%` (stored and bound under the full `!label` key).
4777                            // Saves/restores rather than blindly unbinding, so a
4778                            // tuple key that collides with a live outer `:=`
4779                            // binding doesn't get deleted afterward.
4780                            let tuple_bindings = self.bind_tuple_keys(tuple_obj);
4781
4782                            // Get the actual value from the tuple (@ field)
4783                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4784
4785                            // Evaluate the step
4786                            let step_result = match &step.node {
4787                                AstNode::Variable(_) => {
4788                                    // Variable lookup - check context (which now has bindings)
4789                                    self.evaluate_internal(&step.node, tuple)?
4790                                }
4791                                AstNode::Object(_) | AstNode::ArrayGroup(_) => {
4792                                    // Object / array constructor step (e.g.
4793                                    // `Product.[`Product Name`, %.OrderID]`) -
4794                                    // evaluate on the tuple's `@` value with the
4795                                    // carried `!label`/`$focus` bindings in scope
4796                                    // so an embedded `%` resolves.
4797                                    self.evaluate_internal(&step.node, &actual_data)?
4798                                }
4799                                AstNode::FunctionApplication(inner) => {
4800                                    // A parenthesized step `(expr)` consuming a tuple stream
4801                                    // (e.g. `Account.Order.Product.( %.OrderID )` or
4802                                    // `Employee@$e.(Contact)[...]`): evaluate the INNER
4803                                    // expression on the tuple's `@` value with `$` bound to
4804                                    // it, mirroring the non-tuple FunctionApplication step
4805                                    // handling. Routing the wrapper node itself through
4806                                    // evaluate_internal raises "Function application can only
4807                                    // be used in path expressions".
4808                                    let saved_dollar = self.context.lookup("$").cloned();
4809                                    self.context.bind("$".to_string(), actual_data.clone());
4810                                    // Keep tuple wrappers from the inner path alive:
4811                                    // when `inner` is itself a tuple-carrying path
4812                                    // (e.g. `(Order.Product)` whose `Product` is
4813                                    // `%`-tagged), its `!label` wrappers must survive
4814                                    // to be merged into this tuple by the rewrap below
4815                                    // (they feed a later `%`/`%.%`). Without this the
4816                                    // inner path projects to `@` and drops the labels.
4817                                    let saved_keep = self.keep_tuple_stream;
4818                                    self.keep_tuple_stream = true;
4819                                    let v = self.evaluate_internal(inner, &actual_data);
4820                                    self.keep_tuple_stream = saved_keep;
4821                                    match saved_dollar {
4822                                        Some(s) => self.context.bind("$".to_string(), s),
4823                                        None => self.context.unbind("$"),
4824                                    }
4825                                    v?
4826                                }
4827                                _ => unreachable!(), // We only match specific types above
4828                            };
4829
4830                            // Apply this step's own filter stages (e.g. the
4831                            // `[$substring(title,0,3)='The']` on `.$[...]` in
4832                            // `library.books#$pos.$[...].$pos`) while the tuple
4833                            // bindings are still in scope, so the predicate can
4834                            // reference them and non-matching tuples are dropped.
4835                            let step_result = if step.stages.is_empty() {
4836                                step_result
4837                            } else {
4838                                self.apply_stages(step_result, &step.stages)?
4839                            };
4840
4841                            // Restore previous bindings
4842                            tuple_bindings.restore(self);
4843
4844                            // Rewrap results as tuples carrying this incoming
4845                            // tuple's focus/index/ancestor bindings, so that
4846                            // DOWNSTREAM steps keep seeing them: a predicate like
4847                            // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
4848                            // later `%`/`%.%` in `Account.Order.(Product).{...}`,
4849                            // or a further path step all read those bindings from
4850                            // the tuple wrapper (see AstNode::Variable's tuple
4851                            // fallback). Without rewrapping, the tuple chain is
4852                            // severed after a parenthesized/object/variable step
4853                            // and those references resolve to nothing. The
4854                            // wrappers are projected back to their `@` values by
4855                            // the top-level `unwrap_tuple_output` pass.
4856                            let carried: Vec<(String, JValue)> = tuple_obj
4857                                .iter()
4858                                .filter(|(k, _)| {
4859                                    (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
4860                                })
4861                                .map(|(k, v)| (k.clone(), v.clone()))
4862                                .collect();
4863                            let wrap = |v: JValue| -> JValue {
4864                                match v {
4865                                    // If the step produced a nested tuple stream
4866                                    // (e.g. `(Product)` whose inner `Product` is
4867                                    // itself `%`-tagged), MERGE the inner tuple's
4868                                    // keys over the carried outer bindings, mirroring
4869                                    // jsonata-js's `res.tupleStream` branch
4870                                    // (`Object.assign(tuple, res[bb])`) -- do NOT
4871                                    // double-wrap, which would bury `@`/`!label`
4872                                    // one level down and break a following `%`/`%.%`.
4873                                    JValue::Object(inner)
4874                                        if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
4875                                    {
4876                                        let mut w = IndexMap::new();
4877                                        for (k, val) in &carried {
4878                                            w.insert(k.clone(), val.clone());
4879                                        }
4880                                        for (k, val) in inner.iter() {
4881                                            w.insert(k.clone(), val.clone());
4882                                        }
4883                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4884                                        JValue::object(w)
4885                                    }
4886                                    other => {
4887                                        let mut w = IndexMap::new();
4888                                        w.insert("@".to_string(), other);
4889                                        for (k, val) in &carried {
4890                                            w.insert(k.clone(), val.clone());
4891                                        }
4892                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4893                                        JValue::object(w)
4894                                    }
4895                                }
4896                            };
4897                            if !step_result.is_null() && !step_result.is_undefined() {
4898                                // Object constructors yield one value per tuple;
4899                                // other steps may yield an array to splice in.
4900                                if matches!(&step.node, AstNode::Object(_)) {
4901                                    results.push(wrap(step_result));
4902                                } else if let JValue::Array(arr) = step_result {
4903                                    for it in arr.iter() {
4904                                        results.push(wrap(it.clone()));
4905                                    }
4906                                } else {
4907                                    results.push(wrap(step_result));
4908                                }
4909                            }
4910                        }
4911                    }
4912
4913                    current = JValue::array(results);
4914                    continue; // Skip the regular step processing
4915                }
4916            }
4917
4918            current = match &step.node {
4919                AstNode::Wildcard => {
4920                    // Wildcard in path
4921                    let stages = &step.stages;
4922                    let wildcard_result = match &current {
4923                        JValue::Object(obj) => {
4924                            let mut result = Vec::new();
4925                            for value in obj.values() {
4926                                // Flatten arrays into the result
4927                                match value {
4928                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4929                                    _ => result.push(value.clone()),
4930                                }
4931                            }
4932                            JValue::array(result)
4933                        }
4934                        JValue::Array(arr) => {
4935                            // Map wildcard over array
4936                            let mut all_values = Vec::new();
4937                            for item in arr.iter() {
4938                                match item {
4939                                    JValue::Object(obj) => {
4940                                        for value in obj.values() {
4941                                            // Flatten arrays
4942                                            match value {
4943                                                JValue::Array(arr) => {
4944                                                    all_values.extend(arr.iter().cloned())
4945                                                }
4946                                                _ => all_values.push(value.clone()),
4947                                            }
4948                                        }
4949                                    }
4950                                    JValue::Array(inner) => {
4951                                        all_values.extend(inner.iter().cloned());
4952                                    }
4953                                    _ => {}
4954                                }
4955                            }
4956                            JValue::array(all_values)
4957                        }
4958                        _ => JValue::Null,
4959                    };
4960
4961                    // Apply stages (predicates) if present
4962                    if !stages.is_empty() {
4963                        self.apply_stages(wildcard_result, stages)?
4964                    } else {
4965                        wildcard_result
4966                    }
4967                }
4968                AstNode::Descendant => {
4969                    // Descendant in path
4970                    match &current {
4971                        JValue::Array(arr) => {
4972                            // Collect descendants from all array elements
4973                            let mut all_descendants = Vec::new();
4974                            for item in arr.iter() {
4975                                all_descendants.extend(self.collect_descendants(item));
4976                            }
4977                            JValue::array(all_descendants)
4978                        }
4979                        _ => {
4980                            // Collect descendants from current value
4981                            let descendants = self.collect_descendants(&current);
4982                            JValue::array(descendants)
4983                        }
4984                    }
4985                }
4986                AstNode::Name(field_name) => {
4987                    // Navigate into object field or map over array, applying stages
4988                    let stages = &step.stages;
4989
4990                    match &current {
4991                        JValue::Object(obj) => {
4992                            // Single object field extraction - NOT array mapping
4993                            // This resets did_array_mapping because we're extracting from
4994                            // a single value, not mapping over an array. The field's value
4995                            // (even if it's an array) should be preserved as-is.
4996                            did_array_mapping = false;
4997                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4998                            // Apply stages if present
4999                            if !stages.is_empty() {
5000                                self.apply_stages(val, stages)?
5001                            } else {
5002                                val
5003                            }
5004                        }
5005                        JValue::Array(arr) => {
5006                            // Array mapping: extract field from each element and apply stages
5007                            did_array_mapping = true; // Track that we did array mapping
5008
5009                            // Fast path: if no elements are tuples and no stages,
5010                            // skip all tuple checking overhead (common case for products.price etc.)
5011                            // Tuples are all-or-nothing (created by index binding #$i),
5012                            // so checking only the first element is sufficient.
5013                            let has_tuples = arr.first().is_some_and(|item| {
5014                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
5015                            });
5016
5017                            if !has_tuples && stages.is_empty() {
5018                                let mut result = Vec::with_capacity(arr.len());
5019                                for item in arr.iter() {
5020                                    match item {
5021                                        JValue::Object(obj) => {
5022                                            if let Some(val) = obj.get(field_name) {
5023                                                if !val.is_null() {
5024                                                    match val {
5025                                                        JValue::Array(arr_val) => {
5026                                                            result.extend(arr_val.iter().cloned())
5027                                                        }
5028                                                        other => result.push(other.clone()),
5029                                                    }
5030                                                }
5031                                            }
5032                                        }
5033                                        JValue::Array(_) => {
5034                                            let nested_result =
5035                                                self.evaluate_path(&[step.clone()], item)?;
5036                                            match nested_result {
5037                                                JValue::Array(nested) => {
5038                                                    result.extend(nested.iter().cloned())
5039                                                }
5040                                                JValue::Null => {}
5041                                                other => result.push(other),
5042                                            }
5043                                        }
5044                                        _ => {}
5045                                    }
5046                                }
5047                                JValue::array(result)
5048                            } else {
5049                                // Full path with tuple support and stages
5050                                let mut result = Vec::new();
5051
5052                                for item in arr.iter() {
5053                                    match item {
5054                                        JValue::Object(obj) => {
5055                                            // Check if this is a tuple stream element
5056                                            let (actual_obj, tuple_bindings) = if obj
5057                                                .get("__tuple__")
5058                                                == Some(&JValue::Bool(true))
5059                                            {
5060                                                // This is a tuple - extract '@' value and preserve bindings
5061                                                if let Some(JValue::Object(inner)) = obj.get("@") {
5062                                                    // Collect index bindings (variables starting with $)
5063                                                    let bindings: Vec<(String, JValue)> = obj
5064                                                        .iter()
5065                                                        .filter(|(k, _)| k.starts_with('$'))
5066                                                        .map(|(k, v)| (k.clone(), v.clone()))
5067                                                        .collect();
5068                                                    (inner.clone(), Some(bindings))
5069                                                } else {
5070                                                    continue; // Invalid tuple
5071                                                }
5072                                            } else {
5073                                                (obj.clone(), None)
5074                                            };
5075
5076                                            let val = actual_obj
5077                                                .get(field_name)
5078                                                .cloned()
5079                                                .unwrap_or(JValue::Null);
5080
5081                                            if !val.is_null() {
5082                                                // Helper to wrap value in tuple if we have bindings
5083                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
5084                                                    if let Some(b) = bindings {
5085                                                        let mut wrapper = IndexMap::new();
5086                                                        wrapper.insert("@".to_string(), v);
5087                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5088                                                        for (k, val) in b {
5089                                                            wrapper.insert(k.clone(), val.clone());
5090                                                        }
5091                                                        JValue::object(wrapper)
5092                                                    } else {
5093                                                        v
5094                                                    }
5095                                                };
5096
5097                                                if !stages.is_empty() {
5098                                                    // Bind this tuple's carried focus/index/ancestor
5099                                                    // bindings so a filter predicate that references
5100                                                    // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
5101                                                    // where the `[$l.isbn=isbn]` stage on the (non-focus)
5102                                                    // `books` step must see `$l` from the enclosing
5103                                                    // `@$l` focus stream. Without this the predicate
5104                                                    // evaluates `$l` as unbound and filters everything out.
5105                                                    let saved_tuple: Vec<(String, Option<JValue>)> =
5106                                                        obj.iter()
5107                                                            .filter_map(|(k, _)| {
5108                                                                if let Some(n) = k.strip_prefix('$')
5109                                                                {
5110                                                                    (!n.is_empty())
5111                                                                        .then(|| n.to_string())
5112                                                                } else if k.starts_with('!') {
5113                                                                    Some(k.clone())
5114                                                                } else {
5115                                                                    None
5116                                                                }
5117                                                            })
5118                                                            .map(|n| {
5119                                                                (
5120                                                                    n.clone(),
5121                                                                    self.context
5122                                                                        .lookup(&n)
5123                                                                        .cloned(),
5124                                                                )
5125                                                            })
5126                                                            .collect();
5127                                                    for (k, v) in obj.iter() {
5128                                                        if let Some(n) = k.strip_prefix('$') {
5129                                                            if !n.is_empty() {
5130                                                                self.context
5131                                                                    .bind(n.to_string(), v.clone());
5132                                                            }
5133                                                        } else if k.starts_with('!') {
5134                                                            self.context.bind(k.clone(), v.clone());
5135                                                        }
5136                                                    }
5137                                                    // Apply stages to the extracted value
5138                                                    let processed_val =
5139                                                        self.apply_stages(val, stages);
5140                                                    for (n, old) in saved_tuple.into_iter().rev() {
5141                                                        match old {
5142                                                            Some(v) => self.context.bind(n, v),
5143                                                            None => self.context.unbind(&n),
5144                                                        }
5145                                                    }
5146                                                    let processed_val = processed_val?;
5147                                                    // Stages always return an array (or null); extend results
5148                                                    match processed_val {
5149                                                        JValue::Array(arr) => {
5150                                                            for item in arr.iter() {
5151                                                                result.push(wrap_in_tuple(
5152                                                                    item.clone(),
5153                                                                    &tuple_bindings,
5154                                                                ));
5155                                                            }
5156                                                        }
5157                                                        JValue::Null => {} // Skip nulls from stage application
5158                                                        other => result.push(wrap_in_tuple(
5159                                                            other,
5160                                                            &tuple_bindings,
5161                                                        )),
5162                                                    }
5163                                                } else {
5164                                                    // No stages: flatten arrays, push scalars
5165                                                    // But preserve tuple bindings!
5166                                                    match val {
5167                                                        JValue::Array(arr) => {
5168                                                            for item in arr.iter() {
5169                                                                result.push(wrap_in_tuple(
5170                                                                    item.clone(),
5171                                                                    &tuple_bindings,
5172                                                                ));
5173                                                            }
5174                                                        }
5175                                                        other => result.push(wrap_in_tuple(
5176                                                            other,
5177                                                            &tuple_bindings,
5178                                                        )),
5179                                                    }
5180                                                }
5181                                            }
5182                                        }
5183                                        JValue::Array(_) => {
5184                                            // Recursively map over nested array
5185                                            let nested_result =
5186                                                self.evaluate_path(&[step.clone()], item)?;
5187                                            match nested_result {
5188                                                JValue::Array(nested) => {
5189                                                    result.extend(nested.iter().cloned())
5190                                                }
5191                                                JValue::Null => {}
5192                                                other => result.push(other),
5193                                            }
5194                                        }
5195                                        _ => {}
5196                                    }
5197                                }
5198
5199                                JValue::array(result)
5200                            }
5201                        }
5202                        JValue::Null => JValue::Null,
5203                        // Accessing field on non-object returns undefined (not an error)
5204                        _ => JValue::Undefined,
5205                    }
5206                }
5207                AstNode::String(string_literal) => {
5208                    // String literal as a path step - evaluate as literal and apply stages
5209                    let stages = &step.stages;
5210                    let val = JValue::string(string_literal.clone());
5211
5212                    if !stages.is_empty() {
5213                        // Apply stages (predicates) to the string literal
5214                        let result = self.apply_stages(val, stages)?;
5215                        // Unwrap single-element arrays back to scalar
5216                        match result {
5217                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
5218                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
5219                            other => other,
5220                        }
5221                    } else {
5222                        val
5223                    }
5224                }
5225                AstNode::Predicate(pred_expr) => {
5226                    // Predicate in path - filter or index into current value
5227                    self.evaluate_predicate(&current, pred_expr)?
5228                }
5229                AstNode::ArrayGroup(elements) => {
5230                    // Array grouping: map expression over array but keep results grouped
5231                    // .[expr] means evaluate expr for each array element
5232                    match &current {
5233                        JValue::Array(arr) => {
5234                            let mut result = Vec::new();
5235                            for item in arr.iter() {
5236                                // For each array item, evaluate all elements and collect results
5237                                let mut group_values = Vec::new();
5238                                for element in elements {
5239                                    let value = self.evaluate_internal(element, item)?;
5240                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
5241                                    // This ensures [[expr]] produces properly nested arrays
5242                                    let should_preserve_array = matches!(
5243                                        element,
5244                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
5245                                    );
5246
5247                                    if should_preserve_array {
5248                                        // Keep the array as a single element to preserve nesting
5249                                        group_values.push(value);
5250                                    } else {
5251                                        // Flatten the value into group_values
5252                                        match value {
5253                                            JValue::Array(arr) => {
5254                                                group_values.extend(arr.iter().cloned())
5255                                            }
5256                                            other => group_values.push(other),
5257                                        }
5258                                    }
5259                                }
5260                                // Each array element gets its own sub-array with all values
5261                                result.push(JValue::array(group_values));
5262                            }
5263                            // jsonata-js's evaluateStep: when this is the path's last
5264                            // step and mapping produced exactly one constructed
5265                            // sub-array, that sub-array IS the path result directly
5266                            // (not wrapped in an outer singleton array) — e.g.
5267                            // `$.[value,epochSeconds]` over a 1-element array yields
5268                            // `[3, 1578381600]`, not `[[3, 1578381600]]`.
5269                            if is_last_step && result.len() == 1 {
5270                                result.into_iter().next().unwrap()
5271                            } else {
5272                                JValue::array(result)
5273                            }
5274                        }
5275                        _ => {
5276                            // For non-arrays, just evaluate the array constructor normally
5277                            let mut result = Vec::new();
5278                            for element in elements {
5279                                let value = self.evaluate_internal(element, &current)?;
5280                                result.push(value);
5281                            }
5282                            JValue::array(result)
5283                        }
5284                    }
5285                }
5286                AstNode::FunctionApplication(expr) => {
5287                    // Function application: map expr over the current value
5288                    // .(expr) means evaluate expr for each element, with $ bound to that element
5289                    // Null/undefined results are filtered out
5290                    //
5291                    // When this parenthesized step is itself tuple-carrying (its
5292                    // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
5293                    // keep the inner path's tuple wrappers so their `!label`
5294                    // bindings survive to the following object/`%` step; the
5295                    // end-of-path projection (or a later consumer) unwraps them.
5296                    let saved_keep = self.keep_tuple_stream;
5297                    if step.is_tuple {
5298                        self.keep_tuple_stream = true;
5299                    }
5300                    let fa_result = match &current {
5301                        JValue::Array(arr) => {
5302                            // Produce the mapped result (compiled fast path or tree-walker fallback).
5303                            // Do NOT return early — singleton unwrapping is applied by the outer
5304                            // path evaluation code after all steps are processed.
5305                            let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
5306                            {
5307                                let shape = arr.first().and_then(build_shape_cache);
5308                                let mut result = Vec::with_capacity(arr.len());
5309                                for item in arr.iter() {
5310                                    let value = if let Some(ref s) = shape {
5311                                        eval_compiled_shaped(
5312                                            &compiled,
5313                                            item,
5314                                            None,
5315                                            s,
5316                                            &self.options,
5317                                            self.start_time,
5318                                        )?
5319                                    } else {
5320                                        eval_compiled(
5321                                            &compiled,
5322                                            item,
5323                                            None,
5324                                            &self.options,
5325                                            self.start_time,
5326                                        )?
5327                                    };
5328                                    if !value.is_null() && !value.is_undefined() {
5329                                        result.push(value);
5330                                    }
5331                                }
5332                                result
5333                            } else {
5334                                let mut result = Vec::new();
5335                                for item in arr.iter() {
5336                                    // Save the current $ binding
5337                                    let saved_dollar = self.context.lookup("$").cloned();
5338
5339                                    // Bind $ to the current item
5340                                    self.context.bind("$".to_string(), item.clone());
5341
5342                                    // Evaluate the expression in the context of this item
5343                                    let value = self.evaluate_internal(expr, item)?;
5344
5345                                    // Restore the previous $ binding
5346                                    if let Some(saved) = saved_dollar {
5347                                        self.context.bind("$".to_string(), saved);
5348                                    } else {
5349                                        self.context.unbind("$");
5350                                    }
5351
5352                                    // Only include non-null/undefined values
5353                                    if !value.is_null() && !value.is_undefined() {
5354                                        result.push(value);
5355                                    }
5356                                }
5357                                result
5358                            };
5359                            // Don't do singleton unwrapping here - let the path result
5360                            // handling deal with it, which respects has_explicit_array_keep
5361                            JValue::array(mapped)
5362                        }
5363                        _ => {
5364                            // For non-arrays, bind $ and evaluate
5365                            let saved_dollar = self.context.lookup("$").cloned();
5366                            self.context.bind("$".to_string(), current.clone());
5367
5368                            let value = self.evaluate_internal(expr, &current)?;
5369
5370                            if let Some(saved) = saved_dollar {
5371                                self.context.bind("$".to_string(), saved);
5372                            } else {
5373                                self.context.unbind("$");
5374                            }
5375
5376                            value
5377                        }
5378                    };
5379                    self.keep_tuple_stream = saved_keep;
5380                    fa_result
5381                }
5382                AstNode::Sort { terms, .. } => {
5383                    // Sort as a path step - sort 'current' by the terms
5384                    self.evaluate_sort(&current, terms)?
5385                }
5386                // Handle complex path steps (e.g., computed properties, object construction)
5387                _ => {
5388                    let saved_keep = self.keep_tuple_stream;
5389                    if step.is_tuple {
5390                        self.keep_tuple_stream = true;
5391                    }
5392                    let v = self.evaluate_path_step(&step.node, &current, data);
5393                    self.keep_tuple_stream = saved_keep;
5394                    v?
5395                }
5396            };
5397        }
5398
5399        // End-of-path tuple projection, mirroring jsonata-js evaluatePath
5400        // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
5401        // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
5402        // wrappers are internal bookkeeping and must not escape into an enclosing
5403        // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
5404        // `=` compare wrapper objects and always yield false). Suppressed only for
5405        // the two consumers that read the carried bindings directly off the
5406        // wrappers (Sort input, ObjectTransform/group-by input), which set
5407        // `keep_tuple_stream`. The top-level `evaluate()` still runs
5408        // `unwrap_tuple_output` as a backstop for wrappers nested inside
5409        // constructed output.
5410        if !self.keep_tuple_stream {
5411            if let JValue::Array(arr) = &current {
5412                let is_tuple_stream = arr.first().is_some_and(|f| {
5413                    matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5414                });
5415                if is_tuple_stream {
5416                    let projected: Vec<JValue> = arr
5417                        .iter()
5418                        .map(|t| match t {
5419                            JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
5420                            other => other.clone(),
5421                        })
5422                        .collect();
5423                    current = JValue::array(projected);
5424                }
5425            }
5426        }
5427
5428        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5429        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5430
5431        // Check for explicit array-keeping operations. Empty predicate `[]` can
5432        // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5433        // stage; it also counts when it sits inside a `Sort` step's input path
5434        // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5435        // the sort and the trailing index so the singleton stays `[4]`.
5436        let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5437
5438        // Unwrap when:
5439        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5440        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5441        //    Note: did_array_mapping is reset to false when extracting from a single object,
5442        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5443        // BUT NOT when there's an explicit array-keeping operation
5444        //
5445        // Important: We DON'T unwrap just because original data was an array - what matters is
5446        // whether the final extraction was from an array mapping context or a single object.
5447        let should_unwrap = !has_explicit_array_keep
5448            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5449
5450        let result = match &current {
5451            // An empty result sequence is "no value" -> undefined (jsonata-js
5452            // treats an empty sequence, e.g. from a filter that matched nothing,
5453            // as undefined so a following `.field` and object/array construction
5454            // drop it rather than keeping an explicit null). `[]` array-keep is
5455            // handled separately above via has_explicit_array_keep.
5456            JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5457            // Unwrap singleton arrays when appropriate
5458            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5459            // Keep arrays otherwise
5460            _ => current,
5461        };
5462
5463        // An explicit `[]` keep-array forces the result to remain an array even
5464        // after a later singleton index collapses it to a scalar (jsonata's
5465        // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5466        let result = if has_explicit_array_keep
5467            && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5468        {
5469            JValue::array(vec![result])
5470        } else {
5471            result
5472        };
5473
5474        if let JValue::Array(arr) = &result {
5475            check_sequence_length(arr.len(), &self.options)?;
5476        }
5477
5478        Ok(result)
5479    }
5480
5481    /// True when a path step carries a tuple-binding flag (`@$var` focus,
5482    /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5483    /// produce/extend a tuple stream rather than be evaluated as a plain step.
5484    ///
5485    fn step_creates_tuple(step: &PathStep) -> bool {
5486        step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5487    }
5488
5489    /// True when a path contains an explicit empty predicate `[]` (keep-array),
5490    /// either directly as a step/stage or nested inside a `Sort` step's input
5491    /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5492    /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5493    /// -> `[4]`).
5494    fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5495        steps.iter().any(|step| {
5496            if let AstNode::Predicate(pred) = &step.node {
5497                if matches!(**pred, AstNode::Boolean(true)) {
5498                    return true;
5499                }
5500            }
5501            if step.stages.iter().any(
5502                |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5503            ) {
5504                return true;
5505            }
5506            if let AstNode::Sort { input, .. } = &step.node {
5507                if let AstNode::Path { steps: inner } = input.as_ref() {
5508                    return Self::path_keeps_singleton_array(inner);
5509                }
5510            }
5511            false
5512        })
5513    }
5514
5515    /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5516    /// scope, saving whatever was previously bound under each of those names
5517    /// so [`TupleKeyBindings::restore`] can put it back afterward.
5518    ///
5519    /// This is the single shared implementation of the
5520    /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5521    /// pattern that recurs across `create_tuple_stream`,
5522    /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5523    /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5524    /// none of those call sites can regress to a blind `unbind` (which
5525    /// deletes rather than restores a same-named outer `:=` binding that was
5526    /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5527    /// binding silently clobbering an outer variable of the same name).
5528    fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5529        let mut saved = Vec::new();
5530        for (key, value) in tuple_obj.iter() {
5531            let name = if let Some(n) = key.strip_prefix('$') {
5532                if n.is_empty() {
5533                    continue;
5534                }
5535                n.to_string()
5536            } else if key.starts_with('!') {
5537                key.clone()
5538            } else {
5539                continue;
5540            };
5541            saved.push((name.clone(), self.context.lookup(&name).cloned()));
5542            self.context.bind(name, value.clone());
5543        }
5544        TupleKeyBindings { saved }
5545    }
5546
5547    /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5548    /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5549    /// vector holds `JValue::Object` tuple wrappers of the shape
5550    /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5551    /// which downstream steps consume via the existing tuple-aware handling in
5552    /// `evaluate_path`.
5553    ///
5554    /// `input` is the previous step's result: either an already-built tuple
5555    /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5556    /// plain value/array entering tuple mode for the first time (each item
5557    /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5558    ///
5559    /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5560    /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5561    /// fast paths only *rebuild* a wrapper around a value pulled from an input
5562    /// element that is already `__tuple__`-tagged, which can only be true if a
5563    /// `create_tuple_stream` call already ran earlier in this evaluation and set
5564    /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5565    /// fire on a value that did NOT come from an existing tuple stream, it must
5566    /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5567    /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5568    fn create_tuple_stream(
5569        &mut self,
5570        step: &PathStep,
5571        input: &JValue,
5572        is_first_path_step: bool,
5573    ) -> Result<Vec<JValue>, EvaluatorError> {
5574        use std::rc::Rc;
5575
5576        // Mark that this evaluate() call produced tuple wrappers, so the
5577        // top-level `evaluate()` knows to run the output-unwrap pass.
5578        self.tuple_stream_created = true;
5579
5580        // Gather the incoming tuple bindings.
5581        let is_tuple_input = matches!(
5582            input,
5583            JValue::Array(arr) if arr.first().is_some_and(|f| {
5584                matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5585            })
5586        );
5587        let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5588            match input {
5589                JValue::Array(arr) => arr
5590                    .iter()
5591                    .filter_map(|t| match t {
5592                        JValue::Object(o) => Some(o.clone()),
5593                        _ => None,
5594                    })
5595                    .collect(),
5596                _ => unreachable!(),
5597            }
5598        } else {
5599            let items: Vec<JValue> = match input {
5600                // Mirrors jsonata-js evaluatePath's inputSequence rule
5601                // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5602                // when the path's FIRST step is a variable reference (`$`/`$$`) the
5603                // input array is taken as a SINGLE sequence value
5604                // (`createSequence(input)`) rather than iterated per-element. We
5605                // only need this for a leading INDEX bind (`$#$pos`): the whole
5606                // array becomes one incoming tuple whose `@` is the array, then
5607                // the inner position counter walks its elements so `$pos` runs
5608                // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5609                // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5610                // the step input, so a single binding would yield one copy of the
5611                // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5612                // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5613                // `$.$#$pos` (a later step) still iterates per-element.
5614                JValue::Array(arr)
5615                    if !(is_first_path_step
5616                        && matches!(&step.node, AstNode::Variable(_))
5617                        && step.index_var.is_some()) =>
5618                {
5619                    arr.iter().cloned().collect()
5620                }
5621                single => vec![single.clone()],
5622            };
5623            items
5624                .into_iter()
5625                .map(|item| {
5626                    let mut wrapper = IndexMap::new();
5627                    wrapper.insert("@".to_string(), item);
5628                    wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5629                    Rc::new(wrapper)
5630                })
5631                .collect()
5632        };
5633
5634        // A sort step in a tuple stream orders the WHOLE stream (not per element)
5635        // and re-tuples with the index = sorted position, mirroring jsonata-js
5636        // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5637        // array, then number the sorted values, then filter by `$pos`.
5638        if let AstNode::Sort { terms, .. } = &step.node {
5639            let stream = JValue::array(
5640                incoming
5641                    .iter()
5642                    .map(|t| JValue::object((**t).clone()))
5643                    .collect(),
5644            );
5645            // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5646            // carried keys bound), returning the wrappers in sorted order.
5647            let sorted = self.evaluate_sort(&stream, terms)?;
5648            let sorted_arr: Vec<JValue> = match sorted {
5649                JValue::Array(a) => a.iter().cloned().collect(),
5650                JValue::Null | JValue::Undefined => Vec::new(),
5651                other => vec![other],
5652            };
5653            let mut result = Vec::new();
5654            for (ss, elem) in sorted_arr.into_iter().enumerate() {
5655                let mut new_tuple = match elem {
5656                    JValue::Object(o) => (*o).clone(),
5657                    other => {
5658                        let mut m = IndexMap::new();
5659                        m.insert("@".to_string(), other);
5660                        m
5661                    }
5662                };
5663                if let Some(index_var) = &step.index_var {
5664                    new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
5665                }
5666                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5667                result.push(JValue::object(new_tuple));
5668            }
5669            return Ok(result);
5670        }
5671
5672        let mut result = Vec::new();
5673        for tuple_obj in incoming {
5674            // Bind every carried tuple key into a real scope frame so the step
5675            // expression can see prior focus/index/ancestor bindings, mirroring
5676            // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
5677            // Saves/restores rather than blindly unbinding, so a tuple key
5678            // whose name collides with a live outer `:=` binding doesn't get
5679            // deleted once this tuple row's evaluation is done.
5680            let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
5681
5682            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
5683            let step_value = self.evaluate_internal(&step.node, &actual_data);
5684
5685            let mut step_value = step_value?;
5686            // When the step carries an ORDERED index stage (a second `#$var`,
5687            // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
5688            // BUILT tuple stream in order (filter then re-number) so the filter
5689            // sees the per-tuple focus/index bindings and each index reflects the
5690            // position at its point in the sequence. Those steps defer all stage
5691            // application to `apply_tuple_stages` after the stream is built.
5692            let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
5693            if !step.stages.is_empty() && !has_index_stage {
5694                // A `%` inside a filter predicate refers to the ancestry of
5695                // THIS step (its own input for a level-1 `%`, or an earlier
5696                // step's input for a `%.%` chain). ast_transform tags this step
5697                // with `ancestor_label`; bind it to the step's input so the
5698                // level-1 `%` resolves. The `%.%` chain's deeper references use
5699                // labels carried in the INCOMING tuple, so those bindings
5700                // (`tuple_bindings`) must stay live through `apply_stages` --
5701                // their restore is deferred until after it (previously they
5702                // were unbound first, which silently broke `%.%` inside
5703                // predicates).
5704                let own_label = match &step.ancestor_label {
5705                    Some(label) if !tuple_bindings.contains(label) => {
5706                        self.context.bind(label.clone(), actual_data.clone());
5707                        Some(label.clone())
5708                    }
5709                    _ => None,
5710                };
5711                step_value = self.apply_stages(step_value, &step.stages)?;
5712                if let Some(label) = own_label {
5713                    self.context.unbind(&label);
5714                }
5715            }
5716
5717            tuple_bindings.restore(self);
5718
5719            let row: Vec<JValue> = match step_value {
5720                JValue::Undefined => continue,
5721                JValue::Array(arr) => arr.iter().cloned().collect(),
5722                other => vec![other],
5723            };
5724
5725            for (position, value) in row.into_iter().enumerate() {
5726                if value.is_undefined() {
5727                    continue;
5728                }
5729                let mut new_tuple = (*tuple_obj).clone();
5730                if let Some(focus_var) = &step.focus {
5731                    // Focus binding keeps `@` as this step's INPUT (already carried
5732                    // in the cloned tuple) and binds the result to `$focus`,
5733                    // matching jsonata-js: `tuple[expr.focus] = res[bb];
5734                    // tuple['@'] = tupleBindings[ee]['@'];`.
5735                    new_tuple.insert(format!("${}", focus_var), value);
5736                } else {
5737                    new_tuple.insert("@".to_string(), value);
5738                }
5739                if let Some(index_var) = &step.index_var {
5740                    // Index binding records the position of this value WITHIN the
5741                    // per-binding result row (jsonata-js evaluateTupleStep: the
5742                    // inner `bb` counter, `tuple[expr.index] = bb`), which resets
5743                    // for each incoming tuple.
5744                    new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
5745                }
5746                if let Some(ancestor_label) = &step.ancestor_label {
5747                    // `%` ancestor: preserve this step's INPUT under the label.
5748                    new_tuple.insert(ancestor_label.clone(), actual_data.clone());
5749                }
5750                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5751                result.push(JValue::object(new_tuple));
5752            }
5753        }
5754
5755        // Apply ordered filter/index stages to the built tuple stream when a
5756        // second index binding deferred them (see the has_index_stage comment
5757        // in the build loop above).
5758        if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
5759            result = self.apply_tuple_stages(result, &step.stages)?;
5760        }
5761
5762        Ok(result)
5763    }
5764
5765    /// Apply a step's stages, in order, to an already-built tuple stream --
5766    /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
5767    /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
5768    /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
5769    /// stage sets its variable on every surviving tuple to that tuple's position
5770    /// in the CURRENT stream. Used for steps carrying a second `#$var` index
5771    /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
5772    /// pre-filter position and `$ib2` the post-filter position.
5773    fn apply_tuple_stages(
5774        &mut self,
5775        mut tuples: Vec<JValue>,
5776        stages: &[Stage],
5777    ) -> Result<Vec<JValue>, EvaluatorError> {
5778        for stage in stages {
5779            match stage {
5780                Stage::Filter(pred) => {
5781                    let mut kept = Vec::with_capacity(tuples.len());
5782                    for tup in tuples.into_iter() {
5783                        let JValue::Object(obj) = &tup else {
5784                            continue;
5785                        };
5786                        // Bind this tuple's carried focus/index/ancestor keys so
5787                        // the predicate can reference them (save/restore rather
5788                        // than blind unbind -- see bind_tuple_keys).
5789                        let tuple_bindings = self.bind_tuple_keys(obj);
5790                        let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
5791                        let pred_res = self.evaluate_internal(pred, &at);
5792                        tuple_bindings.restore(self);
5793                        if self.is_truthy(&pred_res?) {
5794                            kept.push(tup);
5795                        }
5796                    }
5797                    tuples = kept;
5798                }
5799                Stage::Index(var) => {
5800                    for (pos, tup) in tuples.iter_mut().enumerate() {
5801                        if let JValue::Object(obj) = tup {
5802                            let mut m = (**obj).clone();
5803                            m.insert(format!("${}", var), JValue::from(pos as i64));
5804                            *tup = JValue::object(m);
5805                        }
5806                    }
5807                }
5808            }
5809        }
5810        Ok(tuples)
5811    }
5812
5813    /// Helper to evaluate a complex path step
5814    fn evaluate_path_step(
5815        &mut self,
5816        step: &AstNode,
5817        current: &JValue,
5818        original_data: &JValue,
5819    ) -> Result<JValue, EvaluatorError> {
5820        // Special case: array mapping with object construction
5821        // e.g., items.{"name": name, "price": price}
5822        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
5823            match (current, step) {
5824                (JValue::Array(arr), AstNode::Object(pairs)) => {
5825                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
5826                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
5827                        let shape = arr.first().and_then(build_shape_cache);
5828                        let mut mapped = Vec::with_capacity(arr.len());
5829                        for item in arr.iter() {
5830                            let result = if let Some(ref s) = shape {
5831                                eval_compiled_shaped(
5832                                    &compiled,
5833                                    item,
5834                                    None,
5835                                    s,
5836                                    &self.options,
5837                                    self.start_time,
5838                                )?
5839                            } else {
5840                                eval_compiled(
5841                                    &compiled,
5842                                    item,
5843                                    None,
5844                                    &self.options,
5845                                    self.start_time,
5846                                )?
5847                            };
5848                            if !result.is_undefined() {
5849                                mapped.push(result);
5850                            }
5851                        }
5852                        return Ok(JValue::array(mapped));
5853                    }
5854                    // Fallback: full AST evaluation per element
5855                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
5856                        .iter()
5857                        .map(|item| self.evaluate_internal(step, item))
5858                        .collect();
5859                    Ok(JValue::array(mapped?))
5860                }
5861                _ => unreachable!(),
5862            }
5863        } else {
5864            // Special case: array.$ should map $ over the array, returning each element
5865            // e.g., [1, 2, 3].$ returns [1, 2, 3]
5866            if let AstNode::Variable(name) = step {
5867                if name.is_empty() {
5868                    // Bare $ - map over array if current is an array
5869                    if let JValue::Array(arr) = current {
5870                        // Map $ over each element - $ refers to each element in turn
5871                        return Ok(JValue::Array(arr.clone()));
5872                    } else {
5873                        // For non-arrays, $ refers to the current value
5874                        return Ok(current.clone());
5875                    }
5876                }
5877            }
5878
5879            // Special case: Variable access on tuple arrays (from index binding #$var)
5880            // When current is a tuple array, we need to evaluate the variable against each tuple
5881            // so that tuple bindings ($i, etc.) can be found
5882            if matches!(step, AstNode::Variable(_)) {
5883                if let JValue::Array(arr) = current {
5884                    // Check if this is a tuple array
5885                    let is_tuple_array = arr.first().is_some_and(|first| {
5886                        if let JValue::Object(obj) = first {
5887                            obj.get("__tuple__") == Some(&JValue::Bool(true))
5888                        } else {
5889                            false
5890                        }
5891                    });
5892
5893                    if is_tuple_array {
5894                        // Map the variable lookup over each tuple
5895                        let mut results = Vec::new();
5896                        for tuple in arr.iter() {
5897                            // Evaluate the variable in the context of this tuple
5898                            // This allows tuple bindings ($i, etc.) to be found
5899                            let val = self.evaluate_internal(step, tuple)?;
5900                            if !val.is_null() && !val.is_undefined() {
5901                                results.push(val);
5902                            }
5903                        }
5904                        return Ok(JValue::array(results));
5905                    }
5906                }
5907            }
5908
5909            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
5910            // rather than being used to index/access the current value
5911            // e.g., items[price > 50] where [price > 50] is a filter operation
5912            // or $x.price where $x is a variable binding
5913            // or $$.field where $$ is the parent context
5914            // or [0..9] where it's an array constructor
5915            // or $^(field) where it's a sort operator
5916            // or (expr).field where (expr) is a block that evaluates to a value
5917            if matches!(
5918                step,
5919                AstNode::Binary { .. }
5920                    | AstNode::Function { .. }
5921                    | AstNode::Variable(_)
5922                    | AstNode::ParentVariable(_)
5923                    | AstNode::Parent(_)
5924                    | AstNode::Array(_)
5925                    | AstNode::Object(_)
5926                    | AstNode::Sort { .. }
5927                    | AstNode::Block(_)
5928            ) {
5929                // Evaluate the step in the context of original_data and return the result directly
5930                return self.evaluate_internal(step, original_data);
5931            }
5932
5933            // Standard path step evaluation for indexing/accessing current value
5934            let step_value = self.evaluate_internal(step, original_data)?;
5935            Ok(match (current, &step_value) {
5936                (JValue::Object(obj), JValue::String(key)) => {
5937                    obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
5938                }
5939                (JValue::Array(arr), JValue::Number(n)) => {
5940                    let index = *n as i64;
5941                    let len = arr.len() as i64;
5942
5943                    // Handle negative indexing (offset from end)
5944                    let actual_idx = if index < 0 { len + index } else { index };
5945
5946                    if actual_idx < 0 || actual_idx >= len {
5947                        JValue::Undefined
5948                    } else {
5949                        arr[actual_idx as usize].clone()
5950                    }
5951                }
5952                _ => JValue::Undefined,
5953            })
5954        }
5955    }
5956
5957    /// Evaluate a binary operation
5958    fn evaluate_binary_op(
5959        &mut self,
5960        op: crate::ast::BinaryOp,
5961        lhs: &AstNode,
5962        rhs: &AstNode,
5963        data: &JValue,
5964    ) -> Result<JValue, EvaluatorError> {
5965        use crate::ast::BinaryOp;
5966
5967        // Special handling for coalescing operator (??)
5968        // Returns right side if left is undefined (produces no value)
5969        // Note: literal null is a value, so it's NOT replaced
5970        if op == BinaryOp::Coalesce {
5971            // Try to evaluate the left side
5972            return match self.evaluate_internal(lhs, data) {
5973                Ok(value) => {
5974                    // Successfully evaluated to a value (even if it's null)
5975                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
5976                    if matches!(lhs, AstNode::Null) {
5977                        Ok(value)
5978                    }
5979                    // For paths and variables, undefined (no match/unbound) - use RHS
5980                    else if value.is_undefined()
5981                        && (matches!(lhs, AstNode::Path { .. })
5982                            || matches!(lhs, AstNode::String(_))
5983                            || matches!(lhs, AstNode::Variable(_)))
5984                    {
5985                        self.evaluate_internal(rhs, data)
5986                    } else {
5987                        Ok(value)
5988                    }
5989                }
5990                Err(_) => {
5991                    // Evaluation failed (e.g., undefined variable) - use RHS
5992                    self.evaluate_internal(rhs, data)
5993                }
5994            };
5995        }
5996
5997        // Special handling for default operator (?:)
5998        // Returns right side if left is falsy or a non-value (like a function)
5999        if op == BinaryOp::Default {
6000            let left = self.evaluate_internal(lhs, data)?;
6001            if self.is_truthy_for_default(&left) {
6002                return Ok(left);
6003            }
6004            return self.evaluate_internal(rhs, data);
6005        }
6006
6007        // Special handling for chain/pipe operator (~>)
6008        // Pipes the LHS result to the RHS function as the first argument
6009        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
6010        if op == BinaryOp::ChainPipe {
6011            // Handle regex on RHS - treat as $match(lhs, regex)
6012            if let AstNode::Regex { pattern, flags } = rhs {
6013                // Evaluate LHS
6014                let lhs_value = self.evaluate_internal(lhs, data)?;
6015                // Do regex match inline
6016                return match lhs_value {
6017                    JValue::String(s) => {
6018                        // Build the regex
6019                        let case_insensitive = flags.contains('i');
6020                        let regex_pattern = if case_insensitive {
6021                            format!("(?i){}", pattern)
6022                        } else {
6023                            pattern.clone()
6024                        };
6025                        match regex::Regex::new(&regex_pattern) {
6026                            Ok(re) => {
6027                                if let Some(m) = re.find(&s) {
6028                                    // Return match object
6029                                    let mut result = IndexMap::new();
6030                                    result.insert(
6031                                        "match".to_string(),
6032                                        JValue::string(m.as_str().to_string()),
6033                                    );
6034                                    result.insert(
6035                                        "start".to_string(),
6036                                        JValue::Number(m.start() as f64),
6037                                    );
6038                                    result
6039                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
6040
6041                                    // Capture groups
6042                                    let mut groups = Vec::new();
6043                                    for cap in re.captures_iter(&s).take(1) {
6044                                        for i in 1..cap.len() {
6045                                            if let Some(c) = cap.get(i) {
6046                                                groups.push(JValue::string(c.as_str().to_string()));
6047                                            }
6048                                        }
6049                                    }
6050                                    if !groups.is_empty() {
6051                                        result.insert("groups".to_string(), JValue::array(groups));
6052                                    }
6053
6054                                    Ok(JValue::object(result))
6055                                } else {
6056                                    Ok(JValue::Null)
6057                                }
6058                            }
6059                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
6060                                "Invalid regex: {}",
6061                                e
6062                            ))),
6063                        }
6064                    }
6065                    JValue::Null => Ok(JValue::Null),
6066                    _ => Err(EvaluatorError::TypeError(
6067                        "Left side of ~> /regex/ must be a string".to_string(),
6068                    )),
6069                };
6070            }
6071
6072            // Early check: if LHS evaluates to undefined, return undefined
6073            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
6074            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
6075            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
6076                return Ok(JValue::Undefined);
6077            }
6078
6079            // Handle different RHS types
6080            match rhs {
6081                AstNode::Function {
6082                    name,
6083                    args,
6084                    is_builtin,
6085                } => {
6086                    // RHS is a function call
6087                    // Check if the function call has placeholder arguments (partial application)
6088                    let has_placeholder =
6089                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6090
6091                    if has_placeholder {
6092                        // Partial application: replace the first placeholder with LHS value
6093                        let lhs_value = self.evaluate_internal(lhs, data)?;
6094                        let mut filled_args = Vec::new();
6095                        let mut lhs_used = false;
6096
6097                        for arg in args.iter() {
6098                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
6099                                // Replace first placeholder with evaluated LHS
6100                                // We need to create a temporary binding to pass the value
6101                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
6102                                self.context.bind(temp_name.clone(), lhs_value.clone());
6103                                filled_args.push(AstNode::Variable(temp_name));
6104                                lhs_used = true;
6105                            } else {
6106                                filled_args.push(arg.clone());
6107                            }
6108                        }
6109
6110                        // Evaluate the function with filled args
6111                        let result =
6112                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
6113
6114                        // Clean up temp bindings
6115                        for (i, arg) in args.iter().enumerate() {
6116                            if matches!(arg, AstNode::Placeholder) {
6117                                self.context.unbind(&format!("__pipe_arg_{}", i));
6118                            }
6119                        }
6120
6121                        // Unwrap singleton results from chain operator
6122                        return result.map(|v| self.unwrap_singleton(v));
6123                    } else {
6124                        // No placeholders: build args list with LHS as first argument
6125                        let mut all_args = vec![lhs.clone()];
6126                        all_args.extend_from_slice(args);
6127                        // Unwrap singleton results from chain operator
6128                        return self
6129                            .evaluate_function_call(name, &all_args, *is_builtin, data)
6130                            .map(|v| self.unwrap_singleton(v));
6131                    }
6132                }
6133                AstNode::Variable(var_name) => {
6134                    // RHS is a function reference (no parens)
6135                    // e.g., $average($tempReadings) ~> $round
6136                    let all_args = vec![lhs.clone()];
6137                    // Unwrap singleton results from chain operator
6138                    return self
6139                        .evaluate_function_call(var_name, &all_args, true, data)
6140                        .map(|v| self.unwrap_singleton(v));
6141                }
6142                AstNode::Binary {
6143                    op: BinaryOp::ChainPipe,
6144                    ..
6145                } => {
6146                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
6147                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
6148                    let lhs_value = self.evaluate_internal(lhs, data)?;
6149                    return self.evaluate_internal(rhs, &lhs_value);
6150                }
6151                AstNode::Transform { .. } => {
6152                    // RHS is a transform - invoke it with LHS as input
6153                    // Evaluate LHS first
6154                    let lhs_value = self.evaluate_internal(lhs, data)?;
6155
6156                    // Bind $ to the LHS value, then evaluate the transform
6157                    let saved_binding = self.context.lookup("$").cloned();
6158                    self.context.bind("$".to_string(), lhs_value.clone());
6159
6160                    let result = self.evaluate_internal(rhs, data);
6161
6162                    // Restore $ binding
6163                    if let Some(saved) = saved_binding {
6164                        self.context.bind("$".to_string(), saved);
6165                    } else {
6166                        self.context.unbind("$");
6167                    }
6168
6169                    // Unwrap singleton results from chain operator
6170                    return result.map(|v| self.unwrap_singleton(v));
6171                }
6172                AstNode::Lambda {
6173                    params,
6174                    body,
6175                    signature,
6176                    thunk,
6177                } => {
6178                    // RHS is a lambda - invoke it with LHS as argument
6179                    let lhs_value = self.evaluate_internal(lhs, data)?;
6180                    // Unwrap singleton results from chain operator
6181                    return self
6182                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
6183                        .map(|v| self.unwrap_singleton(v));
6184                }
6185                AstNode::Path { steps } => {
6186                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
6187                    // If the first step is a function call, we need to add LHS as first argument
6188                    if let Some(first_step) = steps.first() {
6189                        match &first_step.node {
6190                            AstNode::Function {
6191                                name,
6192                                args,
6193                                is_builtin,
6194                            } => {
6195                                // Prepend LHS to the function arguments
6196                                let mut all_args = vec![lhs.clone()];
6197                                all_args.extend_from_slice(args);
6198
6199                                // Call the function
6200                                let mut result = self.evaluate_function_call(
6201                                    name,
6202                                    &all_args,
6203                                    *is_builtin,
6204                                    data,
6205                                )?;
6206
6207                                // Apply stages from the first step (e.g., predicates)
6208                                for stage in &first_step.stages {
6209                                    match stage {
6210                                        Stage::Filter(filter_expr) => {
6211                                            result = self.evaluate_predicate_as_stage(
6212                                                &result,
6213                                                filter_expr,
6214                                            )?;
6215                                        }
6216                                        Stage::Index(_) => {}
6217                                    }
6218                                }
6219
6220                                // Apply remaining path steps if any
6221                                if steps.len() > 1 {
6222                                    let remaining_path = AstNode::Path {
6223                                        steps: steps[1..].to_vec(),
6224                                    };
6225                                    result = self.evaluate_internal(&remaining_path, &result)?;
6226                                }
6227
6228                                // Unwrap singleton results from chain operator, unless there are stages
6229                                // Stages (like predicates) indicate we want to preserve array structure
6230                                if !first_step.stages.is_empty() || steps.len() > 1 {
6231                                    return Ok(result);
6232                                } else {
6233                                    return Ok(self.unwrap_singleton(result));
6234                                }
6235                            }
6236                            AstNode::Variable(var_name) => {
6237                                // Variable that should resolve to a function
6238                                let all_args = vec![lhs.clone()];
6239                                let mut result =
6240                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
6241
6242                                // Apply stages from the first step
6243                                for stage in &first_step.stages {
6244                                    match stage {
6245                                        Stage::Filter(filter_expr) => {
6246                                            result = self.evaluate_predicate_as_stage(
6247                                                &result,
6248                                                filter_expr,
6249                                            )?;
6250                                        }
6251                                        Stage::Index(_) => {}
6252                                    }
6253                                }
6254
6255                                // Apply remaining path steps if any
6256                                if steps.len() > 1 {
6257                                    let remaining_path = AstNode::Path {
6258                                        steps: steps[1..].to_vec(),
6259                                    };
6260                                    result = self.evaluate_internal(&remaining_path, &result)?;
6261                                }
6262
6263                                // Unwrap singleton results from chain operator, unless there are stages
6264                                // Stages (like predicates) indicate we want to preserve array structure
6265                                if !first_step.stages.is_empty() || steps.len() > 1 {
6266                                    return Ok(result);
6267                                } else {
6268                                    return Ok(self.unwrap_singleton(result));
6269                                }
6270                            }
6271                            _ => {
6272                                // Other path types - just evaluate normally with LHS as context
6273                                let lhs_value = self.evaluate_internal(lhs, data)?;
6274                                return self
6275                                    .evaluate_internal(rhs, &lhs_value)
6276                                    .map(|v| self.unwrap_singleton(v));
6277                            }
6278                        }
6279                    }
6280
6281                    // Empty path? Shouldn't happen, but handle it
6282                    let lhs_value = self.evaluate_internal(lhs, data)?;
6283                    return self
6284                        .evaluate_internal(rhs, &lhs_value)
6285                        .map(|v| self.unwrap_singleton(v));
6286                }
6287                _ => {
6288                    return Err(EvaluatorError::TypeError(
6289                        "Right side of ~> must be a function call or function reference"
6290                            .to_string(),
6291                    ));
6292                }
6293            }
6294        }
6295
6296        // Special handling for variable binding (:=)
6297        if op == BinaryOp::ColonEqual {
6298            // Extract variable name from LHS
6299            let var_name = match lhs {
6300                AstNode::Variable(name) => name.clone(),
6301                _ => {
6302                    return Err(EvaluatorError::TypeError(
6303                        "Left side of := must be a variable".to_string(),
6304                    ))
6305                }
6306            };
6307
6308            // Check if RHS is a lambda - store it specially
6309            if let AstNode::Lambda {
6310                params,
6311                body,
6312                signature,
6313                thunk,
6314            } = rhs
6315            {
6316                // Store the lambda AST for later invocation
6317                // Capture only the free variables referenced by the lambda body
6318                let captured_env = self.capture_environment_for(body, params);
6319                let compiled_body = if !thunk {
6320                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
6321                    try_compile_expr_with_allowed_vars(body, &var_refs)
6322                } else {
6323                    None
6324                };
6325                let stored_lambda = StoredLambda {
6326                    params: params.clone(),
6327                    body: (**body).clone(),
6328                    compiled_body,
6329                    signature: signature.clone(),
6330                    captured_env,
6331                    captured_data: Some(data.clone()),
6332                    thunk: *thunk,
6333                };
6334                let lambda_params = stored_lambda.params.clone();
6335                let lambda_sig = stored_lambda.signature.clone();
6336                self.context.bind_lambda(var_name.clone(), stored_lambda);
6337
6338                // Return a lambda marker value (include _lambda_id so it can be found later)
6339                let lambda_repr = JValue::lambda(
6340                    var_name.as_str(),
6341                    lambda_params,
6342                    Some(var_name.clone()),
6343                    lambda_sig,
6344                );
6345                return Ok(lambda_repr);
6346            }
6347
6348            // Check if RHS is a pure function composition (ChainPipe between function references)
6349            // e.g., $uppertrim := $trim ~> $uppercase
6350            // This creates a lambda that composes the functions.
6351            // But NOT for data ~> function, which should be evaluated immediately.
6352            // e.g., $result := data ~> $map($fn) should evaluate the pipe
6353            if let AstNode::Binary {
6354                op: BinaryOp::ChainPipe,
6355                lhs: chain_lhs,
6356                rhs: chain_rhs,
6357            } = rhs
6358            {
6359                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
6360                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
6361                let is_function_composition = match chain_lhs.as_ref() {
6362                    // LHS is a function reference like $trim or $sum
6363                    AstNode::Variable(name)
6364                        if self.is_builtin_function(name)
6365                            || self.context.lookup_lambda(name).is_some() =>
6366                    {
6367                        true
6368                    }
6369                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
6370                    AstNode::Binary {
6371                        op: BinaryOp::ChainPipe,
6372                        ..
6373                    } => true,
6374                    // A function call with placeholder creates a partial application
6375                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
6376                    AstNode::Function { args, .. }
6377                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
6378                    {
6379                        true
6380                    }
6381                    // Anything else (data, function calls, arrays, etc.) is not pure composition
6382                    _ => false,
6383                };
6384
6385                if is_function_composition {
6386                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
6387                    // The original chain is $trim ~> $uppercase (left-associative)
6388                    // We want to create: ($ ~> $trim) ~> $uppercase
6389                    let param_name = "$".to_string();
6390
6391                    // First create $ ~> $trim
6392                    let first_pipe = AstNode::Binary {
6393                        op: BinaryOp::ChainPipe,
6394                        lhs: Box::new(AstNode::Variable(param_name.clone())),
6395                        rhs: chain_lhs.clone(),
6396                    };
6397
6398                    // Then wrap with ~> $uppercase (or the rest of the chain)
6399                    let composed_body = AstNode::Binary {
6400                        op: BinaryOp::ChainPipe,
6401                        lhs: Box::new(first_pipe),
6402                        rhs: chain_rhs.clone(),
6403                    };
6404
6405                    let stored_lambda = StoredLambda {
6406                        params: vec![param_name],
6407                        body: composed_body,
6408                        compiled_body: None, // ChainPipe body is not compilable
6409                        signature: None,
6410                        captured_env: self.capture_current_environment(),
6411                        captured_data: Some(data.clone()),
6412                        thunk: false,
6413                    };
6414                    self.context.bind_lambda(var_name.clone(), stored_lambda);
6415
6416                    // Return a lambda marker value (include _lambda_id for later lookup)
6417                    let lambda_repr = JValue::lambda(
6418                        var_name.as_str(),
6419                        vec!["$".to_string()],
6420                        Some(var_name.clone()),
6421                        None::<String>,
6422                    );
6423                    return Ok(lambda_repr);
6424                }
6425                // If not function composition, fall through to normal evaluation below
6426            }
6427
6428            // Evaluate the RHS
6429            let value = self.evaluate_internal(rhs, data)?;
6430
6431            // If the value is a lambda, copy the stored lambda to the new variable name
6432            if let Some(stored) = self.lookup_lambda_from_value(&value) {
6433                self.context.bind_lambda(var_name.clone(), stored);
6434            }
6435
6436            // Bind even if undefined (null) so inner scopes can shadow outer variables
6437            self.context.bind(var_name, value.clone());
6438            return Ok(value);
6439        }
6440
6441        // Special handling for 'In' operator - check for array filtering
6442        // Must evaluate lhs first to determine if this is array filtering
6443        if op == BinaryOp::In {
6444            let left = self.evaluate_internal(lhs, data)?;
6445
6446            // Check if this is array filtering: array[predicate]
6447            if matches!(left, JValue::Array(_)) {
6448                // Try evaluating rhs in current context to see if it's a simple index
6449                let right_result = self.evaluate_internal(rhs, data);
6450
6451                if let Ok(JValue::Number(_)) = right_result {
6452                    // Simple numeric index: array[n]
6453                    return self.array_index(&left, &right_result.unwrap());
6454                } else {
6455                    // This is array filtering: array[predicate]
6456                    // Evaluate the predicate for each array item
6457                    return self.array_filter(lhs, rhs, &left, data);
6458                }
6459            }
6460        }
6461
6462        // Special handling for logical operators (short-circuit evaluation)
6463        if op == BinaryOp::And {
6464            let left = self.evaluate_internal(lhs, data)?;
6465            if !self.is_truthy(&left) {
6466                // Short-circuit: if left is falsy, return false without evaluating right
6467                return Ok(JValue::Bool(false));
6468            }
6469            let right = self.evaluate_internal(rhs, data)?;
6470            return Ok(JValue::Bool(self.is_truthy(&right)));
6471        }
6472
6473        if op == BinaryOp::Or {
6474            let left = self.evaluate_internal(lhs, data)?;
6475            if self.is_truthy(&left) {
6476                // Short-circuit: if left is truthy, return true without evaluating right
6477                return Ok(JValue::Bool(true));
6478            }
6479            let right = self.evaluate_internal(rhs, data)?;
6480            return Ok(JValue::Bool(self.is_truthy(&right)));
6481        }
6482
6483        // Check if operands are explicit null literals (vs undefined from variables)
6484        let left_is_explicit_null = matches!(lhs, AstNode::Null);
6485        let right_is_explicit_null = matches!(rhs, AstNode::Null);
6486
6487        // Standard evaluation: evaluate both operands
6488        let left = self.evaluate_internal(lhs, data)?;
6489        let right = self.evaluate_internal(rhs, data)?;
6490
6491        match op {
6492            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6493            BinaryOp::Subtract => {
6494                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6495            }
6496            BinaryOp::Multiply => {
6497                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6498            }
6499            BinaryOp::Divide => {
6500                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6501            }
6502            BinaryOp::Modulo => {
6503                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6504            }
6505
6506            BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
6507            BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
6508            BinaryOp::LessThan => {
6509                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6510            }
6511            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6512                &left,
6513                &right,
6514                left_is_explicit_null,
6515                right_is_explicit_null,
6516            ),
6517            BinaryOp::GreaterThan => {
6518                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6519            }
6520            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6521                &left,
6522                &right,
6523                left_is_explicit_null,
6524                right_is_explicit_null,
6525            ),
6526
6527            // And/Or handled above with short-circuit evaluation
6528            BinaryOp::And | BinaryOp::Or => unreachable!(),
6529
6530            BinaryOp::Concatenate => self.concatenate(&left, &right),
6531            BinaryOp::Range => self.range(&left, &right),
6532            BinaryOp::In => self.in_operator(&left, &right),
6533
6534            // Focus binding: should be resolved by ast_transform pass (Task 2)
6535            BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6536                "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6537            )),
6538
6539            // Index binding: should be resolved by ast_transform pass (Task 4,
6540            // which retired the dedicated AstNode::IndexBind variant in favor
6541            // of this generic Binary marker, mirroring FocusBind above)
6542            BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6543                "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6544            )),
6545
6546            // These operators are all handled as special cases earlier in evaluate_binary_op
6547            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6548                unreachable!()
6549            }
6550        }
6551    }
6552
6553    /// Evaluate a unary operation
6554    fn evaluate_unary_op(
6555        &mut self,
6556        op: crate::ast::UnaryOp,
6557        operand: &AstNode,
6558        data: &JValue,
6559    ) -> Result<JValue, EvaluatorError> {
6560        use crate::ast::UnaryOp;
6561
6562        let value = self.evaluate_internal(operand, data)?;
6563
6564        match op {
6565            UnaryOp::Negate => match value {
6566                // undefined returns undefined
6567                JValue::Null | JValue::Undefined => Ok(JValue::Null),
6568                JValue::Number(n) => Ok(JValue::Number(-n)),
6569                _ => Err(EvaluatorError::TypeError(
6570                    "D1002: Cannot negate non-number value".to_string(),
6571                )),
6572            },
6573            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6574        }
6575    }
6576
6577    /// Try to fuse an aggregate function call with its Path argument.
6578    /// Handles patterns like:
6579    /// - $sum(arr.field) → iterate arr, extract field, accumulate
6580    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6581    ///
6582    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6583    fn try_fused_aggregate(
6584        &mut self,
6585        name: &str,
6586        arg: &AstNode,
6587        data: &JValue,
6588    ) -> Result<Option<JValue>, EvaluatorError> {
6589        // Only applies to numeric aggregates
6590        if !matches!(name, "sum" | "max" | "min" | "average") {
6591            return Ok(None);
6592        }
6593
6594        // Argument must be a Path
6595        let AstNode::Path { steps } = arg else {
6596            return Ok(None);
6597        };
6598
6599        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6600        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6601        if steps.len() != 2 {
6602            return Ok(None);
6603        }
6604
6605        // Last step must be a simple Name (the field to extract)
6606        let field_step = &steps[1];
6607        if !field_step.stages.is_empty() {
6608            return Ok(None);
6609        }
6610        let AstNode::Name(extract_field) = &field_step.node else {
6611            return Ok(None);
6612        };
6613
6614        // First step: Name with optional filter stage
6615        let arr_step = &steps[0];
6616        let AstNode::Name(arr_name) = &arr_step.node else {
6617            return Ok(None);
6618        };
6619
6620        // Get the source array from data
6621        let arr = match data {
6622            JValue::Object(obj) => match obj.get(arr_name) {
6623                Some(JValue::Array(arr)) => arr,
6624                _ => return Ok(None),
6625            },
6626            _ => return Ok(None),
6627        };
6628
6629        // Check for filter stage — try CompiledExpr for the predicate
6630        let filter_compiled = match arr_step.stages.as_slice() {
6631            [] => None,
6632            [Stage::Filter(pred)] => try_compile_expr(pred),
6633            _ => return Ok(None),
6634        };
6635        // If filter stage exists but wasn't compilable, bail out
6636        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6637            return Ok(None);
6638        }
6639
6640        // Build shape cache for the array
6641        let shape = arr.first().and_then(build_shape_cache);
6642
6643        // Fused iteration: filter (optional) + extract + aggregate
6644        let mut total = 0.0f64;
6645        let mut count = 0usize;
6646        let mut max_val = f64::NEG_INFINITY;
6647        let mut min_val = f64::INFINITY;
6648        let mut has_any = false;
6649
6650        for item in arr.iter() {
6651            // Apply compiled filter if present
6652            if let Some(ref compiled) = filter_compiled {
6653                let result = if let Some(ref s) = shape {
6654                    eval_compiled_shaped(compiled, item, None, s, &self.options, self.start_time)?
6655                } else {
6656                    eval_compiled(compiled, item, None, &self.options, self.start_time)?
6657                };
6658                if !compiled_is_truthy(&result) {
6659                    continue;
6660                }
6661            }
6662
6663            // Extract field value
6664            let val = match item {
6665                JValue::Object(obj) => match obj.get(extract_field) {
6666                    Some(JValue::Number(n)) => *n,
6667                    Some(_) | None => continue, // Skip non-numeric / missing
6668                },
6669                _ => continue,
6670            };
6671
6672            has_any = true;
6673            match name {
6674                "sum" => total += val,
6675                "max" => max_val = max_val.max(val),
6676                "min" => min_val = min_val.min(val),
6677                "average" => {
6678                    total += val;
6679                    count += 1;
6680                }
6681                _ => unreachable!(),
6682            }
6683        }
6684
6685        if !has_any {
6686            return Ok(Some(match name {
6687                "sum" => JValue::from(0i64),
6688                "average" | "max" | "min" => JValue::Null,
6689                _ => unreachable!(),
6690            }));
6691        }
6692
6693        Ok(Some(match name {
6694            "sum" => JValue::Number(total),
6695            "max" => JValue::Number(max_val),
6696            "min" => JValue::Number(min_val),
6697            "average" => JValue::Number(total / count as f64),
6698            _ => unreachable!(),
6699        }))
6700    }
6701
6702    /// Evaluate a function call
6703    fn evaluate_function_call(
6704        &mut self,
6705        name: &str,
6706        args: &[AstNode],
6707        is_builtin: bool,
6708        data: &JValue,
6709    ) -> Result<JValue, EvaluatorError> {
6710        use crate::functions;
6711
6712        // Check for partial application (any argument is a Placeholder)
6713        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6714        if has_placeholder {
6715            return self.create_partial_application(name, args, is_builtin, data);
6716        }
6717
6718        // FIRST check if this variable holds a function value (lambda or builtin reference)
6719        // This is critical for:
6720        // 1. Allowing function parameters to shadow stored lambdas
6721        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
6722        // 2. Calling built-in functions passed as parameters
6723        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
6724        if let Some(value) = self.context.lookup(name).cloned() {
6725            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
6726                let mut evaluated_args = Vec::with_capacity(args.len());
6727                for arg in args {
6728                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6729                }
6730                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6731            }
6732            if let JValue::Builtin { name: builtin_name } = &value {
6733                // This is a built-in function reference (e.g., $f bound to $sum)
6734                let mut evaluated_args = Vec::with_capacity(args.len());
6735                for arg in args {
6736                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6737                }
6738                return self.call_builtin_with_values(builtin_name, &evaluated_args);
6739            }
6740        }
6741
6742        // THEN check if this is a stored lambda (user-defined function by name)
6743        // This only applies if not shadowed by a binding above
6744        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
6745            let mut evaluated_args = Vec::with_capacity(args.len());
6746            for arg in args {
6747                evaluated_args.push(self.evaluate_internal(arg, data)?);
6748            }
6749            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6750        }
6751
6752        // If the function was called without $ prefix and it's not a stored lambda,
6753        // it's an error (unknown function without $ prefix)
6754        if !is_builtin && name != "__lambda__" {
6755            return Err(EvaluatorError::ReferenceError(format!(
6756                "Unknown function: {}",
6757                name
6758            )));
6759        }
6760
6761        // Special handling for $exists function
6762        // It needs to know if the argument is explicit null vs undefined
6763        if name == "exists" && args.len() == 1 {
6764            let arg = &args[0];
6765
6766            // Check if it's an explicit null literal
6767            if matches!(arg, AstNode::Null) {
6768                return Ok(JValue::Bool(true)); // Explicit null exists
6769            }
6770
6771            // Check if it's a function reference
6772            if let AstNode::Variable(var_name) = arg {
6773                if self.is_builtin_function(var_name) {
6774                    return Ok(JValue::Bool(true)); // Built-in function exists
6775                }
6776
6777                // Check if it's a stored lambda
6778                if self.context.lookup_lambda(var_name).is_some() {
6779                    return Ok(JValue::Bool(true)); // Lambda exists
6780                }
6781
6782                // Check if the variable is defined
6783                if let Some(val) = self.context.lookup(var_name) {
6784                    // A variable bound to the undefined marker doesn't "exist"
6785                    if val.is_undefined() {
6786                        return Ok(JValue::Bool(false));
6787                    }
6788                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
6789                } else {
6790                    return Ok(JValue::Bool(false)); // Variable is undefined
6791                }
6792            }
6793
6794            // For other expressions, evaluate and check if non-null/non-undefined
6795            let value = self.evaluate_internal(arg, data)?;
6796            return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
6797        }
6798
6799        // Check if any arguments are undefined variables or undefined paths
6800        // Functions like $not() should return undefined when given undefined values
6801        for arg in args {
6802            // Check for undefined variable (e.g., $undefined_var)
6803            if let AstNode::Variable(var_name) = arg {
6804                // Skip built-in function names - they're function references, not undefined variables
6805                if !var_name.is_empty()
6806                    && !self.is_builtin_function(var_name)
6807                    && self.context.lookup(var_name).is_none()
6808                {
6809                    // Undefined variable - for functions that should propagate undefined
6810                    if propagates_undefined(name) {
6811                        return Ok(JValue::Null); // Return undefined
6812                    }
6813                }
6814            }
6815            // Check for simple field name (e.g., blah) that evaluates to undefined
6816            if let AstNode::Name(field_name) = arg {
6817                let field_exists =
6818                    matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
6819                if !field_exists && propagates_undefined(name) {
6820                    return Ok(JValue::Null);
6821                }
6822            }
6823            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
6824            // Field accesses are represented as AstNode::Path. String literals should never
6825            // be checked for undefined propagation.
6826            // Check for Path expressions that evaluate to undefined
6827            if let AstNode::Path { steps } = arg {
6828                // For paths that evaluate to null, we need to determine if it's because:
6829                // 1. A field doesn't exist (undefined) - should propagate as undefined
6830                // 2. A field exists with value null - should throw T0410
6831                //
6832                // We can distinguish these by checking if the path is accessing a field
6833                // that doesn't exist on an object vs one that has an explicit null value.
6834                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
6835                    // Path evaluated to null - now check if it's truly undefined
6836                    // For single-step paths, check if the field exists
6837                    if steps.len() == 1 {
6838                        // Get field name - could be Name (identifier) or String (quoted)
6839                        let field_name = match &steps[0].node {
6840                            AstNode::Name(n) => Some(n.as_str()),
6841                            AstNode::String(s) => Some(s.as_str()),
6842                            _ => None,
6843                        };
6844                        if let Some(field) = field_name {
6845                            match data {
6846                                JValue::Object(obj) => {
6847                                    if !obj.contains_key(field) {
6848                                        // Field doesn't exist - return undefined
6849                                        if propagates_undefined(name) {
6850                                            return Ok(JValue::Null);
6851                                        }
6852                                    }
6853                                    // Field exists with value null - continue to throw T0410
6854                                }
6855                                // Trying to access field on null data - return undefined
6856                                JValue::Null if propagates_undefined(name) => {
6857                                    return Ok(JValue::Null);
6858                                }
6859                                _ => {}
6860                            }
6861                        }
6862                    }
6863                    // For multi-step paths, check if any intermediate step failed
6864                    else if steps.len() > 1 {
6865                        // Evaluate each step to find where it breaks
6866                        let mut current = data;
6867                        let mut failed_due_to_missing_field = false;
6868
6869                        for (i, step) in steps.iter().enumerate() {
6870                            if let AstNode::Name(field_name) = &step.node {
6871                                match current {
6872                                    JValue::Object(obj) => {
6873                                        if let Some(val) = obj.get(field_name) {
6874                                            current = val;
6875                                        } else {
6876                                            // Field doesn't exist
6877                                            failed_due_to_missing_field = true;
6878                                            break;
6879                                        }
6880                                    }
6881                                    JValue::Array(_) => {
6882                                        // Array access - evaluate normally
6883                                        break;
6884                                    }
6885                                    JValue::Null => {
6886                                        // Hit null in the middle of the path
6887                                        if i > 0 {
6888                                            // Previous field had null value - not undefined
6889                                            failed_due_to_missing_field = false;
6890                                        }
6891                                        break;
6892                                    }
6893                                    _ => break,
6894                                }
6895                            }
6896                        }
6897
6898                        if failed_due_to_missing_field && propagates_undefined(name) {
6899                            return Ok(JValue::Null);
6900                        }
6901                    }
6902                }
6903            }
6904        }
6905
6906        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
6907        // try to fuse filter+extract+aggregate into a single pass.
6908        if args.len() == 1 {
6909            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
6910                return Ok(result);
6911            }
6912        }
6913
6914        let mut evaluated_args = Vec::with_capacity(args.len());
6915        for arg in args {
6916            evaluated_args.push(self.evaluate_internal(arg, data)?);
6917        }
6918
6919        // JSONata feature: when a function is called with no arguments but expects
6920        // at least one, use the current context value (data) as the implicit first argument
6921        // This also applies when functions expecting N arguments receive N-1 arguments,
6922        // in which case the context value becomes the first argument
6923        let context_functions_zero_arg = [
6924            "string",
6925            "number",
6926            "boolean",
6927            "uppercase",
6928            "lowercase",
6929            "fromMillis",
6930        ];
6931        let context_functions_missing_first = [
6932            "substringBefore",
6933            "substringAfter",
6934            "contains",
6935            "split",
6936            "replace",
6937        ];
6938
6939        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
6940            // Use the current context value as the implicit argument
6941            evaluated_args.push(data.clone());
6942        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
6943            // These functions expect 2+ arguments, but received 1
6944            // Only insert context if it's a compatible type (string for string functions)
6945            // Otherwise, let the function throw T0411 for wrong argument count
6946            if matches!(data, JValue::String(_)) {
6947                evaluated_args.insert(0, data.clone());
6948            }
6949        }
6950
6951        // Special handling for $string() with no explicit arguments
6952        // After context insertion, check if the argument is null (undefined context)
6953        if name == "string"
6954            && args.is_empty()
6955            && !evaluated_args.is_empty()
6956            && evaluated_args[0].is_null()
6957        {
6958            // Context was null/undefined, so return undefined
6959            return Ok(JValue::Null);
6960        }
6961
6962        match name {
6963            "string" => {
6964                if evaluated_args.len() > 2 {
6965                    return Err(EvaluatorError::EvaluationError(
6966                        "string() takes at most 2 arguments".to_string(),
6967                    ));
6968                }
6969
6970                let prettify = if evaluated_args.len() == 2 {
6971                    match &evaluated_args[1] {
6972                        JValue::Bool(b) => Some(*b),
6973                        _ => {
6974                            return Err(EvaluatorError::TypeError(
6975                                "string() prettify parameter must be a boolean".to_string(),
6976                            ))
6977                        }
6978                    }
6979                } else {
6980                    None
6981                };
6982
6983                Ok(functions::string::string(&evaluated_args[0], prettify)?)
6984            }
6985            "length" => {
6986                if evaluated_args.len() != 1 {
6987                    return Err(EvaluatorError::EvaluationError(
6988                        "length() requires exactly 1 argument".to_string(),
6989                    ));
6990                }
6991                match &evaluated_args[0] {
6992                    JValue::String(s) => Ok(functions::string::length(s)?),
6993                    _ => Err(EvaluatorError::TypeError(
6994                        "T0410: Argument 1 of function length does not match function signature"
6995                            .to_string(),
6996                    )),
6997                }
6998            }
6999            "uppercase" => {
7000                if evaluated_args.len() != 1 {
7001                    return Err(EvaluatorError::EvaluationError(
7002                        "uppercase() requires exactly 1 argument".to_string(),
7003                    ));
7004                }
7005                if evaluated_args[0].is_undefined() {
7006                    return Ok(JValue::Undefined);
7007                }
7008                match &evaluated_args[0] {
7009                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
7010                    _ => Err(EvaluatorError::TypeError(
7011                        "T0410: Argument 1 of function uppercase does not match function signature"
7012                            .to_string(),
7013                    )),
7014                }
7015            }
7016            "lowercase" => {
7017                if evaluated_args.len() != 1 {
7018                    return Err(EvaluatorError::EvaluationError(
7019                        "lowercase() requires exactly 1 argument".to_string(),
7020                    ));
7021                }
7022                if evaluated_args[0].is_undefined() {
7023                    return Ok(JValue::Undefined);
7024                }
7025                match &evaluated_args[0] {
7026                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
7027                    _ => Err(EvaluatorError::TypeError(
7028                        "T0410: Argument 1 of function lowercase does not match function signature"
7029                            .to_string(),
7030                    )),
7031                }
7032            }
7033            "number" => {
7034                if evaluated_args.is_empty() {
7035                    return Err(EvaluatorError::EvaluationError(
7036                        "number() requires at least 1 argument".to_string(),
7037                    ));
7038                }
7039                if evaluated_args.len() > 1 {
7040                    return Err(EvaluatorError::TypeError(
7041                        "T0410: Argument 2 of function number does not match function signature"
7042                            .to_string(),
7043                    ));
7044                }
7045                if evaluated_args[0].is_undefined() {
7046                    return Ok(JValue::Undefined);
7047                }
7048                Ok(functions::numeric::number(&evaluated_args[0])?)
7049            }
7050            "sum" => {
7051                if evaluated_args.len() != 1 {
7052                    return Err(EvaluatorError::EvaluationError(
7053                        "sum() requires exactly 1 argument".to_string(),
7054                    ));
7055                }
7056                // Return undefined if argument is undefined
7057                if evaluated_args[0].is_undefined() {
7058                    return Ok(JValue::Undefined);
7059                }
7060                match &evaluated_args[0] {
7061                    JValue::Null => Ok(JValue::Null),
7062                    JValue::Array(arr) => {
7063                        // Use zero-clone iterator-based sum
7064                        Ok(aggregation::sum(arr)?)
7065                    }
7066                    // Non-array values: extract number directly
7067                    JValue::Number(n) => Ok(JValue::Number(*n)),
7068                    other => Ok(functions::numeric::sum(&[other.clone()])?),
7069                }
7070            }
7071            "count" => {
7072                if evaluated_args.len() != 1 {
7073                    return Err(EvaluatorError::EvaluationError(
7074                        "count() requires exactly 1 argument".to_string(),
7075                    ));
7076                }
7077                // Return 0 if argument is undefined
7078                if evaluated_args[0].is_undefined() {
7079                    return Ok(JValue::from(0i64));
7080                }
7081                match &evaluated_args[0] {
7082                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
7083                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
7084                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
7085                }
7086            }
7087            "substring" => {
7088                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7089                    return Err(EvaluatorError::EvaluationError(
7090                        "substring() requires 2 or 3 arguments".to_string(),
7091                    ));
7092                }
7093                if evaluated_args[0].is_undefined() {
7094                    return Ok(JValue::Undefined);
7095                }
7096                match (&evaluated_args[0], &evaluated_args[1]) {
7097                    (JValue::String(s), JValue::Number(start)) => {
7098                        let length = if evaluated_args.len() == 3 {
7099                            match &evaluated_args[2] {
7100                                JValue::Number(l) => Some(*l as i64),
7101                                _ => return Err(EvaluatorError::TypeError(
7102                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
7103                                )),
7104                            }
7105                        } else {
7106                            None
7107                        };
7108                        Ok(functions::string::substring(s, *start as i64, length)?)
7109                    }
7110                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7111                        "T0410: Argument 2 of function substring does not match function signature"
7112                            .to_string(),
7113                    )),
7114                    _ => Err(EvaluatorError::TypeError(
7115                        "T0410: Argument 1 of function substring does not match function signature"
7116                            .to_string(),
7117                    )),
7118                }
7119            }
7120            "substringBefore" => {
7121                if evaluated_args.len() != 2 {
7122                    return Err(EvaluatorError::TypeError(
7123                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
7124                    ));
7125                }
7126                if evaluated_args[0].is_undefined() {
7127                    return Ok(JValue::Undefined);
7128                }
7129                match (&evaluated_args[0], &evaluated_args[1]) {
7130                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
7131                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7132                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
7133                    )),
7134                    _ => Err(EvaluatorError::TypeError(
7135                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
7136                    )),
7137                }
7138            }
7139            "substringAfter" => {
7140                if evaluated_args.len() != 2 {
7141                    return Err(EvaluatorError::TypeError(
7142                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
7143                    ));
7144                }
7145                if evaluated_args[0].is_undefined() {
7146                    return Ok(JValue::Undefined);
7147                }
7148                match (&evaluated_args[0], &evaluated_args[1]) {
7149                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
7150                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7151                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
7152                    )),
7153                    _ => Err(EvaluatorError::TypeError(
7154                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
7155                    )),
7156                }
7157            }
7158            "pad" => {
7159                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7160                    return Err(EvaluatorError::EvaluationError(
7161                        "pad() requires 2 or 3 arguments".to_string(),
7162                    ));
7163                }
7164
7165                // First argument: string to pad
7166                let string = match &evaluated_args[0] {
7167                    JValue::String(s) => s.clone(),
7168                    JValue::Null => return Ok(JValue::Null),
7169                    JValue::Undefined => return Ok(JValue::Undefined),
7170                    _ => {
7171                        return Err(EvaluatorError::TypeError(
7172                            "pad() first argument must be a string".to_string(),
7173                        ))
7174                    }
7175                };
7176
7177                // Second argument: width (negative = left pad, positive = right pad)
7178                let width = match &evaluated_args.get(1) {
7179                    Some(JValue::Number(n)) => *n as i32,
7180                    _ => {
7181                        return Err(EvaluatorError::TypeError(
7182                            "pad() second argument must be a number".to_string(),
7183                        ))
7184                    }
7185                };
7186
7187                // Third argument: padding string (optional, defaults to space)
7188                let pad_string = match evaluated_args.get(2) {
7189                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
7190                    _ => Rc::from(" "),
7191                };
7192
7193                let abs_width = width.unsigned_abs() as usize;
7194                // Count Unicode characters (code points), not bytes
7195                let char_count = string.chars().count();
7196
7197                if char_count >= abs_width {
7198                    // String is already long enough
7199                    return Ok(JValue::string(string));
7200                }
7201
7202                let padding_needed = abs_width - char_count;
7203
7204                let pad_chars: Vec<char> = pad_string.chars().collect();
7205                let mut padding = String::with_capacity(padding_needed);
7206                for i in 0..padding_needed {
7207                    padding.push(pad_chars[i % pad_chars.len()]);
7208                }
7209
7210                let result = if width < 0 {
7211                    // Left pad (negative width)
7212                    format!("{}{}", padding, string)
7213                } else {
7214                    // Right pad (positive width)
7215                    format!("{}{}", string, padding)
7216                };
7217
7218                Ok(JValue::string(result))
7219            }
7220
7221            "trim" => {
7222                if evaluated_args.is_empty() {
7223                    return Ok(JValue::Null); // undefined
7224                }
7225                if evaluated_args.len() != 1 {
7226                    return Err(EvaluatorError::EvaluationError(
7227                        "trim() requires at most 1 argument".to_string(),
7228                    ));
7229                }
7230                match &evaluated_args[0] {
7231                    JValue::Null => Ok(JValue::Null),
7232                    JValue::String(s) => Ok(functions::string::trim(s)?),
7233                    _ => Err(EvaluatorError::TypeError(
7234                        "trim() requires a string argument".to_string(),
7235                    )),
7236                }
7237            }
7238            "contains" => {
7239                if evaluated_args.len() != 2 {
7240                    return Err(EvaluatorError::EvaluationError(
7241                        "contains() requires exactly 2 arguments".to_string(),
7242                    ));
7243                }
7244                if evaluated_args[0].is_null() {
7245                    return Ok(JValue::Null);
7246                }
7247                if evaluated_args[0].is_undefined() {
7248                    return Ok(JValue::Undefined);
7249                }
7250                match &evaluated_args[0] {
7251                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
7252                    _ => Err(EvaluatorError::TypeError(
7253                        "contains() requires a string as the first argument".to_string(),
7254                    )),
7255                }
7256            }
7257            "split" => {
7258                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7259                    return Err(EvaluatorError::EvaluationError(
7260                        "split() requires 2 or 3 arguments".to_string(),
7261                    ));
7262                }
7263                if evaluated_args[0].is_null() {
7264                    return Ok(JValue::Null);
7265                }
7266                if evaluated_args[0].is_undefined() {
7267                    return Ok(JValue::Undefined);
7268                }
7269                match &evaluated_args[0] {
7270                    JValue::String(s) => {
7271                        let limit = if evaluated_args.len() == 3 {
7272                            match &evaluated_args[2] {
7273                                JValue::Number(n) => {
7274                                    let f = *n;
7275                                    // Negative limit is an error
7276                                    if f < 0.0 {
7277                                        return Err(EvaluatorError::EvaluationError(
7278                                            "D3020: Third argument of split function must be a positive number".to_string(),
7279                                        ));
7280                                    }
7281                                    // Floor the value for non-integer limits
7282                                    Some(f.floor() as usize)
7283                                }
7284                                _ => {
7285                                    return Err(EvaluatorError::TypeError(
7286                                        "split() limit must be a number".to_string(),
7287                                    ))
7288                                }
7289                            }
7290                        } else {
7291                            None
7292                        };
7293                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
7294                    }
7295                    _ => Err(EvaluatorError::TypeError(
7296                        "split() requires a string as the first argument".to_string(),
7297                    )),
7298                }
7299            }
7300            "join" => {
7301                // Special case: if first arg is undefined, return undefined
7302                // But if separator (2nd arg) is undefined, use empty string (default)
7303                if evaluated_args.is_empty() {
7304                    return Err(EvaluatorError::TypeError(
7305                        "T0410: Argument 1 of function $join does not match function signature"
7306                            .to_string(),
7307                    ));
7308                }
7309                if evaluated_args[0].is_null() {
7310                    return Ok(JValue::Null);
7311                }
7312                if evaluated_args[0].is_undefined() {
7313                    return Ok(JValue::Undefined);
7314                }
7315
7316                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
7317                // The signature handles coercion and validation
7318                use crate::signature::Signature;
7319
7320                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
7321                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7322                })?;
7323
7324                let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
7325                    Ok(args) => args,
7326                    Err(crate::signature::SignatureError::UndefinedArgument) => {
7327                        // This can happen if the separator is undefined
7328                        // In that case, just validate the first arg and use default separator
7329                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
7330                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7331                        })?;
7332
7333                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
7334                            Ok(args) => args,
7335                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
7336                                index,
7337                                expected,
7338                            }) => {
7339                                return Err(EvaluatorError::TypeError(format!(
7340                                    "T0412: Argument {} of function $join must be an array of {}",
7341                                    index, expected
7342                                )));
7343                            }
7344                            Err(e) => {
7345                                return Err(EvaluatorError::TypeError(format!(
7346                                    "Signature validation failed: {}",
7347                                    e
7348                                )));
7349                            }
7350                        }
7351                    }
7352                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
7353                        index,
7354                        expected,
7355                    }) => {
7356                        return Err(EvaluatorError::TypeError(
7357                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
7358                        ));
7359                    }
7360                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
7361                        index,
7362                        expected,
7363                    }) => {
7364                        return Err(EvaluatorError::TypeError(format!(
7365                            "T0412: Argument {} of function $join must be an array of {}",
7366                            index, expected
7367                        )));
7368                    }
7369                    Err(e) => {
7370                        return Err(EvaluatorError::TypeError(format!(
7371                            "Signature validation failed: {}",
7372                            e
7373                        )));
7374                    }
7375                };
7376
7377                // After coercion, first arg is guaranteed to be an array of strings
7378                match &coerced_args[0] {
7379                    JValue::Array(arr) => {
7380                        let separator = if coerced_args.len() == 2 {
7381                            match &coerced_args[1] {
7382                                JValue::String(s) => Some(&**s),
7383                                JValue::Null => None, // Undefined separator -> use empty string
7384                                _ => None,            // Signature should have validated this
7385                            }
7386                        } else {
7387                            None // No separator provided -> use empty string
7388                        };
7389                        Ok(functions::string::join(arr, separator)?)
7390                    }
7391                    JValue::Null => Ok(JValue::Null),
7392                    _ => unreachable!("Signature validation should ensure array type"),
7393                }
7394            }
7395            "replace" => {
7396                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
7397                    return Err(EvaluatorError::EvaluationError(
7398                        "replace() requires 3 or 4 arguments".to_string(),
7399                    ));
7400                }
7401                if evaluated_args[0].is_null() {
7402                    return Ok(JValue::Null);
7403                }
7404                if evaluated_args[0].is_undefined() {
7405                    return Ok(JValue::Undefined);
7406                }
7407
7408                // Check if replacement (3rd arg) is a function/lambda
7409                let replacement_is_lambda = matches!(
7410                    evaluated_args[2],
7411                    JValue::Lambda { .. } | JValue::Builtin { .. }
7412                );
7413
7414                if replacement_is_lambda {
7415                    // Lambda replacement mode
7416                    return self.replace_with_lambda(
7417                        &evaluated_args[0],
7418                        &evaluated_args[1],
7419                        &evaluated_args[2],
7420                        if evaluated_args.len() == 4 {
7421                            Some(&evaluated_args[3])
7422                        } else {
7423                            None
7424                        },
7425                        data,
7426                    );
7427                }
7428
7429                // String replacement mode
7430                match (&evaluated_args[0], &evaluated_args[2]) {
7431                    (JValue::String(s), JValue::String(replacement)) => {
7432                        let limit = if evaluated_args.len() == 4 {
7433                            match &evaluated_args[3] {
7434                                JValue::Number(n) => {
7435                                    let lim_f64 = *n;
7436                                    if lim_f64 < 0.0 {
7437                                        return Err(EvaluatorError::EvaluationError(format!(
7438                                            "D3011: Limit must be non-negative, got {}",
7439                                            lim_f64
7440                                        )));
7441                                    }
7442                                    Some(lim_f64 as usize)
7443                                }
7444                                _ => {
7445                                    return Err(EvaluatorError::TypeError(
7446                                        "replace() limit must be a number".to_string(),
7447                                    ))
7448                                }
7449                            }
7450                        } else {
7451                            None
7452                        };
7453                        Ok(functions::string::replace(
7454                            s,
7455                            &evaluated_args[1],
7456                            replacement,
7457                            limit,
7458                        )?)
7459                    }
7460                    _ => Err(EvaluatorError::TypeError(
7461                        "replace() requires string arguments".to_string(),
7462                    )),
7463                }
7464            }
7465            "match" => {
7466                // $match(str, pattern [, limit])
7467                // Returns array of match objects for regex matches or custom matcher function
7468                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7469                    return Err(EvaluatorError::EvaluationError(
7470                        "match() requires 1 to 3 arguments".to_string(),
7471                    ));
7472                }
7473                if evaluated_args[0].is_null() {
7474                    return Ok(JValue::Null);
7475                }
7476                if evaluated_args[0].is_undefined() {
7477                    return Ok(JValue::Undefined);
7478                }
7479
7480                let s = match &evaluated_args[0] {
7481                    JValue::String(s) => s.clone(),
7482                    _ => {
7483                        return Err(EvaluatorError::TypeError(
7484                            "match() first argument must be a string".to_string(),
7485                        ))
7486                    }
7487                };
7488
7489                // Get optional limit
7490                let limit = if evaluated_args.len() == 3 {
7491                    match &evaluated_args[2] {
7492                        JValue::Number(n) => Some(*n as usize),
7493                        JValue::Null => None,
7494                        _ => {
7495                            return Err(EvaluatorError::TypeError(
7496                                "match() limit must be a number".to_string(),
7497                            ))
7498                        }
7499                    }
7500                } else {
7501                    None
7502                };
7503
7504                // Check if second argument is a custom matcher function (lambda)
7505                let pattern_value = evaluated_args.get(1);
7506                let is_custom_matcher = pattern_value.is_some_and(|val| {
7507                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7508                });
7509
7510                if is_custom_matcher {
7511                    // Custom matcher function support
7512                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7513                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
7514                }
7515
7516                // Get regex pattern from second argument
7517                let (pattern, flags) = match pattern_value {
7518                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7519                        EvaluatorError::TypeError(
7520                            "match() second argument must be a regex pattern or matcher function"
7521                                .to_string(),
7522                        )
7523                    })?,
7524                    None => (".*".to_string(), "".to_string()),
7525                };
7526
7527                // Build regex
7528                let is_global = flags.contains('g');
7529                let regex_pattern = if flags.contains('i') {
7530                    format!("(?i){}", pattern)
7531                } else {
7532                    pattern.clone()
7533                };
7534
7535                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
7536                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7537                })?;
7538
7539                let mut results = Vec::new();
7540                let mut count = 0;
7541
7542                for caps in re.captures_iter(&s) {
7543                    if let Some(lim) = limit {
7544                        if count >= lim {
7545                            break;
7546                        }
7547                    }
7548
7549                    let full_match = caps.get(0).unwrap();
7550                    let mut match_obj = IndexMap::new();
7551                    match_obj.insert(
7552                        "match".to_string(),
7553                        JValue::string(full_match.as_str().to_string()),
7554                    );
7555                    match_obj.insert(
7556                        "index".to_string(),
7557                        JValue::Number(full_match.start() as f64),
7558                    );
7559
7560                    // Collect capture groups
7561                    let mut groups: Vec<JValue> = Vec::new();
7562                    for i in 1..caps.len() {
7563                        if let Some(group) = caps.get(i) {
7564                            groups.push(JValue::string(group.as_str().to_string()));
7565                        } else {
7566                            groups.push(JValue::Null);
7567                        }
7568                    }
7569                    if !groups.is_empty() {
7570                        match_obj.insert("groups".to_string(), JValue::array(groups));
7571                    }
7572
7573                    results.push(JValue::object(match_obj));
7574                    count += 1;
7575
7576                    // If not global, only return first match
7577                    if !is_global {
7578                        break;
7579                    }
7580                }
7581
7582                if results.is_empty() {
7583                    Ok(JValue::Null)
7584                } else if results.len() == 1 && !is_global {
7585                    // Single match (non-global) returns the match object directly
7586                    Ok(results.into_iter().next().unwrap())
7587                } else {
7588                    Ok(JValue::array(results))
7589                }
7590            }
7591            "max" => {
7592                if evaluated_args.len() != 1 {
7593                    return Err(EvaluatorError::EvaluationError(
7594                        "max() requires exactly 1 argument".to_string(),
7595                    ));
7596                }
7597                // Check for undefined
7598                if evaluated_args[0].is_undefined() {
7599                    return Ok(JValue::Undefined);
7600                }
7601                match &evaluated_args[0] {
7602                    JValue::Null => Ok(JValue::Null),
7603                    JValue::Array(arr) => {
7604                        // Use zero-clone iterator-based max
7605                        Ok(aggregation::max(arr)?)
7606                    }
7607                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7608                    _ => Err(EvaluatorError::TypeError(
7609                        "max() requires an array or number argument".to_string(),
7610                    )),
7611                }
7612            }
7613            "min" => {
7614                if evaluated_args.len() != 1 {
7615                    return Err(EvaluatorError::EvaluationError(
7616                        "min() requires exactly 1 argument".to_string(),
7617                    ));
7618                }
7619                // Check for undefined
7620                if evaluated_args[0].is_undefined() {
7621                    return Ok(JValue::Undefined);
7622                }
7623                match &evaluated_args[0] {
7624                    JValue::Null => Ok(JValue::Null),
7625                    JValue::Array(arr) => {
7626                        // Use zero-clone iterator-based min
7627                        Ok(aggregation::min(arr)?)
7628                    }
7629                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7630                    _ => Err(EvaluatorError::TypeError(
7631                        "min() requires an array or number argument".to_string(),
7632                    )),
7633                }
7634            }
7635            "average" => {
7636                if evaluated_args.len() != 1 {
7637                    return Err(EvaluatorError::EvaluationError(
7638                        "average() requires exactly 1 argument".to_string(),
7639                    ));
7640                }
7641                // Return undefined if argument is undefined
7642                if evaluated_args[0].is_undefined() {
7643                    return Ok(JValue::Undefined);
7644                }
7645                match &evaluated_args[0] {
7646                    JValue::Null => Ok(JValue::Null),
7647                    JValue::Array(arr) => {
7648                        // Use zero-clone iterator-based average
7649                        Ok(aggregation::average(arr)?)
7650                    }
7651                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7652                    _ => Err(EvaluatorError::TypeError(
7653                        "average() requires an array or number argument".to_string(),
7654                    )),
7655                }
7656            }
7657            "abs" => {
7658                if evaluated_args.len() != 1 {
7659                    return Err(EvaluatorError::EvaluationError(
7660                        "abs() requires exactly 1 argument".to_string(),
7661                    ));
7662                }
7663                match &evaluated_args[0] {
7664                    JValue::Null => Ok(JValue::Null),
7665                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
7666                    _ => Err(EvaluatorError::TypeError(
7667                        "abs() requires a number argument".to_string(),
7668                    )),
7669                }
7670            }
7671            "floor" => {
7672                if evaluated_args.len() != 1 {
7673                    return Err(EvaluatorError::EvaluationError(
7674                        "floor() requires exactly 1 argument".to_string(),
7675                    ));
7676                }
7677                match &evaluated_args[0] {
7678                    JValue::Null => Ok(JValue::Null),
7679                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
7680                    _ => Err(EvaluatorError::TypeError(
7681                        "floor() requires a number argument".to_string(),
7682                    )),
7683                }
7684            }
7685            "ceil" => {
7686                if evaluated_args.len() != 1 {
7687                    return Err(EvaluatorError::EvaluationError(
7688                        "ceil() requires exactly 1 argument".to_string(),
7689                    ));
7690                }
7691                match &evaluated_args[0] {
7692                    JValue::Null => Ok(JValue::Null),
7693                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
7694                    _ => Err(EvaluatorError::TypeError(
7695                        "ceil() requires a number argument".to_string(),
7696                    )),
7697                }
7698            }
7699            "round" => {
7700                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7701                    return Err(EvaluatorError::EvaluationError(
7702                        "round() requires 1 or 2 arguments".to_string(),
7703                    ));
7704                }
7705                match &evaluated_args[0] {
7706                    JValue::Null => Ok(JValue::Null),
7707                    JValue::Number(n) => {
7708                        let precision = if evaluated_args.len() == 2 {
7709                            match &evaluated_args[1] {
7710                                JValue::Number(p) => Some(*p as i32),
7711                                _ => {
7712                                    return Err(EvaluatorError::TypeError(
7713                                        "round() precision must be a number".to_string(),
7714                                    ))
7715                                }
7716                            }
7717                        } else {
7718                            None
7719                        };
7720                        Ok(functions::numeric::round(*n, precision)?)
7721                    }
7722                    _ => Err(EvaluatorError::TypeError(
7723                        "round() requires a number argument".to_string(),
7724                    )),
7725                }
7726            }
7727            "sqrt" => {
7728                if evaluated_args.len() != 1 {
7729                    return Err(EvaluatorError::EvaluationError(
7730                        "sqrt() requires exactly 1 argument".to_string(),
7731                    ));
7732                }
7733                match &evaluated_args[0] {
7734                    JValue::Null => Ok(JValue::Null),
7735                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
7736                    _ => Err(EvaluatorError::TypeError(
7737                        "sqrt() requires a number argument".to_string(),
7738                    )),
7739                }
7740            }
7741            "power" => {
7742                if evaluated_args.len() != 2 {
7743                    return Err(EvaluatorError::EvaluationError(
7744                        "power() requires exactly 2 arguments".to_string(),
7745                    ));
7746                }
7747                if evaluated_args[0].is_null() {
7748                    return Ok(JValue::Null);
7749                }
7750                if evaluated_args[0].is_undefined() {
7751                    return Ok(JValue::Undefined);
7752                }
7753                match (&evaluated_args[0], &evaluated_args[1]) {
7754                    (JValue::Number(base), JValue::Number(exp)) => {
7755                        Ok(functions::numeric::power(*base, *exp)?)
7756                    }
7757                    _ => Err(EvaluatorError::TypeError(
7758                        "power() requires number arguments".to_string(),
7759                    )),
7760                }
7761            }
7762            "formatNumber" => {
7763                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7764                    return Err(EvaluatorError::EvaluationError(
7765                        "formatNumber() requires 2 or 3 arguments".to_string(),
7766                    ));
7767                }
7768                if evaluated_args[0].is_null() {
7769                    return Ok(JValue::Null);
7770                }
7771                if evaluated_args[0].is_undefined() {
7772                    return Ok(JValue::Undefined);
7773                }
7774                match (&evaluated_args[0], &evaluated_args[1]) {
7775                    (JValue::Number(num), JValue::String(picture)) => {
7776                        let options = if evaluated_args.len() == 3 {
7777                            Some(&evaluated_args[2])
7778                        } else {
7779                            None
7780                        };
7781                        Ok(functions::numeric::format_number(*num, picture, options)?)
7782                    }
7783                    _ => Err(EvaluatorError::TypeError(
7784                        "formatNumber() requires a number and a string".to_string(),
7785                    )),
7786                }
7787            }
7788            "formatBase" => {
7789                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7790                    return Err(EvaluatorError::EvaluationError(
7791                        "formatBase() requires 1 or 2 arguments".to_string(),
7792                    ));
7793                }
7794                // Handle undefined input
7795                if evaluated_args[0].is_null() {
7796                    return Ok(JValue::Null);
7797                }
7798                if evaluated_args[0].is_undefined() {
7799                    return Ok(JValue::Undefined);
7800                }
7801                match &evaluated_args[0] {
7802                    JValue::Number(num) => {
7803                        let radix = if evaluated_args.len() == 2 {
7804                            match &evaluated_args[1] {
7805                                JValue::Number(r) => Some(r.trunc() as i64),
7806                                _ => {
7807                                    return Err(EvaluatorError::TypeError(
7808                                        "formatBase() radix must be a number".to_string(),
7809                                    ))
7810                                }
7811                            }
7812                        } else {
7813                            None
7814                        };
7815                        Ok(functions::numeric::format_base(*num, radix)?)
7816                    }
7817                    _ => Err(EvaluatorError::TypeError(
7818                        "formatBase() requires a number".to_string(),
7819                    )),
7820                }
7821            }
7822            "formatInteger" => {
7823                if evaluated_args.len() != 2 {
7824                    return Err(EvaluatorError::EvaluationError(
7825                        "formatInteger() requires exactly 2 arguments".to_string(),
7826                    ));
7827                }
7828                match (&evaluated_args[0], &evaluated_args[1]) {
7829                    (JValue::Number(n), JValue::String(picture)) => {
7830                        Ok(crate::datetime::format_integer(*n, picture)?)
7831                    }
7832                    (JValue::Null, _) => Ok(JValue::Null),
7833                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7834                    _ => Err(EvaluatorError::TypeError(
7835                        "formatInteger() requires a number and a string".to_string(),
7836                    )),
7837                }
7838            }
7839            "parseInteger" => {
7840                if evaluated_args.len() != 2 {
7841                    return Err(EvaluatorError::EvaluationError(
7842                        "parseInteger() requires exactly 2 arguments".to_string(),
7843                    ));
7844                }
7845                match (&evaluated_args[0], &evaluated_args[1]) {
7846                    (JValue::String(value), JValue::String(picture)) => {
7847                        Ok(crate::datetime::parse_integer(value, picture)?)
7848                    }
7849                    (JValue::Null, _) => Ok(JValue::Null),
7850                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7851                    _ => Err(EvaluatorError::TypeError(
7852                        "parseInteger() requires a string and a string".to_string(),
7853                    )),
7854                }
7855            }
7856            "append" => {
7857                if evaluated_args.len() != 2 {
7858                    return Err(EvaluatorError::EvaluationError(
7859                        "append() requires exactly 2 arguments".to_string(),
7860                    ));
7861                }
7862                // Handle null/undefined arguments
7863                let first = &evaluated_args[0];
7864                let second = &evaluated_args[1];
7865
7866                // If second arg is null/undefined, return first as-is (no change)
7867                if second.is_null() || second.is_undefined() {
7868                    return Ok(first.clone());
7869                }
7870
7871                // If first arg is null/undefined, return second as-is (appending to nothing gives second)
7872                if first.is_null() || first.is_undefined() {
7873                    return Ok(second.clone());
7874                }
7875
7876                // Convert both to arrays if needed, then append
7877                let arr = match first {
7878                    JValue::Array(a) => a.to_vec(),
7879                    other => vec![other.clone()], // Wrap non-array in array
7880                };
7881
7882                // Pre-check combined size before concatenating, mirroring
7883                // jsonata-js's append() (`arg1.length + arg2.length > options.sequence`).
7884                let second_len = match second {
7885                    JValue::Array(a) => a.len(),
7886                    _ => 1,
7887                };
7888                check_sequence_length(arr.len() + second_len, &self.options)?;
7889
7890                Ok(functions::array::append(&arr, second)?)
7891            }
7892            "reverse" => {
7893                if evaluated_args.len() != 1 {
7894                    return Err(EvaluatorError::EvaluationError(
7895                        "reverse() requires exactly 1 argument".to_string(),
7896                    ));
7897                }
7898                match &evaluated_args[0] {
7899                    JValue::Null => Ok(JValue::Null),
7900                    JValue::Undefined => Ok(JValue::Undefined),
7901                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
7902                    _ => Err(EvaluatorError::TypeError(
7903                        "reverse() requires an array argument".to_string(),
7904                    )),
7905                }
7906            }
7907            "shuffle" => {
7908                if evaluated_args.len() != 1 {
7909                    return Err(EvaluatorError::EvaluationError(
7910                        "shuffle() requires exactly 1 argument".to_string(),
7911                    ));
7912                }
7913                if evaluated_args[0].is_null() {
7914                    return Ok(JValue::Null);
7915                }
7916                if evaluated_args[0].is_undefined() {
7917                    return Ok(JValue::Undefined);
7918                }
7919                match &evaluated_args[0] {
7920                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
7921                    _ => Err(EvaluatorError::TypeError(
7922                        "shuffle() requires an array argument".to_string(),
7923                    )),
7924                }
7925            }
7926
7927            "sift" => {
7928                // $sift(object, function) or $sift(function) - filter object by predicate
7929                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7930                    return Err(EvaluatorError::EvaluationError(
7931                        "sift() requires 1 or 2 arguments".to_string(),
7932                    ));
7933                }
7934
7935                // Determine which argument is the function
7936                let func_arg = if evaluated_args.len() == 1 {
7937                    &args[0]
7938                } else {
7939                    &args[1]
7940                };
7941
7942                // Detect how many parameters the callback expects
7943                let param_count = self.get_callback_param_count(func_arg);
7944
7945                // Helper function to sift a single object
7946                let sift_object = |evaluator: &mut Self,
7947                                   obj: &IndexMap<String, JValue>,
7948                                   func_node: &AstNode,
7949                                   context_data: &JValue,
7950                                   param_count: usize|
7951                 -> Result<JValue, EvaluatorError> {
7952                    // Only create the object value if callback uses 3 parameters
7953                    let obj_value = if param_count >= 3 {
7954                        Some(JValue::object(obj.clone()))
7955                    } else {
7956                        None
7957                    };
7958
7959                    let mut result = IndexMap::new();
7960                    for (key, value) in obj.iter() {
7961                        // Build argument list based on what callback expects
7962                        let call_args = match param_count {
7963                            1 => vec![value.clone()],
7964                            2 => vec![value.clone(), JValue::string(key.clone())],
7965                            _ => vec![
7966                                value.clone(),
7967                                JValue::string(key.clone()),
7968                                obj_value.as_ref().unwrap().clone(),
7969                            ],
7970                        };
7971
7972                        let pred_result =
7973                            evaluator.apply_function(func_node, &call_args, context_data)?;
7974                        if evaluator.is_truthy(&pred_result) {
7975                            result.insert(key.clone(), value.clone());
7976                        }
7977                    }
7978                    // Return undefined for empty results (will be filtered by function application)
7979                    if result.is_empty() {
7980                        Ok(JValue::Undefined)
7981                    } else {
7982                        Ok(JValue::object(result))
7983                    }
7984                };
7985
7986                // Handle partial application - if only 1 arg, use current context as object
7987                if evaluated_args.len() == 1 {
7988                    // $sift(function) - use current context data as object
7989                    match data {
7990                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
7991                        JValue::Array(arr) => {
7992                            // Map sift over each object in the array
7993                            let mut results = Vec::new();
7994                            for item in arr.iter() {
7995                                if let JValue::Object(o) = item {
7996                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
7997                                    // sift_object returns undefined for empty results
7998                                    if !sifted.is_undefined() {
7999                                        results.push(sifted);
8000                                    }
8001                                }
8002                            }
8003                            Ok(JValue::array(results))
8004                        }
8005                        JValue::Null => Ok(JValue::Null),
8006                        _ => Ok(JValue::Undefined),
8007                    }
8008                } else {
8009                    // $sift(object, function)
8010                    match &evaluated_args[0] {
8011                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
8012                        JValue::Null => Ok(JValue::Null),
8013                        _ => Err(EvaluatorError::TypeError(
8014                            "sift() first argument must be an object".to_string(),
8015                        )),
8016                    }
8017                }
8018            }
8019
8020            "zip" => {
8021                if evaluated_args.is_empty() {
8022                    return Err(EvaluatorError::EvaluationError(
8023                        "zip() requires at least 1 argument".to_string(),
8024                    ));
8025                }
8026
8027                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
8028                // If any argument is null/undefined, return empty array
8029                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
8030                for arg in &evaluated_args {
8031                    match arg {
8032                        JValue::Array(arr) => {
8033                            if arr.is_empty() {
8034                                // Empty array means result is empty
8035                                return Ok(JValue::array(vec![]));
8036                            }
8037                            arrays.push(arr.to_vec());
8038                        }
8039                        JValue::Null | JValue::Undefined => {
8040                            // Null/undefined means result is empty
8041                            return Ok(JValue::array(vec![]));
8042                        }
8043                        other => {
8044                            // Wrap non-array values in single-element array
8045                            arrays.push(vec![other.clone()]);
8046                        }
8047                    }
8048                }
8049
8050                if arrays.is_empty() {
8051                    return Ok(JValue::array(vec![]));
8052                }
8053
8054                // Find the length of the shortest array
8055                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
8056
8057                // Zip the arrays together
8058                let mut result = Vec::with_capacity(min_len);
8059                for i in 0..min_len {
8060                    let mut tuple = Vec::with_capacity(arrays.len());
8061                    for array in &arrays {
8062                        tuple.push(array[i].clone());
8063                    }
8064                    result.push(JValue::array(tuple));
8065                }
8066
8067                Ok(JValue::array(result))
8068            }
8069
8070            "sort" => {
8071                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8072                    return Err(EvaluatorError::EvaluationError(
8073                        "sort() requires 1 or 2 arguments".to_string(),
8074                    ));
8075                }
8076
8077                // Use pre-evaluated first argument (avoid double evaluation)
8078                let array_value = &evaluated_args[0];
8079
8080                // Handle undefined input
8081                if array_value.is_null() {
8082                    return Ok(JValue::Null);
8083                }
8084                if array_value.is_undefined() {
8085                    return Ok(JValue::Undefined);
8086                }
8087
8088                let mut arr = match array_value {
8089                    JValue::Array(arr) => arr.to_vec(),
8090                    other => vec![other.clone()],
8091                };
8092
8093                if args.len() == 2 {
8094                    // Sort using the comparator from raw args (need unevaluated lambda AST)
8095                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
8096                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
8097                    Ok(JValue::array(arr))
8098                } else {
8099                    // Default sort (no comparator)
8100                    Ok(functions::array::sort(&arr)?)
8101                }
8102            }
8103            "distinct" => {
8104                if evaluated_args.len() != 1 {
8105                    return Err(EvaluatorError::EvaluationError(
8106                        "distinct() requires exactly 1 argument".to_string(),
8107                    ));
8108                }
8109                match &evaluated_args[0] {
8110                    JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
8111                    // Non-array input, and arrays of length <= 1, pass through
8112                    // unchanged (jsonata-js functions.js:
8113                    // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
8114                    other => Ok(other.clone()),
8115                }
8116            }
8117            "exists" => {
8118                if evaluated_args.len() != 1 {
8119                    return Err(EvaluatorError::EvaluationError(
8120                        "exists() requires exactly 1 argument".to_string(),
8121                    ));
8122                }
8123                Ok(functions::array::exists(&evaluated_args[0])?)
8124            }
8125            "keys" => {
8126                if evaluated_args.len() != 1 {
8127                    return Err(EvaluatorError::EvaluationError(
8128                        "keys() requires exactly 1 argument".to_string(),
8129                    ));
8130                }
8131
8132                // Helper to unwrap single-element arrays
8133                let unwrap_single = |keys: Vec<JValue>| -> JValue {
8134                    if keys.len() == 1 {
8135                        keys.into_iter().next().unwrap()
8136                    } else {
8137                        JValue::array(keys)
8138                    }
8139                };
8140
8141                match &evaluated_args[0] {
8142                    JValue::Null => Ok(JValue::Null),
8143                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
8144                    JValue::Object(obj) => {
8145                        // Return undefined for empty objects
8146                        if obj.is_empty() {
8147                            Ok(JValue::Null)
8148                        } else {
8149                            let keys: Vec<JValue> =
8150                                obj.keys().map(|k| JValue::string(k.clone())).collect();
8151                            check_sequence_length(keys.len(), &self.options)?;
8152                            Ok(unwrap_single(keys))
8153                        }
8154                    }
8155                    JValue::Array(arr) => {
8156                        // For arrays, collect keys from all objects
8157                        let mut all_keys = Vec::new();
8158                        for item in arr.iter() {
8159                            // Skip lambda/builtin values
8160                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
8161                                continue;
8162                            }
8163                            if let JValue::Object(obj) = item {
8164                                for key in obj.keys() {
8165                                    if !all_keys.contains(&JValue::string(key.clone())) {
8166                                        all_keys.push(JValue::string(key.clone()));
8167                                    }
8168                                }
8169                            }
8170                        }
8171                        if all_keys.is_empty() {
8172                            Ok(JValue::Null)
8173                        } else {
8174                            check_sequence_length(all_keys.len(), &self.options)?;
8175                            Ok(unwrap_single(all_keys))
8176                        }
8177                    }
8178                    // Non-object types return undefined
8179                    _ => Ok(JValue::Null),
8180                }
8181            }
8182            "lookup" => {
8183                if evaluated_args.len() != 2 {
8184                    return Err(EvaluatorError::EvaluationError(
8185                        "lookup() requires exactly 2 arguments".to_string(),
8186                    ));
8187                }
8188                if evaluated_args[0].is_null() {
8189                    return Ok(JValue::Null);
8190                }
8191                if evaluated_args[0].is_undefined() {
8192                    return Ok(JValue::Undefined);
8193                }
8194
8195                let key = match &evaluated_args[1] {
8196                    JValue::String(k) => &**k,
8197                    _ => {
8198                        return Err(EvaluatorError::TypeError(
8199                            "lookup() requires a string key".to_string(),
8200                        ))
8201                    }
8202                };
8203
8204                // Helper function to recursively lookup in values
8205                fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
8206                    match val {
8207                        JValue::Array(arr) => {
8208                            let mut results = Vec::new();
8209                            for item in arr.iter() {
8210                                let nested = lookup_recursive(item, key);
8211                                results.extend(nested.iter().cloned());
8212                            }
8213                            results
8214                        }
8215                        JValue::Object(obj) => {
8216                            if let Some(v) = obj.get(key) {
8217                                vec![v.clone()]
8218                            } else {
8219                                vec![]
8220                            }
8221                        }
8222                        _ => vec![],
8223                    }
8224                }
8225
8226                let results = lookup_recursive(&evaluated_args[0], key);
8227                if results.is_empty() {
8228                    Ok(JValue::Null)
8229                } else if results.len() == 1 {
8230                    Ok(results[0].clone())
8231                } else {
8232                    check_sequence_length(results.len(), &self.options)?;
8233                    Ok(JValue::array(results))
8234                }
8235            }
8236            "spread" => {
8237                if evaluated_args.len() != 1 {
8238                    return Err(EvaluatorError::EvaluationError(
8239                        "spread() requires exactly 1 argument".to_string(),
8240                    ));
8241                }
8242                match &evaluated_args[0] {
8243                    JValue::Null => Ok(JValue::Null),
8244                    // Not a container - pass through unchanged (e.g. so $string() still
8245                    // sees the function value and applies its own function->"" rule).
8246                    lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
8247                    JValue::Object(obj) => {
8248                        // functions::object::spread() always returns an array with one
8249                        // element per key (mirrors jsonata-js's push-per-key loop through
8250                        // this.createSequence()), so it needs the same cap as the
8251                        // array-fanout branch below and as the "keys" arm's single-object
8252                        // branch.
8253                        check_sequence_length(obj.len(), &self.options)?;
8254                        Ok(functions::object::spread(obj)?)
8255                    }
8256                    JValue::Array(arr) => {
8257                        // Spread each object in the array
8258                        let mut result = Vec::new();
8259                        for item in arr.iter() {
8260                            match item {
8261                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
8262                                    // Skip lambdas in array
8263                                    continue;
8264                                }
8265                                JValue::Object(obj) => {
8266                                    let spread_result = functions::object::spread(obj)?;
8267                                    if let JValue::Array(spread_items) = spread_result {
8268                                        result.extend(spread_items.iter().cloned());
8269                                    } else {
8270                                        result.push(spread_result);
8271                                    }
8272                                }
8273                                // Non-objects in array are returned unchanged
8274                                other => result.push(other.clone()),
8275                            }
8276                        }
8277                        check_sequence_length(result.len(), &self.options)?;
8278                        Ok(JValue::array(result))
8279                    }
8280                    // Non-objects are returned unchanged
8281                    other => Ok(other.clone()),
8282                }
8283            }
8284            "merge" => {
8285                if evaluated_args.is_empty() {
8286                    return Err(EvaluatorError::EvaluationError(
8287                        "merge() requires at least 1 argument".to_string(),
8288                    ));
8289                }
8290                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
8291                // vs multiple object arguments: $merge(obj1, obj2)
8292                if evaluated_args.len() == 1 {
8293                    match &evaluated_args[0] {
8294                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
8295                        JValue::Null => Ok(JValue::Null),
8296                        JValue::Undefined => Ok(JValue::Undefined),
8297                        JValue::Object(_) => {
8298                            // Single object - just return it
8299                            Ok(evaluated_args[0].clone())
8300                        }
8301                        _ => Err(EvaluatorError::TypeError(
8302                            "merge() requires objects or an array of objects".to_string(),
8303                        )),
8304                    }
8305                } else {
8306                    Ok(functions::object::merge(&evaluated_args)?)
8307                }
8308            }
8309
8310            "map" => {
8311                if args.len() != 2 {
8312                    return Err(EvaluatorError::EvaluationError(
8313                        "map() requires exactly 2 arguments".to_string(),
8314                    ));
8315                }
8316
8317                // Evaluate the array argument
8318                let array = self.evaluate_internal(&args[0], data)?;
8319
8320                match array {
8321                    JValue::Array(arr) => {
8322                        // Detect how many parameters the callback expects
8323                        let param_count = self.get_callback_param_count(&args[1]);
8324
8325                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
8326                        if param_count == 1 {
8327                            if let AstNode::Lambda {
8328                                params,
8329                                body,
8330                                signature: None,
8331                                thunk: false,
8332                            } = &args[1]
8333                            {
8334                                let var_refs: Vec<&str> =
8335                                    params.iter().map(|s| s.as_str()).collect();
8336                                if let Some(compiled) =
8337                                    try_compile_expr_with_allowed_vars(body, &var_refs)
8338                                {
8339                                    let param_name = params[0].as_str();
8340                                    let mut result = Vec::with_capacity(arr.len());
8341                                    let mut vars = HashMap::new();
8342                                    for item in arr.iter() {
8343                                        vars.insert(param_name, item);
8344                                        let mapped = eval_compiled(
8345                                            &compiled,
8346                                            data,
8347                                            Some(&vars),
8348                                            &self.options,
8349                                            self.start_time,
8350                                        )?;
8351                                        if !mapped.is_undefined() {
8352                                            result.push(mapped);
8353                                        }
8354                                    }
8355                                    check_sequence_length(result.len(), &self.options)?;
8356                                    return Ok(JValue::array(result));
8357                                }
8358                            }
8359                            // Stored lambda variable fast path: $var with pre-compiled body
8360                            if let AstNode::Variable(var_name) = &args[1] {
8361                                if let Some(stored) = self.context.lookup_lambda(var_name) {
8362                                    if let Some(ref ce) = stored.compiled_body.clone() {
8363                                        let param_name = stored.params[0].clone();
8364                                        let captured_data = stored.captured_data.clone();
8365                                        let captured_env_clone = stored.captured_env.clone();
8366                                        let ce_clone = ce.clone();
8367                                        if !captured_env_clone.values().any(|v| {
8368                                            matches!(
8369                                                v,
8370                                                JValue::Lambda { .. } | JValue::Builtin { .. }
8371                                            )
8372                                        }) {
8373                                            let call_data = captured_data.as_ref().unwrap_or(data);
8374                                            let mut result = Vec::with_capacity(arr.len());
8375                                            let mut vars: HashMap<&str, &JValue> =
8376                                                captured_env_clone
8377                                                    .iter()
8378                                                    .map(|(k, v)| (k.as_str(), v))
8379                                                    .collect();
8380                                            for item in arr.iter() {
8381                                                vars.insert(param_name.as_str(), item);
8382                                                let mapped = eval_compiled(
8383                                                    &ce_clone,
8384                                                    call_data,
8385                                                    Some(&vars),
8386                                                    &self.options,
8387                                                    self.start_time,
8388                                                )?;
8389                                                if !mapped.is_undefined() {
8390                                                    result.push(mapped);
8391                                                }
8392                                            }
8393                                            check_sequence_length(result.len(), &self.options)?;
8394                                            return Ok(JValue::array(result));
8395                                        }
8396                                    }
8397                                }
8398                            }
8399                        }
8400
8401                        // Only create the array value if callback uses 3 parameters
8402                        let arr_value = if param_count >= 3 {
8403                            Some(JValue::Array(arr.clone()))
8404                        } else {
8405                            None
8406                        };
8407
8408                        let mut result = Vec::with_capacity(arr.len());
8409                        for (index, item) in arr.iter().enumerate() {
8410                            // Build argument list based on what callback expects
8411                            let call_args = match param_count {
8412                                1 => vec![item.clone()],
8413                                2 => vec![item.clone(), JValue::Number(index as f64)],
8414                                _ => vec![
8415                                    item.clone(),
8416                                    JValue::Number(index as f64),
8417                                    arr_value.as_ref().unwrap().clone(),
8418                                ],
8419                            };
8420
8421                            let mapped = self.apply_function(&args[1], &call_args, data)?;
8422                            // Filter out undefined results but keep explicit null (JSONata map semantics)
8423                            // undefined comes from missing else clause, null is explicit
8424                            if !mapped.is_undefined() {
8425                                result.push(mapped);
8426                            }
8427                        }
8428                        check_sequence_length(result.len(), &self.options)?;
8429                        Ok(JValue::array(result))
8430                    }
8431                    JValue::Null => Ok(JValue::Null),
8432                    JValue::Undefined => Ok(JValue::Undefined),
8433                    _ => Err(EvaluatorError::TypeError(
8434                        "map() first argument must be an array".to_string(),
8435                    )),
8436                }
8437            }
8438
8439            "filter" => {
8440                if args.len() != 2 {
8441                    return Err(EvaluatorError::EvaluationError(
8442                        "filter() requires exactly 2 arguments".to_string(),
8443                    ));
8444                }
8445
8446                // Evaluate the array argument
8447                let array = self.evaluate_internal(&args[0], data)?;
8448
8449                // Handle undefined input - return undefined
8450                if array.is_undefined() {
8451                    return Ok(JValue::Undefined);
8452                }
8453
8454                // Handle null input
8455                if array.is_null() {
8456                    return Ok(JValue::Undefined);
8457                }
8458
8459                // Coerce non-array values to single-element arrays
8460                // Track if input was a single value to unwrap result appropriately
8461                // Use references to avoid upfront cloning of all elements
8462                let single_holder;
8463                let (items, was_single_value): (&[JValue], bool) = match &array {
8464                    JValue::Array(arr) => (arr.as_slice(), false),
8465                    _ => {
8466                        single_holder = [array];
8467                        (&single_holder[..], true)
8468                    }
8469                };
8470
8471                // Detect how many parameters the callback expects
8472                let param_count = self.get_callback_param_count(&args[1]);
8473
8474                // CompiledExpr fast path: direct lambda with 1 param, compilable body
8475                if param_count == 1 {
8476                    if let AstNode::Lambda {
8477                        params,
8478                        body,
8479                        signature: None,
8480                        thunk: false,
8481                    } = &args[1]
8482                    {
8483                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8484                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8485                        {
8486                            let param_name = params[0].as_str();
8487                            let mut result = Vec::with_capacity(items.len() / 2);
8488                            let mut vars = HashMap::new();
8489                            for item in items.iter() {
8490                                vars.insert(param_name, item);
8491                                let pred_result = eval_compiled(
8492                                    &compiled,
8493                                    data,
8494                                    Some(&vars),
8495                                    &self.options,
8496                                    self.start_time,
8497                                )?;
8498                                if compiled_is_truthy(&pred_result) {
8499                                    result.push(item.clone());
8500                                }
8501                            }
8502                            if was_single_value {
8503                                if result.len() == 1 {
8504                                    return Ok(result.remove(0));
8505                                } else if result.is_empty() {
8506                                    return Ok(JValue::Undefined);
8507                                }
8508                            }
8509                            check_sequence_length(result.len(), &self.options)?;
8510                            return Ok(JValue::array(result));
8511                        }
8512                    }
8513                    // Stored lambda variable fast path: $var with pre-compiled body
8514                    if let AstNode::Variable(var_name) = &args[1] {
8515                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8516                            if let Some(ref ce) = stored.compiled_body.clone() {
8517                                let param_name = stored.params[0].clone();
8518                                let captured_data = stored.captured_data.clone();
8519                                let captured_env_clone = stored.captured_env.clone();
8520                                let ce_clone = ce.clone();
8521                                if !captured_env_clone.values().any(|v| {
8522                                    matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8523                                }) {
8524                                    let call_data = captured_data.as_ref().unwrap_or(data);
8525                                    let mut result = Vec::with_capacity(items.len() / 2);
8526                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
8527                                        .iter()
8528                                        .map(|(k, v)| (k.as_str(), v))
8529                                        .collect();
8530                                    for item in items.iter() {
8531                                        vars.insert(param_name.as_str(), item);
8532                                        let pred_result = eval_compiled(
8533                                            &ce_clone,
8534                                            call_data,
8535                                            Some(&vars),
8536                                            &self.options,
8537                                            self.start_time,
8538                                        )?;
8539                                        if compiled_is_truthy(&pred_result) {
8540                                            result.push(item.clone());
8541                                        }
8542                                    }
8543                                    if was_single_value {
8544                                        if result.len() == 1 {
8545                                            return Ok(result.remove(0));
8546                                        } else if result.is_empty() {
8547                                            return Ok(JValue::Undefined);
8548                                        }
8549                                    }
8550                                    check_sequence_length(result.len(), &self.options)?;
8551                                    return Ok(JValue::array(result));
8552                                }
8553                            }
8554                        }
8555                    }
8556                }
8557
8558                // Only create the array value if callback uses 3 parameters
8559                let arr_value = if param_count >= 3 {
8560                    Some(JValue::array(items.to_vec()))
8561                } else {
8562                    None
8563                };
8564
8565                let mut result = Vec::with_capacity(items.len() / 2);
8566
8567                for (index, item) in items.iter().enumerate() {
8568                    // Build argument list based on what callback expects
8569                    let call_args = match param_count {
8570                        1 => vec![item.clone()],
8571                        2 => vec![item.clone(), JValue::Number(index as f64)],
8572                        _ => vec![
8573                            item.clone(),
8574                            JValue::Number(index as f64),
8575                            arr_value.as_ref().unwrap().clone(),
8576                        ],
8577                    };
8578
8579                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8580                    if self.is_truthy(&predicate_result) {
8581                        result.push(item.clone());
8582                    }
8583                }
8584
8585                // If input was a single value, return the single matching item
8586                // (or undefined if no match)
8587                if was_single_value {
8588                    if result.len() == 1 {
8589                        return Ok(result.remove(0));
8590                    } else if result.is_empty() {
8591                        return Ok(JValue::Undefined);
8592                    }
8593                }
8594
8595                check_sequence_length(result.len(), &self.options)?;
8596                Ok(JValue::array(result))
8597            }
8598
8599            "reduce" => {
8600                if args.len() < 2 || args.len() > 3 {
8601                    return Err(EvaluatorError::EvaluationError(
8602                        "reduce() requires 2 or 3 arguments".to_string(),
8603                    ));
8604                }
8605
8606                // Check that the callback function has at least 2 parameters
8607                if let AstNode::Lambda { params, .. } = &args[1] {
8608                    if params.len() < 2 {
8609                        return Err(EvaluatorError::EvaluationError(
8610                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
8611                        ));
8612                    }
8613                } else if let AstNode::Function { name, .. } = &args[1] {
8614                    // For now, we can't validate built-in function signatures here
8615                    // But user-defined functions via lambda will be validated above
8616                    let _ = name; // avoid unused warning
8617                }
8618
8619                // Evaluate the array argument
8620                let array = self.evaluate_internal(&args[0], data)?;
8621
8622                // Convert single value to array (JSONata reduce accepts single values)
8623                // Use references to avoid upfront cloning of all elements
8624                let single_holder;
8625                let items: &[JValue] = match &array {
8626                    JValue::Array(arr) => arr.as_slice(),
8627                    JValue::Null => return Ok(JValue::Null),
8628                    _ => {
8629                        single_holder = [array];
8630                        &single_holder[..]
8631                    }
8632                };
8633
8634                if items.is_empty() {
8635                    // Return initial value if provided, otherwise null
8636                    return if args.len() == 3 {
8637                        self.evaluate_internal(&args[2], data)
8638                    } else {
8639                        Ok(JValue::Null)
8640                    };
8641                }
8642
8643                // Get initial accumulator
8644                let mut accumulator = if args.len() == 3 {
8645                    self.evaluate_internal(&args[2], data)?
8646                } else {
8647                    items[0].clone()
8648                };
8649
8650                let start_idx = if args.len() == 3 { 0 } else { 1 };
8651
8652                // Detect how many parameters the callback expects
8653                let param_count = self.get_callback_param_count(&args[1]);
8654
8655                // CompiledExpr fast path: direct lambda with 2 params, compilable body
8656                if param_count == 2 {
8657                    if let AstNode::Lambda {
8658                        params,
8659                        body,
8660                        signature: None,
8661                        thunk: false,
8662                    } = &args[1]
8663                    {
8664                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8665                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8666                        {
8667                            let acc_name = params[0].as_str();
8668                            let item_name = params[1].as_str();
8669                            for item in items[start_idx..].iter() {
8670                                let vars: HashMap<&str, &JValue> =
8671                                    HashMap::from([(acc_name, &accumulator), (item_name, item)]);
8672                                accumulator = eval_compiled(
8673                                    &compiled,
8674                                    data,
8675                                    Some(&vars),
8676                                    &self.options,
8677                                    self.start_time,
8678                                )?;
8679                            }
8680                            return Ok(accumulator);
8681                        }
8682                    }
8683                    // Stored lambda variable fast path: $var with pre-compiled body
8684                    if let AstNode::Variable(var_name) = &args[1] {
8685                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8686                            if stored.params.len() == 2 {
8687                                if let Some(ref ce) = stored.compiled_body.clone() {
8688                                    let acc_param = stored.params[0].clone();
8689                                    let item_param = stored.params[1].clone();
8690                                    let captured_data = stored.captured_data.clone();
8691                                    let captured_env_clone = stored.captured_env.clone();
8692                                    let ce_clone = ce.clone();
8693                                    if !captured_env_clone.values().any(|v| {
8694                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8695                                    }) {
8696                                        let call_data = captured_data.as_ref().unwrap_or(data);
8697                                        for item in items[start_idx..].iter() {
8698                                            let mut vars: HashMap<&str, &JValue> =
8699                                                captured_env_clone
8700                                                    .iter()
8701                                                    .map(|(k, v)| (k.as_str(), v))
8702                                                    .collect();
8703                                            vars.insert(acc_param.as_str(), &accumulator);
8704                                            vars.insert(item_param.as_str(), item);
8705                                            // Evaluate and drop vars before assigning accumulator
8706                                            // to satisfy borrow checker (vars borrows accumulator)
8707                                            let new_acc = eval_compiled(
8708                                                &ce_clone,
8709                                                call_data,
8710                                                Some(&vars),
8711                                                &self.options,
8712                                                self.start_time,
8713                                            )?;
8714                                            drop(vars);
8715                                            accumulator = new_acc;
8716                                        }
8717                                        return Ok(accumulator);
8718                                    }
8719                                }
8720                            }
8721                        }
8722                    }
8723                }
8724
8725                // Only create the array value if callback uses 4 parameters
8726                let arr_value = if param_count >= 4 {
8727                    Some(JValue::array(items.to_vec()))
8728                } else {
8729                    None
8730                };
8731
8732                // Apply function to each element
8733                for (idx, item) in items[start_idx..].iter().enumerate() {
8734                    // For reduce, the function receives (accumulator, value, index, array)
8735                    // Callbacks may use any subset of these parameters
8736                    let actual_idx = start_idx + idx;
8737
8738                    // Build argument list based on what callback expects
8739                    let call_args = match param_count {
8740                        2 => vec![accumulator.clone(), item.clone()],
8741                        3 => vec![
8742                            accumulator.clone(),
8743                            item.clone(),
8744                            JValue::Number(actual_idx as f64),
8745                        ],
8746                        _ => vec![
8747                            accumulator.clone(),
8748                            item.clone(),
8749                            JValue::Number(actual_idx as f64),
8750                            arr_value.as_ref().unwrap().clone(),
8751                        ],
8752                    };
8753
8754                    accumulator = self.apply_function(&args[1], &call_args, data)?;
8755                }
8756
8757                Ok(accumulator)
8758            }
8759
8760            "single" => {
8761                if args.is_empty() || args.len() > 2 {
8762                    return Err(EvaluatorError::EvaluationError(
8763                        "single() requires 1 or 2 arguments".to_string(),
8764                    ));
8765                }
8766
8767                // Evaluate the array argument
8768                let array = self.evaluate_internal(&args[0], data)?;
8769
8770                // Convert to array (wrap single values)
8771                let arr = match array {
8772                    JValue::Array(arr) => arr.to_vec(),
8773                    JValue::Null => return Ok(JValue::Null),
8774                    other => vec![other],
8775                };
8776
8777                if args.len() == 1 {
8778                    // No predicate - array must have exactly 1 element
8779                    match arr.len() {
8780                        0 => Err(EvaluatorError::EvaluationError(
8781                            "single() argument is empty".to_string(),
8782                        )),
8783                        1 => Ok(arr.into_iter().next().unwrap()),
8784                        count => Err(EvaluatorError::EvaluationError(format!(
8785                            "single() argument has {} values (expected exactly 1)",
8786                            count
8787                        ))),
8788                    }
8789                } else {
8790                    // With predicate - find exactly 1 matching element
8791                    let arr_value = JValue::array(arr.clone());
8792                    let mut matches = Vec::new();
8793                    for (index, item) in arr.into_iter().enumerate() {
8794                        // Apply predicate function with (item, index, array)
8795                        let predicate_result = self.apply_function(
8796                            &args[1],
8797                            &[
8798                                item.clone(),
8799                                JValue::Number(index as f64),
8800                                arr_value.clone(),
8801                            ],
8802                            data,
8803                        )?;
8804                        if self.is_truthy(&predicate_result) {
8805                            matches.push(item);
8806                        }
8807                    }
8808
8809                    match matches.len() {
8810                        0 => Err(EvaluatorError::EvaluationError(
8811                            "single() predicate matches no values".to_string(),
8812                        )),
8813                        1 => Ok(matches.into_iter().next().unwrap()),
8814                        count => Err(EvaluatorError::EvaluationError(format!(
8815                            "single() predicate matches {} values (expected exactly 1)",
8816                            count
8817                        ))),
8818                    }
8819                }
8820            }
8821
8822            "each" => {
8823                // $each(object, function) - iterate over object, applying function to each value/key pair
8824                // Returns an array of the function results
8825                if args.is_empty() || args.len() > 2 {
8826                    return Err(EvaluatorError::EvaluationError(
8827                        "each() requires 1 or 2 arguments".to_string(),
8828                    ));
8829                }
8830
8831                // Determine which argument is the object and which is the function
8832                let (obj_value, func_arg) = if args.len() == 1 {
8833                    // Single argument: use current data as object
8834                    (data.clone(), &args[0])
8835                } else {
8836                    // Two arguments: first is object, second is function
8837                    (self.evaluate_internal(&args[0], data)?, &args[1])
8838                };
8839
8840                // Detect how many parameters the callback expects
8841                let param_count = self.get_callback_param_count(func_arg);
8842
8843                match obj_value {
8844                    JValue::Object(obj) => {
8845                        let mut result = Vec::new();
8846                        for (key, value) in obj.iter() {
8847                            // Build argument list based on what callback expects
8848                            // The callback receives the value as the first argument and key as second
8849                            let call_args = match param_count {
8850                                1 => vec![value.clone()],
8851                                _ => vec![value.clone(), JValue::string(key.clone())],
8852                            };
8853
8854                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
8855                            // Skip undefined results (similar to map behavior)
8856                            if !fn_result.is_null() && !fn_result.is_undefined() {
8857                                result.push(fn_result);
8858                            }
8859                        }
8860                        check_sequence_length(result.len(), &self.options)?;
8861                        Ok(JValue::array(result))
8862                    }
8863                    JValue::Null => Ok(JValue::Null),
8864                    _ => Err(EvaluatorError::TypeError(
8865                        "each() first argument must be an object".to_string(),
8866                    )),
8867                }
8868            }
8869
8870            "not" => {
8871                if evaluated_args.len() != 1 {
8872                    return Err(EvaluatorError::EvaluationError(
8873                        "not() requires exactly 1 argument".to_string(),
8874                    ));
8875                }
8876                // $not(x) returns the logical negation of x
8877                // null is falsy, so $not(null) = true; undefined stays undefined
8878                if evaluated_args[0].is_undefined() {
8879                    return Ok(JValue::Undefined);
8880                }
8881                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
8882            }
8883            "boolean" => {
8884                if evaluated_args.len() != 1 {
8885                    return Err(EvaluatorError::EvaluationError(
8886                        "boolean() requires exactly 1 argument".to_string(),
8887                    ));
8888                }
8889                if evaluated_args[0].is_undefined() {
8890                    return Ok(JValue::Undefined);
8891                }
8892                Ok(functions::boolean::boolean(&evaluated_args[0])?)
8893            }
8894            "type" => {
8895                if evaluated_args.len() != 1 {
8896                    return Err(EvaluatorError::EvaluationError(
8897                        "type() requires exactly 1 argument".to_string(),
8898                    ));
8899                }
8900                // Return type string
8901                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
8902                // We use a special marker object to distinguish undefined from null
8903                match &evaluated_args[0] {
8904                    JValue::Null => Ok(JValue::string("null")),
8905                    JValue::Bool(_) => Ok(JValue::string("boolean")),
8906                    JValue::Number(_) => Ok(JValue::string("number")),
8907                    JValue::String(_) => Ok(JValue::string("string")),
8908                    JValue::Array(_) => Ok(JValue::string("array")),
8909                    JValue::Object(_) => Ok(JValue::string("object")),
8910                    JValue::Undefined => Ok(JValue::Undefined),
8911                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
8912                        Ok(JValue::string("function"))
8913                    }
8914                    JValue::Regex { .. } => Ok(JValue::string("regex")),
8915                }
8916            }
8917
8918            "base64encode" => {
8919                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8920                    return Ok(JValue::Null);
8921                }
8922                if evaluated_args.len() != 1 {
8923                    return Err(EvaluatorError::EvaluationError(
8924                        "base64encode() requires exactly 1 argument".to_string(),
8925                    ));
8926                }
8927                match &evaluated_args[0] {
8928                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
8929                    _ => Err(EvaluatorError::TypeError(
8930                        "base64encode() requires a string argument".to_string(),
8931                    )),
8932                }
8933            }
8934            "base64decode" => {
8935                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8936                    return Ok(JValue::Null);
8937                }
8938                if evaluated_args.len() != 1 {
8939                    return Err(EvaluatorError::EvaluationError(
8940                        "base64decode() requires exactly 1 argument".to_string(),
8941                    ));
8942                }
8943                match &evaluated_args[0] {
8944                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
8945                    _ => Err(EvaluatorError::TypeError(
8946                        "base64decode() requires a string argument".to_string(),
8947                    )),
8948                }
8949            }
8950            "encodeUrlComponent" => {
8951                if evaluated_args.len() != 1 {
8952                    return Err(EvaluatorError::EvaluationError(
8953                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
8954                    ));
8955                }
8956                if evaluated_args[0].is_null() {
8957                    return Ok(JValue::Null);
8958                }
8959                if evaluated_args[0].is_undefined() {
8960                    return Ok(JValue::Undefined);
8961                }
8962                match &evaluated_args[0] {
8963                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
8964                    _ => Err(EvaluatorError::TypeError(
8965                        "encodeUrlComponent() requires a string argument".to_string(),
8966                    )),
8967                }
8968            }
8969            "decodeUrlComponent" => {
8970                if evaluated_args.len() != 1 {
8971                    return Err(EvaluatorError::EvaluationError(
8972                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
8973                    ));
8974                }
8975                if evaluated_args[0].is_null() {
8976                    return Ok(JValue::Null);
8977                }
8978                if evaluated_args[0].is_undefined() {
8979                    return Ok(JValue::Undefined);
8980                }
8981                match &evaluated_args[0] {
8982                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
8983                    _ => Err(EvaluatorError::TypeError(
8984                        "decodeUrlComponent() requires a string argument".to_string(),
8985                    )),
8986                }
8987            }
8988            "encodeUrl" => {
8989                if evaluated_args.len() != 1 {
8990                    return Err(EvaluatorError::EvaluationError(
8991                        "encodeUrl() requires exactly 1 argument".to_string(),
8992                    ));
8993                }
8994                if evaluated_args[0].is_null() {
8995                    return Ok(JValue::Null);
8996                }
8997                if evaluated_args[0].is_undefined() {
8998                    return Ok(JValue::Undefined);
8999                }
9000                match &evaluated_args[0] {
9001                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
9002                    _ => Err(EvaluatorError::TypeError(
9003                        "encodeUrl() requires a string argument".to_string(),
9004                    )),
9005                }
9006            }
9007            "decodeUrl" => {
9008                if evaluated_args.len() != 1 {
9009                    return Err(EvaluatorError::EvaluationError(
9010                        "decodeUrl() requires exactly 1 argument".to_string(),
9011                    ));
9012                }
9013                if evaluated_args[0].is_null() {
9014                    return Ok(JValue::Null);
9015                }
9016                if evaluated_args[0].is_undefined() {
9017                    return Ok(JValue::Undefined);
9018                }
9019                match &evaluated_args[0] {
9020                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
9021                    _ => Err(EvaluatorError::TypeError(
9022                        "decodeUrl() requires a string argument".to_string(),
9023                    )),
9024                }
9025            }
9026
9027            "error" => {
9028                // $error(message) - throw error with custom message
9029                if evaluated_args.is_empty() {
9030                    // No message provided
9031                    return Err(EvaluatorError::EvaluationError(
9032                        "D3137: $error() function evaluated".to_string(),
9033                    ));
9034                }
9035
9036                match &evaluated_args[0] {
9037                    JValue::String(s) => {
9038                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
9039                    }
9040                    _ => Err(EvaluatorError::TypeError(
9041                        "T0410: Argument 1 of function error does not match function signature"
9042                            .to_string(),
9043                    )),
9044                }
9045            }
9046            "assert" => {
9047                // $assert(condition, message) - throw error if condition is false
9048                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9049                    return Err(EvaluatorError::EvaluationError(
9050                        "assert() requires 1 or 2 arguments".to_string(),
9051                    ));
9052                }
9053
9054                // First argument must be a boolean
9055                let condition = match &evaluated_args[0] {
9056                    JValue::Bool(b) => *b,
9057                    _ => {
9058                        return Err(EvaluatorError::TypeError(
9059                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
9060                        ));
9061                    }
9062                };
9063
9064                if !condition {
9065                    let message = if evaluated_args.len() == 2 {
9066                        match &evaluated_args[1] {
9067                            JValue::String(s) => s.clone(),
9068                            _ => Rc::from("$assert() statement failed"),
9069                        }
9070                    } else {
9071                        Rc::from("$assert() statement failed")
9072                    };
9073                    return Err(EvaluatorError::EvaluationError(format!(
9074                        "D3141: {}",
9075                        message
9076                    )));
9077                }
9078
9079                Ok(JValue::Null)
9080            }
9081
9082            "eval" => {
9083                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
9084                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9085                    return Err(EvaluatorError::EvaluationError(
9086                        "T0410: Argument 1 of function $eval must be a string".to_string(),
9087                    ));
9088                }
9089
9090                // If the first argument is null/undefined, return undefined
9091                if evaluated_args[0].is_null() {
9092                    return Ok(JValue::Null);
9093                }
9094                if evaluated_args[0].is_undefined() {
9095                    return Ok(JValue::Undefined);
9096                }
9097
9098                // First argument must be a string expression
9099                let expr_str = match &evaluated_args[0] {
9100                    JValue::String(s) => &**s,
9101                    _ => {
9102                        return Err(EvaluatorError::EvaluationError(
9103                            "T0410: Argument 1 of function $eval must be a string".to_string(),
9104                        ));
9105                    }
9106                };
9107
9108                // Parse the expression
9109                let parsed_ast = match parser::parse(expr_str) {
9110                    Ok(ast) => ast,
9111                    Err(e) => {
9112                        // D3120 is the error code for parse errors in $eval
9113                        return Err(EvaluatorError::EvaluationError(format!(
9114                            "D3120: The expression passed to $eval cannot be parsed: {}",
9115                            e
9116                        )));
9117                    }
9118                };
9119
9120                // Determine the context to use for evaluation
9121                let eval_context = if evaluated_args.len() == 2 {
9122                    &evaluated_args[1]
9123                } else {
9124                    data
9125                };
9126
9127                // Evaluate the parsed expression
9128                match self.evaluate_internal(&parsed_ast, eval_context) {
9129                    Ok(result) => Ok(result),
9130                    Err(e) => {
9131                        // D3121 is the error code for evaluation errors in $eval
9132                        let err_msg = e.to_string();
9133                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
9134                            Err(EvaluatorError::EvaluationError(format!(
9135                                "D3121: {}",
9136                                err_msg
9137                            )))
9138                        } else {
9139                            Err(e)
9140                        }
9141                    }
9142                }
9143            }
9144
9145            "now" => {
9146                if !evaluated_args.is_empty() {
9147                    return Err(EvaluatorError::EvaluationError(
9148                        "now() takes no arguments".to_string(),
9149                    ));
9150                }
9151                Ok(crate::datetime::now())
9152            }
9153
9154            "millis" => {
9155                if !evaluated_args.is_empty() {
9156                    return Err(EvaluatorError::EvaluationError(
9157                        "millis() takes no arguments".to_string(),
9158                    ));
9159                }
9160                Ok(crate::datetime::millis())
9161            }
9162
9163            "toMillis" => {
9164                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9165                    return Err(EvaluatorError::EvaluationError(
9166                        "toMillis() requires 1 or 2 arguments".to_string(),
9167                    ));
9168                }
9169
9170                match &evaluated_args[0] {
9171                    JValue::String(s) => {
9172                        // Optional second argument is a picture string for custom parsing
9173                        if evaluated_args.len() == 2 {
9174                            match &evaluated_args[1] {
9175                                JValue::String(picture) => {
9176                                    // Use custom picture format parsing
9177                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
9178                                }
9179                                JValue::Null => Ok(JValue::Null),
9180                                JValue::Undefined => Ok(JValue::Undefined),
9181                                _ => Err(EvaluatorError::TypeError(
9182                                    "toMillis() second argument must be a string".to_string(),
9183                                )),
9184                            }
9185                        } else {
9186                            // Use ISO 8601 partial date parsing
9187                            Ok(crate::datetime::to_millis(s)?)
9188                        }
9189                    }
9190                    JValue::Null => Ok(JValue::Null),
9191                    JValue::Undefined => Ok(JValue::Undefined),
9192                    _ => Err(EvaluatorError::TypeError(
9193                        "toMillis() requires a string argument".to_string(),
9194                    )),
9195                }
9196            }
9197
9198            "fromMillis" => {
9199                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
9200                    return Err(EvaluatorError::EvaluationError(
9201                        "fromMillis() requires 1 to 3 arguments".to_string(),
9202                    ));
9203                }
9204
9205                match &evaluated_args[0] {
9206                    JValue::Number(n) => {
9207                        let millis = (if n.fract() == 0.0 {
9208                            Ok(*n as i64)
9209                        } else {
9210                            Err(())
9211                        })
9212                        .map_err(|_| {
9213                            EvaluatorError::TypeError(
9214                                "fromMillis() requires an integer".to_string(),
9215                            )
9216                        })?;
9217
9218                        let picture = match evaluated_args.get(1) {
9219                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9220                            Some(JValue::String(s)) => Some(s.to_string()),
9221                            Some(_) => {
9222                                return Err(EvaluatorError::TypeError(
9223                                    "fromMillis() second argument must be a string".to_string(),
9224                                ))
9225                            }
9226                        };
9227                        let timezone = match evaluated_args.get(2) {
9228                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9229                            Some(JValue::String(s)) => Some(s.to_string()),
9230                            Some(_) => {
9231                                return Err(EvaluatorError::TypeError(
9232                                    "fromMillis() third argument must be a string".to_string(),
9233                                ))
9234                            }
9235                        };
9236
9237                        Ok(crate::datetime::from_millis_with_picture(
9238                            millis,
9239                            picture.as_deref(),
9240                            timezone.as_deref(),
9241                        )?)
9242                    }
9243                    JValue::Null => Ok(JValue::Null),
9244                    JValue::Undefined => Ok(JValue::Undefined),
9245                    _ => Err(EvaluatorError::TypeError(
9246                        "fromMillis() requires a number argument".to_string(),
9247                    )),
9248                }
9249            }
9250
9251            _ => Err(EvaluatorError::ReferenceError(format!(
9252                "Unknown function: {}",
9253                name
9254            ))),
9255        }
9256    }
9257
9258    /// Apply a function (lambda or expression) to values
9259    ///
9260    /// This handles both:
9261    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
9262    /// 2. Simple expressions: price * 2 - evaluates with values as context
9263    fn apply_function(
9264        &mut self,
9265        func_node: &AstNode,
9266        values: &[JValue],
9267        data: &JValue,
9268    ) -> Result<JValue, EvaluatorError> {
9269        match func_node {
9270            AstNode::Lambda {
9271                params,
9272                body,
9273                signature,
9274                thunk,
9275            } => {
9276                // Direct lambda - invoke it
9277                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
9278            }
9279            AstNode::Function {
9280                name,
9281                args,
9282                is_builtin,
9283            } => {
9284                // Function call - check if it has placeholders (partial application)
9285                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
9286
9287                if has_placeholder {
9288                    // This is a partial application - evaluate it to get the lambda value
9289                    let partial_lambda =
9290                        self.create_partial_application(name, args, *is_builtin, data)?;
9291
9292                    // Now invoke the partial lambda with the provided values
9293                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
9294                        return self.invoke_stored_lambda(&stored, values, data);
9295                    }
9296                    Err(EvaluatorError::EvaluationError(
9297                        "Failed to apply partial application".to_string(),
9298                    ))
9299                } else {
9300                    // Regular function call without placeholders
9301                    // Evaluate it and apply if it returns a function
9302                    let result = self.evaluate_internal(func_node, data)?;
9303
9304                    // Check if result is a lambda value
9305                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
9306                        return self.invoke_stored_lambda(&stored, values, data);
9307                    }
9308
9309                    // Otherwise just return the result
9310                    Ok(result)
9311                }
9312            }
9313            AstNode::Variable(var_name) => {
9314                // Check if this variable holds a stored lambda
9315                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
9316                    self.invoke_stored_lambda(&stored_lambda, values, data)
9317                } else if let Some(value) = self.context.lookup(var_name).cloned() {
9318                    // Check if this variable holds a lambda value
9319                    // This handles lambdas passed as bound arguments in partial applications
9320                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
9321                        return self.invoke_stored_lambda(&stored, values, data);
9322                    }
9323                    // Regular variable value - evaluate with first value as context
9324                    if values.is_empty() {
9325                        self.evaluate_internal(func_node, data)
9326                    } else {
9327                        self.evaluate_internal(func_node, &values[0])
9328                    }
9329                } else if self.is_builtin_function(var_name) {
9330                    // This is a built-in function reference (e.g., $string, $number)
9331                    // Call it directly with the provided values (already evaluated)
9332                    self.call_builtin_with_values(var_name, values)
9333                } else {
9334                    // Unknown variable - evaluate with first value as context
9335                    if values.is_empty() {
9336                        self.evaluate_internal(func_node, data)
9337                    } else {
9338                        self.evaluate_internal(func_node, &values[0])
9339                    }
9340                }
9341            }
9342            _ => {
9343                // For non-lambda expressions, evaluate with first value as context
9344                if values.is_empty() {
9345                    self.evaluate_internal(func_node, data)
9346                } else {
9347                    self.evaluate_internal(func_node, &values[0])
9348                }
9349            }
9350        }
9351    }
9352
9353    /// Execute a transform operator on the bound $ value
9354    fn execute_transform(
9355        &mut self,
9356        location: &AstNode,
9357        update: &AstNode,
9358        delete: Option<&AstNode>,
9359        _original_data: &JValue,
9360    ) -> Result<JValue, EvaluatorError> {
9361        // Get the input value from $ binding
9362        let input = self
9363            .context
9364            .lookup("$")
9365            .ok_or_else(|| {
9366                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
9367            })?
9368            .clone();
9369
9370        // Evaluate location expression on the input to get objects to transform
9371        let located_objects = self.evaluate_internal(location, &input)?;
9372
9373        // Collect target objects into a vector for comparison
9374        let targets: Vec<JValue> = match located_objects {
9375            JValue::Array(arr) => arr.to_vec(),
9376            JValue::Object(_) => vec![located_objects],
9377            JValue::Null => Vec::new(),
9378            other => vec![other],
9379        };
9380
9381        // Validate update parameter - must be an object constructor
9382        // We need to check this before evaluation in case of errors
9383        // For now, we'll validate after evaluation in the transform helper
9384
9385        // Parse delete field names if provided
9386        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
9387            let delete_val = self.evaluate_internal(delete_node, &input)?;
9388            match delete_val {
9389                JValue::Array(arr) => arr
9390                    .iter()
9391                    .filter_map(|v| match v {
9392                        JValue::String(s) => Some(s.to_string()),
9393                        _ => None,
9394                    })
9395                    .collect(),
9396                JValue::String(s) => vec![s.to_string()],
9397                JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
9398                _ => {
9399                    // Delete parameter must be an array of strings or a string
9400                    return Err(EvaluatorError::EvaluationError(
9401                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
9402                    ));
9403                }
9404            }
9405        } else {
9406            Vec::new()
9407        };
9408
9409        // Recursive helper to apply transformation throughout the structure
9410        fn apply_transform_deep(
9411            evaluator: &mut Evaluator,
9412            value: &JValue,
9413            targets: &[JValue],
9414            update: &AstNode,
9415            delete_fields: &[String],
9416        ) -> Result<JValue, EvaluatorError> {
9417            // Check if this value is one of the targets to transform
9418            // Use JValue's PartialEq for semantic equality comparison
9419            if targets.iter().any(|t| t == value) {
9420                // Transform this object
9421                if let JValue::Object(map_rc) = value.clone() {
9422                    let mut map = (*map_rc).clone();
9423                    let update_val = evaluator.evaluate_internal(update, value)?;
9424                    // Validate that update evaluates to an object or null (undefined)
9425                    match update_val {
9426                        JValue::Object(update_map) => {
9427                            for (key, val) in update_map.iter() {
9428                                map.insert(key.clone(), val.clone());
9429                            }
9430                        }
9431                        JValue::Null | JValue::Undefined => {
9432                            // Null/undefined means no updates, just continue to deletions
9433                        }
9434                        _ => {
9435                            return Err(EvaluatorError::EvaluationError(
9436                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
9437                            ));
9438                        }
9439                    }
9440                    for field in delete_fields {
9441                        map.shift_remove(field);
9442                    }
9443                    return Ok(JValue::object(map));
9444                }
9445                return Ok(value.clone());
9446            }
9447
9448            // Otherwise, recursively process children to find and transform targets
9449            match value {
9450                JValue::Object(map) => {
9451                    let mut new_map = IndexMap::new();
9452                    for (k, v) in map.iter() {
9453                        new_map.insert(
9454                            k.clone(),
9455                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
9456                        );
9457                    }
9458                    Ok(JValue::object(new_map))
9459                }
9460                JValue::Array(arr) => {
9461                    let mut new_arr = Vec::new();
9462                    for item in arr.iter() {
9463                        new_arr.push(apply_transform_deep(
9464                            evaluator,
9465                            item,
9466                            targets,
9467                            update,
9468                            delete_fields,
9469                        )?);
9470                    }
9471                    Ok(JValue::array(new_arr))
9472                }
9473                _ => Ok(value.clone()),
9474            }
9475        }
9476
9477        // Apply transformation recursively starting from input
9478        apply_transform_deep(self, &input, &targets, update, &delete_fields)
9479    }
9480
9481    /// Helper to invoke a lambda with given parameters
9482    fn invoke_lambda(
9483        &mut self,
9484        params: &[String],
9485        body: &AstNode,
9486        signature: Option<&String>,
9487        values: &[JValue],
9488        data: &JValue,
9489        thunk: bool,
9490    ) -> Result<JValue, EvaluatorError> {
9491        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
9492    }
9493
9494    /// Invoke a lambda with optional captured environment (for closures)
9495    fn invoke_lambda_with_env(
9496        &mut self,
9497        params: &[String],
9498        body: &AstNode,
9499        signature: Option<&String>,
9500        values: &[JValue],
9501        data: &JValue,
9502        captured_env: Option<&HashMap<String, JValue>>,
9503        captured_data: Option<&JValue>,
9504        thunk: bool,
9505    ) -> Result<JValue, EvaluatorError> {
9506        // If this is a thunk (has tail calls), use TCO trampoline
9507        if thunk {
9508            let stored = StoredLambda {
9509                params: params.to_vec(),
9510                body: body.clone(),
9511                compiled_body: None, // Thunks use TCO, not the compiled fast path
9512                signature: signature.cloned(),
9513                captured_env: captured_env.cloned().unwrap_or_default(),
9514                captured_data: captured_data.cloned(),
9515                thunk,
9516            };
9517            return self.invoke_lambda_with_tco(&stored, values, data);
9518        }
9519
9520        // Validate signature if present, and get coerced arguments
9521        // Push a new scope for this lambda invocation
9522        self.context.push_scope();
9523
9524        // First apply captured environment (for closures)
9525        if let Some(env) = captured_env {
9526            for (name, value) in env {
9527                self.context.bind(name.clone(), value.clone());
9528            }
9529        }
9530
9531        if let Some(sig_str) = signature {
9532            // Validate and coerce arguments with signature
9533            let coerced_values = match crate::signature::Signature::parse(sig_str) {
9534                Ok(sig) => match sig.validate_and_coerce(values, data) {
9535                    Ok(coerced) => coerced,
9536                    Err(e) => {
9537                        self.context.pop_scope();
9538                        match e {
9539                            crate::signature::SignatureError::UndefinedArgument => {
9540                                return Ok(JValue::Null);
9541                            }
9542                            crate::signature::SignatureError::ArgumentTypeMismatch {
9543                                index,
9544                                expected,
9545                            } => {
9546                                return Err(EvaluatorError::TypeError(
9547                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9548                                    ));
9549                            }
9550                            crate::signature::SignatureError::ArrayTypeMismatch {
9551                                index,
9552                                expected,
9553                            } => {
9554                                return Err(EvaluatorError::TypeError(format!(
9555                                    "T0412: Argument {} of function must be an array of {}",
9556                                    index, expected
9557                                )));
9558                            }
9559                            crate::signature::SignatureError::ContextTypeMismatch {
9560                                index,
9561                                expected,
9562                            } => {
9563                                return Err(EvaluatorError::TypeError(format!(
9564                                    "T0411: Context value at argument {} does not match function signature (expected {})",
9565                                    index, expected
9566                                )));
9567                            }
9568                            _ => {
9569                                return Err(EvaluatorError::TypeError(format!(
9570                                    "Signature validation failed: {}",
9571                                    e
9572                                )));
9573                            }
9574                        }
9575                    }
9576                },
9577                Err(e) => {
9578                    self.context.pop_scope();
9579                    return Err(EvaluatorError::EvaluationError(format!(
9580                        "Invalid signature: {}",
9581                        e
9582                    )));
9583                }
9584            };
9585            // Bind coerced values to params
9586            for (i, param) in params.iter().enumerate() {
9587                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9588                self.context.bind(param.clone(), value);
9589            }
9590        } else {
9591            // No signature - bind directly from values slice (no allocation)
9592            for (i, param) in params.iter().enumerate() {
9593                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9594                self.context.bind(param.clone(), value);
9595            }
9596        }
9597
9598        // Check if this is a partial application (body is a special marker string)
9599        if let AstNode::String(body_str) = body {
9600            if body_str.starts_with("__partial_call:") {
9601                // Parse the partial call info
9602                let parts: Vec<&str> = body_str.split(':').collect();
9603                if parts.len() >= 4 {
9604                    let func_name = parts[1];
9605                    let is_builtin = parts[2] == "true";
9606                    let total_args: usize = parts[3].parse().unwrap_or(0);
9607
9608                    // Get placeholder positions from captured env
9609                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
9610                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
9611                            positions
9612                                .iter()
9613                                .filter_map(|v| v.as_f64().map(|n| n as usize))
9614                                .collect()
9615                        } else {
9616                            vec![]
9617                        }
9618                    } else {
9619                        vec![]
9620                    };
9621
9622                    // Reconstruct the full argument list
9623                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
9624
9625                    // Fill in bound arguments from captured environment
9626                    if let Some(env) = captured_env {
9627                        for (key, value) in env {
9628                            if key.starts_with("__bound_arg_") {
9629                                if let Ok(pos) = key[12..].parse::<usize>() {
9630                                    if pos < total_args {
9631                                        full_args[pos] = value.clone();
9632                                    }
9633                                }
9634                            }
9635                        }
9636                    }
9637
9638                    // Fill in placeholder positions with provided values
9639                    for (i, &pos) in placeholder_positions.iter().enumerate() {
9640                        if pos < total_args {
9641                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
9642                            full_args[pos] = value;
9643                        }
9644                    }
9645
9646                    // Pop lambda scope, then push a new scope for temp args
9647                    self.context.pop_scope();
9648                    self.context.push_scope();
9649
9650                    // Build AST nodes for the function call arguments
9651                    let mut temp_args: Vec<AstNode> = Vec::new();
9652                    for (i, value) in full_args.iter().enumerate() {
9653                        let temp_name = format!("__temp_arg_{}", i);
9654                        self.context.bind(temp_name.clone(), value.clone());
9655                        temp_args.push(AstNode::Variable(temp_name));
9656                    }
9657
9658                    // Call the original function
9659                    let result =
9660                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
9661
9662                    // Pop temp scope
9663                    self.context.pop_scope();
9664
9665                    return result;
9666                }
9667            }
9668        }
9669
9670        // Evaluate lambda body (normal case)
9671        // Use captured_data for lexical scoping if available, otherwise use call-site data
9672        let body_data = captured_data.unwrap_or(data);
9673        let result = self.evaluate_internal(body, body_data)?;
9674
9675        // Pop lambda scope, preserving any lambdas referenced by the return value
9676        // Fast path: scalar results can never contain lambda references
9677        let is_scalar = matches!(
9678            &result,
9679            JValue::Number(_)
9680                | JValue::Bool(_)
9681                | JValue::String(_)
9682                | JValue::Null
9683                | JValue::Undefined
9684        );
9685        if is_scalar {
9686            self.context.pop_scope();
9687        } else {
9688            let lambdas_to_keep = self.extract_lambda_ids(&result);
9689            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9690        }
9691
9692        Ok(result)
9693    }
9694
9695    /// Invoke a lambda with tail call optimization using a trampoline
9696    /// This method uses an iterative loop to handle tail-recursive calls without
9697    /// growing the stack, enabling deep recursion for tail-recursive functions.
9698    fn invoke_lambda_with_tco(
9699        &mut self,
9700        stored_lambda: &StoredLambda,
9701        initial_args: &[JValue],
9702        data: &JValue,
9703    ) -> Result<JValue, EvaluatorError> {
9704        let mut current_lambda = stored_lambda.clone();
9705        let mut current_args = initial_args.to_vec();
9706        let mut current_data = data.clone();
9707
9708        // Maximum number of tail call iterations to prevent infinite loops
9709        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
9710        const MAX_TCO_ITERATIONS: usize = 100_000;
9711        let mut iterations = 0;
9712
9713        // Push a persistent scope for the TCO trampoline loop.
9714        // This scope persists across all iterations so that lambdas defined
9715        // in one iteration (like recursive $iter) remain available in subsequent ones.
9716        self.context.push_scope();
9717
9718        // Trampoline loop - keeps evaluating until we get a final value
9719        let result = loop {
9720            iterations += 1;
9721            // The hardcoded iteration cap is a backstop for when no timeout is
9722            // configured; it must not preempt a configured timeout (which is the
9723            // more specific, user-controlled guardrail). Without this gate, an
9724            // infinite tail-recursive loop with a cheap per-iteration body hits
9725            // this cap in single-digit-to-tens of milliseconds and reports the
9726            // misleading "U1001: Stack overflow" (TCO does not grow the stack;
9727            // there is no depth-500 stack here) instead of D1012, for *any*
9728            // realistic `timeout_ms` (100ms, 1s, the docs' own 5000ms default) -
9729            // defeating the purpose of the timeout guardrail for exactly the
9730            // scenario it exists to catch (see jsonata-js's own `$inf := function
9731            // (){$inf()}; $inf()` guardrails-documentation example).
9732            if self.options.timeout_ms.is_none() && iterations > MAX_TCO_ITERATIONS {
9733                self.context.pop_scope();
9734                return Err(EvaluatorError::EvaluationError(
9735                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
9736                ));
9737            }
9738            if let Err(e) = check_loop_timeout(&self.options, self.start_time) {
9739                self.context.pop_scope();
9740                return Err(e);
9741            }
9742
9743            // Evaluate the lambda body within the persistent scope
9744            let result =
9745                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
9746
9747            match result {
9748                LambdaResult::JValue(v) => break v,
9749                LambdaResult::TailCall { lambda, args, data } => {
9750                    // Continue with the tail call - no stack growth
9751                    current_lambda = *lambda;
9752                    current_args = args;
9753                    current_data = data;
9754                }
9755            }
9756        };
9757
9758        // Pop the persistent TCO scope, preserving lambdas referenced by the result
9759        let lambdas_to_keep = self.extract_lambda_ids(&result);
9760        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9761
9762        Ok(result)
9763    }
9764
9765    /// Evaluate a lambda body, detecting tail calls for TCO
9766    /// Returns either a final value or a tail call continuation.
9767    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
9768    /// manages the persistent scope for the trampoline loop.
9769    fn invoke_lambda_body_for_tco(
9770        &mut self,
9771        lambda: &StoredLambda,
9772        values: &[JValue],
9773        data: &JValue,
9774    ) -> Result<LambdaResult, EvaluatorError> {
9775        // Validate signature if present
9776        let coerced_values = if let Some(sig_str) = &lambda.signature {
9777            match crate::signature::Signature::parse(sig_str) {
9778                Ok(sig) => match sig.validate_and_coerce(values, data) {
9779                    Ok(coerced) => coerced,
9780                    Err(e) => match e {
9781                        crate::signature::SignatureError::UndefinedArgument => {
9782                            return Ok(LambdaResult::JValue(JValue::Null));
9783                        }
9784                        crate::signature::SignatureError::ArgumentTypeMismatch {
9785                            index,
9786                            expected,
9787                        } => {
9788                            return Err(EvaluatorError::TypeError(
9789                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9790                                    ));
9791                        }
9792                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
9793                            return Err(EvaluatorError::TypeError(format!(
9794                                "T0412: Argument {} of function must be an array of {}",
9795                                index, expected
9796                            )));
9797                        }
9798                        crate::signature::SignatureError::ContextTypeMismatch {
9799                            index,
9800                            expected,
9801                        } => {
9802                            return Err(EvaluatorError::TypeError(format!(
9803                                "T0411: Context value at argument {} does not match function signature (expected {})",
9804                                index, expected
9805                            )));
9806                        }
9807                        _ => {
9808                            return Err(EvaluatorError::TypeError(format!(
9809                                "Signature validation failed: {}",
9810                                e
9811                            )));
9812                        }
9813                    },
9814                },
9815                Err(e) => {
9816                    return Err(EvaluatorError::EvaluationError(format!(
9817                        "Invalid signature: {}",
9818                        e
9819                    )));
9820                }
9821            }
9822        } else {
9823            values.to_vec()
9824        };
9825
9826        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
9827        // Apply captured environment
9828        for (name, value) in &lambda.captured_env {
9829            self.context.bind(name.clone(), value.clone());
9830        }
9831
9832        // Bind parameters
9833        for (i, param) in lambda.params.iter().enumerate() {
9834            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
9835            self.context.bind(param.clone(), value);
9836        }
9837
9838        // Evaluate the body with tail call detection
9839        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
9840        self.evaluate_for_tco(&lambda.body, body_data)
9841    }
9842
9843    /// Evaluate an expression for TCO, detecting tail calls
9844    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
9845    fn evaluate_for_tco(
9846        &mut self,
9847        node: &AstNode,
9848        data: &JValue,
9849    ) -> Result<LambdaResult, EvaluatorError> {
9850        match node {
9851            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
9852            AstNode::Conditional {
9853                condition,
9854                then_branch,
9855                else_branch,
9856            } => {
9857                let cond_value = self.evaluate_internal(condition, data)?;
9858                let is_truthy = self.is_truthy(&cond_value);
9859
9860                if is_truthy {
9861                    self.evaluate_for_tco(then_branch, data)
9862                } else if let Some(else_expr) = else_branch {
9863                    self.evaluate_for_tco(else_expr, data)
9864                } else {
9865                    Ok(LambdaResult::JValue(JValue::Null))
9866                }
9867            }
9868
9869            // Block: evaluate all but last normally, last for TCO
9870            AstNode::Block(exprs) => {
9871                if exprs.is_empty() {
9872                    return Ok(LambdaResult::JValue(JValue::Null));
9873                }
9874
9875                // Evaluate all expressions except the last
9876                let mut result = JValue::Null;
9877                for (i, expr) in exprs.iter().enumerate() {
9878                    if i == exprs.len() - 1 {
9879                        // Last expression - evaluate for TCO
9880                        return self.evaluate_for_tco(expr, data);
9881                    } else {
9882                        result = self.evaluate_internal(expr, data)?;
9883                    }
9884                }
9885                Ok(LambdaResult::JValue(result))
9886            }
9887
9888            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
9889            AstNode::Binary {
9890                op: BinaryOp::ColonEqual,
9891                lhs,
9892                rhs,
9893            } => {
9894                // This is var := value; get the variable name
9895                let var_name = match lhs.as_ref() {
9896                    AstNode::Variable(name) => name.clone(),
9897                    _ => {
9898                        // Not a simple variable binding, evaluate normally
9899                        let result = self.evaluate_internal(node, data)?;
9900                        return Ok(LambdaResult::JValue(result));
9901                    }
9902                };
9903
9904                // Check if RHS is a lambda - store it specially
9905                if let AstNode::Lambda {
9906                    params,
9907                    body,
9908                    signature,
9909                    thunk,
9910                } = rhs.as_ref()
9911                {
9912                    let captured_env = self.capture_environment_for(body, params);
9913                    let compiled_body = if !thunk {
9914                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9915                        try_compile_expr_with_allowed_vars(body, &var_refs)
9916                    } else {
9917                        None
9918                    };
9919                    let stored_lambda = StoredLambda {
9920                        params: params.clone(),
9921                        body: (**body).clone(),
9922                        compiled_body,
9923                        signature: signature.clone(),
9924                        captured_env,
9925                        captured_data: Some(data.clone()),
9926                        thunk: *thunk,
9927                    };
9928                    self.context.bind_lambda(var_name, stored_lambda);
9929                    let lambda_repr =
9930                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
9931                    return Ok(LambdaResult::JValue(lambda_repr));
9932                }
9933
9934                // Evaluate the RHS
9935                let value = self.evaluate_internal(rhs, data)?;
9936                self.context.bind(var_name, value.clone());
9937                Ok(LambdaResult::JValue(value))
9938            }
9939
9940            // Function call - this is where TCO happens
9941            AstNode::Function { name, args, .. } => {
9942                // Check if this is a call to a stored lambda (user function)
9943                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
9944                    if stored_lambda.thunk {
9945                        let mut evaluated_args = Vec::with_capacity(args.len());
9946                        for arg in args {
9947                            evaluated_args.push(self.evaluate_internal(arg, data)?);
9948                        }
9949                        return Ok(LambdaResult::TailCall {
9950                            lambda: Box::new(stored_lambda),
9951                            args: evaluated_args,
9952                            data: data.clone(),
9953                        });
9954                    }
9955                }
9956                // Not a thunk lambda - evaluate normally
9957                let result = self.evaluate_internal(node, data)?;
9958                Ok(LambdaResult::JValue(result))
9959            }
9960
9961            // Call node (calling a lambda value)
9962            AstNode::Call { procedure, args } => {
9963                // Evaluate the procedure to get the callable
9964                let callable = self.evaluate_internal(procedure, data)?;
9965
9966                // Check if it's a lambda with TCO
9967                if let JValue::Lambda { lambda_id, .. } = &callable {
9968                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
9969                        if stored_lambda.thunk {
9970                            let mut evaluated_args = Vec::with_capacity(args.len());
9971                            for arg in args {
9972                                evaluated_args.push(self.evaluate_internal(arg, data)?);
9973                            }
9974                            return Ok(LambdaResult::TailCall {
9975                                lambda: Box::new(stored_lambda),
9976                                args: evaluated_args,
9977                                data: data.clone(),
9978                            });
9979                        }
9980                    }
9981                }
9982                // Not a thunk - evaluate normally
9983                let result = self.evaluate_internal(node, data)?;
9984                Ok(LambdaResult::JValue(result))
9985            }
9986
9987            // Variable reference that might be a function call
9988            // This handles cases like $f($x) where $f is referenced by name
9989            AstNode::Variable(_) => {
9990                let result = self.evaluate_internal(node, data)?;
9991                Ok(LambdaResult::JValue(result))
9992            }
9993
9994            // Any other expression - evaluate normally
9995            _ => {
9996                let result = self.evaluate_internal(node, data)?;
9997                Ok(LambdaResult::JValue(result))
9998            }
9999        }
10000    }
10001
10002    /// Match with custom matcher function
10003    ///
10004    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
10005    /// The matcher function is called with the string and returns:
10006    /// { match: string, start: number, end: number, groups: [], next: function }
10007    /// The next function is called repeatedly to get subsequent matches
10008    fn match_with_custom_matcher(
10009        &mut self,
10010        str_value: &str,
10011        matcher_node: &AstNode,
10012        limit: Option<usize>,
10013        data: &JValue,
10014    ) -> Result<JValue, EvaluatorError> {
10015        let mut results = Vec::new();
10016        let mut count = 0;
10017
10018        // Call the matcher function with the string
10019        let str_val = JValue::string(str_value.to_string());
10020        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
10021
10022        // Iterate through matches following the 'next' chain
10023        while !current_match.is_undefined() && !current_match.is_null() {
10024            // Check limit
10025            if let Some(lim) = limit {
10026                if count >= lim {
10027                    break;
10028                }
10029            }
10030
10031            // Extract match information from the result object
10032            if let JValue::Object(ref match_obj) = current_match {
10033                // Validate that this is a proper match object
10034                let has_match = match_obj.contains_key("match");
10035                let has_start = match_obj.contains_key("start");
10036                let has_end = match_obj.contains_key("end");
10037                let has_groups = match_obj.contains_key("groups");
10038                let has_next = match_obj.contains_key("next");
10039
10040                if !has_match && !has_start && !has_end && !has_groups && !has_next {
10041                    // Invalid matcher result - T1010 error
10042                    return Err(EvaluatorError::EvaluationError(
10043                        "T1010: The matcher function did not return the correct object structure"
10044                            .to_string(),
10045                    ));
10046                }
10047
10048                // Build the result match object (match, index, groups)
10049                let mut result_obj = IndexMap::new();
10050
10051                if let Some(match_val) = match_obj.get("match") {
10052                    result_obj.insert("match".to_string(), match_val.clone());
10053                }
10054
10055                if let Some(start_val) = match_obj.get("start") {
10056                    result_obj.insert("index".to_string(), start_val.clone());
10057                }
10058
10059                if let Some(groups_val) = match_obj.get("groups") {
10060                    result_obj.insert("groups".to_string(), groups_val.clone());
10061                }
10062
10063                results.push(JValue::object(result_obj));
10064                count += 1;
10065
10066                // Get the next match by calling the 'next' function
10067                if let Some(next_func) = match_obj.get("next") {
10068                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
10069                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
10070                        continue;
10071                    }
10072                }
10073
10074                // No next function or couldn't call it - stop iteration
10075                break;
10076            } else {
10077                // Not a valid match object
10078                break;
10079            }
10080        }
10081
10082        // Return results
10083        if results.is_empty() {
10084            Ok(JValue::Undefined)
10085        } else {
10086            Ok(JValue::array(results))
10087        }
10088    }
10089
10090    /// Replace with lambda/function callback
10091    ///
10092    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
10093    /// The function receives a match object with: match, start, end, groups
10094    fn replace_with_lambda(
10095        &mut self,
10096        str_value: &JValue,
10097        pattern_value: &JValue,
10098        lambda_value: &JValue,
10099        limit_value: Option<&JValue>,
10100        data: &JValue,
10101    ) -> Result<JValue, EvaluatorError> {
10102        // Extract string
10103        let s = match str_value {
10104            JValue::String(s) => &**s,
10105            _ => {
10106                return Err(EvaluatorError::TypeError(
10107                    "replace() requires string arguments".to_string(),
10108                ))
10109            }
10110        };
10111
10112        // Extract regex pattern
10113        let (pattern, flags) =
10114            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
10115                EvaluatorError::TypeError(
10116                    "replace() pattern must be a regex when using lambda replacement".to_string(),
10117                )
10118            })?;
10119
10120        // Build regex
10121        let re = crate::functions::string::build_regex(&pattern, &flags)?;
10122
10123        // Parse limit
10124        let limit = if let Some(lim_val) = limit_value {
10125            match lim_val {
10126                JValue::Number(n) => {
10127                    let lim_f64 = *n;
10128                    if lim_f64 < 0.0 {
10129                        return Err(EvaluatorError::EvaluationError(format!(
10130                            "D3011: Limit must be non-negative, got {}",
10131                            lim_f64
10132                        )));
10133                    }
10134                    Some(lim_f64 as usize)
10135                }
10136                _ => {
10137                    return Err(EvaluatorError::TypeError(
10138                        "replace() limit must be a number".to_string(),
10139                    ))
10140                }
10141            }
10142        } else {
10143            None
10144        };
10145
10146        // Iterate through matches and replace using lambda
10147        let mut result = String::new();
10148        let mut last_end = 0;
10149        let mut count = 0;
10150
10151        for cap in re.captures_iter(s) {
10152            // Check limit
10153            if let Some(lim) = limit {
10154                if count >= lim {
10155                    break;
10156                }
10157            }
10158
10159            let m = cap.get(0).unwrap();
10160            let match_start = m.start();
10161            let match_end = m.end();
10162            let match_str = m.as_str();
10163
10164            // Add text before match
10165            result.push_str(&s[last_end..match_start]);
10166
10167            // Build match object
10168            let groups: Vec<JValue> = (1..cap.len())
10169                .map(|i| {
10170                    cap.get(i)
10171                        .map(|m| JValue::string(m.as_str().to_string()))
10172                        .unwrap_or(JValue::Null)
10173                })
10174                .collect();
10175
10176            let mut match_map = IndexMap::new();
10177            match_map.insert("match".to_string(), JValue::string(match_str));
10178            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
10179            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
10180            match_map.insert("groups".to_string(), JValue::array(groups));
10181            let match_obj = JValue::object(match_map);
10182
10183            // Invoke lambda with match object
10184            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
10185                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
10186            })?;
10187            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
10188            let replacement_str = match lambda_result {
10189                JValue::String(s) => s,
10190                _ => {
10191                    return Err(EvaluatorError::TypeError(format!(
10192                        "D3012: Replacement function must return a string, got {:?}",
10193                        lambda_result
10194                    )))
10195                }
10196            };
10197
10198            // Add replacement
10199            result.push_str(&replacement_str);
10200
10201            last_end = match_end;
10202            count += 1;
10203        }
10204
10205        // Add remaining text after last match
10206        result.push_str(&s[last_end..]);
10207
10208        Ok(JValue::string(result))
10209    }
10210
10211    /// Capture the current environment bindings for closure support
10212    fn capture_current_environment(&self) -> HashMap<String, JValue> {
10213        self.context.all_bindings()
10214    }
10215
10216    /// Capture only the variables referenced by a lambda body (selective capture).
10217    /// This avoids cloning the entire environment when only a few variables are needed.
10218    fn capture_environment_for(
10219        &self,
10220        body: &AstNode,
10221        params: &[String],
10222    ) -> HashMap<String, JValue> {
10223        let free_vars = Self::collect_free_variables(body, params);
10224        if free_vars.is_empty() {
10225            return HashMap::new();
10226        }
10227        let mut result = HashMap::new();
10228        for var_name in &free_vars {
10229            if let Some(value) = self.context.lookup(var_name) {
10230                result.insert(var_name.clone(), value.clone());
10231            }
10232        }
10233        result
10234    }
10235
10236    /// Collect all free variables in an AST node that are not bound by the given params.
10237    /// A "free variable" is one that is referenced but not defined within the expression.
10238    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
10239        let mut free_vars = HashSet::new();
10240        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
10241        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
10242        free_vars
10243    }
10244
10245    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
10246        match node {
10247            AstNode::Variable(name) => {
10248                if !bound.contains(name.as_str()) {
10249                    free.insert(name.clone());
10250                }
10251            }
10252            AstNode::Function { name, args, .. } => {
10253                // Function name references a variable (e.g., $f(...))
10254                if !bound.contains(name.as_str()) {
10255                    free.insert(name.clone());
10256                }
10257                for arg in args {
10258                    Self::collect_free_vars_walk(arg, bound, free);
10259                }
10260            }
10261            AstNode::Lambda { params, body, .. } => {
10262                // Inner lambda introduces new bindings
10263                let mut inner_bound = bound.clone();
10264                for p in params {
10265                    inner_bound.insert(p.as_str());
10266                }
10267                Self::collect_free_vars_walk(body, &inner_bound, free);
10268            }
10269            AstNode::Binary { op, lhs, rhs } => {
10270                Self::collect_free_vars_walk(lhs, bound, free);
10271                Self::collect_free_vars_walk(rhs, bound, free);
10272                // For ColonEqual, note: the binding is visible after this expr in blocks,
10273                // but block handling takes care of that separately
10274                let _ = op;
10275            }
10276            AstNode::Unary { operand, .. } => {
10277                Self::collect_free_vars_walk(operand, bound, free);
10278            }
10279            AstNode::Path { steps } => {
10280                for step in steps {
10281                    Self::collect_free_vars_walk(&step.node, bound, free);
10282                    for stage in &step.stages {
10283                        match stage {
10284                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
10285                            // An index stage binds a variable; it introduces no
10286                            // free variable references.
10287                            Stage::Index(_) => {}
10288                        }
10289                    }
10290                }
10291            }
10292            AstNode::Call { procedure, args } => {
10293                Self::collect_free_vars_walk(procedure, bound, free);
10294                for arg in args {
10295                    Self::collect_free_vars_walk(arg, bound, free);
10296                }
10297            }
10298            AstNode::Conditional {
10299                condition,
10300                then_branch,
10301                else_branch,
10302            } => {
10303                Self::collect_free_vars_walk(condition, bound, free);
10304                Self::collect_free_vars_walk(then_branch, bound, free);
10305                if let Some(else_expr) = else_branch {
10306                    Self::collect_free_vars_walk(else_expr, bound, free);
10307                }
10308            }
10309            AstNode::Block(exprs) => {
10310                let mut block_bound = bound.clone();
10311                for expr in exprs {
10312                    Self::collect_free_vars_walk(expr, &block_bound, free);
10313                    // Bindings introduced via := become bound for subsequent expressions
10314                    if let AstNode::Binary {
10315                        op: BinaryOp::ColonEqual,
10316                        lhs,
10317                        ..
10318                    } = expr
10319                    {
10320                        if let AstNode::Variable(var_name) = lhs.as_ref() {
10321                            block_bound.insert(var_name.as_str());
10322                        }
10323                    }
10324                }
10325            }
10326            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
10327                for expr in exprs {
10328                    Self::collect_free_vars_walk(expr, bound, free);
10329                }
10330            }
10331            AstNode::Object(pairs) => {
10332                for (key, value) in pairs {
10333                    Self::collect_free_vars_walk(key, bound, free);
10334                    Self::collect_free_vars_walk(value, bound, free);
10335                }
10336            }
10337            AstNode::ObjectTransform { input, pattern } => {
10338                Self::collect_free_vars_walk(input, bound, free);
10339                for (key, value) in pattern {
10340                    Self::collect_free_vars_walk(key, bound, free);
10341                    Self::collect_free_vars_walk(value, bound, free);
10342                }
10343            }
10344            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
10345                Self::collect_free_vars_walk(expr, bound, free);
10346            }
10347            AstNode::Sort { input, terms } => {
10348                Self::collect_free_vars_walk(input, bound, free);
10349                for (expr, _) in terms {
10350                    Self::collect_free_vars_walk(expr, bound, free);
10351                }
10352            }
10353            AstNode::Transform {
10354                location,
10355                update,
10356                delete,
10357            } => {
10358                Self::collect_free_vars_walk(location, bound, free);
10359                Self::collect_free_vars_walk(update, bound, free);
10360                if let Some(del) = delete {
10361                    Self::collect_free_vars_walk(del, bound, free);
10362                }
10363            }
10364            // Leaf nodes with no variable references
10365            AstNode::String(_)
10366            | AstNode::Name(_)
10367            | AstNode::Number(_)
10368            | AstNode::Boolean(_)
10369            | AstNode::Null
10370            | AstNode::Undefined
10371            | AstNode::Placeholder
10372            | AstNode::Regex { .. }
10373            | AstNode::Wildcard
10374            | AstNode::Descendant
10375            | AstNode::Parent(_)
10376            | AstNode::ParentVariable(_) => {}
10377        }
10378    }
10379
10380    /// Check if a name refers to a built-in function
10381    fn is_builtin_function(&self, name: &str) -> bool {
10382        matches!(
10383            name,
10384            // String functions
10385            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
10386            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
10387            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
10388            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
10389
10390            // Numeric functions
10391            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
10392            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
10393
10394            // Aggregation functions
10395            "sum" | "max" | "min" | "average" |
10396
10397            // Boolean/logic functions
10398            "boolean" | "not" | "exists" |
10399
10400            // Array functions
10401            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
10402
10403            // Object functions
10404            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
10405
10406            // Higher-order functions
10407            "map" | "filter" | "reduce" | "singletonArray" |
10408
10409            // Date/time functions
10410            "now" | "millis" | "fromMillis" | "toMillis"
10411        )
10412    }
10413
10414    /// Call a built-in function directly with pre-evaluated Values
10415    /// This is used when passing built-in functions to higher-order functions like $map
10416    fn call_builtin_with_values(
10417        &mut self,
10418        name: &str,
10419        values: &[JValue],
10420    ) -> Result<JValue, EvaluatorError> {
10421        use crate::functions;
10422
10423        if values.is_empty() {
10424            return Err(EvaluatorError::EvaluationError(format!(
10425                "{}() requires at least 1 argument",
10426                name
10427            )));
10428        }
10429
10430        let arg = &values[0];
10431
10432        match name {
10433            "string" => Ok(functions::string::string(arg, None)?),
10434            "number" => Ok(functions::numeric::number(arg)?),
10435            "boolean" => Ok(functions::boolean::boolean(arg)?),
10436            "not" => {
10437                let b = functions::boolean::boolean(arg)?;
10438                match b {
10439                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
10440                    _ => Err(EvaluatorError::TypeError(
10441                        "not() requires a boolean".to_string(),
10442                    )),
10443                }
10444            }
10445            "exists" => Ok(JValue::Bool(!arg.is_null())),
10446            "abs" => match arg {
10447                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
10448                _ => Err(EvaluatorError::TypeError(
10449                    "abs() requires a number argument".to_string(),
10450                )),
10451            },
10452            "floor" => match arg {
10453                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
10454                _ => Err(EvaluatorError::TypeError(
10455                    "floor() requires a number argument".to_string(),
10456                )),
10457            },
10458            "ceil" => match arg {
10459                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
10460                _ => Err(EvaluatorError::TypeError(
10461                    "ceil() requires a number argument".to_string(),
10462                )),
10463            },
10464            "round" => match arg {
10465                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
10466                _ => Err(EvaluatorError::TypeError(
10467                    "round() requires a number argument".to_string(),
10468                )),
10469            },
10470            "sqrt" => match arg {
10471                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
10472                _ => Err(EvaluatorError::TypeError(
10473                    "sqrt() requires a number argument".to_string(),
10474                )),
10475            },
10476            "uppercase" => match arg {
10477                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
10478                JValue::Null => Ok(JValue::Null),
10479                _ => Err(EvaluatorError::TypeError(
10480                    "uppercase() requires a string argument".to_string(),
10481                )),
10482            },
10483            "lowercase" => match arg {
10484                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
10485                JValue::Null => Ok(JValue::Null),
10486                _ => Err(EvaluatorError::TypeError(
10487                    "lowercase() requires a string argument".to_string(),
10488                )),
10489            },
10490            "trim" => match arg {
10491                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
10492                JValue::Null => Ok(JValue::Null),
10493                _ => Err(EvaluatorError::TypeError(
10494                    "trim() requires a string argument".to_string(),
10495                )),
10496            },
10497            "length" => match arg {
10498                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
10499                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10500                JValue::Null => Ok(JValue::Null),
10501                _ => Err(EvaluatorError::TypeError(
10502                    "length() requires a string or array argument".to_string(),
10503                )),
10504            },
10505            "sum" => match arg {
10506                JValue::Array(arr) => {
10507                    let mut total = 0.0;
10508                    for item in arr.iter() {
10509                        match item {
10510                            JValue::Number(n) => {
10511                                total += *n;
10512                            }
10513                            _ => {
10514                                return Err(EvaluatorError::TypeError(
10515                                    "sum() requires all array elements to be numbers".to_string(),
10516                                ));
10517                            }
10518                        }
10519                    }
10520                    Ok(JValue::Number(total))
10521                }
10522                JValue::Number(n) => Ok(JValue::Number(*n)),
10523                JValue::Null => Ok(JValue::Null),
10524                _ => Err(EvaluatorError::TypeError(
10525                    "sum() requires an array of numbers".to_string(),
10526                )),
10527            },
10528            "count" => {
10529                match arg {
10530                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10531                    JValue::Null => Ok(JValue::Number(0.0)),
10532                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10533                }
10534            }
10535            "max" => match arg {
10536                JValue::Array(arr) => {
10537                    let mut max_val: Option<f64> = None;
10538                    for item in arr.iter() {
10539                        if let JValue::Number(n) = item {
10540                            let f = *n;
10541                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
10542                        }
10543                    }
10544                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10545                }
10546                JValue::Number(n) => Ok(JValue::Number(*n)),
10547                JValue::Null => Ok(JValue::Null),
10548                _ => Err(EvaluatorError::TypeError(
10549                    "max() requires an array of numbers".to_string(),
10550                )),
10551            },
10552            "min" => match arg {
10553                JValue::Array(arr) => {
10554                    let mut min_val: Option<f64> = None;
10555                    for item in arr.iter() {
10556                        if let JValue::Number(n) = item {
10557                            let f = *n;
10558                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
10559                        }
10560                    }
10561                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10562                }
10563                JValue::Number(n) => Ok(JValue::Number(*n)),
10564                JValue::Null => Ok(JValue::Null),
10565                _ => Err(EvaluatorError::TypeError(
10566                    "min() requires an array of numbers".to_string(),
10567                )),
10568            },
10569            "average" => match arg {
10570                JValue::Array(arr) => {
10571                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
10572                    if nums.is_empty() {
10573                        Ok(JValue::Null)
10574                    } else {
10575                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
10576                        Ok(JValue::Number(avg))
10577                    }
10578                }
10579                JValue::Number(n) => Ok(JValue::Number(*n)),
10580                JValue::Null => Ok(JValue::Null),
10581                _ => Err(EvaluatorError::TypeError(
10582                    "average() requires an array of numbers".to_string(),
10583                )),
10584            },
10585            "append" => {
10586                // append(array1, array2) - append second array to first
10587                if values.len() < 2 {
10588                    return Err(EvaluatorError::EvaluationError(
10589                        "append() requires 2 arguments".to_string(),
10590                    ));
10591                }
10592                let first = &values[0];
10593                let second = &values[1];
10594
10595                // Convert first to array if needed
10596                let mut result = match first {
10597                    JValue::Array(arr) => arr.to_vec(),
10598                    JValue::Null => vec![],
10599                    other => vec![other.clone()],
10600                };
10601
10602                // Append second (flatten if array)
10603                match second {
10604                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
10605                    JValue::Null => {}
10606                    other => result.push(other.clone()),
10607                }
10608
10609                check_sequence_length(result.len(), &self.options)?;
10610                Ok(JValue::array(result))
10611            }
10612            "reverse" => match arg {
10613                JValue::Array(arr) => {
10614                    let mut reversed = arr.to_vec();
10615                    reversed.reverse();
10616                    Ok(JValue::array(reversed))
10617                }
10618                JValue::Null => Ok(JValue::Null),
10619                _ => Err(EvaluatorError::TypeError(
10620                    "reverse() requires an array".to_string(),
10621                )),
10622            },
10623            "keys" => match arg {
10624                JValue::Object(obj) => {
10625                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
10626                    check_sequence_length(keys.len(), &self.options)?;
10627                    Ok(JValue::array(keys))
10628                }
10629                JValue::Null => Ok(JValue::Null),
10630                _ => Err(EvaluatorError::TypeError(
10631                    "keys() requires an object".to_string(),
10632                )),
10633            },
10634
10635            // Add more functions as needed
10636            _ => Err(EvaluatorError::ReferenceError(format!(
10637                "Built-in function {} cannot be called with values directly",
10638                name
10639            ))),
10640        }
10641    }
10642
10643    /// Collect all descendant values recursively
10644    fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
10645        let mut descendants = Vec::new();
10646
10647        match value {
10648            JValue::Null => {
10649                // Null has no descendants, return empty
10650                return descendants;
10651            }
10652            JValue::Object(obj) => {
10653                // Include the current object
10654                descendants.push(value.clone());
10655
10656                for val in obj.values() {
10657                    // Recursively collect descendants
10658                    descendants.extend(self.collect_descendants(val));
10659                }
10660            }
10661            JValue::Array(arr) => {
10662                // DO NOT include the array itself - only recurse into elements
10663                // This matches JavaScript behavior: arrays are traversed but not collected
10664                for val in arr.iter() {
10665                    // Recursively collect descendants
10666                    descendants.extend(self.collect_descendants(val));
10667                }
10668            }
10669            _ => {
10670                // For primitives (string, number, boolean), just include the value itself
10671                descendants.push(value.clone());
10672            }
10673        }
10674
10675        descendants
10676    }
10677
10678    /// Evaluate a predicate (array filter or index)
10679    fn evaluate_predicate(
10680        &mut self,
10681        current: &JValue,
10682        predicate: &AstNode,
10683    ) -> Result<JValue, EvaluatorError> {
10684        // Special case: empty brackets [] (represented as Boolean(true))
10685        // This forces the value to be wrapped in an array
10686        if matches!(predicate, AstNode::Boolean(true)) {
10687            return match current {
10688                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
10689                JValue::Null => Ok(JValue::Null),
10690                other => Ok(JValue::array(vec![other.clone()])),
10691            };
10692        }
10693
10694        match current {
10695            JValue::Array(_arr) => {
10696                // Standalone predicates do simple array operations (no mapping over sub-arrays)
10697
10698                // First, try to evaluate predicate as a simple number (array index)
10699                if let AstNode::Number(n) = predicate {
10700                    // Direct array indexing
10701                    return self.array_index(current, &JValue::Number(*n));
10702                }
10703
10704                // Fast path: if predicate is definitely a filter expression (comparison/logical),
10705                // skip speculative numeric evaluation and go directly to filter logic
10706                if Self::is_filter_predicate(predicate) {
10707                    // Try CompiledExpr fast path
10708                    if let Some(compiled) = try_compile_expr(predicate) {
10709                        let shape = _arr.first().and_then(build_shape_cache);
10710                        let mut filtered = Vec::with_capacity(_arr.len());
10711                        for item in _arr.iter() {
10712                            let result = if let Some(ref s) = shape {
10713                                eval_compiled_shaped(
10714                                    &compiled,
10715                                    item,
10716                                    None,
10717                                    s,
10718                                    &self.options,
10719                                    self.start_time,
10720                                )?
10721                            } else {
10722                                eval_compiled(
10723                                    &compiled,
10724                                    item,
10725                                    None,
10726                                    &self.options,
10727                                    self.start_time,
10728                                )?
10729                            };
10730                            if compiled_is_truthy(&result) {
10731                                filtered.push(item.clone());
10732                            }
10733                        }
10734                        return Ok(JValue::array(filtered));
10735                    }
10736                    // Fallback: full AST evaluation per element
10737                    let mut filtered = Vec::new();
10738                    for item in _arr.iter() {
10739                        let item_result = self.evaluate_internal(predicate, item)?;
10740                        if self.is_truthy(&item_result) {
10741                            filtered.push(item.clone());
10742                        }
10743                    }
10744                    return Ok(JValue::array(filtered));
10745                }
10746
10747                // Try to evaluate the predicate to see if it's a numeric index
10748                // If evaluation succeeds and yields a number, use it as an index
10749                // If evaluation fails (e.g., comparison error), treat as filter
10750                match self.evaluate_internal(predicate, current) {
10751                    Ok(JValue::Number(_)) => {
10752                        // It's a numeric index
10753                        let pred_result = self.evaluate_internal(predicate, current)?;
10754                        return self.array_index(current, &pred_result);
10755                    }
10756                    Ok(JValue::Array(indices)) => {
10757                        // Multiple array selectors [[indices]]
10758                        // Check if array contains any non-numeric values
10759                        let has_non_numeric =
10760                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
10761
10762                        if has_non_numeric {
10763                            // If array contains non-numeric values, return entire array
10764                            return Ok(current.clone());
10765                        }
10766
10767                        // Collect numeric indices, handling negative indices
10768                        let arr_len = _arr.len() as i64;
10769                        let mut resolved_indices: Vec<i64> = indices
10770                            .iter()
10771                            .filter_map(|v| {
10772                                if let JValue::Number(n) = v {
10773                                    let idx = *n as i64;
10774                                    // Resolve negative indices
10775                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
10776                                    // Only include valid indices
10777                                    if actual_idx >= 0 && actual_idx < arr_len {
10778                                        Some(actual_idx)
10779                                    } else {
10780                                        None
10781                                    }
10782                                } else {
10783                                    None
10784                                }
10785                            })
10786                            .collect();
10787
10788                        // Sort and deduplicate indices
10789                        resolved_indices.sort();
10790                        resolved_indices.dedup();
10791
10792                        // Select elements at each sorted index
10793                        let result: Vec<JValue> = resolved_indices
10794                            .iter()
10795                            .map(|&idx| _arr[idx as usize].clone())
10796                            .collect();
10797
10798                        return Ok(JValue::array(result));
10799                    }
10800                    Ok(_) => {
10801                        // Evaluated successfully but not a number - might be a filter
10802                        // Fall through to filter logic
10803                    }
10804                    Err(_) => {
10805                        // Evaluation failed - it's likely a filter expression
10806                        // Fall through to filter logic
10807                    }
10808                }
10809
10810                // Try CompiledExpr fast path for filter expressions
10811                if let Some(compiled) = try_compile_expr(predicate) {
10812                    let shape = _arr.first().and_then(build_shape_cache);
10813                    let mut filtered = Vec::with_capacity(_arr.len());
10814                    for item in _arr.iter() {
10815                        let result = if let Some(ref s) = shape {
10816                            eval_compiled_shaped(
10817                                &compiled,
10818                                item,
10819                                None,
10820                                s,
10821                                &self.options,
10822                                self.start_time,
10823                            )?
10824                        } else {
10825                            eval_compiled(&compiled, item, None, &self.options, self.start_time)?
10826                        };
10827                        if compiled_is_truthy(&result) {
10828                            filtered.push(item.clone());
10829                        }
10830                    }
10831                    return Ok(JValue::array(filtered));
10832                }
10833
10834                // It's a filter expression - evaluate the predicate for each array element
10835                let mut filtered = Vec::new();
10836                for item in _arr.iter() {
10837                    let item_result = self.evaluate_internal(predicate, item)?;
10838
10839                    // If result is truthy, include this item
10840                    if self.is_truthy(&item_result) {
10841                        filtered.push(item.clone());
10842                    }
10843                }
10844
10845                Ok(JValue::array(filtered))
10846            }
10847            JValue::Object(obj) => {
10848                // For objects, predicate can be either:
10849                // 1. A string - property access (computed property name)
10850                // 2. A boolean expression - filter (return object if truthy)
10851                let pred_result = self.evaluate_internal(predicate, current)?;
10852
10853                // If it's a string, use it as a key for property access
10854                if let JValue::String(key) = &pred_result {
10855                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
10856                }
10857
10858                // Otherwise, treat as a filter expression
10859                // If the predicate is truthy, return the object; otherwise return undefined
10860                if self.is_truthy(&pred_result) {
10861                    Ok(current.clone())
10862                } else {
10863                    Ok(JValue::Undefined)
10864                }
10865            }
10866            _ => {
10867                // For primitive values (string, number, boolean):
10868                // In JSONata, scalars are treated as single-element arrays when indexed.
10869                // So value[0] returns value, value[1] returns undefined.
10870
10871                // First check if predicate is a numeric literal
10872                if let AstNode::Number(n) = predicate {
10873                    // For scalars, index 0 or -1 returns the value, others return undefined
10874                    let idx = n.floor() as i64;
10875                    if idx == 0 || idx == -1 {
10876                        return Ok(current.clone());
10877                    } else {
10878                        return Ok(JValue::Undefined);
10879                    }
10880                }
10881
10882                // Try to evaluate the predicate to see if it's a numeric index
10883                let pred_result = self.evaluate_internal(predicate, current)?;
10884
10885                if let JValue::Number(n) = &pred_result {
10886                    // It's a numeric index - treat scalar as single-element array
10887                    let idx = n.floor() as i64;
10888                    if idx == 0 || idx == -1 {
10889                        return Ok(current.clone());
10890                    } else {
10891                        return Ok(JValue::Undefined);
10892                    }
10893                }
10894
10895                // For non-numeric predicates, treat as a filter:
10896                // value[true] returns value, value[false] returns undefined
10897                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
10898                if self.is_truthy(&pred_result) {
10899                    Ok(current.clone())
10900                } else {
10901                    // Return undefined (not null) so $map can filter it out
10902                    Ok(JValue::Undefined)
10903                }
10904            }
10905        }
10906    }
10907
10908    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
10909    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
10910    fn evaluate_sort_term(
10911        &mut self,
10912        term_expr: &AstNode,
10913        element: &JValue,
10914    ) -> Result<JValue, EvaluatorError> {
10915        // For tuples (from index binding), extract the actual value from @ field
10916        let actual_element = if let JValue::Object(obj) = element {
10917            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
10918                obj.get("@").cloned().unwrap_or(JValue::Null)
10919            } else {
10920                element.clone()
10921            }
10922        } else {
10923            element.clone()
10924        };
10925
10926        // For simple field access (Path with single Name step), check if field exists
10927        if let AstNode::Path { steps } = term_expr {
10928            if steps.len() == 1 && steps[0].stages.is_empty() {
10929                if let AstNode::Name(field_name) = &steps[0].node {
10930                    // Check if the field exists in the element
10931                    if let JValue::Object(obj) = &actual_element {
10932                        return match obj.get(field_name) {
10933                            Some(val) => Ok(val.clone()),  // Field exists (may be null)
10934                            None => Ok(JValue::Undefined), // Field is missing
10935                        };
10936                    } else {
10937                        // Not an object - return undefined
10938                        return Ok(JValue::Undefined);
10939                    }
10940                }
10941            }
10942        }
10943
10944        // For complex expressions, evaluate against the tuple's `@` value (the
10945        // real element), not the wrapper. The tuple's carried focus/index/ancestor
10946        // bindings are reachable via context (bound by evaluate_sort), so a term
10947        // like `$`, `%.Price`, or `$pos` still resolves correctly.
10948        let result = self.evaluate_internal(term_expr, &actual_element)?;
10949
10950        // If the result is null from a complex expression, we can't easily tell if it's
10951        // "missing field" or "explicit null". For now, treat null results as undefined
10952        // to maintain compatibility with existing tests.
10953        // TODO: For full JS compatibility, would need deeper analysis of the expression
10954        if result.is_null() {
10955            return Ok(JValue::Undefined);
10956        }
10957
10958        Ok(result)
10959    }
10960
10961    /// Evaluate sort operator
10962    fn evaluate_sort(
10963        &mut self,
10964        data: &JValue,
10965        terms: &[(AstNode, bool)],
10966    ) -> Result<JValue, EvaluatorError> {
10967        // If data is null, return null
10968        if data.is_null() {
10969            return Ok(JValue::Null);
10970        }
10971
10972        // If data is not an array, return it as-is (can't sort a single value)
10973        let array = match data {
10974            JValue::Array(arr) => arr.clone(),
10975            other => return Ok(other.clone()),
10976        };
10977
10978        // If empty array, return as-is
10979        if array.is_empty() {
10980            return Ok(JValue::Array(array));
10981        }
10982
10983        // Evaluate sort keys for each element
10984        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
10985
10986        for (idx, element) in array.iter().enumerate() {
10987            let mut sort_keys = Vec::new();
10988
10989            // When sorting a tuple stream (the input path had a `%`/`@`/`#`
10990            // step, so each element is a `{@, !label, $var, __tuple__}`
10991            // wrapper), bind its carried ancestor/focus/index keys into scope
10992            // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
10993            // create_tuple_stream's per-tuple frame binding. Sort terms attach
10994            // to a synthetic step after the last input step, so `%` refers to
10995            // the last input step's ancestry, carried under `!label` here.
10996            // Saves/restores rather than blindly unbinding, so a tuple key
10997            // that collides with a live outer `:=` binding doesn't get
10998            // deleted once this row's sort terms are evaluated.
10999            let tuple_bindings = match element {
11000                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11001                    Some(self.bind_tuple_keys(obj))
11002                }
11003                _ => None,
11004            };
11005
11006            // When sorting a tuple stream, `$` and the term's data context are the
11007            // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
11008            // otherwise a term like `^($)` would try to order by the wrapper
11009            // object and raise T2008. The carried focus/index/ancestor keys stay
11010            // reachable via the context bindings established just above.
11011            let term_data = match element {
11012                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11013                    obj.get("@").cloned().unwrap_or(JValue::Null)
11014                }
11015                other => other.clone(),
11016            };
11017
11018            // Evaluate each sort term with $ bound to the element
11019            for (term_expr, _ascending) in terms {
11020                // Save current $ binding
11021                let saved_dollar = self.context.lookup("$").cloned();
11022
11023                // Bind $ to current element
11024                self.context.bind("$".to_string(), term_data.clone());
11025
11026                // Evaluate the sort expression, distinguishing missing fields from explicit null
11027                let sort_value = self.evaluate_sort_term(term_expr, element)?;
11028
11029                // Restore $ binding
11030                if let Some(val) = saved_dollar {
11031                    self.context.bind("$".to_string(), val);
11032                } else {
11033                    self.context.unbind("$");
11034                }
11035
11036                sort_keys.push(sort_value);
11037            }
11038
11039            if let Some(tuple_bindings) = tuple_bindings {
11040                tuple_bindings.restore(self);
11041            }
11042
11043            indexed_array.push((idx, sort_keys));
11044        }
11045
11046        // Validate that all sort keys are comparable (same type, or undefined)
11047        // Undefined values (missing fields) are allowed and sort to the end
11048        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
11049        for term_idx in 0..terms.len() {
11050            let mut first_valid_type: Option<&str> = None;
11051
11052            for (_idx, sort_keys) in &indexed_array {
11053                let sort_value = &sort_keys[term_idx];
11054
11055                // Skip undefined markers (missing fields) - these are allowed and sort to end
11056                if sort_value.is_undefined() {
11057                    continue;
11058                }
11059
11060                // Get the type name for this value
11061                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
11062                let value_type = match sort_value {
11063                    JValue::Number(_) => "number",
11064                    JValue::String(_) => "string",
11065                    JValue::Bool(_) => "boolean",
11066                    JValue::Array(_) => "array",
11067                    JValue::Object(_) => "object", // This catches non-undefined objects
11068                    JValue::Null => "null",        // Explicit null from data
11069                    _ => "unknown",
11070                };
11071
11072                // Check that sort keys are only numbers or strings
11073                // Null, boolean, array, and object types are not valid for sorting
11074                if value_type != "number" && value_type != "string" {
11075                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
11076                }
11077
11078                // Check if this matches the first valid type we saw
11079                if let Some(first_type) = first_valid_type {
11080                    if first_type != value_type {
11081                        return Err(EvaluatorError::TypeError(format!(
11082                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
11083                            first_type, value_type
11084                        )));
11085                    }
11086                } else {
11087                    first_valid_type = Some(value_type);
11088                }
11089            }
11090        }
11091
11092        // Sort the indexed array
11093        indexed_array.sort_by(|a, b| {
11094            // Compare sort keys in order
11095            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
11096                let left = &a.1[i];
11097                let right = &b.1[i];
11098
11099                let cmp = self.compare_values(left, right);
11100
11101                if cmp != std::cmp::Ordering::Equal {
11102                    return if *ascending { cmp } else { cmp.reverse() };
11103                }
11104            }
11105
11106            // If all keys are equal, maintain original order (stable sort)
11107            a.0.cmp(&b.0)
11108        });
11109
11110        // Extract sorted elements
11111        let sorted: Vec<JValue> = indexed_array
11112            .iter()
11113            .map(|(idx, _)| array[*idx].clone())
11114            .collect();
11115
11116        Ok(JValue::array(sorted))
11117    }
11118
11119    /// Compare two values for sorting (JSONata semantics)
11120    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
11121        // Handle undefined markers first - they sort to the end
11122        let left_undef = left.is_undefined();
11123        let right_undef = right.is_undefined();
11124
11125        if left_undef && right_undef {
11126            return Ordering::Equal;
11127        }
11128        if left_undef {
11129            return Ordering::Greater; // Undefined sorts last
11130        }
11131        if right_undef {
11132            return Ordering::Less;
11133        }
11134
11135        match (left, right) {
11136            // Nulls also sort last (explicit null in data)
11137            (JValue::Null, JValue::Null) => Ordering::Equal,
11138            (JValue::Null, _) => Ordering::Greater,
11139            (_, JValue::Null) => Ordering::Less,
11140
11141            // Numbers
11142            (JValue::Number(a), JValue::Number(b)) => {
11143                let a_f64 = *a;
11144                let b_f64 = *b;
11145                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
11146            }
11147
11148            // Strings
11149            (JValue::String(a), JValue::String(b)) => a.cmp(b),
11150
11151            // Booleans
11152            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
11153
11154            // Arrays (lexicographic comparison)
11155            (JValue::Array(a), JValue::Array(b)) => {
11156                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
11157                    let cmp = self.compare_values(a_elem, b_elem);
11158                    if cmp != Ordering::Equal {
11159                        return cmp;
11160                    }
11161                }
11162                a.len().cmp(&b.len())
11163            }
11164
11165            // Different types: use type ordering
11166            // null < bool < number < string < array < object
11167            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
11168            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
11169            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
11170            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
11171
11172            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
11173            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
11174            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
11175            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
11176
11177            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
11178            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
11179            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
11180            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
11181
11182            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
11183            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
11184            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
11185            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
11186
11187            (JValue::Object(_), _) => Ordering::Greater,
11188            _ => Ordering::Equal,
11189        }
11190    }
11191
11192    /// Check if a value is truthy (JSONata semantics).
11193    fn is_truthy(&self, value: &JValue) -> bool {
11194        match value {
11195            JValue::Null | JValue::Undefined => false,
11196            JValue::Bool(b) => *b,
11197            JValue::Number(n) => *n != 0.0,
11198            JValue::String(s) => !s.is_empty(),
11199            JValue::Array(arr) => !arr.is_empty(),
11200            JValue::Object(obj) => !obj.is_empty(),
11201            _ => false,
11202        }
11203    }
11204
11205    /// Check if a value is truthy for the default operator (?:)
11206    /// This has special semantics:
11207    /// - Lambda/function objects are not values, so they're falsy
11208    /// - Arrays containing only falsy elements are falsy
11209    /// - Otherwise, use standard truthiness
11210    fn is_truthy_for_default(&self, value: &JValue) -> bool {
11211        match value {
11212            // Lambda/function values are not data values, so they're falsy
11213            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
11214            // Arrays need special handling - check if all elements are falsy
11215            JValue::Array(arr) => {
11216                if arr.is_empty() {
11217                    return false;
11218                }
11219                // Array is truthy only if it contains at least one truthy element
11220                arr.iter().any(|elem| self.is_truthy(elem))
11221            }
11222            // For all other types, use standard truthiness
11223            _ => self.is_truthy(value),
11224        }
11225    }
11226
11227    /// Unwrap singleton arrays to scalar values
11228    /// This is used when no explicit array-keeping operation (like []) was used
11229    fn unwrap_singleton(&self, value: JValue) -> JValue {
11230        match value {
11231            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
11232            _ => value,
11233        }
11234    }
11235
11236    /// Extract lambda IDs from a value (used for closure preservation)
11237    /// Finds any lambda_id references in the value so they can be preserved
11238    /// when exiting a block scope
11239    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
11240        // Fast path: scalars can never contain lambda references
11241        match value {
11242            JValue::Number(_)
11243            | JValue::Bool(_)
11244            | JValue::String(_)
11245            | JValue::Null
11246            | JValue::Undefined
11247            | JValue::Regex { .. }
11248            | JValue::Builtin { .. } => return Vec::new(),
11249            _ => {}
11250        }
11251        let mut ids = Vec::new();
11252        self.collect_lambda_ids(value, &mut ids);
11253        ids
11254    }
11255
11256    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
11257        match value {
11258            JValue::Lambda { lambda_id, .. } => {
11259                let id_str = lambda_id.to_string();
11260                if !ids.contains(&id_str) {
11261                    ids.push(id_str);
11262                    // Transitively follow the stored lambda's captured_env
11263                    // to find all referenced lambdas. This is critical for
11264                    // closures like the Y-combinator where returned lambdas
11265                    // capture other lambdas in their environment.
11266                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
11267                        let env_values: Vec<JValue> =
11268                            stored.captured_env.values().cloned().collect();
11269                        for env_value in &env_values {
11270                            self.collect_lambda_ids(env_value, ids);
11271                        }
11272                    }
11273                }
11274            }
11275            JValue::Object(map) => {
11276                // Recurse into object values
11277                for v in map.values() {
11278                    self.collect_lambda_ids(v, ids);
11279                }
11280            }
11281            JValue::Array(arr) => {
11282                // Recurse into array elements
11283                for v in arr.iter() {
11284                    self.collect_lambda_ids(v, ids);
11285                }
11286            }
11287            _ => {}
11288        }
11289    }
11290
11291    /// Equality comparison (JSONata semantics)
11292    fn equals(&self, left: &JValue, right: &JValue) -> bool {
11293        crate::functions::array::values_equal(left, right)
11294    }
11295
11296    /// Addition
11297    fn add(
11298        &self,
11299        left: &JValue,
11300        right: &JValue,
11301        left_is_explicit_null: bool,
11302        right_is_explicit_null: bool,
11303    ) -> Result<JValue, EvaluatorError> {
11304        match (left, right) {
11305            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
11306            // Explicit null literal with number -> T2002 error
11307            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
11308                Err(EvaluatorError::TypeError(
11309                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11310                ))
11311            }
11312            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
11313                Err(EvaluatorError::TypeError(
11314                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
11315                ))
11316            }
11317            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
11318                Err(EvaluatorError::TypeError(
11319                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11320                ))
11321            }
11322            // Undefined variable (null/undefined) with number -> undefined result
11323            (JValue::Null | JValue::Undefined, JValue::Number(_))
11324            | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
11325            // Boolean with anything (including undefined) -> T2001 error
11326            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
11327                "T2001: The left side of the '+' operator must evaluate to a number or a string"
11328                    .to_string(),
11329            )),
11330            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
11331                "T2001: The right side of the '+' operator must evaluate to a number or a string"
11332                    .to_string(),
11333            )),
11334            // Undefined with undefined -> undefined
11335            (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
11336                Ok(JValue::Null)
11337            }
11338            _ => Err(EvaluatorError::TypeError(format!(
11339                "Cannot add {:?} and {:?}",
11340                left, right
11341            ))),
11342        }
11343    }
11344
11345    /// Subtraction
11346    fn subtract(
11347        &self,
11348        left: &JValue,
11349        right: &JValue,
11350        left_is_explicit_null: bool,
11351        right_is_explicit_null: bool,
11352    ) -> Result<JValue, EvaluatorError> {
11353        match (left, right) {
11354            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
11355            // Explicit null literal -> error
11356            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11357                "T2002: The left side of the - operator must evaluate to a number".to_string(),
11358            )),
11359            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11360                "T2002: The right side of the - operator must evaluate to a number".to_string(),
11361            )),
11362            // Undefined variables -> undefined result
11363            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11364                Ok(JValue::Null)
11365            }
11366            _ => Err(EvaluatorError::TypeError(format!(
11367                "Cannot subtract {:?} and {:?}",
11368                left, right
11369            ))),
11370        }
11371    }
11372
11373    /// Multiplication
11374    fn multiply(
11375        &self,
11376        left: &JValue,
11377        right: &JValue,
11378        left_is_explicit_null: bool,
11379        right_is_explicit_null: bool,
11380    ) -> Result<JValue, EvaluatorError> {
11381        match (left, right) {
11382            (JValue::Number(a), JValue::Number(b)) => {
11383                let result = *a * *b;
11384                // Check for overflow to Infinity
11385                if result.is_infinite() {
11386                    return Err(EvaluatorError::EvaluationError(
11387                        "D1001: Number out of range".to_string(),
11388                    ));
11389                }
11390                Ok(JValue::Number(result))
11391            }
11392            // Explicit null literal -> error
11393            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11394                "T2002: The left side of the * operator must evaluate to a number".to_string(),
11395            )),
11396            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11397                "T2002: The right side of the * operator must evaluate to a number".to_string(),
11398            )),
11399            // Undefined variables -> undefined result
11400            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11401                Ok(JValue::Null)
11402            }
11403            _ => Err(EvaluatorError::TypeError(format!(
11404                "Cannot multiply {:?} and {:?}",
11405                left, right
11406            ))),
11407        }
11408    }
11409
11410    /// Division
11411    fn divide(
11412        &self,
11413        left: &JValue,
11414        right: &JValue,
11415        left_is_explicit_null: bool,
11416        right_is_explicit_null: bool,
11417    ) -> Result<JValue, EvaluatorError> {
11418        match (left, right) {
11419            (JValue::Number(a), JValue::Number(b)) => {
11420                let denominator = *b;
11421                if denominator == 0.0 {
11422                    return Err(EvaluatorError::EvaluationError(
11423                        "Division by zero".to_string(),
11424                    ));
11425                }
11426                Ok(JValue::Number(*a / denominator))
11427            }
11428            // Explicit null literal -> error
11429            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11430                "T2002: The left side of the / operator must evaluate to a number".to_string(),
11431            )),
11432            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11433                "T2002: The right side of the / operator must evaluate to a number".to_string(),
11434            )),
11435            // Undefined variables -> undefined result
11436            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11437                Ok(JValue::Null)
11438            }
11439            _ => Err(EvaluatorError::TypeError(format!(
11440                "Cannot divide {:?} and {:?}",
11441                left, right
11442            ))),
11443        }
11444    }
11445
11446    /// Modulo
11447    fn modulo(
11448        &self,
11449        left: &JValue,
11450        right: &JValue,
11451        left_is_explicit_null: bool,
11452        right_is_explicit_null: bool,
11453    ) -> Result<JValue, EvaluatorError> {
11454        match (left, right) {
11455            (JValue::Number(a), JValue::Number(b)) => {
11456                let denominator = *b;
11457                if denominator == 0.0 {
11458                    return Err(EvaluatorError::EvaluationError(
11459                        "Division by zero".to_string(),
11460                    ));
11461                }
11462                Ok(JValue::Number(*a % denominator))
11463            }
11464            // Explicit null literal -> error
11465            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11466                "T2002: The left side of the % operator must evaluate to a number".to_string(),
11467            )),
11468            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11469                "T2002: The right side of the % operator must evaluate to a number".to_string(),
11470            )),
11471            // Undefined variables -> undefined result
11472            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11473                Ok(JValue::Null)
11474            }
11475            _ => Err(EvaluatorError::TypeError(format!(
11476                "Cannot compute modulo of {:?} and {:?}",
11477                left, right
11478            ))),
11479        }
11480    }
11481
11482    /// Get human-readable type name for error messages
11483    fn type_name(value: &JValue) -> &'static str {
11484        match value {
11485            JValue::Null => "null",
11486            JValue::Bool(_) => "boolean",
11487            JValue::Number(_) => "number",
11488            JValue::String(_) => "string",
11489            JValue::Array(_) => "array",
11490            JValue::Object(_) => "object",
11491            _ => "unknown",
11492        }
11493    }
11494
11495    /// Ordered comparison with null/type checking shared across <, <=, >, >=
11496    ///
11497    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
11498    /// `compare_strs` receives (left_str, right_str) for string operands.
11499    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
11500    fn ordered_compare(
11501        &self,
11502        left: &JValue,
11503        right: &JValue,
11504        left_is_explicit_null: bool,
11505        right_is_explicit_null: bool,
11506        op_symbol: &str,
11507        compare_nums: fn(f64, f64) -> bool,
11508        compare_strs: fn(&str, &str) -> bool,
11509    ) -> Result<JValue, EvaluatorError> {
11510        match (left, right) {
11511            (JValue::Number(a), JValue::Number(b)) => {
11512                Ok(JValue::Bool(compare_nums(*a, *b)))
11513            }
11514            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
11515            // Both null/undefined -> return undefined
11516            (JValue::Null, JValue::Null) => Ok(JValue::Null),
11517            // Explicit null literal with any type (except null) -> T2010 error
11518            (JValue::Null, _) if left_is_explicit_null => {
11519                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11520            }
11521            (_, JValue::Null) if right_is_explicit_null => {
11522                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11523            }
11524            // Boolean with undefined -> T2010 error
11525            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
11526                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11527            }
11528            // Number or String with undefined (not explicit null) -> undefined result
11529            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
11530            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
11531                Ok(JValue::Null)
11532            }
11533            // String vs Number -> T2009
11534            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
11535                Err(EvaluatorError::EvaluationError(format!(
11536                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
11537                    op_symbol
11538                )))
11539            }
11540            // Boolean comparisons -> T2010
11541            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
11542                Err(EvaluatorError::EvaluationError(format!(
11543                    "T2010: Cannot compare {} and {}",
11544                    Self::type_name(left), Self::type_name(right)
11545                )))
11546            }
11547            // Other type mismatches
11548            _ => Err(EvaluatorError::EvaluationError(format!(
11549                "T2010: Cannot compare {} and {}",
11550                Self::type_name(left), Self::type_name(right)
11551            ))),
11552        }
11553    }
11554
11555    /// Less than comparison
11556    fn less_than(
11557        &self,
11558        left: &JValue,
11559        right: &JValue,
11560        left_is_explicit_null: bool,
11561        right_is_explicit_null: bool,
11562    ) -> Result<JValue, EvaluatorError> {
11563        self.ordered_compare(
11564            left,
11565            right,
11566            left_is_explicit_null,
11567            right_is_explicit_null,
11568            "<",
11569            |a, b| a < b,
11570            |a, b| a < b,
11571        )
11572    }
11573
11574    /// Less than or equal comparison
11575    fn less_than_or_equal(
11576        &self,
11577        left: &JValue,
11578        right: &JValue,
11579        left_is_explicit_null: bool,
11580        right_is_explicit_null: bool,
11581    ) -> Result<JValue, EvaluatorError> {
11582        self.ordered_compare(
11583            left,
11584            right,
11585            left_is_explicit_null,
11586            right_is_explicit_null,
11587            "<=",
11588            |a, b| a <= b,
11589            |a, b| a <= b,
11590        )
11591    }
11592
11593    /// Greater than comparison
11594    fn greater_than(
11595        &self,
11596        left: &JValue,
11597        right: &JValue,
11598        left_is_explicit_null: bool,
11599        right_is_explicit_null: bool,
11600    ) -> Result<JValue, EvaluatorError> {
11601        self.ordered_compare(
11602            left,
11603            right,
11604            left_is_explicit_null,
11605            right_is_explicit_null,
11606            ">",
11607            |a, b| a > b,
11608            |a, b| a > b,
11609        )
11610    }
11611
11612    /// Greater than or equal comparison
11613    fn greater_than_or_equal(
11614        &self,
11615        left: &JValue,
11616        right: &JValue,
11617        left_is_explicit_null: bool,
11618        right_is_explicit_null: bool,
11619    ) -> Result<JValue, EvaluatorError> {
11620        self.ordered_compare(
11621            left,
11622            right,
11623            left_is_explicit_null,
11624            right_is_explicit_null,
11625            ">=",
11626            |a, b| a >= b,
11627            |a, b| a >= b,
11628        )
11629    }
11630
11631    /// Convert a value to a string for concatenation
11632    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
11633        match value {
11634            JValue::String(s) => Ok(s.to_string()),
11635            JValue::Null => Ok(String::new()),
11636            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
11637                match crate::functions::string::string(value, None) {
11638                    Ok(JValue::String(s)) => Ok(s.to_string()),
11639                    Ok(JValue::Null) => Ok(String::new()),
11640                    _ => Err(EvaluatorError::TypeError(
11641                        "Cannot concatenate complex types".to_string(),
11642                    )),
11643                }
11644            }
11645            _ => Ok(String::new()),
11646        }
11647    }
11648
11649    /// String concatenation
11650    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11651        let left_str = Self::value_to_concat_string(left)?;
11652        let right_str = Self::value_to_concat_string(right)?;
11653        Ok(JValue::string(format!("{}{}", left_str, right_str)))
11654    }
11655
11656    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
11657    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11658        // Check left operand is a number or null
11659        let start_f64 = match left {
11660            JValue::Number(n) => Some(*n),
11661            JValue::Null | JValue::Undefined => None,
11662            _ => {
11663                return Err(EvaluatorError::EvaluationError(
11664                    "T2003: Left operand of range operator must be a number".to_string(),
11665                ));
11666            }
11667        };
11668
11669        // Check left operand is an integer (if it's a number)
11670        if let Some(val) = start_f64 {
11671            if val.fract() != 0.0 {
11672                return Err(EvaluatorError::EvaluationError(
11673                    "T2003: Left operand of range operator must be an integer".to_string(),
11674                ));
11675            }
11676        }
11677
11678        // Check right operand is a number or null
11679        let end_f64 = match right {
11680            JValue::Number(n) => Some(*n),
11681            JValue::Null | JValue::Undefined => None,
11682            _ => {
11683                return Err(EvaluatorError::EvaluationError(
11684                    "T2004: Right operand of range operator must be a number".to_string(),
11685                ));
11686            }
11687        };
11688
11689        // Check right operand is an integer (if it's a number)
11690        if let Some(val) = end_f64 {
11691            if val.fract() != 0.0 {
11692                return Err(EvaluatorError::EvaluationError(
11693                    "T2004: Right operand of range operator must be an integer".to_string(),
11694                ));
11695            }
11696        }
11697
11698        // If either operand is null, return empty array
11699        if start_f64.is_none() || end_f64.is_none() {
11700            return Ok(JValue::array(vec![]));
11701        }
11702
11703        let start = start_f64.unwrap() as i64;
11704        let end = end_f64.unwrap() as i64;
11705
11706        // Check range size limit (10 million elements max)
11707        let size = if start <= end {
11708            (end - start + 1) as usize
11709        } else {
11710            0
11711        };
11712        if size > 10_000_000 {
11713            return Err(EvaluatorError::EvaluationError(
11714                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
11715            ));
11716        }
11717        check_sequence_length(size, &self.options)?;
11718
11719        let mut result = Vec::with_capacity(size);
11720        if start <= end {
11721            for i in start..=end {
11722                result.push(JValue::Number(i as f64));
11723            }
11724        }
11725        // Note: if start > end, return empty array (not reversed)
11726        Ok(JValue::array(result))
11727    }
11728
11729    /// In operator (checks if left is in right array/object)
11730    /// Array indexing: array[index]
11731    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
11732        match (array, index) {
11733            (JValue::Array(arr), JValue::Number(n)) => {
11734                let idx = *n as i64;
11735                let len = arr.len() as i64;
11736
11737                // Handle negative indexing (offset from end)
11738                let actual_idx = if idx < 0 { len + idx } else { idx };
11739
11740                if actual_idx < 0 || actual_idx >= len {
11741                    Ok(JValue::Undefined)
11742                } else {
11743                    Ok(arr[actual_idx as usize].clone())
11744                }
11745            }
11746            _ => Err(EvaluatorError::TypeError(
11747                "Array indexing requires array and number".to_string(),
11748            )),
11749        }
11750    }
11751
11752    /// Array filtering: array[predicate]
11753    /// Evaluates the predicate for each item in the array and returns items where predicate is true
11754    fn array_filter(
11755        &mut self,
11756        _lhs_node: &AstNode,
11757        rhs_node: &AstNode,
11758        array: &JValue,
11759        _original_data: &JValue,
11760    ) -> Result<JValue, EvaluatorError> {
11761        match array {
11762            JValue::Array(arr) => {
11763                // Pre-allocate with estimated capacity (assume ~50% will match)
11764                let mut filtered = Vec::with_capacity(arr.len() / 2);
11765
11766                for item in arr.iter() {
11767                    // Evaluate the predicate in the context of this array item
11768                    // The item becomes the new "current context" ($)
11769                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
11770
11771                    // Check if the predicate is truthy
11772                    if self.is_truthy(&predicate_result) {
11773                        filtered.push(item.clone());
11774                    }
11775                }
11776
11777                Ok(JValue::array(filtered))
11778            }
11779            _ => Err(EvaluatorError::TypeError(
11780                "Array filtering requires an array".to_string(),
11781            )),
11782        }
11783    }
11784
11785    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11786        // If either side is undefined/null, return false (not an error)
11787        // This matches JavaScript behavior
11788        if left.is_null() || right.is_null() {
11789            return Ok(JValue::Bool(false));
11790        }
11791
11792        match right {
11793            JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
11794            JValue::Object(obj) => {
11795                if let JValue::String(key) = left {
11796                    Ok(JValue::Bool(obj.contains_key(&**key)))
11797                } else {
11798                    Ok(JValue::Bool(false))
11799                }
11800            }
11801            // If right side is not an array or object (e.g., string, number),
11802            // wrap it in an array for comparison
11803            other => Ok(JValue::Bool(self.equals(left, other))),
11804        }
11805    }
11806
11807    /// Create a partially applied function from a function call with placeholder arguments
11808    /// This evaluates non-placeholder arguments and creates a new lambda that takes
11809    /// the placeholder positions as parameters.
11810    fn create_partial_application(
11811        &mut self,
11812        name: &str,
11813        args: &[AstNode],
11814        is_builtin: bool,
11815        data: &JValue,
11816    ) -> Result<JValue, EvaluatorError> {
11817        // First, look up the function to ensure it exists
11818        let is_lambda = self.context.lookup_lambda(name).is_some()
11819            || (self
11820                .context
11821                .lookup(name)
11822                .map(|v| matches!(v, JValue::Lambda { .. }))
11823                .unwrap_or(false));
11824
11825        // Built-in functions must be called with $ prefix for partial application
11826        // Without $, it's an error (T1007) suggesting the user forgot the $
11827        if !is_lambda && !is_builtin {
11828            // Check if it's a built-in function called without $
11829            if self.is_builtin_function(name) {
11830                return Err(EvaluatorError::EvaluationError(format!(
11831                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
11832                    name
11833                )));
11834            }
11835            return Err(EvaluatorError::EvaluationError(
11836                "T1008: Attempted to partially apply a non-function".to_string(),
11837            ));
11838        }
11839
11840        // Evaluate non-placeholder arguments and track placeholder positions
11841        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
11842        let mut placeholder_positions: Vec<usize> = Vec::new();
11843
11844        for (i, arg) in args.iter().enumerate() {
11845            if matches!(arg, AstNode::Placeholder) {
11846                placeholder_positions.push(i);
11847            } else {
11848                let value = self.evaluate_internal(arg, data)?;
11849                bound_args.push((i, value));
11850            }
11851        }
11852
11853        // Generate parameter names for each placeholder
11854        let param_names: Vec<String> = placeholder_positions
11855            .iter()
11856            .enumerate()
11857            .map(|(i, _)| format!("__p{}", i))
11858            .collect();
11859
11860        // Store the partial application info as a special lambda
11861        // When invoked, it will call the original function with bound + placeholder args
11862        let partial_id = format!(
11863            "__partial_{}_{}_{}",
11864            name,
11865            placeholder_positions.len(),
11866            bound_args.len()
11867        );
11868
11869        // Create a stored lambda that represents this partial application
11870        // The body is a marker that we'll interpret specially during invocation
11871        let stored_lambda = StoredLambda {
11872            params: param_names.clone(),
11873            body: AstNode::String(format!(
11874                "__partial_call:{}:{}:{}",
11875                name,
11876                is_builtin,
11877                args.len()
11878            )),
11879            compiled_body: None, // Partial application uses a special body marker
11880            signature: None,
11881            captured_env: {
11882                let mut env = self.capture_current_environment();
11883                // Store the bound arguments in the captured environment
11884                for (pos, value) in &bound_args {
11885                    env.insert(format!("__bound_arg_{}", pos), value.clone());
11886                }
11887                // Store placeholder positions
11888                env.insert(
11889                    "__placeholder_positions".to_string(),
11890                    JValue::array(
11891                        placeholder_positions
11892                            .iter()
11893                            .map(|p| JValue::Number(*p as f64))
11894                            .collect::<Vec<_>>(),
11895                    ),
11896                );
11897                // Store total argument count
11898                env.insert(
11899                    "__total_args".to_string(),
11900                    JValue::Number(args.len() as f64),
11901                );
11902                env
11903            },
11904            captured_data: Some(data.clone()),
11905            thunk: false,
11906        };
11907
11908        self.context.bind_lambda(partial_id.clone(), stored_lambda);
11909
11910        // Return a lambda object that can be invoked
11911        let lambda_obj = JValue::lambda(
11912            partial_id.as_str(),
11913            param_names,
11914            Some(name.to_string()),
11915            None::<String>,
11916        );
11917
11918        Ok(lambda_obj)
11919    }
11920}
11921
11922impl Default for Evaluator {
11923    fn default() -> Self {
11924        Self::new()
11925    }
11926}
11927
11928#[cfg(test)]
11929mod tests {
11930    use super::*;
11931    use crate::ast::{BinaryOp, UnaryOp};
11932
11933    // --- Task 7: tuple-wrapper output leak -----------------------------------
11934    //
11935    // `%`/`@`/`#` are implemented internally via a tuple-stream representation
11936    // (`create_tuple_stream`): each element gets wrapped as
11937    // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
11938    // consume/re-wrap these, but the *final* evaluate() result can still carry
11939    // a lingering wrapper -- confirmed for real by dumping actual output before
11940    // this fix (see task-7-report.md for the raw before/after). These tests
11941    // pin both the bare top-level case (Task 5's brief `#` example) and the
11942    // object/array-construction-nested case (found while verifying the brief's
11943    // illustrative fix against real output -- a plain per-element Array-only
11944    // recursion does not reach into a constructed object's field values).
11945
11946    fn dataset5_for_tuple_tests() -> JValue {
11947        let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
11948        serde_json::from_str::<serde_json::Value>(s).unwrap().into()
11949    }
11950
11951    fn assert_no_tuple_wrapper(value: &JValue) {
11952        match value {
11953            JValue::Object(obj) => {
11954                assert!(
11955                    obj.get("__tuple__").is_none(),
11956                    "tuple wrapper leaked into output: {:?}",
11957                    value
11958                );
11959                for v in obj.values() {
11960                    assert_no_tuple_wrapper(v);
11961                }
11962            }
11963            JValue::Array(arr) => {
11964                for item in arr.iter() {
11965                    assert_no_tuple_wrapper(item);
11966                }
11967            }
11968            _ => {}
11969        }
11970    }
11971
11972    #[test]
11973    fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
11974        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11975        let ast = crate::parser::parse("items#$i").unwrap();
11976        let mut evaluator = Evaluator::new();
11977        let result = evaluator.evaluate(&ast, &data).unwrap();
11978        assert_no_tuple_wrapper(&result);
11979        assert_eq!(
11980            result,
11981            JValue::array(vec![
11982                JValue::from(1i64),
11983                JValue::from(2i64),
11984                JValue::from(3i64)
11985            ])
11986        );
11987    }
11988
11989    #[test]
11990    fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
11991        // Confirmed by Task 6 to evaluate to the correct @-values but stay
11992        // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
11993        let data = dataset5_for_tuple_tests();
11994        let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
11995        let mut evaluator = Evaluator::new();
11996        let result = evaluator.evaluate(&ast, &data).unwrap();
11997        assert_no_tuple_wrapper(&result);
11998        assert_eq!(
11999            result,
12000            JValue::array(vec![
12001                JValue::string("040657863"),
12002                JValue::string("0406654603"),
12003            ])
12004        );
12005    }
12006
12007    #[test]
12008    fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
12009        // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
12010        let data = dataset5_for_tuple_tests();
12011        let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
12012            .unwrap();
12013        let mut evaluator = Evaluator::new();
12014        let result = evaluator.evaluate(&ast, &data).unwrap();
12015        assert_no_tuple_wrapper(&result);
12016        assert_eq!(
12017            result,
12018            JValue::array(vec![
12019                JValue::string("0406654608"),
12020                JValue::string("0406634348"),
12021            ])
12022        );
12023    }
12024
12025    #[test]
12026    fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
12027        // A tuple-producing expression nested inside a constructed object's field
12028        // value: the top-level result is a plain (non-tuple) Object, so a naive
12029        // "unwrap only if the whole value is a tuple wrapper" check would miss
12030        // this -- must recurse into field values too.
12031        let data = dataset5_for_tuple_tests();
12032        let ast =
12033            crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
12034                .unwrap();
12035        let mut evaluator = Evaluator::new();
12036        let result = evaluator.evaluate(&ast, &data).unwrap();
12037        assert_no_tuple_wrapper(&result);
12038        assert_eq!(
12039            result,
12040            JValue::from(serde_json::json!({
12041                "skus": ["040657863", "0406654603"]
12042            }))
12043        );
12044    }
12045
12046    #[test]
12047    fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
12048        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12049        let ast = crate::parser::parse("[items#$i]").unwrap();
12050        let mut evaluator = Evaluator::new();
12051        let result = evaluator.evaluate(&ast, &data).unwrap();
12052        assert_no_tuple_wrapper(&result);
12053    }
12054
12055    #[test]
12056    fn test_evaluate_literals() {
12057        let mut evaluator = Evaluator::new();
12058        let data = JValue::Null;
12059
12060        // String literal
12061        let result = evaluator
12062            .evaluate(&AstNode::string("hello"), &data)
12063            .unwrap();
12064        assert_eq!(result, JValue::string("hello"));
12065
12066        // Number literal
12067        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
12068        assert_eq!(result, JValue::from(42i64));
12069
12070        // Boolean literal
12071        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
12072        assert_eq!(result, JValue::Bool(true));
12073
12074        // Null literal
12075        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
12076        assert_eq!(result, JValue::Null);
12077    }
12078
12079    #[test]
12080    fn test_evaluate_variables() {
12081        let mut evaluator = Evaluator::new();
12082        let data = JValue::Null;
12083
12084        // Bind a variable
12085        evaluator
12086            .context
12087            .bind("x".to_string(), JValue::from(100i64));
12088
12089        // Look up the variable
12090        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
12091        assert_eq!(result, JValue::from(100i64));
12092
12093        // Undefined variable returns null (undefined in JSONata semantics)
12094        let result = evaluator
12095            .evaluate(&AstNode::variable("undefined"), &data)
12096            .unwrap();
12097        assert_eq!(result, JValue::Null);
12098    }
12099
12100    #[test]
12101    fn test_evaluate_path() {
12102        let mut evaluator = Evaluator::new();
12103        let data = JValue::from(serde_json::json!({
12104            "foo": {
12105                "bar": {
12106                    "baz": 42
12107                }
12108            }
12109        }));
12110        // Simple path
12111        let path = AstNode::Path {
12112            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
12113        };
12114        let result = evaluator.evaluate(&path, &data).unwrap();
12115        assert_eq!(
12116            result,
12117            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
12118        );
12119
12120        // Nested path
12121        let path = AstNode::Path {
12122            steps: vec![
12123                PathStep::new(AstNode::Name("foo".to_string())),
12124                PathStep::new(AstNode::Name("bar".to_string())),
12125                PathStep::new(AstNode::Name("baz".to_string())),
12126            ],
12127        };
12128        let result = evaluator.evaluate(&path, &data).unwrap();
12129        assert_eq!(result, JValue::from(42i64));
12130
12131        // Missing path returns undefined (not null - see issue #32)
12132        let path = AstNode::Path {
12133            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
12134        };
12135        let result = evaluator.evaluate(&path, &data).unwrap();
12136        assert_eq!(result, JValue::Undefined);
12137    }
12138
12139    #[test]
12140    fn test_arithmetic_operations() {
12141        let mut evaluator = Evaluator::new();
12142        let data = JValue::Null;
12143
12144        // Addition
12145        let expr = AstNode::Binary {
12146            op: BinaryOp::Add,
12147            lhs: Box::new(AstNode::number(10.0)),
12148            rhs: Box::new(AstNode::number(5.0)),
12149        };
12150        let result = evaluator.evaluate(&expr, &data).unwrap();
12151        assert_eq!(result, JValue::Number(15.0));
12152
12153        // Subtraction
12154        let expr = AstNode::Binary {
12155            op: BinaryOp::Subtract,
12156            lhs: Box::new(AstNode::number(10.0)),
12157            rhs: Box::new(AstNode::number(5.0)),
12158        };
12159        let result = evaluator.evaluate(&expr, &data).unwrap();
12160        assert_eq!(result, JValue::Number(5.0));
12161
12162        // Multiplication
12163        let expr = AstNode::Binary {
12164            op: BinaryOp::Multiply,
12165            lhs: Box::new(AstNode::number(10.0)),
12166            rhs: Box::new(AstNode::number(5.0)),
12167        };
12168        let result = evaluator.evaluate(&expr, &data).unwrap();
12169        assert_eq!(result, JValue::Number(50.0));
12170
12171        // Division
12172        let expr = AstNode::Binary {
12173            op: BinaryOp::Divide,
12174            lhs: Box::new(AstNode::number(10.0)),
12175            rhs: Box::new(AstNode::number(5.0)),
12176        };
12177        let result = evaluator.evaluate(&expr, &data).unwrap();
12178        assert_eq!(result, JValue::Number(2.0));
12179
12180        // Modulo
12181        let expr = AstNode::Binary {
12182            op: BinaryOp::Modulo,
12183            lhs: Box::new(AstNode::number(10.0)),
12184            rhs: Box::new(AstNode::number(3.0)),
12185        };
12186        let result = evaluator.evaluate(&expr, &data).unwrap();
12187        assert_eq!(result, JValue::Number(1.0));
12188    }
12189
12190    #[test]
12191    fn test_division_by_zero() {
12192        let mut evaluator = Evaluator::new();
12193        let data = JValue::Null;
12194
12195        let expr = AstNode::Binary {
12196            op: BinaryOp::Divide,
12197            lhs: Box::new(AstNode::number(10.0)),
12198            rhs: Box::new(AstNode::number(0.0)),
12199        };
12200        let result = evaluator.evaluate(&expr, &data);
12201        assert!(result.is_err());
12202    }
12203
12204    #[test]
12205    fn test_comparison_operations() {
12206        let mut evaluator = Evaluator::new();
12207        let data = JValue::Null;
12208
12209        // Equal
12210        let expr = AstNode::Binary {
12211            op: BinaryOp::Equal,
12212            lhs: Box::new(AstNode::number(5.0)),
12213            rhs: Box::new(AstNode::number(5.0)),
12214        };
12215        assert_eq!(
12216            evaluator.evaluate(&expr, &data).unwrap(),
12217            JValue::Bool(true)
12218        );
12219
12220        // Not equal
12221        let expr = AstNode::Binary {
12222            op: BinaryOp::NotEqual,
12223            lhs: Box::new(AstNode::number(5.0)),
12224            rhs: Box::new(AstNode::number(3.0)),
12225        };
12226        assert_eq!(
12227            evaluator.evaluate(&expr, &data).unwrap(),
12228            JValue::Bool(true)
12229        );
12230
12231        // Less than
12232        let expr = AstNode::Binary {
12233            op: BinaryOp::LessThan,
12234            lhs: Box::new(AstNode::number(3.0)),
12235            rhs: Box::new(AstNode::number(5.0)),
12236        };
12237        assert_eq!(
12238            evaluator.evaluate(&expr, &data).unwrap(),
12239            JValue::Bool(true)
12240        );
12241
12242        // Greater than
12243        let expr = AstNode::Binary {
12244            op: BinaryOp::GreaterThan,
12245            lhs: Box::new(AstNode::number(5.0)),
12246            rhs: Box::new(AstNode::number(3.0)),
12247        };
12248        assert_eq!(
12249            evaluator.evaluate(&expr, &data).unwrap(),
12250            JValue::Bool(true)
12251        );
12252    }
12253
12254    #[test]
12255    fn test_logical_operations() {
12256        let mut evaluator = Evaluator::new();
12257        let data = JValue::Null;
12258
12259        // And - both true
12260        let expr = AstNode::Binary {
12261            op: BinaryOp::And,
12262            lhs: Box::new(AstNode::boolean(true)),
12263            rhs: Box::new(AstNode::boolean(true)),
12264        };
12265        assert_eq!(
12266            evaluator.evaluate(&expr, &data).unwrap(),
12267            JValue::Bool(true)
12268        );
12269
12270        // And - first false
12271        let expr = AstNode::Binary {
12272            op: BinaryOp::And,
12273            lhs: Box::new(AstNode::boolean(false)),
12274            rhs: Box::new(AstNode::boolean(true)),
12275        };
12276        assert_eq!(
12277            evaluator.evaluate(&expr, &data).unwrap(),
12278            JValue::Bool(false)
12279        );
12280
12281        // Or - first true
12282        let expr = AstNode::Binary {
12283            op: BinaryOp::Or,
12284            lhs: Box::new(AstNode::boolean(true)),
12285            rhs: Box::new(AstNode::boolean(false)),
12286        };
12287        assert_eq!(
12288            evaluator.evaluate(&expr, &data).unwrap(),
12289            JValue::Bool(true)
12290        );
12291
12292        // Or - both false
12293        let expr = AstNode::Binary {
12294            op: BinaryOp::Or,
12295            lhs: Box::new(AstNode::boolean(false)),
12296            rhs: Box::new(AstNode::boolean(false)),
12297        };
12298        assert_eq!(
12299            evaluator.evaluate(&expr, &data).unwrap(),
12300            JValue::Bool(false)
12301        );
12302    }
12303
12304    #[test]
12305    fn test_string_concatenation() {
12306        let mut evaluator = Evaluator::new();
12307        let data = JValue::Null;
12308
12309        let expr = AstNode::Binary {
12310            op: BinaryOp::Concatenate,
12311            lhs: Box::new(AstNode::string("Hello")),
12312            rhs: Box::new(AstNode::string(" World")),
12313        };
12314        let result = evaluator.evaluate(&expr, &data).unwrap();
12315        assert_eq!(result, JValue::string("Hello World"));
12316    }
12317
12318    #[test]
12319    fn test_range_operator() {
12320        let mut evaluator = Evaluator::new();
12321        let data = JValue::Null;
12322
12323        // Forward range
12324        let expr = AstNode::Binary {
12325            op: BinaryOp::Range,
12326            lhs: Box::new(AstNode::number(1.0)),
12327            rhs: Box::new(AstNode::number(5.0)),
12328        };
12329        let result = evaluator.evaluate(&expr, &data).unwrap();
12330        assert_eq!(
12331            result,
12332            JValue::array(vec![
12333                JValue::Number(1.0),
12334                JValue::Number(2.0),
12335                JValue::Number(3.0),
12336                JValue::Number(4.0),
12337                JValue::Number(5.0)
12338            ])
12339        );
12340
12341        // Backward range (start > end) returns empty array
12342        let expr = AstNode::Binary {
12343            op: BinaryOp::Range,
12344            lhs: Box::new(AstNode::number(5.0)),
12345            rhs: Box::new(AstNode::number(1.0)),
12346        };
12347        let result = evaluator.evaluate(&expr, &data).unwrap();
12348        assert_eq!(result, JValue::array(vec![]));
12349    }
12350
12351    #[test]
12352    fn test_in_operator() {
12353        let mut evaluator = Evaluator::new();
12354        let data = JValue::Null;
12355
12356        // In array
12357        let expr = AstNode::Binary {
12358            op: BinaryOp::In,
12359            lhs: Box::new(AstNode::number(3.0)),
12360            rhs: Box::new(AstNode::Array(vec![
12361                AstNode::number(1.0),
12362                AstNode::number(2.0),
12363                AstNode::number(3.0),
12364            ])),
12365        };
12366        let result = evaluator.evaluate(&expr, &data).unwrap();
12367        assert_eq!(result, JValue::Bool(true));
12368
12369        // Not in array
12370        let expr = AstNode::Binary {
12371            op: BinaryOp::In,
12372            lhs: Box::new(AstNode::number(5.0)),
12373            rhs: Box::new(AstNode::Array(vec![
12374                AstNode::number(1.0),
12375                AstNode::number(2.0),
12376                AstNode::number(3.0),
12377            ])),
12378        };
12379        let result = evaluator.evaluate(&expr, &data).unwrap();
12380        assert_eq!(result, JValue::Bool(false));
12381    }
12382
12383    #[test]
12384    fn test_unary_operations() {
12385        let mut evaluator = Evaluator::new();
12386        let data = JValue::Null;
12387
12388        // Negation
12389        let expr = AstNode::Unary {
12390            op: UnaryOp::Negate,
12391            operand: Box::new(AstNode::number(5.0)),
12392        };
12393        let result = evaluator.evaluate(&expr, &data).unwrap();
12394        assert_eq!(result, JValue::Number(-5.0));
12395
12396        // Not
12397        let expr = AstNode::Unary {
12398            op: UnaryOp::Not,
12399            operand: Box::new(AstNode::boolean(true)),
12400        };
12401        let result = evaluator.evaluate(&expr, &data).unwrap();
12402        assert_eq!(result, JValue::Bool(false));
12403    }
12404
12405    #[test]
12406    fn test_array_construction() {
12407        let mut evaluator = Evaluator::new();
12408        let data = JValue::Null;
12409
12410        let expr = AstNode::Array(vec![
12411            AstNode::number(1.0),
12412            AstNode::number(2.0),
12413            AstNode::number(3.0),
12414        ]);
12415        let result = evaluator.evaluate(&expr, &data).unwrap();
12416        // Whole number literals are preserved as integers
12417        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
12418    }
12419
12420    #[test]
12421    fn test_object_construction() {
12422        let mut evaluator = Evaluator::new();
12423        let data = JValue::Null;
12424
12425        let expr = AstNode::Object(vec![
12426            (AstNode::string("name"), AstNode::string("Alice")),
12427            (AstNode::string("age"), AstNode::number(30.0)),
12428        ]);
12429        let result = evaluator.evaluate(&expr, &data).unwrap();
12430        // Whole number literals are preserved as integers
12431        let mut expected = IndexMap::new();
12432        expected.insert("name".to_string(), JValue::string("Alice"));
12433        expected.insert("age".to_string(), JValue::Number(30.0));
12434        assert_eq!(result, JValue::object(expected));
12435    }
12436
12437    #[test]
12438    fn test_conditional() {
12439        let mut evaluator = Evaluator::new();
12440        let data = JValue::Null;
12441
12442        // True condition
12443        let expr = AstNode::Conditional {
12444            condition: Box::new(AstNode::boolean(true)),
12445            then_branch: Box::new(AstNode::string("yes")),
12446            else_branch: Some(Box::new(AstNode::string("no"))),
12447        };
12448        let result = evaluator.evaluate(&expr, &data).unwrap();
12449        assert_eq!(result, JValue::string("yes"));
12450
12451        // False condition
12452        let expr = AstNode::Conditional {
12453            condition: Box::new(AstNode::boolean(false)),
12454            then_branch: Box::new(AstNode::string("yes")),
12455            else_branch: Some(Box::new(AstNode::string("no"))),
12456        };
12457        let result = evaluator.evaluate(&expr, &data).unwrap();
12458        assert_eq!(result, JValue::string("no"));
12459
12460        // No else branch returns undefined (not null)
12461        let expr = AstNode::Conditional {
12462            condition: Box::new(AstNode::boolean(false)),
12463            then_branch: Box::new(AstNode::string("yes")),
12464            else_branch: None,
12465        };
12466        let result = evaluator.evaluate(&expr, &data).unwrap();
12467        assert_eq!(result, JValue::Undefined);
12468    }
12469
12470    #[test]
12471    fn test_block_expression() {
12472        let mut evaluator = Evaluator::new();
12473        let data = JValue::Null;
12474
12475        let expr = AstNode::Block(vec![
12476            AstNode::number(1.0),
12477            AstNode::number(2.0),
12478            AstNode::number(3.0),
12479        ]);
12480        let result = evaluator.evaluate(&expr, &data).unwrap();
12481        // Block returns the last expression; whole numbers are preserved as integers
12482        assert_eq!(result, JValue::from(3i64));
12483    }
12484
12485    #[test]
12486    fn test_function_calls() {
12487        let mut evaluator = Evaluator::new();
12488        let data = JValue::Null;
12489
12490        // uppercase function
12491        let expr = AstNode::Function {
12492            name: "uppercase".to_string(),
12493            args: vec![AstNode::string("hello")],
12494            is_builtin: true,
12495        };
12496        let result = evaluator.evaluate(&expr, &data).unwrap();
12497        assert_eq!(result, JValue::string("HELLO"));
12498
12499        // lowercase function
12500        let expr = AstNode::Function {
12501            name: "lowercase".to_string(),
12502            args: vec![AstNode::string("HELLO")],
12503            is_builtin: true,
12504        };
12505        let result = evaluator.evaluate(&expr, &data).unwrap();
12506        assert_eq!(result, JValue::string("hello"));
12507
12508        // length function
12509        let expr = AstNode::Function {
12510            name: "length".to_string(),
12511            args: vec![AstNode::string("hello")],
12512            is_builtin: true,
12513        };
12514        let result = evaluator.evaluate(&expr, &data).unwrap();
12515        assert_eq!(result, JValue::from(5i64));
12516
12517        // sum function
12518        let expr = AstNode::Function {
12519            name: "sum".to_string(),
12520            args: vec![AstNode::Array(vec![
12521                AstNode::number(1.0),
12522                AstNode::number(2.0),
12523                AstNode::number(3.0),
12524            ])],
12525            is_builtin: true,
12526        };
12527        let result = evaluator.evaluate(&expr, &data).unwrap();
12528        assert_eq!(result, JValue::Number(6.0));
12529
12530        // count function
12531        let expr = AstNode::Function {
12532            name: "count".to_string(),
12533            args: vec![AstNode::Array(vec![
12534                AstNode::number(1.0),
12535                AstNode::number(2.0),
12536                AstNode::number(3.0),
12537            ])],
12538            is_builtin: true,
12539        };
12540        let result = evaluator.evaluate(&expr, &data).unwrap();
12541        assert_eq!(result, JValue::from(3i64));
12542    }
12543
12544    #[test]
12545    fn test_complex_nested_data() {
12546        let mut evaluator = Evaluator::new();
12547        let data = JValue::from(serde_json::json!({
12548            "users": [
12549                {"name": "Alice", "age": 30},
12550                {"name": "Bob", "age": 25},
12551                {"name": "Charlie", "age": 35}
12552            ],
12553            "metadata": {
12554                "total": 3,
12555                "version": "1.0"
12556            }
12557        }));
12558        // Access nested field
12559        let path = AstNode::Path {
12560            steps: vec![
12561                PathStep::new(AstNode::Name("metadata".to_string())),
12562                PathStep::new(AstNode::Name("version".to_string())),
12563            ],
12564        };
12565        let result = evaluator.evaluate(&path, &data).unwrap();
12566        assert_eq!(result, JValue::string("1.0"));
12567    }
12568
12569    #[test]
12570    fn test_error_handling() {
12571        let mut evaluator = Evaluator::new();
12572        let data = JValue::Null;
12573
12574        // Type error: adding string and number
12575        let expr = AstNode::Binary {
12576            op: BinaryOp::Add,
12577            lhs: Box::new(AstNode::string("hello")),
12578            rhs: Box::new(AstNode::number(5.0)),
12579        };
12580        let result = evaluator.evaluate(&expr, &data);
12581        assert!(result.is_err());
12582
12583        // Reference error: undefined function
12584        let expr = AstNode::Function {
12585            name: "undefined_function".to_string(),
12586            args: vec![],
12587            is_builtin: false,
12588        };
12589        let result = evaluator.evaluate(&expr, &data);
12590        assert!(result.is_err());
12591    }
12592
12593    #[test]
12594    fn test_truthiness() {
12595        let evaluator = Evaluator::new();
12596
12597        assert!(!evaluator.is_truthy(&JValue::Null));
12598        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
12599        assert!(evaluator.is_truthy(&JValue::Bool(true)));
12600        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
12601        assert!(evaluator.is_truthy(&JValue::from(1i64)));
12602        assert!(!evaluator.is_truthy(&JValue::string("")));
12603        assert!(evaluator.is_truthy(&JValue::string("hello")));
12604        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
12605        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
12606    }
12607
12608    #[test]
12609    fn test_integration_with_parser() {
12610        use crate::parser::parse;
12611
12612        let mut evaluator = Evaluator::new();
12613        let data = JValue::from(serde_json::json!({
12614            "price": 10,
12615            "quantity": 5
12616        }));
12617        // Test simple path
12618        let ast = parse("price").unwrap();
12619        let result = evaluator.evaluate(&ast, &data).unwrap();
12620        assert_eq!(result, JValue::from(10i64));
12621
12622        // Test arithmetic
12623        let ast = parse("price * quantity").unwrap();
12624        let result = evaluator.evaluate(&ast, &data).unwrap();
12625        // Note: Arithmetic operations produce f64 results in JSON
12626        assert_eq!(result, JValue::Number(50.0));
12627
12628        // Test comparison
12629        let ast = parse("price > 5").unwrap();
12630        let result = evaluator.evaluate(&ast, &data).unwrap();
12631        assert_eq!(result, JValue::Bool(true));
12632    }
12633
12634    #[test]
12635    fn test_evaluate_dollar_function_uppercase() {
12636        use crate::parser::parse;
12637
12638        let mut evaluator = Evaluator::new();
12639        let ast = parse(r#"$uppercase("hello")"#).unwrap();
12640        let empty = JValue::object(IndexMap::new());
12641        let result = evaluator.evaluate(&ast, &empty).unwrap();
12642        assert_eq!(result, JValue::string("HELLO"));
12643    }
12644
12645    #[test]
12646    fn test_evaluate_dollar_function_sum() {
12647        use crate::parser::parse;
12648
12649        let mut evaluator = Evaluator::new();
12650        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
12651        let empty = JValue::object(IndexMap::new());
12652        let result = evaluator.evaluate(&ast, &empty).unwrap();
12653        assert_eq!(result, JValue::Number(15.0));
12654    }
12655
12656    #[test]
12657    fn test_evaluate_nested_dollar_functions() {
12658        use crate::parser::parse;
12659
12660        let mut evaluator = Evaluator::new();
12661        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
12662        let empty = JValue::object(IndexMap::new());
12663        let result = evaluator.evaluate(&ast, &empty).unwrap();
12664        // length() returns an integer, not a float
12665        assert_eq!(result, JValue::Number(5.0));
12666    }
12667
12668    #[test]
12669    fn test_array_mapping() {
12670        use crate::parser::parse;
12671
12672        let mut evaluator = Evaluator::new();
12673        let data: JValue = serde_json::from_str(
12674            r#"{
12675            "products": [
12676                {"id": 1, "name": "Laptop", "price": 999.99},
12677                {"id": 2, "name": "Mouse", "price": 29.99},
12678                {"id": 3, "name": "Keyboard", "price": 79.99}
12679            ]
12680        }"#,
12681        )
12682        .map(|v: serde_json::Value| JValue::from(v))
12683        .unwrap();
12684
12685        // Test mapping over array to extract field
12686        let ast = parse("products.name").unwrap();
12687        let result = evaluator.evaluate(&ast, &data).unwrap();
12688        assert_eq!(
12689            result,
12690            JValue::array(vec![
12691                JValue::string("Laptop"),
12692                JValue::string("Mouse"),
12693                JValue::string("Keyboard")
12694            ])
12695        );
12696
12697        // Test mapping over array to extract prices
12698        let ast = parse("products.price").unwrap();
12699        let result = evaluator.evaluate(&ast, &data).unwrap();
12700        assert_eq!(
12701            result,
12702            JValue::array(vec![
12703                JValue::Number(999.99),
12704                JValue::Number(29.99),
12705                JValue::Number(79.99)
12706            ])
12707        );
12708
12709        // Test with $sum function on mapped array
12710        let ast = parse("$sum(products.price)").unwrap();
12711        let result = evaluator.evaluate(&ast, &data).unwrap();
12712        assert_eq!(result, JValue::Number(1109.97));
12713    }
12714
12715    #[test]
12716    fn test_empty_brackets() {
12717        use crate::parser::parse;
12718
12719        let mut evaluator = Evaluator::new();
12720
12721        // Test empty brackets on simple value - should wrap in array
12722        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
12723        let ast = parse("foo[]").unwrap();
12724        let result = evaluator.evaluate(&ast, &data).unwrap();
12725        assert_eq!(
12726            result,
12727            JValue::array(vec![JValue::string("bar")]),
12728            "Empty brackets should wrap value in array"
12729        );
12730
12731        // Test empty brackets on array - should return array as-is
12732        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
12733        let ast2 = parse("arr[]").unwrap();
12734        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
12735        assert_eq!(
12736            result2,
12737            JValue::array(vec![
12738                JValue::Number(1.0),
12739                JValue::Number(2.0),
12740                JValue::Number(3.0)
12741            ]),
12742            "Empty brackets should preserve array"
12743        );
12744    }
12745
12746    // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
12747    // Expected values below are ground-truthed against jsonata-js 2.x.
12748
12749    #[test]
12750    fn test_index_bind_makes_variable_available_in_next_step() {
12751        // `#$o` binds each Order's position; `$o` must resolve in the later step.
12752        let data: JValue = serde_json::json!({
12753            "Account": {
12754                "Order": [
12755                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
12756                    {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
12757                ]
12758            }
12759        })
12760        .into();
12761        let ast =
12762            crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
12763        let mut evaluator = Evaluator::new();
12764        let result = evaluator.evaluate(&ast, &data).unwrap();
12765        assert_eq!(
12766            result,
12767            serde_json::json!([
12768                {"name": "Hat", "idx": 0},
12769                {"name": "Cap", "idx": 1},
12770                {"name": "Sock", "idx": 1}
12771            ])
12772            .into()
12773        );
12774    }
12775
12776    #[test]
12777    fn test_index_bind_with_predicate_stage() {
12778        // Mirrors reference joins/index[13]: index binding, then a predicate on
12779        // the next step, carrying the index binding through.
12780        let data: JValue = serde_json::json!({
12781            "Account": {
12782                "Order": [
12783                    {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
12784                    {"Product": [{"ProductID": 9, "Name": "C"}]}
12785                ]
12786            }
12787        })
12788        .into();
12789        let ast =
12790            crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
12791                .unwrap();
12792        let mut evaluator = Evaluator::new();
12793        let result = evaluator.evaluate(&ast, &data).unwrap();
12794        assert_eq!(
12795            result,
12796            serde_json::json!([
12797                {"n": "B", "idx": 0},
12798                {"n": "C", "idx": 1}
12799            ])
12800            .into()
12801        );
12802    }
12803
12804    #[test]
12805    fn test_focus_bind_makes_variable_available_in_next_step() {
12806        // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
12807        // does NOT advance the context `@`); the variable itself is what carries
12808        // forward. This asserts the real jsonata-js behaviour.
12809        let data: JValue = serde_json::json!({
12810            "Account": {
12811                "Order": [
12812                    {"OrderID": "o1"},
12813                    {"OrderID": "o2"}
12814                ]
12815            }
12816        })
12817        .into();
12818        let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
12819        let mut evaluator = Evaluator::new();
12820        let result = evaluator.evaluate(&ast, &data).unwrap();
12821        assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
12822    }
12823
12824    #[test]
12825    fn test_parent_reference_resolves_to_enclosing_step_value() {
12826        let data: JValue = serde_json::json!({
12827            "Account": {
12828                "Order": [
12829                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
12830                ]
12831            }
12832        })
12833        .into();
12834        let ast =
12835            crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
12836                .unwrap();
12837        let mut evaluator = Evaluator::new();
12838        let result = evaluator.evaluate(&ast, &data).unwrap();
12839        assert_eq!(
12840            result,
12841            serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
12842        );
12843    }
12844
12845    // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
12846    // a tuple-carried `$name`/`!label` key straight into the top scope and then
12847    // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
12848    // same-named outer `:=` binding that happened to be live in that scope
12849    // frame. Expected values below are verified against jsonata-js (2.2.1
12850    // reference, `tests/jsonata-js`).
12851
12852    #[test]
12853    fn test_chained_focus_bind_does_not_clobber_outer_variable() {
12854        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12855        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
12856        let mut evaluator = Evaluator::new();
12857        let result = evaluator.evaluate(&ast, &data).unwrap();
12858        assert_eq!(result, serde_json::json!("OUT").into());
12859    }
12860
12861    #[test]
12862    fn test_chained_index_bind_does_not_clobber_outer_variable() {
12863        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12864        let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
12865        let mut evaluator = Evaluator::new();
12866        let result = evaluator.evaluate(&ast, &data).unwrap();
12867        assert_eq!(result, serde_json::json!("OUT").into());
12868    }
12869
12870    #[test]
12871    fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
12872        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12873        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
12874        let mut evaluator = Evaluator::new();
12875        let result = evaluator.evaluate(&ast, &data).unwrap();
12876        assert_eq!(result, serde_json::json!("OUT").into());
12877    }
12878
12879    #[test]
12880    fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
12881        let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
12882        let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
12883        let mut evaluator = Evaluator::new();
12884        let result = evaluator.evaluate(&ast, &data).unwrap();
12885        assert_eq!(result, serde_json::json!("OUT").into());
12886    }
12887}