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
2537impl EvaluatorError {
2538    /// The underlying message, without the outer "Type error: "/
2539    /// "Reference error: "/"Evaluation error: " prefix that `Display` (via
2540    /// thiserror's `#[error("Type error: {0}")]` etc.) would add. This is
2541    /// what JSONata-spec-coded messages like "T2002: ..." actually look
2542    /// like — the coded prefix is INSIDE this string, not added by
2543    /// `Display`. Used by both the Python bindings (`src/lib.rs`) and the
2544    /// `jsonata` CLI so the two never need to duplicate this unwrap.
2545    pub fn message(&self) -> &str {
2546        match self {
2547            EvaluatorError::TypeError(m) => m,
2548            EvaluatorError::ReferenceError(m) => m,
2549            EvaluatorError::EvaluationError(m) => m,
2550        }
2551    }
2552}
2553
2554#[cfg(test)]
2555mod evaluator_error_message_tests {
2556    use super::EvaluatorError;
2557
2558    #[test]
2559    fn message_strips_the_display_prefix() {
2560        let e = EvaluatorError::TypeError(
2561            "T2002: The left side of the + operator must evaluate to a number".to_string(),
2562        );
2563        assert_eq!(
2564            e.message(),
2565            "T2002: The left side of the + operator must evaluate to a number"
2566        );
2567        // Display, by contrast, adds the "Type error: " wrapper -- this is
2568        // exactly the distinction `message()` exists to avoid.
2569        assert_eq!(
2570            e.to_string(),
2571            "Type error: T2002: The left side of the + operator must evaluate to a number"
2572        );
2573    }
2574
2575    #[test]
2576    fn message_works_for_all_variants() {
2577        assert_eq!(
2578            EvaluatorError::ReferenceError("$foo is not defined".to_string()).message(),
2579            "$foo is not defined"
2580        );
2581        assert_eq!(
2582            EvaluatorError::EvaluationError("something went wrong".to_string()).message(),
2583            "something went wrong"
2584        );
2585    }
2586}
2587
2588/// Result of evaluating a lambda body that may be a tail call
2589/// Used for trampoline-based tail call optimization
2590enum LambdaResult {
2591    /// Final value - evaluation is complete
2592    JValue(JValue),
2593    /// Tail call - need to continue with another lambda invocation
2594    TailCall {
2595        /// The lambda to call (boxed to reduce enum size)
2596        lambda: Box<StoredLambda>,
2597        /// Arguments for the call
2598        args: Vec<JValue>,
2599        /// Data context for the call
2600        data: JValue,
2601    },
2602}
2603
2604/// Lambda storage
2605/// Stores the AST of a lambda function along with its parameters, optional signature,
2606/// and captured environment for closures
2607#[derive(Clone, Debug)]
2608pub struct StoredLambda {
2609    pub params: Vec<String>,
2610    pub body: AstNode,
2611    /// Pre-compiled body for use in tight inner loops (HOF fast path).
2612    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2613    pub(crate) compiled_body: Option<CompiledExpr>,
2614    pub signature: Option<String>,
2615    /// Captured environment bindings for closures
2616    pub captured_env: HashMap<String, JValue>,
2617    /// Captured data context for lexical scoping of bare field names
2618    pub captured_data: Option<JValue>,
2619    /// Whether this lambda's body contains tail calls that can be optimized
2620    pub thunk: bool,
2621}
2622
2623/// A single scope in the scope stack
2624struct Scope {
2625    bindings: HashMap<String, JValue>,
2626    lambdas: HashMap<String, StoredLambda>,
2627}
2628
2629impl Scope {
2630    fn new() -> Self {
2631        Scope {
2632            bindings: HashMap::new(),
2633            lambdas: HashMap::new(),
2634        }
2635    }
2636}
2637
2638/// Evaluation context
2639///
2640/// Holds variable bindings and other state needed during evaluation.
2641/// Uses a scope stack for efficient push/pop instead of clone/restore.
2642pub struct Context {
2643    scope_stack: Vec<Scope>,
2644    parent_data: Option<JValue>,
2645}
2646
2647impl Context {
2648    pub fn new() -> Self {
2649        Context {
2650            scope_stack: vec![Scope::new()],
2651            parent_data: None,
2652        }
2653    }
2654
2655    /// Push a new scope onto the stack
2656    fn push_scope(&mut self) {
2657        self.scope_stack.push(Scope::new());
2658    }
2659
2660    /// Pop the top scope from the stack
2661    fn pop_scope(&mut self) {
2662        if self.scope_stack.len() > 1 {
2663            self.scope_stack.pop();
2664        }
2665    }
2666
2667    /// Pop scope but preserve specified lambdas by moving them to the current top scope
2668    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2669        if self.scope_stack.len() > 1 {
2670            let popped = self.scope_stack.pop().unwrap();
2671            if !lambda_ids.is_empty() {
2672                let top = self.scope_stack.last_mut().unwrap();
2673                for id in lambda_ids {
2674                    if let Some(stored) = popped.lambdas.get(id) {
2675                        top.lambdas.insert(id.clone(), stored.clone());
2676                    }
2677                }
2678            }
2679        }
2680    }
2681
2682    /// Clear all bindings and lambdas in the top scope without deallocating
2683    fn clear_current_scope(&mut self) {
2684        let top = self.scope_stack.last_mut().unwrap();
2685        top.bindings.clear();
2686        top.lambdas.clear();
2687    }
2688
2689    pub fn bind(&mut self, name: String, value: JValue) {
2690        self.scope_stack
2691            .last_mut()
2692            .unwrap()
2693            .bindings
2694            .insert(name, value);
2695    }
2696
2697    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2698        self.scope_stack
2699            .last_mut()
2700            .unwrap()
2701            .lambdas
2702            .insert(name, lambda);
2703    }
2704
2705    pub fn unbind(&mut self, name: &str) {
2706        // Remove from top scope only
2707        let top = self.scope_stack.last_mut().unwrap();
2708        top.bindings.remove(name);
2709        top.lambdas.remove(name);
2710    }
2711
2712    pub fn lookup(&self, name: &str) -> Option<&JValue> {
2713        // Walk scope stack from top to bottom
2714        for scope in self.scope_stack.iter().rev() {
2715            if let Some(value) = scope.bindings.get(name) {
2716                return Some(value);
2717            }
2718        }
2719        None
2720    }
2721
2722    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2723        // Walk scope stack from top to bottom
2724        for scope in self.scope_stack.iter().rev() {
2725            if let Some(lambda) = scope.lambdas.get(name) {
2726                return Some(lambda);
2727            }
2728        }
2729        None
2730    }
2731
2732    pub fn set_parent(&mut self, data: JValue) {
2733        self.parent_data = Some(data);
2734    }
2735
2736    pub fn get_parent(&self) -> Option<&JValue> {
2737        self.parent_data.as_ref()
2738    }
2739
2740    /// Collect all bindings across all scopes (for environment capture).
2741    /// Higher scopes shadow lower scopes.
2742    fn all_bindings(&self) -> HashMap<String, JValue> {
2743        let mut result = HashMap::new();
2744        for scope in &self.scope_stack {
2745            for (k, v) in &scope.bindings {
2746                result.insert(k.clone(), v.clone());
2747            }
2748        }
2749        result
2750    }
2751}
2752
2753impl Default for Context {
2754    fn default() -> Self {
2755        Self::new()
2756    }
2757}
2758
2759/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2760/// ...}`) from a value about to leave the evaluator.
2761///
2762/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2763/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2764/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2765/// path step consumes and re-wraps these as evaluation proceeds, but the
2766/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2767/// because the tuple-producing expression itself is the whole result (a bare
2768/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2769/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2770/// wrapper ends up embedded in a field value or array element rather than at
2771/// the top level. This recurses through both array elements and (non-tuple)
2772/// object field values so both shapes are cleaned up, not just a bare
2773/// top-level tuple array.
2774/// Merge a group of tuple wrappers into a single tuple, appending each key's
2775/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2776/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2777/// a key present in one tuple stays a scalar; a key present in several becomes an
2778/// array of the collected values (used by group-by value evaluation so a group
2779/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2780/// N collected focus values).
2781fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2782    fn append(acc: Option<JValue>, v: JValue) -> JValue {
2783        match acc {
2784            None => v,
2785            Some(a) => {
2786                let mut out: Vec<JValue> = match a {
2787                    JValue::Array(arr) => arr.iter().cloned().collect(),
2788                    other => vec![other],
2789                };
2790                match v {
2791                    JValue::Array(arr) => out.extend(arr.iter().cloned()),
2792                    other => out.push(other),
2793                }
2794                JValue::array(out)
2795            }
2796        }
2797    }
2798    let mut result: IndexMap<String, JValue> = IndexMap::new();
2799    for tuple in group {
2800        if let JValue::Object(obj) = tuple {
2801            for (k, v) in obj.iter() {
2802                if k == "__tuple__" {
2803                    result.insert(k.clone(), v.clone());
2804                    continue;
2805                }
2806                let merged = append(result.shift_remove(k), v.clone());
2807                result.insert(k.clone(), merged);
2808            }
2809        }
2810    }
2811    result
2812}
2813
2814fn unwrap_tuple_output(value: JValue) -> JValue {
2815    match value {
2816        JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2817            .get("@")
2818            .cloned()
2819            .map(unwrap_tuple_output)
2820            .unwrap_or(JValue::Undefined),
2821        JValue::Object(obj) => {
2822            let mut new_map = IndexMap::with_capacity(obj.len());
2823            for (k, v) in obj.iter() {
2824                new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2825            }
2826            JValue::object(new_map)
2827        }
2828        JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2829        other => other,
2830    }
2831}
2832
2833/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2834/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2835/// (if anything) was bound under that name beforehand. `restore` puts the
2836/// prior value back -- or removes the binding entirely if there wasn't
2837/// one -- rather than unconditionally unbinding, so a tuple key that
2838/// happens to share a name with a live outer `:=` binding in the same
2839/// scope frame doesn't get permanently deleted once the tuple-row
2840/// evaluation finishes.
2841struct TupleKeyBindings {
2842    saved: Vec<(String, Option<JValue>)>,
2843}
2844
2845impl TupleKeyBindings {
2846    /// True if `name` was one of the keys this guard bound (used by callers
2847    /// that need to know whether a given tuple key is already in scope
2848    /// before binding it a second time under a different role, e.g.
2849    /// `create_tuple_stream`'s ancestor-label handling).
2850    fn contains(&self, name: &str) -> bool {
2851        self.saved.iter().any(|(n, _)| n == name)
2852    }
2853
2854    fn restore(self, evaluator: &mut Evaluator) {
2855        for (name, prior) in self.saved {
2856            match prior {
2857                Some(value) => evaluator.context.bind(name, value),
2858                None => evaluator.context.unbind(&name),
2859            }
2860        }
2861    }
2862}
2863
2864/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
2865/// evaluator options. All fields default to `None` = unlimited (current behavior).
2866#[derive(Default, Clone, Debug)]
2867pub struct EvaluatorOptions {
2868    /// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
2869    pub timeout_ms: Option<u64>,
2870    /// Maximum AST-recursion stack depth. Exceeding it raises D1011 if this is the
2871    /// tighter of this value and the hardcoded native-stack safety ceiling (302);
2872    /// otherwise the hardcoded ceiling still raises U1001 (see GitHub issue #34).
2873    pub max_stack_depth: Option<usize>,
2874    /// Maximum length of a query-result sequence (map/filter/wildcard/descendants/
2875    /// keys/lookup/append/spread/each/range/path-mapping). Exceeding it raises D2015.
2876    /// Does NOT currently apply to literal array construction (`MakeArray`/
2877    /// `ArrayConstruct`) — NOTE this is a deliberate, temporary divergence from
2878    /// upstream, not a match: jsonata-js DOES cap flat/non-nested array literals
2879    /// (via `fn.append`'s `createSequence` hook in `evaluateUnary`'s `[` case).
2880    /// Deferred until the separate `MakeArray(u16)` truncation bug is fixed (see
2881    /// the design spec's "Sequence length → D2015" section).
2882    pub max_sequence_length: Option<usize>,
2883}
2884
2885/// Checks a constructed query-result sequence's length against the configured
2886/// `max_sequence_length` guardrail. Call this at sites that build a query-result
2887/// sequence (map/filter/wildcard/descendants/keys/lookup/append/spread/each/range/
2888/// path-mapping). NOT currently called at literal array construction (`[1,2,3]`) —
2889/// unlike upstream jsonata-js, which caps flat/non-nested literals too via
2890/// `fn.append`'s `createSequence()` hook. See `EvaluatorOptions::max_sequence_length`
2891/// doc comment above for why this is a deliberate, temporary gap.
2892pub(crate) fn check_sequence_length(
2893    len: usize,
2894    options: &EvaluatorOptions,
2895) -> Result<(), EvaluatorError> {
2896    if let Some(max) = options.max_sequence_length {
2897        if len > max {
2898            return Err(EvaluatorError::EvaluationError(format!(
2899                "D2015: The maximum sequence length of {} was exceeded.",
2900                max
2901            )));
2902        }
2903    }
2904    Ok(())
2905}
2906
2907/// Per-iteration D1012 check for loop-based compiled/VM constructs (map/filter/
2908/// reduce element loops, FilterByBytecode) that don't pass through
2909/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed.
2910#[inline]
2911pub(crate) fn check_loop_timeout(
2912    options: &EvaluatorOptions,
2913    start_time: Option<Instant>,
2914) -> Result<(), EvaluatorError> {
2915    if let Some(timeout_ms) = options.timeout_ms {
2916        if let Some(start) = start_time {
2917            if start.elapsed().as_millis() as u64 > timeout_ms {
2918                return Err(EvaluatorError::EvaluationError(format!(
2919                    "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
2920                    timeout_ms
2921                )));
2922            }
2923        }
2924    }
2925    Ok(())
2926}
2927
2928/// Evaluator for JSONata expressions
2929pub struct Evaluator {
2930    context: Context,
2931    recursion_depth: usize,
2932    max_recursion_depth: usize,
2933    /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
2934    /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
2935    /// using the AST node's pointer address (as before) collided whenever the same
2936    /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
2937    /// or other repeated recursion), aliasing unrelated closures that shared an id.
2938    next_lambda_id: u64,
2939    /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
2940    /// wrapper during this top-level `evaluate()` call. Reset at the start of
2941    /// `evaluate()` and checked at the end to decide whether the (recursive,
2942    /// O(result size)) tuple-unwrap pass is needed before returning to the
2943    /// caller — keeps the vast majority of evaluations, which never touch
2944    /// `%`/`@`/`#`, at zero added cost.
2945    tuple_stream_created: bool,
2946    /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
2947    /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
2948    /// by the two consumers that read those carried bindings directly from the
2949    /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
2950    /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
2951    /// evaluating its input path (key/value expressions read `$focus` off the
2952    /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
2953    /// of projecting each tuple's `@`.
2954    keep_tuple_stream: bool,
2955    options: EvaluatorOptions,
2956    /// Set in `evaluate()` (only when `options.timeout_ms` is configured) and
2957    /// checked in `evaluate_internal`'s per-node checkpoint for D1012.
2958    start_time: Option<Instant>,
2959}
2960
2961impl Evaluator {
2962    pub fn new() -> Self {
2963        Evaluator {
2964            context: Context::new(),
2965            recursion_depth: 0,
2966            // Limit recursion depth to prevent stack overflow
2967            // True TCO would allow deeper recursion but requires parser-level thunk marking
2968            max_recursion_depth: 302,
2969            next_lambda_id: 0,
2970            tuple_stream_created: false,
2971            keep_tuple_stream: false,
2972            options: EvaluatorOptions::default(),
2973            start_time: None,
2974        }
2975    }
2976
2977    pub fn with_context(context: Context) -> Self {
2978        Evaluator {
2979            context,
2980            recursion_depth: 0,
2981            max_recursion_depth: 302,
2982            next_lambda_id: 0,
2983            tuple_stream_created: false,
2984            keep_tuple_stream: false,
2985            options: EvaluatorOptions::default(),
2986            start_time: None,
2987        }
2988    }
2989
2990    /// Construct an `Evaluator` with guardrail options. `Evaluator::new()`/
2991    /// `with_context()` remain unchanged (unlimited options) for existing callers.
2992    pub fn with_options(context: Context, options: EvaluatorOptions) -> Self {
2993        Evaluator {
2994            context,
2995            recursion_depth: 0,
2996            max_recursion_depth: 302,
2997            next_lambda_id: 0,
2998            tuple_stream_created: false,
2999            keep_tuple_stream: false,
3000            options,
3001            start_time: None,
3002        }
3003    }
3004
3005    /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
3006    fn fresh_lambda_id(&mut self) -> u64 {
3007        let id = self.next_lambda_id;
3008        self.next_lambda_id += 1;
3009        id
3010    }
3011
3012    /// Invoke a stored lambda with its captured environment and data.
3013    /// This is the standard way to call a StoredLambda, handling the
3014    /// captured_env and captured_data extraction boilerplate.
3015    fn invoke_stored_lambda(
3016        &mut self,
3017        stored: &StoredLambda,
3018        args: &[JValue],
3019        data: &JValue,
3020    ) -> Result<JValue, EvaluatorError> {
3021        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
3022        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
3023        // and no captured lambda/builtin values (those require Context for runtime lookup).
3024        if let Some(ref ce) = stored.compiled_body {
3025            if stored.signature.is_none()
3026                && !stored.thunk
3027                && !stored
3028                    .captured_env
3029                    .values()
3030                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
3031            {
3032                let call_data = stored.captured_data.as_ref().unwrap_or(data);
3033                let vars: HashMap<&str, &JValue> = stored
3034                    .params
3035                    .iter()
3036                    .zip(args.iter())
3037                    .map(|(p, v)| (p.as_str(), v))
3038                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
3039                    .collect();
3040                return eval_compiled(ce, call_data, Some(&vars), &self.options, self.start_time);
3041            }
3042        }
3043
3044        let captured_env = if stored.captured_env.is_empty() {
3045            None
3046        } else {
3047            Some(&stored.captured_env)
3048        };
3049        let captured_data = stored.captured_data.as_ref();
3050        self.invoke_lambda_with_env(
3051            &stored.params,
3052            &stored.body,
3053            stored.signature.as_ref(),
3054            args,
3055            data,
3056            captured_env,
3057            captured_data,
3058            stored.thunk,
3059        )
3060    }
3061
3062    /// Look up a StoredLambda from a JValue that may be a lambda marker.
3063    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
3064    /// with a valid lambda_id that references a stored lambda.
3065    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
3066        if let JValue::Lambda { lambda_id, .. } = value {
3067            return self.context.lookup_lambda(lambda_id).cloned();
3068        }
3069        None
3070    }
3071
3072    /// Get the number of parameters a callback function expects by inspecting its AST.
3073    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
3074    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
3075    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
3076        match func_node {
3077            AstNode::Lambda { params, .. } => params.len(),
3078            AstNode::Variable(var_name) => {
3079                // Check if this variable holds a stored lambda
3080                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
3081                    return stored_lambda.params.len();
3082                }
3083                // Also check if it's a lambda value in bindings (e.g., from partial application)
3084                if let Some(value) = self.context.lookup(var_name) {
3085                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
3086                        return stored_lambda.params.len();
3087                    }
3088                }
3089                // Unknown, return max to be safe
3090                usize::MAX
3091            }
3092            AstNode::Function { .. } => {
3093                // For function references, we can't easily determine param count
3094                // Return max to be safe
3095                usize::MAX
3096            }
3097            _ => usize::MAX,
3098        }
3099    }
3100
3101    /// Specialized sort using pre-extracted keys (Schwartzian transform).
3102    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
3103    /// avoiding O(N log N) hash lookups during comparisons.
3104    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
3105        if arr.len() <= 1 {
3106            return;
3107        }
3108
3109        // Phase 1: Extract sort keys -- one IndexMap lookup per element
3110        let keys: Vec<SortKey> = arr
3111            .iter()
3112            .map(|item| match item {
3113                JValue::Object(obj) => match obj.get(&spec.field) {
3114                    Some(JValue::Number(n)) => SortKey::Num(*n),
3115                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
3116                    _ => SortKey::None,
3117                },
3118                _ => SortKey::None,
3119            })
3120            .collect();
3121
3122        // Phase 2: Build index permutation sorted by pre-extracted keys
3123        let mut perm: Vec<usize> = (0..arr.len()).collect();
3124        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
3125
3126        // Phase 3: Apply permutation in-place via cycle-following
3127        let mut placed = vec![false; arr.len()];
3128        for i in 0..arr.len() {
3129            if placed[i] || perm[i] == i {
3130                continue;
3131            }
3132            let mut j = i;
3133            loop {
3134                let target = perm[j];
3135                placed[j] = true;
3136                if target == i {
3137                    break;
3138                }
3139                arr.swap(j, target);
3140                j = target;
3141            }
3142        }
3143    }
3144
3145    /// Merge sort implementation using a comparator function.
3146    /// This replaces the O(n²) bubble sort for better performance on large arrays.
3147    /// The comparator returns true if the first element should come AFTER the second.
3148    fn merge_sort_with_comparator(
3149        &mut self,
3150        arr: &mut [JValue],
3151        comparator: &AstNode,
3152        data: &JValue,
3153    ) -> Result<(), EvaluatorError> {
3154        if arr.len() <= 1 {
3155            return Ok(());
3156        }
3157
3158        // Try specialized fast path for simple field comparisons like
3159        // function($l, $r) { $l.price > $r.price }
3160        if let AstNode::Lambda { params, body, .. } = comparator {
3161            if params.len() >= 2 {
3162                if let Some(spec) = try_specialize_sort_comparator(body, &params[0], &params[1]) {
3163                    Self::merge_sort_specialized(arr, &spec);
3164                    return Ok(());
3165                }
3166            }
3167        }
3168
3169        let mid = arr.len() / 2;
3170
3171        // Sort left half
3172        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
3173
3174        // Sort right half
3175        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
3176
3177        // Merge the sorted halves
3178        let mut temp = Vec::with_capacity(arr.len());
3179        let (left, right) = arr.split_at(mid);
3180
3181        let mut i = 0;
3182        let mut j = 0;
3183
3184        // For lambda comparators, use a reusable scope to avoid
3185        // push_scope/pop_scope per comparison (~n log n total comparisons)
3186        if let AstNode::Lambda { params, body, .. } = comparator {
3187            if params.len() >= 2 {
3188                // Pre-clone param names once outside the loop
3189                let param0 = params[0].clone();
3190                let param1 = params[1].clone();
3191                self.context.push_scope();
3192                while i < left.len() && j < right.len() {
3193                    // Reuse scope: clear and rebind instead of push/pop
3194                    self.context.clear_current_scope();
3195                    self.context.bind(param0.clone(), left[i].clone());
3196                    self.context.bind(param1.clone(), right[j].clone());
3197
3198                    let cmp_result = self.evaluate_internal(body, data)?;
3199
3200                    if self.is_truthy(&cmp_result) {
3201                        temp.push(right[j].clone());
3202                        j += 1;
3203                    } else {
3204                        temp.push(left[i].clone());
3205                        i += 1;
3206                    }
3207                }
3208                self.context.pop_scope();
3209            } else {
3210                // Unexpected param count - fall back to generic path
3211                while i < left.len() && j < right.len() {
3212                    let cmp_result = self.apply_function(
3213                        comparator,
3214                        &[left[i].clone(), right[j].clone()],
3215                        data,
3216                    )?;
3217                    if self.is_truthy(&cmp_result) {
3218                        temp.push(right[j].clone());
3219                        j += 1;
3220                    } else {
3221                        temp.push(left[i].clone());
3222                        i += 1;
3223                    }
3224                }
3225            }
3226        } else {
3227            // Non-lambda comparator: use generic apply_function path
3228            while i < left.len() && j < right.len() {
3229                let cmp_result =
3230                    self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
3231                if self.is_truthy(&cmp_result) {
3232                    temp.push(right[j].clone());
3233                    j += 1;
3234                } else {
3235                    temp.push(left[i].clone());
3236                    i += 1;
3237                }
3238            }
3239        }
3240
3241        // Copy remaining elements
3242        temp.extend_from_slice(&left[i..]);
3243        temp.extend_from_slice(&right[j..]);
3244
3245        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
3246        for (i, val) in temp.into_iter().enumerate() {
3247            arr[i] = val;
3248        }
3249
3250        Ok(())
3251    }
3252
3253    /// Evaluate an AST node against data
3254    ///
3255    /// This is the main entry point for evaluation. It sets up the parent context
3256    /// to be the root data if not already set.
3257    ///
3258    /// Also the single choke point for stripping any lingering tuple-stream
3259    /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
3260    /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
3261    /// tuple-stream representation (see `create_tuple_stream`), and without this
3262    /// a bare (or object/array-nested) tuple-producing expression would leak
3263    /// that internal representation into user-visible output instead of the
3264    /// plain value.
3265    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
3266        // Set parent context to root data if not already set
3267        if self.context.get_parent().is_none() {
3268            self.context.set_parent(data.clone());
3269        }
3270
3271        if self.options.timeout_ms.is_some() {
3272            self.start_time = Some(Instant::now());
3273        }
3274
3275        self.tuple_stream_created = false;
3276        let result = self.evaluate_internal(node, data)?;
3277        Ok(if self.tuple_stream_created {
3278            unwrap_tuple_output(result)
3279        } else {
3280            result
3281        })
3282    }
3283
3284    /// Fast evaluation for leaf nodes that don't need recursion tracking.
3285    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
3286    /// Returns None for anything requiring the full evaluator.
3287    #[inline(always)]
3288    fn evaluate_leaf(
3289        &mut self,
3290        node: &AstNode,
3291        data: &JValue,
3292    ) -> Option<Result<JValue, EvaluatorError>> {
3293        match node {
3294            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
3295            AstNode::Number(n) => {
3296                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3297                    Some(Ok(JValue::from(*n as i64)))
3298                } else {
3299                    Some(Ok(JValue::Number(*n)))
3300                }
3301            }
3302            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
3303            AstNode::Null => Some(Ok(JValue::Null)),
3304            AstNode::Undefined => Some(Ok(JValue::Undefined)),
3305            AstNode::Name(field_name) => match data {
3306                // Array mapping and other cases need full evaluator
3307                JValue::Object(obj) => Some(Ok(obj
3308                    .get(field_name)
3309                    .cloned()
3310                    .unwrap_or(JValue::Undefined))),
3311                _ => None,
3312            },
3313            AstNode::Variable(name) if !name.is_empty() => {
3314                // Simple variable lookup — only fast-path when no tuple data
3315                if let JValue::Object(obj) = data {
3316                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3317                        return None; // Tuple data needs full evaluator
3318                    }
3319                }
3320                // May be a lambda/builtin — needs full evaluator if None
3321                self.context.lookup(name).map(|value| Ok(value.clone()))
3322            }
3323            _ => None,
3324        }
3325    }
3326
3327    /// Internal evaluation method
3328    fn evaluate_internal(
3329        &mut self,
3330        node: &AstNode,
3331        data: &JValue,
3332    ) -> Result<JValue, EvaluatorError> {
3333        // Fast path for leaf nodes — skip recursion tracking overhead
3334        if let Some(result) = self.evaluate_leaf(node, data) {
3335            return result;
3336        }
3337
3338        // Check recursion depth to prevent stack overflow. `effective_limit` is
3339        // whichever is tighter: the user's `max_stack_depth` guardrail or the
3340        // hardcoded native-stack-safety ceiling (`max_recursion_depth`, always
3341        // 302, GitHub issue #34). The hardcoded ceiling is an always-on backstop
3342        // regardless of user options — only a user limit BELOW it can produce
3343        // D1011; hitting the hardcoded ceiling itself (no option set, or an
3344        // option set at/above 302) still produces U1001.
3345        self.recursion_depth += 1;
3346        let effective_limit = match self.options.max_stack_depth {
3347            Some(limit) => limit.min(self.max_recursion_depth),
3348            None => self.max_recursion_depth,
3349        };
3350        if self.recursion_depth > effective_limit {
3351            self.recursion_depth -= 1;
3352            return Err(EvaluatorError::EvaluationError(
3353                if effective_limit < self.max_recursion_depth {
3354                    "D1011: Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive".to_string()
3355                } else {
3356                    format!(
3357                        "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
3358                        effective_limit
3359                    )
3360                },
3361            ));
3362        }
3363
3364        // Check evaluation timeout (D1012). `start_time` is only set (in
3365        // `evaluate()`) when `options.timeout_ms` is configured, so this is a
3366        // single `is_none()` branch of overhead when no timeout is set.
3367        if let Some(timeout_ms) = self.options.timeout_ms {
3368            if let Some(start) = self.start_time {
3369                if start.elapsed().as_millis() as u64 > timeout_ms {
3370                    self.recursion_depth -= 1;
3371                    return Err(EvaluatorError::EvaluationError(format!(
3372                        "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3373                        timeout_ms
3374                    )));
3375                }
3376            }
3377        }
3378
3379        // The soft depth counter above is calibrated against a comfortably
3380        // large native stack. Hosts with a much smaller default thread stack
3381        // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
3382        // stack well before this counter trips, crashing the process instead
3383        // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
3384        // transparently swaps in a bigger stack segment when headroom is
3385        // low, so this stays a no-op cost on the common shallow path.
3386        const RED_ZONE: usize = 128 * 1024;
3387        const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
3388        let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
3389            self.evaluate_internal_impl(node, data)
3390        });
3391
3392        self.recursion_depth -= 1;
3393        result
3394    }
3395
3396    /// Internal evaluation implementation (separated to allow depth tracking)
3397    fn evaluate_internal_impl(
3398        &mut self,
3399        node: &AstNode,
3400        data: &JValue,
3401    ) -> Result<JValue, EvaluatorError> {
3402        match node {
3403            AstNode::String(s) => Ok(JValue::string(s.clone())),
3404
3405            // Name nodes represent field access on the current data
3406            AstNode::Name(field_name) => {
3407                match data {
3408                    JValue::Object(obj) => {
3409                        Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3410                    }
3411                    JValue::Array(arr) => {
3412                        // Map over array
3413                        let mut result = Vec::new();
3414                        for item in arr.iter() {
3415                            if let JValue::Object(obj) = item {
3416                                if let Some(val) = obj.get(field_name) {
3417                                    result.push(val.clone());
3418                                }
3419                            }
3420                        }
3421                        if result.is_empty() {
3422                            Ok(JValue::Undefined)
3423                        } else if result.len() == 1 {
3424                            Ok(result.into_iter().next().unwrap())
3425                        } else {
3426                            Ok(JValue::array(result))
3427                        }
3428                    }
3429                    _ => Ok(JValue::Undefined),
3430                }
3431            }
3432
3433            AstNode::Number(n) => {
3434                // Preserve integer-ness: if the number is a whole number, create an integer JValue
3435                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3436                    // It's a whole number that can be represented as i64
3437                    Ok(JValue::from(*n as i64))
3438                } else {
3439                    Ok(JValue::Number(*n))
3440                }
3441            }
3442            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3443            AstNode::Null => Ok(JValue::Null),
3444            AstNode::Undefined => Ok(JValue::Undefined),
3445            AstNode::Placeholder => {
3446                // Placeholders should only appear as function arguments
3447                // If we reach here, it's an error
3448                Err(EvaluatorError::EvaluationError(
3449                    "Placeholder '?' can only be used as a function argument".to_string(),
3450                ))
3451            }
3452            AstNode::Regex { pattern, flags } => {
3453                // Return a regex object as a special JSON value
3454                // This will be recognized by functions like $split, $match, $replace
3455                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3456            }
3457
3458            AstNode::Variable(name) => {
3459                // Special case: $ alone (empty name) refers to current context
3460                // First check if $ is bound in the context (for closures that captured $)
3461                // Otherwise, use the data parameter
3462                if name.is_empty() {
3463                    if let Some(value) = self.context.lookup("$") {
3464                        return Ok(value.clone());
3465                    }
3466                    // If data is a tuple, return the @ value
3467                    if let JValue::Object(obj) = data {
3468                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3469                            if let Some(inner) = obj.get("@") {
3470                                return Ok(inner.clone());
3471                            }
3472                        }
3473                    }
3474                    return Ok(data.clone());
3475                }
3476
3477                // Check variable bindings FIRST
3478                // This allows function parameters to shadow outer lambdas with the same name
3479                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3480                if let Some(value) = self.context.lookup(name) {
3481                    return Ok(value.clone());
3482                }
3483
3484                // Check tuple bindings in data (for index binding operator #$var)
3485                // When iterating over a tuple stream, $var can reference the bound index
3486                if let JValue::Object(obj) = data {
3487                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3488                        // Check for the variable in tuple bindings (stored as "$name")
3489                        let binding_key = format!("${}", name);
3490                        if let Some(binding_value) = obj.get(&binding_key) {
3491                            return Ok(binding_value.clone());
3492                        }
3493                    }
3494                }
3495
3496                // Then check if this is a stored lambda (user-defined functions)
3497                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3498                    // Return a lambda representation that can be passed to higher-order functions
3499                    // Include _lambda_id pointing to the stored lambda so it can be found
3500                    // when captured in closures
3501                    let lambda_repr = JValue::lambda(
3502                        name.as_str(),
3503                        stored_lambda.params.clone(),
3504                        Some(name.to_string()),
3505                        stored_lambda.signature.clone(),
3506                    );
3507                    return Ok(lambda_repr);
3508                }
3509
3510                // Check if this is a built-in function reference (only if not shadowed)
3511                if self.is_builtin_function(name) {
3512                    // Return a marker for built-in functions
3513                    // This allows built-in functions to be passed to higher-order functions
3514                    let builtin_repr = JValue::builtin(name.as_str());
3515                    return Ok(builtin_repr);
3516                }
3517
3518                // Undefined variable - return null (undefined in JSONata semantics)
3519                // This allows expressions like `$not(undefined_var)` to return undefined
3520                // and comparisons like `3 > $undefined` to return undefined
3521                Ok(JValue::Null)
3522            }
3523
3524            AstNode::ParentVariable(name) => {
3525                // Special case: $$ alone (empty name) refers to parent/root context
3526                if name.is_empty() {
3527                    return self.context.get_parent().cloned().ok_or_else(|| {
3528                        EvaluatorError::ReferenceError("Parent context not available".to_string())
3529                    });
3530                }
3531
3532                // For $$name, we need to evaluate name against parent context
3533                // This is similar to $.name but using parent data
3534                let parent_data = self.context.get_parent().ok_or_else(|| {
3535                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3536                })?;
3537
3538                // Access field on parent context
3539                match parent_data {
3540                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3541                    _ => Ok(JValue::Null),
3542                }
3543            }
3544
3545            AstNode::Path { steps } => self.evaluate_path(steps, data),
3546
3547            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3548
3549            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3550
3551            // Array constructor - JSONata semantics:
3552            AstNode::Array(elements) => {
3553                // - If element is itself an array constructor [...], keep it nested
3554                // - Otherwise, if element evaluates to an array, flatten it
3555                // - Undefined values are excluded
3556                let mut result = Vec::with_capacity(elements.len());
3557                for element in elements {
3558                    // Check if this element is itself an explicit array constructor
3559                    let is_array_constructor = matches!(element, AstNode::Array(_));
3560
3561                    let value = self.evaluate_internal(element, data)?;
3562
3563                    // Skip undefined values in array constructors
3564                    // Note: explicit null is preserved, only undefined (no value) is filtered
3565                    if value.is_undefined() {
3566                        continue;
3567                    }
3568
3569                    if is_array_constructor {
3570                        // Explicit array constructor - keep nested
3571                        result.push(value);
3572                    } else if let JValue::Array(arr) = value {
3573                        // Non-array-constructor that evaluated to array - flatten it
3574                        result.extend(arr.iter().cloned());
3575                    } else {
3576                        // Non-array value - add as-is
3577                        result.push(value);
3578                    }
3579                }
3580                Ok(JValue::array(result))
3581            }
3582
3583            AstNode::Object(pairs) => {
3584                let mut result = IndexMap::with_capacity(pairs.len());
3585
3586                // Check if all keys are string literals — can skip D1009 HashMap
3587                let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3588
3589                if all_literal_keys {
3590                    // Fast path: literal keys, no need for D1009 tracking
3591                    for (key_node, value_node) in pairs.iter() {
3592                        let key = match key_node {
3593                            AstNode::String(s) => s,
3594                            _ => unreachable!(),
3595                        };
3596                        let value = self.evaluate_internal(value_node, data)?;
3597                        if value.is_undefined() {
3598                            continue;
3599                        }
3600                        result.insert(key.clone(), value);
3601                    }
3602                } else {
3603                    let mut key_sources: HashMap<String, usize> = HashMap::new();
3604                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3605                        let key = match self.evaluate_internal(key_node, data)? {
3606                            JValue::String(s) => s,
3607                            JValue::Null => continue,
3608                            other => {
3609                                if other.is_undefined() {
3610                                    continue;
3611                                }
3612                                return Err(EvaluatorError::TypeError(format!(
3613                                    "Object key must be a string, got: {:?}",
3614                                    other
3615                                )));
3616                            }
3617                        };
3618
3619                        if let Some(&existing_idx) = key_sources.get(&*key) {
3620                            if existing_idx != pair_index {
3621                                return Err(EvaluatorError::EvaluationError(format!(
3622                                    "D1009: Multiple key expressions evaluate to same key: {}",
3623                                    key
3624                                )));
3625                            }
3626                        }
3627                        key_sources.insert(key.to_string(), pair_index);
3628
3629                        let value = self.evaluate_internal(value_node, data)?;
3630                        if value.is_undefined() {
3631                            continue;
3632                        }
3633                        result.insert(key.to_string(), value);
3634                    }
3635                }
3636                Ok(JValue::object(result))
3637            }
3638
3639            // Object transform: group items by key, then evaluate value once per group
3640            AstNode::ObjectTransform { input, pattern } => {
3641                // Evaluate the input expression. Keep tuple wrappers alive so the
3642                // group-by key/value expressions can read the carried `$focus`
3643                // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3644                let saved_keep = self.keep_tuple_stream;
3645                self.keep_tuple_stream = true;
3646                let input_value = self.evaluate_internal(input, data);
3647                self.keep_tuple_stream = saved_keep;
3648                let input_value = input_value?;
3649
3650                // If input is undefined, return undefined (not empty object)
3651                if input_value.is_undefined() {
3652                    return Ok(JValue::Undefined);
3653                }
3654
3655                // Handle array input - process each item
3656                let items: Vec<JValue> = match input_value {
3657                    JValue::Array(ref arr) => (**arr).clone(),
3658                    JValue::Null => return Ok(JValue::Null),
3659                    other => vec![other],
3660                };
3661
3662                // If array is empty, add undefined to enable literal JSON object generation
3663                let items = if items.is_empty() {
3664                    vec![JValue::Undefined]
3665                } else {
3666                    items
3667                };
3668
3669                // Grouping over a tuple stream ("reduce" mode, mirroring
3670                // jsonata-js evaluateGroupExpression): each item is a
3671                // `{@, $var, !label, __tuple__}` wrapper. The key/value
3672                // expressions are evaluated against the tuple's `@` value with the
3673                // carried focus/index/ancestor keys bound into scope (so
3674                // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3675                // `$e` AND resolves the relative `Phone` against the Contact `@`),
3676                // and grouped tuples are reduced (per-key values appended) before
3677                // the value expression sees them.
3678                let reduce = items.first().is_some_and(|it| {
3679                    matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3680                });
3681
3682                // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3683                // returns the saved prior values so they can be restored.
3684                let bind_tuple = |ev: &mut Self,
3685                                  tuple: &IndexMap<String, JValue>|
3686                 -> Vec<(String, Option<JValue>)> {
3687                    let mut saved = Vec::new();
3688                    for (k, v) in tuple.iter() {
3689                        let name = if let Some(n) = k.strip_prefix('$') {
3690                            if n.is_empty() {
3691                                continue;
3692                            } else {
3693                                n.to_string()
3694                            }
3695                        } else if k.starts_with('!') {
3696                            k.clone()
3697                        } else {
3698                            continue;
3699                        };
3700                        saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3701                        ev.context.bind(name, v.clone());
3702                    }
3703                    saved
3704                };
3705                let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3706                    for (name, old) in saved.into_iter().rev() {
3707                        match old {
3708                            Some(v) => ev.context.bind(name, v),
3709                            None => ev.context.unbind(&name),
3710                        }
3711                    }
3712                };
3713
3714                // Phase 1: Group items by key expression
3715                // groups maps key -> (grouped_data, expr_index)
3716                // When multiple items have same key, their data is appended together
3717                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3718
3719                // Save the current $ binding to restore later
3720                let saved_dollar = self.context.lookup("$").cloned();
3721
3722                for item in &items {
3723                    // In reduce mode evaluate the key against `@` with tuple keys
3724                    // bound; otherwise against the item itself.
3725                    let (key_data, tuple_saved) = match (reduce, item) {
3726                        (true, JValue::Object(o)) => {
3727                            let saved = bind_tuple(self, o);
3728                            (
3729                                o.get("@").cloned().unwrap_or(JValue::Undefined),
3730                                Some(saved),
3731                            )
3732                        }
3733                        _ => (item.clone(), None),
3734                    };
3735                    self.context.bind("$".to_string(), key_data.clone());
3736
3737                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3738                        // Evaluate key with current item as context
3739                        let key = match self.evaluate_internal(key_node, &key_data)? {
3740                            JValue::String(s) => s,
3741                            JValue::Null => continue, // Skip null keys
3742                            other => {
3743                                // Skip undefined keys
3744                                if other.is_undefined() {
3745                                    continue;
3746                                }
3747                                if let Some(saved) = tuple_saved {
3748                                    restore(self, saved);
3749                                }
3750                                return Err(EvaluatorError::TypeError(format!(
3751                                    "T1003: Object key must be a string, got: {:?}",
3752                                    other
3753                                )));
3754                            }
3755                        };
3756
3757                        // Group items by key
3758                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3759                            // Key already exists - check if from same expression index
3760                            if *existing_idx != pair_index {
3761                                if let Some(saved) = tuple_saved {
3762                                    restore(self, saved);
3763                                }
3764                                // D1009: multiple key expressions evaluate to same key
3765                                return Err(EvaluatorError::EvaluationError(format!(
3766                                    "D1009: Multiple key expressions evaluate to same key: {}",
3767                                    key
3768                                )));
3769                            }
3770                            // Append item to the group
3771                            existing_data.push(item.clone());
3772                        } else {
3773                            // New key - create new group
3774                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3775                        }
3776                    }
3777
3778                    if let Some(saved) = tuple_saved {
3779                        restore(self, saved);
3780                    }
3781                }
3782
3783                // Phase 2: Evaluate value expression for each group
3784                let mut result = IndexMap::new();
3785
3786                for (key, (grouped_data, expr_index)) in groups {
3787                    // Get the value expression for this group
3788                    let (_key_node, value_node) = &pattern[expr_index];
3789
3790                    if reduce {
3791                        // Reduce the grouped tuples into one (per-key values
3792                        // appended), mirroring jsonata-js reduceTupleStream, then
3793                        // evaluate the value against the merged `@` with the merged
3794                        // focus/index/ancestor keys bound.
3795                        let merged = reduce_tuple_stream(&grouped_data);
3796                        let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3797                        let mut tuple_no_at = merged.clone();
3798                        tuple_no_at.shift_remove("@");
3799                        let saved = bind_tuple(self, &tuple_no_at);
3800                        self.context.bind("$".to_string(), context.clone());
3801                        let value = self.evaluate_internal(value_node, &context);
3802                        restore(self, saved);
3803                        let value = value?;
3804                        if !value.is_undefined() {
3805                            result.insert(key, value);
3806                        }
3807                        continue;
3808                    }
3809
3810                    // Determine the context for value evaluation:
3811                    // - If single item, use that item directly
3812                    // - If multiple items, use the array of items
3813                    let context = if grouped_data.len() == 1 {
3814                        grouped_data.into_iter().next().unwrap()
3815                    } else {
3816                        JValue::array(grouped_data)
3817                    };
3818
3819                    // Bind $ to the context for value evaluation
3820                    self.context.bind("$".to_string(), context.clone());
3821
3822                    // Evaluate value expression with grouped context
3823                    let value = self.evaluate_internal(value_node, &context)?;
3824
3825                    // Skip undefined values
3826                    if !value.is_undefined() {
3827                        result.insert(key, value);
3828                    }
3829                }
3830
3831                // Restore the previous $ binding
3832                if let Some(saved) = saved_dollar {
3833                    self.context.bind("$".to_string(), saved);
3834                } else {
3835                    self.context.unbind("$");
3836                }
3837
3838                Ok(JValue::object(result))
3839            }
3840
3841            AstNode::Function {
3842                name,
3843                args,
3844                is_builtin,
3845            } => self.evaluate_function_call(name, args, *is_builtin, data),
3846
3847            // Call: invoke an arbitrary expression as a function
3848            // Used for IIFE patterns like (function($x){...})(5) or chained calls
3849            AstNode::Call { procedure, args } => {
3850                // Evaluate the procedure to get the callable value
3851                let callable = self.evaluate_internal(procedure, data)?;
3852
3853                // Check if it's a lambda value
3854                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3855                    let mut evaluated_args = Vec::with_capacity(args.len());
3856                    for arg in args.iter() {
3857                        evaluated_args.push(self.evaluate_internal(arg, data)?);
3858                    }
3859                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3860                }
3861
3862                // Not a callable value
3863                Err(EvaluatorError::TypeError(format!(
3864                    "Cannot call non-function value: {:?}",
3865                    callable
3866                )))
3867            }
3868
3869            AstNode::Conditional {
3870                condition,
3871                then_branch,
3872                else_branch,
3873            } => {
3874                let condition_value = self.evaluate_internal(condition, data)?;
3875                if self.is_truthy(&condition_value) {
3876                    self.evaluate_internal(then_branch, data)
3877                } else if let Some(else_branch) = else_branch {
3878                    self.evaluate_internal(else_branch, data)
3879                } else {
3880                    // No else branch - return undefined (not null)
3881                    // This allows $map to filter out results from conditionals without else
3882                    Ok(JValue::Undefined)
3883                }
3884            }
3885
3886            AstNode::Block(expressions) => {
3887                // Blocks create a new scope - push scope instead of clone/restore
3888                self.context.push_scope();
3889
3890                let mut result = JValue::Null;
3891                for expr in expressions {
3892                    result = self.evaluate_internal(expr, data)?;
3893                }
3894
3895                // Before popping, preserve any lambdas referenced by the result
3896                // This is essential for closures returned from blocks (IIFE pattern)
3897                let lambdas_to_keep = self.extract_lambda_ids(&result);
3898                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3899
3900                Ok(result)
3901            }
3902
3903            // Lambda: capture current environment for closure support
3904            AstNode::Lambda {
3905                params,
3906                body,
3907                signature,
3908                thunk,
3909            } => {
3910                let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
3911
3912                let compiled_body = if !thunk {
3913                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3914                    try_compile_expr_with_allowed_vars(body, &var_refs)
3915                } else {
3916                    None
3917                };
3918                let stored_lambda = StoredLambda {
3919                    params: params.clone(),
3920                    body: (**body).clone(),
3921                    compiled_body,
3922                    signature: signature.clone(),
3923                    captured_env: self.capture_environment_for(body, params),
3924                    captured_data: Some(data.clone()),
3925                    thunk: *thunk,
3926                };
3927                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3928
3929                let lambda_obj = JValue::lambda(
3930                    lambda_id.as_str(),
3931                    params.clone(),
3932                    None::<String>,
3933                    signature.clone(),
3934                );
3935
3936                Ok(lambda_obj)
3937            }
3938
3939            // Wildcard: collect all values from current object
3940            AstNode::Wildcard => {
3941                match data {
3942                    JValue::Object(obj) => {
3943                        let mut result = Vec::new();
3944                        for value in obj.values() {
3945                            // Flatten arrays into the result
3946                            match value {
3947                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3948                                _ => result.push(value.clone()),
3949                            }
3950                        }
3951                        check_sequence_length(result.len(), &self.options)?;
3952                        Ok(JValue::array(result))
3953                    }
3954                    JValue::Array(arr) => {
3955                        // For arrays, wildcard returns all elements
3956                        Ok(JValue::Array(arr.clone()))
3957                    }
3958                    _ => Ok(JValue::Null),
3959                }
3960            }
3961
3962            // Descendant: recursively traverse all nested values
3963            AstNode::Descendant => {
3964                let descendants = self.collect_descendants(data);
3965                if descendants.is_empty() {
3966                    Ok(JValue::Null) // No descendants means undefined
3967                } else {
3968                    check_sequence_length(descendants.len(), &self.options)?;
3969                    Ok(JValue::array(descendants))
3970                }
3971            }
3972
3973            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3974                "Predicate can only be used in path expressions".to_string(),
3975            )),
3976
3977            // Array grouping: same as Array but prevents flattening in path contexts
3978            AstNode::ArrayGroup(elements) => {
3979                let mut result = Vec::new();
3980                for element in elements {
3981                    let value = self.evaluate_internal(element, data)?;
3982                    result.push(value);
3983                }
3984                Ok(JValue::array(result))
3985            }
3986
3987            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3988                "Function application can only be used in path expressions".to_string(),
3989            )),
3990
3991            AstNode::Sort { input, terms } => {
3992                // Keep the input path's tuple wrappers so the sort terms can read
3993                // the carried `%`/`$focus`/`$index` bindings per element.
3994                let saved = self.keep_tuple_stream;
3995                self.keep_tuple_stream = true;
3996                let value = self.evaluate_internal(input, data);
3997                self.keep_tuple_stream = saved;
3998                self.evaluate_sort(&value?, terms)
3999            }
4000
4001            // Transform: |location|update[,delete]|
4002            AstNode::Transform {
4003                location,
4004                update,
4005                delete,
4006            } => {
4007                // Check if $ is bound (meaning we're being invoked as a lambda)
4008                if self.context.lookup("$").is_some() {
4009                    // Execute the transformation
4010                    self.execute_transform(location, update, delete.as_deref(), data)
4011                } else {
4012                    // Return a lambda representation
4013                    // The transform will be executed when the lambda is invoked
4014                    let transform_lambda = StoredLambda {
4015                        params: vec!["$".to_string()],
4016                        body: AstNode::Transform {
4017                            location: location.clone(),
4018                            update: update.clone(),
4019                            delete: delete.clone(),
4020                        },
4021                        compiled_body: None, // Transform is not a pure compilable expr
4022                        signature: None,
4023                        captured_env: HashMap::new(),
4024                        captured_data: None, // Transform takes $ as parameter
4025                        thunk: false,
4026                    };
4027
4028                    // Store with a generated unique name
4029                    let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
4030                    self.context.bind_lambda(lambda_name, transform_lambda);
4031
4032                    // Return lambda marker
4033                    Ok(JValue::string("<lambda>"))
4034                }
4035            }
4036
4037            // Parent-reference operator (%): ast_transform has already resolved
4038            // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
4039            // tuple step binds that label into scope (create_tuple_stream +
4040            // needs_tuple_context_binding), so resolving it is an ordinary scope
4041            // lookup, mirroring jsonata-js's
4042            // `case 'parent': result = environment.lookup(expr.slot.label);`.
4043            AstNode::Parent(label) => {
4044                if let Some(v) = self.context.lookup(label) {
4045                    return Ok(v.clone());
4046                }
4047                // Fall back to the tuple wrapper carried as `data`: a `%` used
4048                // inside a predicate/stage over a tuple stream -- e.g.
4049                // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
4050                // predicate is evaluated per tuple with the wrapper as data --
4051                // reads its ancestor from the tuple's `!label` key, which isn't
4052                // separately bound into scope here (mirrors AstNode::Variable's
4053                // tuple-binding fallback below).
4054                if let JValue::Object(obj) = data {
4055                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4056                        if let Some(v) = obj.get(label) {
4057                            return Ok(v.clone());
4058                        }
4059                    }
4060                }
4061                Ok(JValue::Undefined)
4062            }
4063        }
4064    }
4065
4066    /// Apply stages (filters/predicates) to a value during field extraction
4067    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
4068    /// This matches the JavaScript reference where stages apply to sequences
4069    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
4070        // Wrap non-arrays in an array for filtering (JSONata semantics)
4071        let mut result = match value {
4072            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
4073            JValue::Array(_) => value,
4074            other => JValue::array(vec![other]),
4075        };
4076
4077        for stage in stages {
4078            match stage {
4079                Stage::Filter(predicate_expr) => {
4080                    // When applying stages, use stage-specific predicate logic
4081                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
4082                }
4083                // Positional index stages are meaningful only over a tuple stream
4084                // (they set a variable to each tuple's position); they are applied
4085                // in `create_tuple_stream`, not on a plain value sequence here.
4086                Stage::Index(_) => {}
4087            }
4088        }
4089        Ok(result)
4090    }
4091
4092    /// Check if an AST node is definitely a filter expression (comparison/logical)
4093    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
4094    fn is_filter_predicate(predicate: &AstNode) -> bool {
4095        match predicate {
4096            AstNode::Binary { op, .. } => matches!(
4097                op,
4098                BinaryOp::GreaterThan
4099                    | BinaryOp::GreaterThanOrEqual
4100                    | BinaryOp::LessThan
4101                    | BinaryOp::LessThanOrEqual
4102                    | BinaryOp::Equal
4103                    | BinaryOp::NotEqual
4104                    | BinaryOp::And
4105                    | BinaryOp::Or
4106                    | BinaryOp::In
4107            ),
4108            AstNode::Unary {
4109                op: crate::ast::UnaryOp::Not,
4110                ..
4111            } => true,
4112            _ => false,
4113        }
4114    }
4115
4116    /// Evaluate a predicate as a stage during field extraction
4117    /// This has different semantics than standalone predicates:
4118    /// - Maps index operations over arrays of extracted values
4119    fn evaluate_predicate_as_stage(
4120        &mut self,
4121        current: &JValue,
4122        predicate: &AstNode,
4123    ) -> Result<JValue, EvaluatorError> {
4124        // Special case: empty brackets [] (represented as Boolean(true))
4125        if matches!(predicate, AstNode::Boolean(true)) {
4126            return match current {
4127                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
4128                JValue::Null => Ok(JValue::Null),
4129                other => Ok(JValue::array(vec![other.clone()])),
4130            };
4131        }
4132
4133        match current {
4134            JValue::Array(arr) => {
4135                // For stages: if we have an array of values (from field extraction),
4136                // apply the predicate to each value if appropriate
4137
4138                // Check if predicate is a numeric index
4139                if let AstNode::Number(n) = predicate {
4140                    // Check if this is an array of arrays (extracted array fields)
4141                    let is_array_of_arrays =
4142                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
4143
4144                    if !is_array_of_arrays {
4145                        // Simple values: just index normally
4146                        return self.array_index(current, &JValue::Number(*n));
4147                    }
4148
4149                    // Array of arrays: map index access over each extracted array
4150                    let mut result = Vec::new();
4151                    for item in arr.iter() {
4152                        match item {
4153                            JValue::Array(_) => {
4154                                let indexed = self.array_index(item, &JValue::Number(*n))?;
4155                                if !indexed.is_null() && !indexed.is_undefined() {
4156                                    result.push(indexed);
4157                                }
4158                            }
4159                            _ => {
4160                                if *n == 0.0 {
4161                                    result.push(item.clone());
4162                                }
4163                            }
4164                        }
4165                    }
4166                    return Ok(JValue::array(result));
4167                }
4168
4169                // Short-circuit: if predicate is definitely a comparison/logical expression,
4170                // skip speculative numeric evaluation and go directly to filter logic
4171                if Self::is_filter_predicate(predicate) {
4172                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
4173                    if let Some(compiled) = try_compile_expr(predicate) {
4174                        let shape = arr.first().and_then(build_shape_cache);
4175                        let mut filtered = Vec::with_capacity(arr.len());
4176                        for item in arr.iter() {
4177                            let result = if let Some(ref s) = shape {
4178                                eval_compiled_shaped(
4179                                    &compiled,
4180                                    item,
4181                                    None,
4182                                    s,
4183                                    &self.options,
4184                                    self.start_time,
4185                                )?
4186                            } else {
4187                                eval_compiled(
4188                                    &compiled,
4189                                    item,
4190                                    None,
4191                                    &self.options,
4192                                    self.start_time,
4193                                )?
4194                            };
4195                            if compiled_is_truthy(&result) {
4196                                filtered.push(item.clone());
4197                            }
4198                        }
4199                        return Ok(JValue::array(filtered));
4200                    }
4201                    // Fallback: full AST evaluation
4202                    let mut filtered = Vec::new();
4203                    for item in arr.iter() {
4204                        let item_result = self.evaluate_internal(predicate, item)?;
4205                        if self.is_truthy(&item_result) {
4206                            filtered.push(item.clone());
4207                        }
4208                    }
4209                    return Ok(JValue::array(filtered));
4210                }
4211
4212                // Try to evaluate the predicate to see if it's a numeric index or array of indices
4213                // If evaluation succeeds and yields a number, use it as an index
4214                // If it yields an array of numbers, use them as multiple indices
4215                // If evaluation fails (e.g., comparison error), treat as filter
4216                match self.evaluate_internal(predicate, current) {
4217                    Ok(JValue::Number(n)) => {
4218                        let n_val = n;
4219                        let is_array_of_arrays =
4220                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
4221
4222                        if !is_array_of_arrays {
4223                            let pred_result = JValue::Number(n_val);
4224                            return self.array_index(current, &pred_result);
4225                        }
4226
4227                        // Array of arrays: map index access
4228                        let mut result = Vec::new();
4229                        let pred_result = JValue::Number(n_val);
4230                        for item in arr.iter() {
4231                            match item {
4232                                JValue::Array(_) => {
4233                                    let indexed = self.array_index(item, &pred_result)?;
4234                                    if !indexed.is_null() && !indexed.is_undefined() {
4235                                        result.push(indexed);
4236                                    }
4237                                }
4238                                _ => {
4239                                    if n_val == 0.0 {
4240                                        result.push(item.clone());
4241                                    }
4242                                }
4243                            }
4244                        }
4245                        return Ok(JValue::array(result));
4246                    }
4247                    Ok(JValue::Array(indices)) => {
4248                        // Array of values - could be indices or filter results
4249                        // Check if all values are numeric
4250                        let has_non_numeric =
4251                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
4252
4253                        if has_non_numeric {
4254                            // Non-numeric values - treat as filter, fall through
4255                        } else {
4256                            // All numeric - use as indices
4257                            let arr_len = arr.len() as i64;
4258                            let mut resolved_indices: Vec<i64> = indices
4259                                .iter()
4260                                .filter_map(|v| {
4261                                    if let JValue::Number(n) = v {
4262                                        let idx = *n as i64;
4263                                        // Resolve negative indices
4264                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
4265                                        // Only include valid indices
4266                                        if actual_idx >= 0 && actual_idx < arr_len {
4267                                            Some(actual_idx)
4268                                        } else {
4269                                            None
4270                                        }
4271                                    } else {
4272                                        None
4273                                    }
4274                                })
4275                                .collect();
4276
4277                            // Sort and deduplicate indices
4278                            resolved_indices.sort();
4279                            resolved_indices.dedup();
4280
4281                            // Select elements at each sorted index
4282                            let result: Vec<JValue> = resolved_indices
4283                                .iter()
4284                                .map(|&idx| arr[idx as usize].clone())
4285                                .collect();
4286
4287                            return Ok(JValue::array(result));
4288                        }
4289                    }
4290                    Ok(_) => {
4291                        // Evaluated successfully but not a number or array - might be a filter
4292                        // Fall through to filter logic
4293                    }
4294                    Err(_) => {
4295                        // Evaluation failed - it's likely a filter expression
4296                        // Fall through to filter logic
4297                    }
4298                }
4299
4300                // It's a filter expression
4301                let mut filtered = Vec::new();
4302                for item in arr.iter() {
4303                    let item_result = self.evaluate_internal(predicate, item)?;
4304                    if self.is_truthy(&item_result) {
4305                        filtered.push(item.clone());
4306                    }
4307                }
4308                Ok(JValue::array(filtered))
4309            }
4310            JValue::Null => {
4311                // Null: return null
4312                Ok(JValue::Null)
4313            }
4314            other => {
4315                // Non-array values: treat as single-element conceptual array
4316                // For numeric predicates: index 0 returns the value, other indices return null
4317                // For boolean predicates: if truthy, return value; if falsy, return null
4318
4319                // Check if predicate is a numeric index
4320                if let AstNode::Number(n) = predicate {
4321                    // Index 0 returns the value, other indices return null
4322                    if *n == 0.0 {
4323                        return Ok(other.clone());
4324                    } else {
4325                        return Ok(JValue::Null);
4326                    }
4327                }
4328
4329                // Try to evaluate the predicate to see if it's a numeric index
4330                match self.evaluate_internal(predicate, other) {
4331                    Ok(JValue::Number(n)) => {
4332                        // Index 0 returns the value, other indices return null
4333                        if n == 0.0 {
4334                            Ok(other.clone())
4335                        } else {
4336                            Ok(JValue::Null)
4337                        }
4338                    }
4339                    Ok(pred_result) => {
4340                        // Boolean filter: return value if truthy, null if falsy
4341                        if self.is_truthy(&pred_result) {
4342                            Ok(other.clone())
4343                        } else {
4344                            Ok(JValue::Null)
4345                        }
4346                    }
4347                    Err(e) => Err(e),
4348                }
4349            }
4350        }
4351    }
4352
4353    /// Evaluate a path expression (e.g., foo.bar.baz)
4354    fn evaluate_path(
4355        &mut self,
4356        steps: &[PathStep],
4357        data: &JValue,
4358    ) -> Result<JValue, EvaluatorError> {
4359        // Avoid cloning by using references and only cloning when necessary
4360        if steps.is_empty() {
4361            return Ok(data.clone());
4362        }
4363
4364        // Fast path: single field access on object
4365        // This is a very common pattern, so optimize it.
4366        // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
4367        // creation handled below.
4368        if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
4369            if let AstNode::Name(field_name) = &steps[0].node {
4370                return match data {
4371                    JValue::Object(obj) => {
4372                        // Check if this is a tuple - extract '@' value
4373                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4374                            if let Some(JValue::Object(inner)) = obj.get("@") {
4375                                Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
4376                            } else {
4377                                Ok(JValue::Undefined)
4378                            }
4379                        } else {
4380                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
4381                        }
4382                    }
4383                    JValue::Array(arr) => {
4384                        // Array mapping: extract field from each element
4385                        // Optimized: use references to access fields without cloning entire objects
4386                        // Check first element for tuple-ness (tuples are all-or-nothing)
4387                        let has_tuples = arr.first().is_some_and(|item| {
4388                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4389                        });
4390
4391                        if !has_tuples {
4392                            // Fast path: no tuples, just direct field lookups
4393                            let mut result = Vec::with_capacity(arr.len());
4394                            for item in arr.iter() {
4395                                if let JValue::Object(obj) = item {
4396                                    if let Some(val) = obj.get(field_name) {
4397                                        if !val.is_null() {
4398                                            match val {
4399                                                JValue::Array(arr_val) => {
4400                                                    result.extend(arr_val.iter().cloned());
4401                                                }
4402                                                other => result.push(other.clone()),
4403                                            }
4404                                        }
4405                                    }
4406                                } else if let JValue::Array(inner_arr) = item {
4407                                    let nested_result = self.evaluate_path(
4408                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
4409                                        &JValue::Array(inner_arr.clone()),
4410                                    )?;
4411                                    match nested_result {
4412                                        JValue::Array(nested) => {
4413                                            result.extend(nested.iter().cloned());
4414                                        }
4415                                        JValue::Null => {}
4416                                        other => result.push(other),
4417                                    }
4418                                }
4419                            }
4420
4421                            if result.is_empty() {
4422                                Ok(JValue::Null)
4423                            } else if result.len() == 1 {
4424                                Ok(result.into_iter().next().unwrap())
4425                            } else {
4426                                check_sequence_length(result.len(), &self.options)?;
4427                                Ok(JValue::array(result))
4428                            }
4429                        } else {
4430                            // Tuple path: per-element tuple handling
4431                            let mut result = Vec::new();
4432                            for item in arr.iter() {
4433                                match item {
4434                                    JValue::Object(obj) => {
4435                                        let is_tuple =
4436                                            obj.get("__tuple__") == Some(&JValue::Bool(true));
4437
4438                                        if is_tuple {
4439                                            let inner = match obj.get("@") {
4440                                                Some(JValue::Object(inner)) => inner,
4441                                                _ => continue,
4442                                            };
4443
4444                                            if let Some(val) = inner.get(field_name) {
4445                                                if !val.is_null() {
4446                                                    // Build tuple wrapper - only clone bindings when needed
4447                                                    let wrap = |v: JValue| -> JValue {
4448                                                        let mut wrapper = IndexMap::new();
4449                                                        wrapper.insert("@".to_string(), v);
4450                                                        wrapper.insert(
4451                                                            "__tuple__".to_string(),
4452                                                            JValue::Bool(true),
4453                                                        );
4454                                                        for (k, v) in obj.iter() {
4455                                                            if k.starts_with('$') {
4456                                                                wrapper
4457                                                                    .insert(k.clone(), v.clone());
4458                                                            }
4459                                                        }
4460                                                        JValue::object(wrapper)
4461                                                    };
4462
4463                                                    match val {
4464                                                        JValue::Array(arr_val) => {
4465                                                            for item in arr_val.iter() {
4466                                                                result.push(wrap(item.clone()));
4467                                                            }
4468                                                        }
4469                                                        other => result.push(wrap(other.clone())),
4470                                                    }
4471                                                }
4472                                            }
4473                                        } else {
4474                                            // Non-tuple: access field directly by reference, only clone the field value
4475                                            if let Some(val) = obj.get(field_name) {
4476                                                if !val.is_null() {
4477                                                    match val {
4478                                                        JValue::Array(arr_val) => {
4479                                                            for item in arr_val.iter() {
4480                                                                result.push(item.clone());
4481                                                            }
4482                                                        }
4483                                                        other => result.push(other.clone()),
4484                                                    }
4485                                                }
4486                                            }
4487                                        }
4488                                    }
4489                                    JValue::Array(inner_arr) => {
4490                                        // Recursively map over nested array
4491                                        let nested_result = self.evaluate_path(
4492                                            &[PathStep::new(AstNode::Name(field_name.clone()))],
4493                                            &JValue::Array(inner_arr.clone()),
4494                                        )?;
4495                                        // Add nested result to our results
4496                                        match nested_result {
4497                                            JValue::Array(nested) => {
4498                                                // Flatten nested arrays from recursive mapping
4499                                                result.extend(nested.iter().cloned());
4500                                            }
4501                                            JValue::Null => {} // Skip nulls from nested arrays
4502                                            other => result.push(other),
4503                                        }
4504                                    }
4505                                    _ => {} // Skip non-object items
4506                                }
4507                            }
4508
4509                            // Return array result
4510                            // JSONata singleton unwrapping: if we have exactly one result,
4511                            // unwrap it (even if it's an array)
4512                            if result.is_empty() {
4513                                Ok(JValue::Null)
4514                            } else if result.len() == 1 {
4515                                Ok(result.into_iter().next().unwrap())
4516                            } else {
4517                                check_sequence_length(result.len(), &self.options)?;
4518                                Ok(JValue::array(result))
4519                            }
4520                        } // end else (tuple path)
4521                    }
4522                    _ => Ok(JValue::Undefined),
4523                };
4524            }
4525        }
4526
4527        // Fast path: 2-step $variable.field with no stages
4528        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4529        if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4530            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4531                (&steps[0].node, &steps[1].node)
4532            {
4533                if !var_name.is_empty() {
4534                    if let Some(value) = self.context.lookup(var_name) {
4535                        match value {
4536                            JValue::Object(obj) => {
4537                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4538                            }
4539                            JValue::Array(arr) => {
4540                                // Map field extraction over array (same as single-step Name on Array)
4541                                let mut result = Vec::with_capacity(arr.len());
4542                                for item in arr.iter() {
4543                                    if let JValue::Object(obj) = item {
4544                                        if let Some(val) = obj.get(field_name) {
4545                                            if !val.is_null() {
4546                                                match val {
4547                                                    JValue::Array(inner) => {
4548                                                        result.extend(inner.iter().cloned());
4549                                                    }
4550                                                    other => result.push(other.clone()),
4551                                                }
4552                                            }
4553                                        }
4554                                    }
4555                                }
4556                                return match result.len() {
4557                                    0 => Ok(JValue::Null),
4558                                    1 => Ok(result.pop().unwrap()),
4559                                    _ => {
4560                                        check_sequence_length(result.len(), &self.options)?;
4561                                        Ok(JValue::array(result))
4562                                    }
4563                                };
4564                            }
4565                            _ => {} // Fall through to general path evaluation
4566                        }
4567                    }
4568                }
4569            }
4570        }
4571
4572        // Track whether we did array mapping (for singleton unwrapping)
4573        let mut did_array_mapping = false;
4574
4575        // For the first step, work with a reference.
4576        // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4577        // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4578        // first path step where tupleBindings is undefined.
4579        let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4580            JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4581        } else {
4582            match &steps[0].node {
4583                AstNode::Wildcard => {
4584                    // Wildcard as first step
4585                    match data {
4586                        JValue::Object(obj) => {
4587                            let mut result = Vec::new();
4588                            for value in obj.values() {
4589                                // Flatten arrays into the result
4590                                match value {
4591                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4592                                    _ => result.push(value.clone()),
4593                                }
4594                            }
4595                            JValue::array(result)
4596                        }
4597                        JValue::Array(arr) => JValue::Array(arr.clone()),
4598                        _ => JValue::Null,
4599                    }
4600                }
4601                AstNode::Descendant => {
4602                    // Descendant as first step
4603                    let descendants = self.collect_descendants(data);
4604                    JValue::array(descendants)
4605                }
4606                AstNode::ParentVariable(name) => {
4607                    // Parent variable as first step
4608                    let parent_data = self.context.get_parent().ok_or_else(|| {
4609                        EvaluatorError::ReferenceError("Parent context not available".to_string())
4610                    })?;
4611
4612                    if name.is_empty() {
4613                        // $$ alone returns parent context
4614                        parent_data.clone()
4615                    } else {
4616                        // $$field accesses field on parent
4617                        match parent_data {
4618                            JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4619                            _ => JValue::Null,
4620                        }
4621                    }
4622                }
4623                AstNode::Name(field_name) => {
4624                    // Field/property access - get the stages for this step
4625                    let stages = &steps[0].stages;
4626
4627                    match data {
4628                        JValue::Object(obj) => {
4629                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4630                            // Apply any stages to the extracted value
4631                            if !stages.is_empty() {
4632                                self.apply_stages(val, stages)?
4633                            } else {
4634                                val
4635                            }
4636                        }
4637                        JValue::Array(arr) => {
4638                            // Array mapping: extract field from each element and apply stages
4639                            let mut result = Vec::new();
4640                            for item in arr.iter() {
4641                                match item {
4642                                    JValue::Object(obj) => {
4643                                        let val = obj
4644                                            .get(field_name)
4645                                            .cloned()
4646                                            .unwrap_or(JValue::Undefined);
4647                                        if !val.is_null() && !val.is_undefined() {
4648                                            if !stages.is_empty() {
4649                                                // Apply stages to the extracted value
4650                                                let processed_val =
4651                                                    self.apply_stages(val, stages)?;
4652                                                // Stages always return an array (or null); extend results
4653                                                match processed_val {
4654                                                    JValue::Array(arr) => {
4655                                                        result.extend(arr.iter().cloned())
4656                                                    }
4657                                                    JValue::Null => {} // Skip nulls from stage application
4658                                                    other => result.push(other), // Shouldn't happen, but handle it
4659                                                }
4660                                            } else {
4661                                                // No stages: flatten arrays, push scalars
4662                                                match val {
4663                                                    JValue::Array(arr) => {
4664                                                        result.extend(arr.iter().cloned())
4665                                                    }
4666                                                    other => result.push(other),
4667                                                }
4668                                            }
4669                                        }
4670                                    }
4671                                    JValue::Array(inner_arr) => {
4672                                        // Recursively map over nested array
4673                                        let nested_result = self.evaluate_path(
4674                                            &[steps[0].clone()],
4675                                            &JValue::Array(inner_arr.clone()),
4676                                        )?;
4677                                        match nested_result {
4678                                            JValue::Array(nested) => {
4679                                                result.extend(nested.iter().cloned())
4680                                            }
4681                                            JValue::Null => {} // Skip nulls from nested arrays
4682                                            other => result.push(other),
4683                                        }
4684                                    }
4685                                    _ => {} // Skip non-object items
4686                                }
4687                            }
4688                            JValue::array(result)
4689                        }
4690                        JValue::Null => JValue::Null,
4691                        // Accessing field on non-object returns undefined (not an error)
4692                        _ => JValue::Undefined,
4693                    }
4694                }
4695                AstNode::String(string_literal) => {
4696                    // String literal in path context - evaluate as literal and apply stages
4697                    // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4698                    let stages = &steps[0].stages;
4699                    let val = JValue::string(string_literal.clone());
4700
4701                    if !stages.is_empty() {
4702                        // Apply stages (predicates) to the string literal
4703                        let result = self.apply_stages(val, stages)?;
4704                        // Unwrap single-element arrays back to scalar
4705                        // (string literals with predicates should return scalar or null, not arrays)
4706                        match result {
4707                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4708                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4709                            other => other,
4710                        }
4711                    } else {
4712                        val
4713                    }
4714                }
4715                AstNode::Predicate(pred_expr) => {
4716                    // Predicate as first step
4717                    self.evaluate_predicate(data, pred_expr)?
4718                }
4719                _ => {
4720                    // Complex first step - evaluate it. When the step is
4721                    // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4722                    // whose `Product` is `%`-tagged, as in
4723                    // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4724                    // inner path's tuple wrappers so the following predicate/step
4725                    // can read the `!label` bindings.
4726                    let saved_keep = self.keep_tuple_stream;
4727                    if steps[0].is_tuple {
4728                        self.keep_tuple_stream = true;
4729                    }
4730                    let v = self.evaluate_path_step(&steps[0].node, data, data);
4731                    self.keep_tuple_stream = saved_keep;
4732                    v?
4733                }
4734            }
4735        };
4736
4737        // Process remaining steps
4738        for (step_idx, step) in steps[1..].iter().enumerate() {
4739            let is_last_step = step_idx == steps.len() - 2;
4740            // Early return if current is null/undefined - no point continuing
4741            // This handles cases like `blah.{}` where blah doesn't exist
4742            if current.is_null() {
4743                return Ok(JValue::Null);
4744            }
4745            if current.is_undefined() {
4746                return Ok(JValue::Undefined);
4747            }
4748
4749            // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4750            // a tuple stream, which selects a single tuple and unwraps it out of
4751            // the array) must stay a tuple stream so the following step keeps
4752            // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4753            // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4754            if let JValue::Object(o) = &current {
4755                if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4756                    current = JValue::array(vec![current.clone()]);
4757                    // The lone wrapper came from a singleton index selection, so
4758                    // the final result should unwrap back to a scalar (a following
4759                    // object step must not leave a spurious 1-element array).
4760                    did_array_mapping = true;
4761                }
4762            }
4763
4764            // Check if current is a tuple array - if so, we need to bind tuple variables
4765            // to context so they're available in nested expressions (like predicates)
4766            let is_tuple_array = if let JValue::Array(arr) = &current {
4767                arr.first().is_some_and(|first| {
4768                    if let JValue::Object(obj) = first {
4769                        obj.get("__tuple__") == Some(&JValue::Bool(true))
4770                    } else {
4771                        false
4772                    }
4773                })
4774            } else {
4775                false
4776            };
4777
4778            // Tuple-binding step (@ focus / # index / % parent): create/extend the
4779            // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4780            // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4781            // wrappers via the existing tuple-aware handling below.
4782            //
4783            // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
4784            // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
4785            // stream, but ONLY when it is consuming an existing tuple stream:
4786            // its ancestor label lives in those incoming tuples, so
4787            // create_tuple_stream's per-tuple frame binding is what lets
4788            // `evaluate_internal(Parent, ..)` resolve it (and any predicate
4789            // stage on the `%` step then resolves in the same frame). A `%`
4790            // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
4791            // predicate, whose input is plain data, not a tuple stream) must
4792            // NOT be routed here -- it's an ordinary scope lookup.
4793            let is_parent_step_over_tuple =
4794                matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
4795            if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
4796                current = JValue::array(self.create_tuple_stream(step, &current, false)?);
4797                continue;
4798            }
4799
4800            // For tuple arrays with certain step types, we need special handling to bind
4801            // tuple variables to context so they're available in nested expressions.
4802            // This is needed for:
4803            // - Object constructors: {"label": $$.items[$i]} needs $i in context
4804            // - Function applications: .($$.items[$i]) needs $i in context
4805            // - Variable lookups: .$i needs to find the tuple binding
4806            //
4807            // Steps like Name (field access) already have proper tuple handling in their
4808            // specific cases, so we don't intercept those here.
4809            let needs_tuple_context_binding = is_tuple_array
4810                && matches!(
4811                    &step.node,
4812                    AstNode::Object(_)
4813                        | AstNode::FunctionApplication(_)
4814                        | AstNode::Variable(_)
4815                        | AstNode::ArrayGroup(_)
4816                );
4817
4818            if needs_tuple_context_binding {
4819                if let JValue::Array(arr) = &current {
4820                    let mut results = Vec::new();
4821
4822                    for tuple in arr.iter() {
4823                        if let JValue::Object(tuple_obj) = tuple {
4824                            // Extract tuple bindings so nested expressions can see
4825                            // them: `$var` focus/index bindings (stored `$name`,
4826                            // bound as `name`) AND `!label` ancestor bindings for
4827                            // `%` (stored and bound under the full `!label` key).
4828                            // Saves/restores rather than blindly unbinding, so a
4829                            // tuple key that collides with a live outer `:=`
4830                            // binding doesn't get deleted afterward.
4831                            let tuple_bindings = self.bind_tuple_keys(tuple_obj);
4832
4833                            // Get the actual value from the tuple (@ field)
4834                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4835
4836                            // Evaluate the step
4837                            let step_result = match &step.node {
4838                                AstNode::Variable(_) => {
4839                                    // Variable lookup - check context (which now has bindings)
4840                                    self.evaluate_internal(&step.node, tuple)?
4841                                }
4842                                AstNode::Object(_) | AstNode::ArrayGroup(_) => {
4843                                    // Object / array constructor step (e.g.
4844                                    // `Product.[`Product Name`, %.OrderID]`) -
4845                                    // evaluate on the tuple's `@` value with the
4846                                    // carried `!label`/`$focus` bindings in scope
4847                                    // so an embedded `%` resolves.
4848                                    self.evaluate_internal(&step.node, &actual_data)?
4849                                }
4850                                AstNode::FunctionApplication(inner) => {
4851                                    // A parenthesized step `(expr)` consuming a tuple stream
4852                                    // (e.g. `Account.Order.Product.( %.OrderID )` or
4853                                    // `Employee@$e.(Contact)[...]`): evaluate the INNER
4854                                    // expression on the tuple's `@` value with `$` bound to
4855                                    // it, mirroring the non-tuple FunctionApplication step
4856                                    // handling. Routing the wrapper node itself through
4857                                    // evaluate_internal raises "Function application can only
4858                                    // be used in path expressions".
4859                                    let saved_dollar = self.context.lookup("$").cloned();
4860                                    self.context.bind("$".to_string(), actual_data.clone());
4861                                    // Keep tuple wrappers from the inner path alive:
4862                                    // when `inner` is itself a tuple-carrying path
4863                                    // (e.g. `(Order.Product)` whose `Product` is
4864                                    // `%`-tagged), its `!label` wrappers must survive
4865                                    // to be merged into this tuple by the rewrap below
4866                                    // (they feed a later `%`/`%.%`). Without this the
4867                                    // inner path projects to `@` and drops the labels.
4868                                    let saved_keep = self.keep_tuple_stream;
4869                                    self.keep_tuple_stream = true;
4870                                    let v = self.evaluate_internal(inner, &actual_data);
4871                                    self.keep_tuple_stream = saved_keep;
4872                                    match saved_dollar {
4873                                        Some(s) => self.context.bind("$".to_string(), s),
4874                                        None => self.context.unbind("$"),
4875                                    }
4876                                    v?
4877                                }
4878                                _ => unreachable!(), // We only match specific types above
4879                            };
4880
4881                            // Apply this step's own filter stages (e.g. the
4882                            // `[$substring(title,0,3)='The']` on `.$[...]` in
4883                            // `library.books#$pos.$[...].$pos`) while the tuple
4884                            // bindings are still in scope, so the predicate can
4885                            // reference them and non-matching tuples are dropped.
4886                            let step_result = if step.stages.is_empty() {
4887                                step_result
4888                            } else {
4889                                self.apply_stages(step_result, &step.stages)?
4890                            };
4891
4892                            // Restore previous bindings
4893                            tuple_bindings.restore(self);
4894
4895                            // Rewrap results as tuples carrying this incoming
4896                            // tuple's focus/index/ancestor bindings, so that
4897                            // DOWNSTREAM steps keep seeing them: a predicate like
4898                            // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
4899                            // later `%`/`%.%` in `Account.Order.(Product).{...}`,
4900                            // or a further path step all read those bindings from
4901                            // the tuple wrapper (see AstNode::Variable's tuple
4902                            // fallback). Without rewrapping, the tuple chain is
4903                            // severed after a parenthesized/object/variable step
4904                            // and those references resolve to nothing. The
4905                            // wrappers are projected back to their `@` values by
4906                            // the top-level `unwrap_tuple_output` pass.
4907                            let carried: Vec<(String, JValue)> = tuple_obj
4908                                .iter()
4909                                .filter(|(k, _)| {
4910                                    (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
4911                                })
4912                                .map(|(k, v)| (k.clone(), v.clone()))
4913                                .collect();
4914                            let wrap = |v: JValue| -> JValue {
4915                                match v {
4916                                    // If the step produced a nested tuple stream
4917                                    // (e.g. `(Product)` whose inner `Product` is
4918                                    // itself `%`-tagged), MERGE the inner tuple's
4919                                    // keys over the carried outer bindings, mirroring
4920                                    // jsonata-js's `res.tupleStream` branch
4921                                    // (`Object.assign(tuple, res[bb])`) -- do NOT
4922                                    // double-wrap, which would bury `@`/`!label`
4923                                    // one level down and break a following `%`/`%.%`.
4924                                    JValue::Object(inner)
4925                                        if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
4926                                    {
4927                                        let mut w = IndexMap::new();
4928                                        for (k, val) in &carried {
4929                                            w.insert(k.clone(), val.clone());
4930                                        }
4931                                        for (k, val) in inner.iter() {
4932                                            w.insert(k.clone(), val.clone());
4933                                        }
4934                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4935                                        JValue::object(w)
4936                                    }
4937                                    other => {
4938                                        let mut w = IndexMap::new();
4939                                        w.insert("@".to_string(), other);
4940                                        for (k, val) in &carried {
4941                                            w.insert(k.clone(), val.clone());
4942                                        }
4943                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4944                                        JValue::object(w)
4945                                    }
4946                                }
4947                            };
4948                            if !step_result.is_null() && !step_result.is_undefined() {
4949                                // Object constructors yield one value per tuple;
4950                                // other steps may yield an array to splice in.
4951                                if matches!(&step.node, AstNode::Object(_)) {
4952                                    results.push(wrap(step_result));
4953                                } else if let JValue::Array(arr) = step_result {
4954                                    for it in arr.iter() {
4955                                        results.push(wrap(it.clone()));
4956                                    }
4957                                } else {
4958                                    results.push(wrap(step_result));
4959                                }
4960                            }
4961                        }
4962                    }
4963
4964                    current = JValue::array(results);
4965                    continue; // Skip the regular step processing
4966                }
4967            }
4968
4969            current = match &step.node {
4970                AstNode::Wildcard => {
4971                    // Wildcard in path
4972                    let stages = &step.stages;
4973                    let wildcard_result = match &current {
4974                        JValue::Object(obj) => {
4975                            let mut result = Vec::new();
4976                            for value in obj.values() {
4977                                // Flatten arrays into the result
4978                                match value {
4979                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4980                                    _ => result.push(value.clone()),
4981                                }
4982                            }
4983                            JValue::array(result)
4984                        }
4985                        JValue::Array(arr) => {
4986                            // Map wildcard over array
4987                            let mut all_values = Vec::new();
4988                            for item in arr.iter() {
4989                                match item {
4990                                    JValue::Object(obj) => {
4991                                        for value in obj.values() {
4992                                            // Flatten arrays
4993                                            match value {
4994                                                JValue::Array(arr) => {
4995                                                    all_values.extend(arr.iter().cloned())
4996                                                }
4997                                                _ => all_values.push(value.clone()),
4998                                            }
4999                                        }
5000                                    }
5001                                    JValue::Array(inner) => {
5002                                        all_values.extend(inner.iter().cloned());
5003                                    }
5004                                    _ => {}
5005                                }
5006                            }
5007                            JValue::array(all_values)
5008                        }
5009                        _ => JValue::Null,
5010                    };
5011
5012                    // Apply stages (predicates) if present
5013                    if !stages.is_empty() {
5014                        self.apply_stages(wildcard_result, stages)?
5015                    } else {
5016                        wildcard_result
5017                    }
5018                }
5019                AstNode::Descendant => {
5020                    // Descendant in path
5021                    match &current {
5022                        JValue::Array(arr) => {
5023                            // Collect descendants from all array elements
5024                            let mut all_descendants = Vec::new();
5025                            for item in arr.iter() {
5026                                all_descendants.extend(self.collect_descendants(item));
5027                            }
5028                            JValue::array(all_descendants)
5029                        }
5030                        _ => {
5031                            // Collect descendants from current value
5032                            let descendants = self.collect_descendants(&current);
5033                            JValue::array(descendants)
5034                        }
5035                    }
5036                }
5037                AstNode::Name(field_name) => {
5038                    // Navigate into object field or map over array, applying stages
5039                    let stages = &step.stages;
5040
5041                    match &current {
5042                        JValue::Object(obj) => {
5043                            // Single object field extraction - NOT array mapping
5044                            // This resets did_array_mapping because we're extracting from
5045                            // a single value, not mapping over an array. The field's value
5046                            // (even if it's an array) should be preserved as-is.
5047                            did_array_mapping = false;
5048                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
5049                            // Apply stages if present
5050                            if !stages.is_empty() {
5051                                self.apply_stages(val, stages)?
5052                            } else {
5053                                val
5054                            }
5055                        }
5056                        JValue::Array(arr) => {
5057                            // Array mapping: extract field from each element and apply stages
5058                            did_array_mapping = true; // Track that we did array mapping
5059
5060                            // Fast path: if no elements are tuples and no stages,
5061                            // skip all tuple checking overhead (common case for products.price etc.)
5062                            // Tuples are all-or-nothing (created by index binding #$i),
5063                            // so checking only the first element is sufficient.
5064                            let has_tuples = arr.first().is_some_and(|item| {
5065                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
5066                            });
5067
5068                            if !has_tuples && stages.is_empty() {
5069                                let mut result = Vec::with_capacity(arr.len());
5070                                for item in arr.iter() {
5071                                    match item {
5072                                        JValue::Object(obj) => {
5073                                            if let Some(val) = obj.get(field_name) {
5074                                                if !val.is_null() {
5075                                                    match val {
5076                                                        JValue::Array(arr_val) => {
5077                                                            result.extend(arr_val.iter().cloned())
5078                                                        }
5079                                                        other => result.push(other.clone()),
5080                                                    }
5081                                                }
5082                                            }
5083                                        }
5084                                        JValue::Array(_) => {
5085                                            let nested_result =
5086                                                self.evaluate_path(&[step.clone()], item)?;
5087                                            match nested_result {
5088                                                JValue::Array(nested) => {
5089                                                    result.extend(nested.iter().cloned())
5090                                                }
5091                                                JValue::Null => {}
5092                                                other => result.push(other),
5093                                            }
5094                                        }
5095                                        _ => {}
5096                                    }
5097                                }
5098                                JValue::array(result)
5099                            } else {
5100                                // Full path with tuple support and stages
5101                                let mut result = Vec::new();
5102
5103                                for item in arr.iter() {
5104                                    match item {
5105                                        JValue::Object(obj) => {
5106                                            // Check if this is a tuple stream element
5107                                            let (actual_obj, tuple_bindings) = if obj
5108                                                .get("__tuple__")
5109                                                == Some(&JValue::Bool(true))
5110                                            {
5111                                                // This is a tuple - extract '@' value and preserve bindings
5112                                                if let Some(JValue::Object(inner)) = obj.get("@") {
5113                                                    // Collect index bindings (variables starting with $)
5114                                                    let bindings: Vec<(String, JValue)> = obj
5115                                                        .iter()
5116                                                        .filter(|(k, _)| k.starts_with('$'))
5117                                                        .map(|(k, v)| (k.clone(), v.clone()))
5118                                                        .collect();
5119                                                    (inner.clone(), Some(bindings))
5120                                                } else {
5121                                                    continue; // Invalid tuple
5122                                                }
5123                                            } else {
5124                                                (obj.clone(), None)
5125                                            };
5126
5127                                            let val = actual_obj
5128                                                .get(field_name)
5129                                                .cloned()
5130                                                .unwrap_or(JValue::Null);
5131
5132                                            if !val.is_null() {
5133                                                // Helper to wrap value in tuple if we have bindings
5134                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
5135                                                    if let Some(b) = bindings {
5136                                                        let mut wrapper = IndexMap::new();
5137                                                        wrapper.insert("@".to_string(), v);
5138                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5139                                                        for (k, val) in b {
5140                                                            wrapper.insert(k.clone(), val.clone());
5141                                                        }
5142                                                        JValue::object(wrapper)
5143                                                    } else {
5144                                                        v
5145                                                    }
5146                                                };
5147
5148                                                if !stages.is_empty() {
5149                                                    // Bind this tuple's carried focus/index/ancestor
5150                                                    // bindings so a filter predicate that references
5151                                                    // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
5152                                                    // where the `[$l.isbn=isbn]` stage on the (non-focus)
5153                                                    // `books` step must see `$l` from the enclosing
5154                                                    // `@$l` focus stream. Without this the predicate
5155                                                    // evaluates `$l` as unbound and filters everything out.
5156                                                    let saved_tuple: Vec<(String, Option<JValue>)> =
5157                                                        obj.iter()
5158                                                            .filter_map(|(k, _)| {
5159                                                                if let Some(n) = k.strip_prefix('$')
5160                                                                {
5161                                                                    (!n.is_empty())
5162                                                                        .then(|| n.to_string())
5163                                                                } else if k.starts_with('!') {
5164                                                                    Some(k.clone())
5165                                                                } else {
5166                                                                    None
5167                                                                }
5168                                                            })
5169                                                            .map(|n| {
5170                                                                (
5171                                                                    n.clone(),
5172                                                                    self.context
5173                                                                        .lookup(&n)
5174                                                                        .cloned(),
5175                                                                )
5176                                                            })
5177                                                            .collect();
5178                                                    for (k, v) in obj.iter() {
5179                                                        if let Some(n) = k.strip_prefix('$') {
5180                                                            if !n.is_empty() {
5181                                                                self.context
5182                                                                    .bind(n.to_string(), v.clone());
5183                                                            }
5184                                                        } else if k.starts_with('!') {
5185                                                            self.context.bind(k.clone(), v.clone());
5186                                                        }
5187                                                    }
5188                                                    // Apply stages to the extracted value
5189                                                    let processed_val =
5190                                                        self.apply_stages(val, stages);
5191                                                    for (n, old) in saved_tuple.into_iter().rev() {
5192                                                        match old {
5193                                                            Some(v) => self.context.bind(n, v),
5194                                                            None => self.context.unbind(&n),
5195                                                        }
5196                                                    }
5197                                                    let processed_val = processed_val?;
5198                                                    // Stages always return an array (or null); extend results
5199                                                    match processed_val {
5200                                                        JValue::Array(arr) => {
5201                                                            for item in arr.iter() {
5202                                                                result.push(wrap_in_tuple(
5203                                                                    item.clone(),
5204                                                                    &tuple_bindings,
5205                                                                ));
5206                                                            }
5207                                                        }
5208                                                        JValue::Null => {} // Skip nulls from stage application
5209                                                        other => result.push(wrap_in_tuple(
5210                                                            other,
5211                                                            &tuple_bindings,
5212                                                        )),
5213                                                    }
5214                                                } else {
5215                                                    // No stages: flatten arrays, push scalars
5216                                                    // But preserve tuple bindings!
5217                                                    match val {
5218                                                        JValue::Array(arr) => {
5219                                                            for item in arr.iter() {
5220                                                                result.push(wrap_in_tuple(
5221                                                                    item.clone(),
5222                                                                    &tuple_bindings,
5223                                                                ));
5224                                                            }
5225                                                        }
5226                                                        other => result.push(wrap_in_tuple(
5227                                                            other,
5228                                                            &tuple_bindings,
5229                                                        )),
5230                                                    }
5231                                                }
5232                                            }
5233                                        }
5234                                        JValue::Array(_) => {
5235                                            // Recursively map over nested array
5236                                            let nested_result =
5237                                                self.evaluate_path(&[step.clone()], item)?;
5238                                            match nested_result {
5239                                                JValue::Array(nested) => {
5240                                                    result.extend(nested.iter().cloned())
5241                                                }
5242                                                JValue::Null => {}
5243                                                other => result.push(other),
5244                                            }
5245                                        }
5246                                        _ => {}
5247                                    }
5248                                }
5249
5250                                JValue::array(result)
5251                            }
5252                        }
5253                        JValue::Null => JValue::Null,
5254                        // Accessing field on non-object returns undefined (not an error)
5255                        _ => JValue::Undefined,
5256                    }
5257                }
5258                AstNode::String(string_literal) => {
5259                    // String literal as a path step - evaluate as literal and apply stages
5260                    let stages = &step.stages;
5261                    let val = JValue::string(string_literal.clone());
5262
5263                    if !stages.is_empty() {
5264                        // Apply stages (predicates) to the string literal
5265                        let result = self.apply_stages(val, stages)?;
5266                        // Unwrap single-element arrays back to scalar
5267                        match result {
5268                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
5269                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
5270                            other => other,
5271                        }
5272                    } else {
5273                        val
5274                    }
5275                }
5276                AstNode::Predicate(pred_expr) => {
5277                    // Predicate in path - filter or index into current value
5278                    self.evaluate_predicate(&current, pred_expr)?
5279                }
5280                AstNode::ArrayGroup(elements) => {
5281                    // Array grouping: map expression over array but keep results grouped
5282                    // .[expr] means evaluate expr for each array element
5283                    match &current {
5284                        JValue::Array(arr) => {
5285                            let mut result = Vec::new();
5286                            for item in arr.iter() {
5287                                // For each array item, evaluate all elements and collect results
5288                                let mut group_values = Vec::new();
5289                                for element in elements {
5290                                    let value = self.evaluate_internal(element, item)?;
5291                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
5292                                    // This ensures [[expr]] produces properly nested arrays
5293                                    let should_preserve_array = matches!(
5294                                        element,
5295                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
5296                                    );
5297
5298                                    if should_preserve_array {
5299                                        // Keep the array as a single element to preserve nesting
5300                                        group_values.push(value);
5301                                    } else {
5302                                        // Flatten the value into group_values
5303                                        match value {
5304                                            JValue::Array(arr) => {
5305                                                group_values.extend(arr.iter().cloned())
5306                                            }
5307                                            other => group_values.push(other),
5308                                        }
5309                                    }
5310                                }
5311                                // Each array element gets its own sub-array with all values
5312                                result.push(JValue::array(group_values));
5313                            }
5314                            // jsonata-js's evaluateStep: when this is the path's last
5315                            // step and mapping produced exactly one constructed
5316                            // sub-array, that sub-array IS the path result directly
5317                            // (not wrapped in an outer singleton array) — e.g.
5318                            // `$.[value,epochSeconds]` over a 1-element array yields
5319                            // `[3, 1578381600]`, not `[[3, 1578381600]]`.
5320                            if is_last_step && result.len() == 1 {
5321                                result.into_iter().next().unwrap()
5322                            } else {
5323                                JValue::array(result)
5324                            }
5325                        }
5326                        _ => {
5327                            // For non-arrays, just evaluate the array constructor normally
5328                            let mut result = Vec::new();
5329                            for element in elements {
5330                                let value = self.evaluate_internal(element, &current)?;
5331                                result.push(value);
5332                            }
5333                            JValue::array(result)
5334                        }
5335                    }
5336                }
5337                AstNode::FunctionApplication(expr) => {
5338                    // Function application: map expr over the current value
5339                    // .(expr) means evaluate expr for each element, with $ bound to that element
5340                    // Null/undefined results are filtered out
5341                    //
5342                    // When this parenthesized step is itself tuple-carrying (its
5343                    // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
5344                    // keep the inner path's tuple wrappers so their `!label`
5345                    // bindings survive to the following object/`%` step; the
5346                    // end-of-path projection (or a later consumer) unwraps them.
5347                    let saved_keep = self.keep_tuple_stream;
5348                    if step.is_tuple {
5349                        self.keep_tuple_stream = true;
5350                    }
5351                    let fa_result = match &current {
5352                        JValue::Array(arr) => {
5353                            // Produce the mapped result (compiled fast path or tree-walker fallback).
5354                            // Do NOT return early — singleton unwrapping is applied by the outer
5355                            // path evaluation code after all steps are processed.
5356                            let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
5357                            {
5358                                let shape = arr.first().and_then(build_shape_cache);
5359                                let mut result = Vec::with_capacity(arr.len());
5360                                for item in arr.iter() {
5361                                    let value = if let Some(ref s) = shape {
5362                                        eval_compiled_shaped(
5363                                            &compiled,
5364                                            item,
5365                                            None,
5366                                            s,
5367                                            &self.options,
5368                                            self.start_time,
5369                                        )?
5370                                    } else {
5371                                        eval_compiled(
5372                                            &compiled,
5373                                            item,
5374                                            None,
5375                                            &self.options,
5376                                            self.start_time,
5377                                        )?
5378                                    };
5379                                    if !value.is_null() && !value.is_undefined() {
5380                                        result.push(value);
5381                                    }
5382                                }
5383                                result
5384                            } else {
5385                                let mut result = Vec::new();
5386                                for item in arr.iter() {
5387                                    // Save the current $ binding
5388                                    let saved_dollar = self.context.lookup("$").cloned();
5389
5390                                    // Bind $ to the current item
5391                                    self.context.bind("$".to_string(), item.clone());
5392
5393                                    // Evaluate the expression in the context of this item
5394                                    let value = self.evaluate_internal(expr, item)?;
5395
5396                                    // Restore the previous $ binding
5397                                    if let Some(saved) = saved_dollar {
5398                                        self.context.bind("$".to_string(), saved);
5399                                    } else {
5400                                        self.context.unbind("$");
5401                                    }
5402
5403                                    // Only include non-null/undefined values
5404                                    if !value.is_null() && !value.is_undefined() {
5405                                        result.push(value);
5406                                    }
5407                                }
5408                                result
5409                            };
5410                            // Don't do singleton unwrapping here - let the path result
5411                            // handling deal with it, which respects has_explicit_array_keep
5412                            JValue::array(mapped)
5413                        }
5414                        _ => {
5415                            // For non-arrays, bind $ and evaluate
5416                            let saved_dollar = self.context.lookup("$").cloned();
5417                            self.context.bind("$".to_string(), current.clone());
5418
5419                            let value = self.evaluate_internal(expr, &current)?;
5420
5421                            if let Some(saved) = saved_dollar {
5422                                self.context.bind("$".to_string(), saved);
5423                            } else {
5424                                self.context.unbind("$");
5425                            }
5426
5427                            value
5428                        }
5429                    };
5430                    self.keep_tuple_stream = saved_keep;
5431                    fa_result
5432                }
5433                AstNode::Sort { terms, .. } => {
5434                    // Sort as a path step - sort 'current' by the terms
5435                    self.evaluate_sort(&current, terms)?
5436                }
5437                // Handle complex path steps (e.g., computed properties, object construction)
5438                _ => {
5439                    let saved_keep = self.keep_tuple_stream;
5440                    if step.is_tuple {
5441                        self.keep_tuple_stream = true;
5442                    }
5443                    let v = self.evaluate_path_step(&step.node, &current, data);
5444                    self.keep_tuple_stream = saved_keep;
5445                    v?
5446                }
5447            };
5448        }
5449
5450        // End-of-path tuple projection, mirroring jsonata-js evaluatePath
5451        // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
5452        // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
5453        // wrappers are internal bookkeeping and must not escape into an enclosing
5454        // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
5455        // `=` compare wrapper objects and always yield false). Suppressed only for
5456        // the two consumers that read the carried bindings directly off the
5457        // wrappers (Sort input, ObjectTransform/group-by input), which set
5458        // `keep_tuple_stream`. The top-level `evaluate()` still runs
5459        // `unwrap_tuple_output` as a backstop for wrappers nested inside
5460        // constructed output.
5461        if !self.keep_tuple_stream {
5462            if let JValue::Array(arr) = &current {
5463                let is_tuple_stream = arr.first().is_some_and(|f| {
5464                    matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5465                });
5466                if is_tuple_stream {
5467                    let projected: Vec<JValue> = arr
5468                        .iter()
5469                        .map(|t| match t {
5470                            JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
5471                            other => other.clone(),
5472                        })
5473                        .collect();
5474                    current = JValue::array(projected);
5475                }
5476            }
5477        }
5478
5479        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5480        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5481
5482        // Check for explicit array-keeping operations. Empty predicate `[]` can
5483        // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5484        // stage; it also counts when it sits inside a `Sort` step's input path
5485        // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5486        // the sort and the trailing index so the singleton stays `[4]`.
5487        let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5488
5489        // Unwrap when:
5490        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5491        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5492        //    Note: did_array_mapping is reset to false when extracting from a single object,
5493        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5494        // BUT NOT when there's an explicit array-keeping operation
5495        //
5496        // Important: We DON'T unwrap just because original data was an array - what matters is
5497        // whether the final extraction was from an array mapping context or a single object.
5498        let should_unwrap = !has_explicit_array_keep
5499            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5500
5501        let result = match &current {
5502            // An empty result sequence is "no value" -> undefined (jsonata-js
5503            // treats an empty sequence, e.g. from a filter that matched nothing,
5504            // as undefined so a following `.field` and object/array construction
5505            // drop it rather than keeping an explicit null). `[]` array-keep is
5506            // handled separately above via has_explicit_array_keep.
5507            JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5508            // Unwrap singleton arrays when appropriate
5509            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5510            // Keep arrays otherwise
5511            _ => current,
5512        };
5513
5514        // An explicit `[]` keep-array forces the result to remain an array even
5515        // after a later singleton index collapses it to a scalar (jsonata's
5516        // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5517        let result = if has_explicit_array_keep
5518            && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5519        {
5520            JValue::array(vec![result])
5521        } else {
5522            result
5523        };
5524
5525        if let JValue::Array(arr) = &result {
5526            check_sequence_length(arr.len(), &self.options)?;
5527        }
5528
5529        Ok(result)
5530    }
5531
5532    /// True when a path step carries a tuple-binding flag (`@$var` focus,
5533    /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5534    /// produce/extend a tuple stream rather than be evaluated as a plain step.
5535    ///
5536    fn step_creates_tuple(step: &PathStep) -> bool {
5537        step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5538    }
5539
5540    /// True when a path contains an explicit empty predicate `[]` (keep-array),
5541    /// either directly as a step/stage or nested inside a `Sort` step's input
5542    /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5543    /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5544    /// -> `[4]`).
5545    fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5546        steps.iter().any(|step| {
5547            if let AstNode::Predicate(pred) = &step.node {
5548                if matches!(**pred, AstNode::Boolean(true)) {
5549                    return true;
5550                }
5551            }
5552            if step.stages.iter().any(
5553                |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5554            ) {
5555                return true;
5556            }
5557            if let AstNode::Sort { input, .. } = &step.node {
5558                if let AstNode::Path { steps: inner } = input.as_ref() {
5559                    return Self::path_keeps_singleton_array(inner);
5560                }
5561            }
5562            false
5563        })
5564    }
5565
5566    /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5567    /// scope, saving whatever was previously bound under each of those names
5568    /// so [`TupleKeyBindings::restore`] can put it back afterward.
5569    ///
5570    /// This is the single shared implementation of the
5571    /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5572    /// pattern that recurs across `create_tuple_stream`,
5573    /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5574    /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5575    /// none of those call sites can regress to a blind `unbind` (which
5576    /// deletes rather than restores a same-named outer `:=` binding that was
5577    /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5578    /// binding silently clobbering an outer variable of the same name).
5579    fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5580        let mut saved = Vec::new();
5581        for (key, value) in tuple_obj.iter() {
5582            let name = if let Some(n) = key.strip_prefix('$') {
5583                if n.is_empty() {
5584                    continue;
5585                }
5586                n.to_string()
5587            } else if key.starts_with('!') {
5588                key.clone()
5589            } else {
5590                continue;
5591            };
5592            saved.push((name.clone(), self.context.lookup(&name).cloned()));
5593            self.context.bind(name, value.clone());
5594        }
5595        TupleKeyBindings { saved }
5596    }
5597
5598    /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5599    /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5600    /// vector holds `JValue::Object` tuple wrappers of the shape
5601    /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5602    /// which downstream steps consume via the existing tuple-aware handling in
5603    /// `evaluate_path`.
5604    ///
5605    /// `input` is the previous step's result: either an already-built tuple
5606    /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5607    /// plain value/array entering tuple mode for the first time (each item
5608    /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5609    ///
5610    /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5611    /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5612    /// fast paths only *rebuild* a wrapper around a value pulled from an input
5613    /// element that is already `__tuple__`-tagged, which can only be true if a
5614    /// `create_tuple_stream` call already ran earlier in this evaluation and set
5615    /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5616    /// fire on a value that did NOT come from an existing tuple stream, it must
5617    /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5618    /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5619    fn create_tuple_stream(
5620        &mut self,
5621        step: &PathStep,
5622        input: &JValue,
5623        is_first_path_step: bool,
5624    ) -> Result<Vec<JValue>, EvaluatorError> {
5625        use std::rc::Rc;
5626
5627        // Mark that this evaluate() call produced tuple wrappers, so the
5628        // top-level `evaluate()` knows to run the output-unwrap pass.
5629        self.tuple_stream_created = true;
5630
5631        // Gather the incoming tuple bindings.
5632        let is_tuple_input = matches!(
5633            input,
5634            JValue::Array(arr) if arr.first().is_some_and(|f| {
5635                matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5636            })
5637        );
5638        let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5639            match input {
5640                JValue::Array(arr) => arr
5641                    .iter()
5642                    .filter_map(|t| match t {
5643                        JValue::Object(o) => Some(o.clone()),
5644                        _ => None,
5645                    })
5646                    .collect(),
5647                _ => unreachable!(),
5648            }
5649        } else {
5650            let items: Vec<JValue> = match input {
5651                // Mirrors jsonata-js evaluatePath's inputSequence rule
5652                // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5653                // when the path's FIRST step is a variable reference (`$`/`$$`) the
5654                // input array is taken as a SINGLE sequence value
5655                // (`createSequence(input)`) rather than iterated per-element. We
5656                // only need this for a leading INDEX bind (`$#$pos`): the whole
5657                // array becomes one incoming tuple whose `@` is the array, then
5658                // the inner position counter walks its elements so `$pos` runs
5659                // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5660                // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5661                // the step input, so a single binding would yield one copy of the
5662                // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5663                // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5664                // `$.$#$pos` (a later step) still iterates per-element.
5665                JValue::Array(arr)
5666                    if !(is_first_path_step
5667                        && matches!(&step.node, AstNode::Variable(_))
5668                        && step.index_var.is_some()) =>
5669                {
5670                    arr.iter().cloned().collect()
5671                }
5672                single => vec![single.clone()],
5673            };
5674            items
5675                .into_iter()
5676                .map(|item| {
5677                    let mut wrapper = IndexMap::new();
5678                    wrapper.insert("@".to_string(), item);
5679                    wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5680                    Rc::new(wrapper)
5681                })
5682                .collect()
5683        };
5684
5685        // A sort step in a tuple stream orders the WHOLE stream (not per element)
5686        // and re-tuples with the index = sorted position, mirroring jsonata-js
5687        // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5688        // array, then number the sorted values, then filter by `$pos`.
5689        if let AstNode::Sort { terms, .. } = &step.node {
5690            let stream = JValue::array(
5691                incoming
5692                    .iter()
5693                    .map(|t| JValue::object((**t).clone()))
5694                    .collect(),
5695            );
5696            // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5697            // carried keys bound), returning the wrappers in sorted order.
5698            let sorted = self.evaluate_sort(&stream, terms)?;
5699            let sorted_arr: Vec<JValue> = match sorted {
5700                JValue::Array(a) => a.iter().cloned().collect(),
5701                JValue::Null | JValue::Undefined => Vec::new(),
5702                other => vec![other],
5703            };
5704            let mut result = Vec::new();
5705            for (ss, elem) in sorted_arr.into_iter().enumerate() {
5706                let mut new_tuple = match elem {
5707                    JValue::Object(o) => (*o).clone(),
5708                    other => {
5709                        let mut m = IndexMap::new();
5710                        m.insert("@".to_string(), other);
5711                        m
5712                    }
5713                };
5714                if let Some(index_var) = &step.index_var {
5715                    new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
5716                }
5717                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5718                result.push(JValue::object(new_tuple));
5719            }
5720            return Ok(result);
5721        }
5722
5723        let mut result = Vec::new();
5724        for tuple_obj in incoming {
5725            // Bind every carried tuple key into a real scope frame so the step
5726            // expression can see prior focus/index/ancestor bindings, mirroring
5727            // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
5728            // Saves/restores rather than blindly unbinding, so a tuple key
5729            // whose name collides with a live outer `:=` binding doesn't get
5730            // deleted once this tuple row's evaluation is done.
5731            let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
5732
5733            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
5734            let step_value = self.evaluate_internal(&step.node, &actual_data);
5735
5736            let mut step_value = step_value?;
5737            // When the step carries an ORDERED index stage (a second `#$var`,
5738            // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
5739            // BUILT tuple stream in order (filter then re-number) so the filter
5740            // sees the per-tuple focus/index bindings and each index reflects the
5741            // position at its point in the sequence. Those steps defer all stage
5742            // application to `apply_tuple_stages` after the stream is built.
5743            let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
5744            if !step.stages.is_empty() && !has_index_stage {
5745                // A `%` inside a filter predicate refers to the ancestry of
5746                // THIS step (its own input for a level-1 `%`, or an earlier
5747                // step's input for a `%.%` chain). ast_transform tags this step
5748                // with `ancestor_label`; bind it to the step's input so the
5749                // level-1 `%` resolves. The `%.%` chain's deeper references use
5750                // labels carried in the INCOMING tuple, so those bindings
5751                // (`tuple_bindings`) must stay live through `apply_stages` --
5752                // their restore is deferred until after it (previously they
5753                // were unbound first, which silently broke `%.%` inside
5754                // predicates).
5755                let own_label = match &step.ancestor_label {
5756                    Some(label) if !tuple_bindings.contains(label) => {
5757                        self.context.bind(label.clone(), actual_data.clone());
5758                        Some(label.clone())
5759                    }
5760                    _ => None,
5761                };
5762                step_value = self.apply_stages(step_value, &step.stages)?;
5763                if let Some(label) = own_label {
5764                    self.context.unbind(&label);
5765                }
5766            }
5767
5768            tuple_bindings.restore(self);
5769
5770            let row: Vec<JValue> = match step_value {
5771                JValue::Undefined => continue,
5772                JValue::Array(arr) => arr.iter().cloned().collect(),
5773                other => vec![other],
5774            };
5775
5776            for (position, value) in row.into_iter().enumerate() {
5777                if value.is_undefined() {
5778                    continue;
5779                }
5780                let mut new_tuple = (*tuple_obj).clone();
5781                if let Some(focus_var) = &step.focus {
5782                    // Focus binding keeps `@` as this step's INPUT (already carried
5783                    // in the cloned tuple) and binds the result to `$focus`,
5784                    // matching jsonata-js: `tuple[expr.focus] = res[bb];
5785                    // tuple['@'] = tupleBindings[ee]['@'];`.
5786                    new_tuple.insert(format!("${}", focus_var), value);
5787                } else {
5788                    new_tuple.insert("@".to_string(), value);
5789                }
5790                if let Some(index_var) = &step.index_var {
5791                    // Index binding records the position of this value WITHIN the
5792                    // per-binding result row (jsonata-js evaluateTupleStep: the
5793                    // inner `bb` counter, `tuple[expr.index] = bb`), which resets
5794                    // for each incoming tuple.
5795                    new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
5796                }
5797                if let Some(ancestor_label) = &step.ancestor_label {
5798                    // `%` ancestor: preserve this step's INPUT under the label.
5799                    new_tuple.insert(ancestor_label.clone(), actual_data.clone());
5800                }
5801                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5802                result.push(JValue::object(new_tuple));
5803            }
5804        }
5805
5806        // Apply ordered filter/index stages to the built tuple stream when a
5807        // second index binding deferred them (see the has_index_stage comment
5808        // in the build loop above).
5809        if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
5810            result = self.apply_tuple_stages(result, &step.stages)?;
5811        }
5812
5813        Ok(result)
5814    }
5815
5816    /// Apply a step's stages, in order, to an already-built tuple stream --
5817    /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
5818    /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
5819    /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
5820    /// stage sets its variable on every surviving tuple to that tuple's position
5821    /// in the CURRENT stream. Used for steps carrying a second `#$var` index
5822    /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
5823    /// pre-filter position and `$ib2` the post-filter position.
5824    fn apply_tuple_stages(
5825        &mut self,
5826        mut tuples: Vec<JValue>,
5827        stages: &[Stage],
5828    ) -> Result<Vec<JValue>, EvaluatorError> {
5829        for stage in stages {
5830            match stage {
5831                Stage::Filter(pred) => {
5832                    let mut kept = Vec::with_capacity(tuples.len());
5833                    for tup in tuples.into_iter() {
5834                        let JValue::Object(obj) = &tup else {
5835                            continue;
5836                        };
5837                        // Bind this tuple's carried focus/index/ancestor keys so
5838                        // the predicate can reference them (save/restore rather
5839                        // than blind unbind -- see bind_tuple_keys).
5840                        let tuple_bindings = self.bind_tuple_keys(obj);
5841                        let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
5842                        let pred_res = self.evaluate_internal(pred, &at);
5843                        tuple_bindings.restore(self);
5844                        if self.is_truthy(&pred_res?) {
5845                            kept.push(tup);
5846                        }
5847                    }
5848                    tuples = kept;
5849                }
5850                Stage::Index(var) => {
5851                    for (pos, tup) in tuples.iter_mut().enumerate() {
5852                        if let JValue::Object(obj) = tup {
5853                            let mut m = (**obj).clone();
5854                            m.insert(format!("${}", var), JValue::from(pos as i64));
5855                            *tup = JValue::object(m);
5856                        }
5857                    }
5858                }
5859            }
5860        }
5861        Ok(tuples)
5862    }
5863
5864    /// Helper to evaluate a complex path step
5865    fn evaluate_path_step(
5866        &mut self,
5867        step: &AstNode,
5868        current: &JValue,
5869        original_data: &JValue,
5870    ) -> Result<JValue, EvaluatorError> {
5871        // Special case: array mapping with object construction
5872        // e.g., items.{"name": name, "price": price}
5873        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
5874            match (current, step) {
5875                (JValue::Array(arr), AstNode::Object(pairs)) => {
5876                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
5877                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
5878                        let shape = arr.first().and_then(build_shape_cache);
5879                        let mut mapped = Vec::with_capacity(arr.len());
5880                        for item in arr.iter() {
5881                            let result = if let Some(ref s) = shape {
5882                                eval_compiled_shaped(
5883                                    &compiled,
5884                                    item,
5885                                    None,
5886                                    s,
5887                                    &self.options,
5888                                    self.start_time,
5889                                )?
5890                            } else {
5891                                eval_compiled(
5892                                    &compiled,
5893                                    item,
5894                                    None,
5895                                    &self.options,
5896                                    self.start_time,
5897                                )?
5898                            };
5899                            if !result.is_undefined() {
5900                                mapped.push(result);
5901                            }
5902                        }
5903                        return Ok(JValue::array(mapped));
5904                    }
5905                    // Fallback: full AST evaluation per element
5906                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
5907                        .iter()
5908                        .map(|item| self.evaluate_internal(step, item))
5909                        .collect();
5910                    Ok(JValue::array(mapped?))
5911                }
5912                _ => unreachable!(),
5913            }
5914        } else {
5915            // Special case: array.$ should map $ over the array, returning each element
5916            // e.g., [1, 2, 3].$ returns [1, 2, 3]
5917            if let AstNode::Variable(name) = step {
5918                if name.is_empty() {
5919                    // Bare $ - map over array if current is an array
5920                    if let JValue::Array(arr) = current {
5921                        // Map $ over each element - $ refers to each element in turn
5922                        return Ok(JValue::Array(arr.clone()));
5923                    } else {
5924                        // For non-arrays, $ refers to the current value
5925                        return Ok(current.clone());
5926                    }
5927                }
5928            }
5929
5930            // Special case: Variable access on tuple arrays (from index binding #$var)
5931            // When current is a tuple array, we need to evaluate the variable against each tuple
5932            // so that tuple bindings ($i, etc.) can be found
5933            if matches!(step, AstNode::Variable(_)) {
5934                if let JValue::Array(arr) = current {
5935                    // Check if this is a tuple array
5936                    let is_tuple_array = arr.first().is_some_and(|first| {
5937                        if let JValue::Object(obj) = first {
5938                            obj.get("__tuple__") == Some(&JValue::Bool(true))
5939                        } else {
5940                            false
5941                        }
5942                    });
5943
5944                    if is_tuple_array {
5945                        // Map the variable lookup over each tuple
5946                        let mut results = Vec::new();
5947                        for tuple in arr.iter() {
5948                            // Evaluate the variable in the context of this tuple
5949                            // This allows tuple bindings ($i, etc.) to be found
5950                            let val = self.evaluate_internal(step, tuple)?;
5951                            if !val.is_null() && !val.is_undefined() {
5952                                results.push(val);
5953                            }
5954                        }
5955                        return Ok(JValue::array(results));
5956                    }
5957                }
5958            }
5959
5960            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
5961            // rather than being used to index/access the current value
5962            // e.g., items[price > 50] where [price > 50] is a filter operation
5963            // or $x.price where $x is a variable binding
5964            // or $$.field where $$ is the parent context
5965            // or [0..9] where it's an array constructor
5966            // or $^(field) where it's a sort operator
5967            // or (expr).field where (expr) is a block that evaluates to a value
5968            if matches!(
5969                step,
5970                AstNode::Binary { .. }
5971                    | AstNode::Function { .. }
5972                    | AstNode::Variable(_)
5973                    | AstNode::ParentVariable(_)
5974                    | AstNode::Parent(_)
5975                    | AstNode::Array(_)
5976                    | AstNode::Object(_)
5977                    | AstNode::Sort { .. }
5978                    | AstNode::Block(_)
5979            ) {
5980                // Evaluate the step in the context of original_data and return the result directly
5981                return self.evaluate_internal(step, original_data);
5982            }
5983
5984            // Standard path step evaluation for indexing/accessing current value
5985            let step_value = self.evaluate_internal(step, original_data)?;
5986            Ok(match (current, &step_value) {
5987                (JValue::Object(obj), JValue::String(key)) => {
5988                    obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
5989                }
5990                (JValue::Array(arr), JValue::Number(n)) => {
5991                    let index = *n as i64;
5992                    let len = arr.len() as i64;
5993
5994                    // Handle negative indexing (offset from end)
5995                    let actual_idx = if index < 0 { len + index } else { index };
5996
5997                    if actual_idx < 0 || actual_idx >= len {
5998                        JValue::Undefined
5999                    } else {
6000                        arr[actual_idx as usize].clone()
6001                    }
6002                }
6003                _ => JValue::Undefined,
6004            })
6005        }
6006    }
6007
6008    /// Evaluate a binary operation
6009    fn evaluate_binary_op(
6010        &mut self,
6011        op: crate::ast::BinaryOp,
6012        lhs: &AstNode,
6013        rhs: &AstNode,
6014        data: &JValue,
6015    ) -> Result<JValue, EvaluatorError> {
6016        use crate::ast::BinaryOp;
6017
6018        // Special handling for coalescing operator (??)
6019        // Returns right side if left is undefined (produces no value)
6020        // Note: literal null is a value, so it's NOT replaced
6021        if op == BinaryOp::Coalesce {
6022            // Try to evaluate the left side
6023            return match self.evaluate_internal(lhs, data) {
6024                Ok(value) => {
6025                    // Successfully evaluated to a value (even if it's null)
6026                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
6027                    if matches!(lhs, AstNode::Null) {
6028                        Ok(value)
6029                    }
6030                    // For paths and variables, undefined (no match/unbound) - use RHS
6031                    else if value.is_undefined()
6032                        && (matches!(lhs, AstNode::Path { .. })
6033                            || matches!(lhs, AstNode::String(_))
6034                            || matches!(lhs, AstNode::Variable(_)))
6035                    {
6036                        self.evaluate_internal(rhs, data)
6037                    } else {
6038                        Ok(value)
6039                    }
6040                }
6041                Err(_) => {
6042                    // Evaluation failed (e.g., undefined variable) - use RHS
6043                    self.evaluate_internal(rhs, data)
6044                }
6045            };
6046        }
6047
6048        // Special handling for default operator (?:)
6049        // Returns right side if left is falsy or a non-value (like a function)
6050        if op == BinaryOp::Default {
6051            let left = self.evaluate_internal(lhs, data)?;
6052            if self.is_truthy_for_default(&left) {
6053                return Ok(left);
6054            }
6055            return self.evaluate_internal(rhs, data);
6056        }
6057
6058        // Special handling for chain/pipe operator (~>)
6059        // Pipes the LHS result to the RHS function as the first argument
6060        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
6061        if op == BinaryOp::ChainPipe {
6062            // Handle regex on RHS - treat as $match(lhs, regex)
6063            if let AstNode::Regex { pattern, flags } = rhs {
6064                // Evaluate LHS
6065                let lhs_value = self.evaluate_internal(lhs, data)?;
6066                // Do regex match inline
6067                return match lhs_value {
6068                    JValue::String(s) => {
6069                        // Build the regex
6070                        let case_insensitive = flags.contains('i');
6071                        let regex_pattern = if case_insensitive {
6072                            format!("(?i){}", pattern)
6073                        } else {
6074                            pattern.clone()
6075                        };
6076                        match regex::Regex::new(&regex_pattern) {
6077                            Ok(re) => {
6078                                if let Some(m) = re.find(&s) {
6079                                    // Return match object
6080                                    let mut result = IndexMap::new();
6081                                    result.insert(
6082                                        "match".to_string(),
6083                                        JValue::string(m.as_str().to_string()),
6084                                    );
6085                                    result.insert(
6086                                        "start".to_string(),
6087                                        JValue::Number(m.start() as f64),
6088                                    );
6089                                    result
6090                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
6091
6092                                    // Capture groups
6093                                    let mut groups = Vec::new();
6094                                    for cap in re.captures_iter(&s).take(1) {
6095                                        for i in 1..cap.len() {
6096                                            if let Some(c) = cap.get(i) {
6097                                                groups.push(JValue::string(c.as_str().to_string()));
6098                                            }
6099                                        }
6100                                    }
6101                                    if !groups.is_empty() {
6102                                        result.insert("groups".to_string(), JValue::array(groups));
6103                                    }
6104
6105                                    Ok(JValue::object(result))
6106                                } else {
6107                                    Ok(JValue::Null)
6108                                }
6109                            }
6110                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
6111                                "Invalid regex: {}",
6112                                e
6113                            ))),
6114                        }
6115                    }
6116                    JValue::Null => Ok(JValue::Null),
6117                    _ => Err(EvaluatorError::TypeError(
6118                        "Left side of ~> /regex/ must be a string".to_string(),
6119                    )),
6120                };
6121            }
6122
6123            // Early check: if LHS evaluates to undefined, return undefined
6124            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
6125            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
6126            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
6127                return Ok(JValue::Undefined);
6128            }
6129
6130            // Handle different RHS types
6131            match rhs {
6132                AstNode::Function {
6133                    name,
6134                    args,
6135                    is_builtin,
6136                } => {
6137                    // RHS is a function call
6138                    // Check if the function call has placeholder arguments (partial application)
6139                    let has_placeholder =
6140                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6141
6142                    if has_placeholder {
6143                        // Partial application: replace the first placeholder with LHS value
6144                        let lhs_value = self.evaluate_internal(lhs, data)?;
6145                        let mut filled_args = Vec::new();
6146                        let mut lhs_used = false;
6147
6148                        for arg in args.iter() {
6149                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
6150                                // Replace first placeholder with evaluated LHS
6151                                // We need to create a temporary binding to pass the value
6152                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
6153                                self.context.bind(temp_name.clone(), lhs_value.clone());
6154                                filled_args.push(AstNode::Variable(temp_name));
6155                                lhs_used = true;
6156                            } else {
6157                                filled_args.push(arg.clone());
6158                            }
6159                        }
6160
6161                        // Evaluate the function with filled args
6162                        let result =
6163                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
6164
6165                        // Clean up temp bindings
6166                        for (i, arg) in args.iter().enumerate() {
6167                            if matches!(arg, AstNode::Placeholder) {
6168                                self.context.unbind(&format!("__pipe_arg_{}", i));
6169                            }
6170                        }
6171
6172                        // Unwrap singleton results from chain operator
6173                        return result.map(|v| self.unwrap_singleton(v));
6174                    } else {
6175                        // No placeholders: build args list with LHS as first argument
6176                        let mut all_args = vec![lhs.clone()];
6177                        all_args.extend_from_slice(args);
6178                        // Unwrap singleton results from chain operator
6179                        return self
6180                            .evaluate_function_call(name, &all_args, *is_builtin, data)
6181                            .map(|v| self.unwrap_singleton(v));
6182                    }
6183                }
6184                AstNode::Variable(var_name) => {
6185                    // RHS is a function reference (no parens)
6186                    // e.g., $average($tempReadings) ~> $round
6187                    let all_args = vec![lhs.clone()];
6188                    // Unwrap singleton results from chain operator
6189                    return self
6190                        .evaluate_function_call(var_name, &all_args, true, data)
6191                        .map(|v| self.unwrap_singleton(v));
6192                }
6193                AstNode::Binary {
6194                    op: BinaryOp::ChainPipe,
6195                    ..
6196                } => {
6197                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
6198                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
6199                    let lhs_value = self.evaluate_internal(lhs, data)?;
6200                    return self.evaluate_internal(rhs, &lhs_value);
6201                }
6202                AstNode::Transform { .. } => {
6203                    // RHS is a transform - invoke it with LHS as input
6204                    // Evaluate LHS first
6205                    let lhs_value = self.evaluate_internal(lhs, data)?;
6206
6207                    // Bind $ to the LHS value, then evaluate the transform
6208                    let saved_binding = self.context.lookup("$").cloned();
6209                    self.context.bind("$".to_string(), lhs_value.clone());
6210
6211                    let result = self.evaluate_internal(rhs, data);
6212
6213                    // Restore $ binding
6214                    if let Some(saved) = saved_binding {
6215                        self.context.bind("$".to_string(), saved);
6216                    } else {
6217                        self.context.unbind("$");
6218                    }
6219
6220                    // Unwrap singleton results from chain operator
6221                    return result.map(|v| self.unwrap_singleton(v));
6222                }
6223                AstNode::Lambda {
6224                    params,
6225                    body,
6226                    signature,
6227                    thunk,
6228                } => {
6229                    // RHS is a lambda - invoke it with LHS as argument
6230                    let lhs_value = self.evaluate_internal(lhs, data)?;
6231                    // Unwrap singleton results from chain operator
6232                    return self
6233                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
6234                        .map(|v| self.unwrap_singleton(v));
6235                }
6236                AstNode::Path { steps } => {
6237                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
6238                    // If the first step is a function call, we need to add LHS as first argument
6239                    if let Some(first_step) = steps.first() {
6240                        match &first_step.node {
6241                            AstNode::Function {
6242                                name,
6243                                args,
6244                                is_builtin,
6245                            } => {
6246                                // Prepend LHS to the function arguments
6247                                let mut all_args = vec![lhs.clone()];
6248                                all_args.extend_from_slice(args);
6249
6250                                // Call the function
6251                                let mut result = self.evaluate_function_call(
6252                                    name,
6253                                    &all_args,
6254                                    *is_builtin,
6255                                    data,
6256                                )?;
6257
6258                                // Apply stages from the first step (e.g., predicates)
6259                                for stage in &first_step.stages {
6260                                    match stage {
6261                                        Stage::Filter(filter_expr) => {
6262                                            result = self.evaluate_predicate_as_stage(
6263                                                &result,
6264                                                filter_expr,
6265                                            )?;
6266                                        }
6267                                        Stage::Index(_) => {}
6268                                    }
6269                                }
6270
6271                                // Apply remaining path steps if any
6272                                if steps.len() > 1 {
6273                                    let remaining_path = AstNode::Path {
6274                                        steps: steps[1..].to_vec(),
6275                                    };
6276                                    result = self.evaluate_internal(&remaining_path, &result)?;
6277                                }
6278
6279                                // Unwrap singleton results from chain operator, unless there are stages
6280                                // Stages (like predicates) indicate we want to preserve array structure
6281                                if !first_step.stages.is_empty() || steps.len() > 1 {
6282                                    return Ok(result);
6283                                } else {
6284                                    return Ok(self.unwrap_singleton(result));
6285                                }
6286                            }
6287                            AstNode::Variable(var_name) => {
6288                                // Variable that should resolve to a function
6289                                let all_args = vec![lhs.clone()];
6290                                let mut result =
6291                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
6292
6293                                // Apply stages from the first step
6294                                for stage in &first_step.stages {
6295                                    match stage {
6296                                        Stage::Filter(filter_expr) => {
6297                                            result = self.evaluate_predicate_as_stage(
6298                                                &result,
6299                                                filter_expr,
6300                                            )?;
6301                                        }
6302                                        Stage::Index(_) => {}
6303                                    }
6304                                }
6305
6306                                // Apply remaining path steps if any
6307                                if steps.len() > 1 {
6308                                    let remaining_path = AstNode::Path {
6309                                        steps: steps[1..].to_vec(),
6310                                    };
6311                                    result = self.evaluate_internal(&remaining_path, &result)?;
6312                                }
6313
6314                                // Unwrap singleton results from chain operator, unless there are stages
6315                                // Stages (like predicates) indicate we want to preserve array structure
6316                                if !first_step.stages.is_empty() || steps.len() > 1 {
6317                                    return Ok(result);
6318                                } else {
6319                                    return Ok(self.unwrap_singleton(result));
6320                                }
6321                            }
6322                            _ => {
6323                                // Other path types - just evaluate normally with LHS as context
6324                                let lhs_value = self.evaluate_internal(lhs, data)?;
6325                                return self
6326                                    .evaluate_internal(rhs, &lhs_value)
6327                                    .map(|v| self.unwrap_singleton(v));
6328                            }
6329                        }
6330                    }
6331
6332                    // Empty path? Shouldn't happen, but handle it
6333                    let lhs_value = self.evaluate_internal(lhs, data)?;
6334                    return self
6335                        .evaluate_internal(rhs, &lhs_value)
6336                        .map(|v| self.unwrap_singleton(v));
6337                }
6338                _ => {
6339                    return Err(EvaluatorError::TypeError(
6340                        "Right side of ~> must be a function call or function reference"
6341                            .to_string(),
6342                    ));
6343                }
6344            }
6345        }
6346
6347        // Special handling for variable binding (:=)
6348        if op == BinaryOp::ColonEqual {
6349            // Extract variable name from LHS
6350            let var_name = match lhs {
6351                AstNode::Variable(name) => name.clone(),
6352                _ => {
6353                    return Err(EvaluatorError::TypeError(
6354                        "Left side of := must be a variable".to_string(),
6355                    ))
6356                }
6357            };
6358
6359            // Check if RHS is a lambda - store it specially
6360            if let AstNode::Lambda {
6361                params,
6362                body,
6363                signature,
6364                thunk,
6365            } = rhs
6366            {
6367                // Store the lambda AST for later invocation
6368                // Capture only the free variables referenced by the lambda body
6369                let captured_env = self.capture_environment_for(body, params);
6370                let compiled_body = if !thunk {
6371                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
6372                    try_compile_expr_with_allowed_vars(body, &var_refs)
6373                } else {
6374                    None
6375                };
6376                let stored_lambda = StoredLambda {
6377                    params: params.clone(),
6378                    body: (**body).clone(),
6379                    compiled_body,
6380                    signature: signature.clone(),
6381                    captured_env,
6382                    captured_data: Some(data.clone()),
6383                    thunk: *thunk,
6384                };
6385                let lambda_params = stored_lambda.params.clone();
6386                let lambda_sig = stored_lambda.signature.clone();
6387                self.context.bind_lambda(var_name.clone(), stored_lambda);
6388
6389                // Return a lambda marker value (include _lambda_id so it can be found later)
6390                let lambda_repr = JValue::lambda(
6391                    var_name.as_str(),
6392                    lambda_params,
6393                    Some(var_name.clone()),
6394                    lambda_sig,
6395                );
6396                return Ok(lambda_repr);
6397            }
6398
6399            // Check if RHS is a pure function composition (ChainPipe between function references)
6400            // e.g., $uppertrim := $trim ~> $uppercase
6401            // This creates a lambda that composes the functions.
6402            // But NOT for data ~> function, which should be evaluated immediately.
6403            // e.g., $result := data ~> $map($fn) should evaluate the pipe
6404            if let AstNode::Binary {
6405                op: BinaryOp::ChainPipe,
6406                lhs: chain_lhs,
6407                rhs: chain_rhs,
6408            } = rhs
6409            {
6410                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
6411                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
6412                let is_function_composition = match chain_lhs.as_ref() {
6413                    // LHS is a function reference like $trim or $sum
6414                    AstNode::Variable(name)
6415                        if self.is_builtin_function(name)
6416                            || self.context.lookup_lambda(name).is_some() =>
6417                    {
6418                        true
6419                    }
6420                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
6421                    AstNode::Binary {
6422                        op: BinaryOp::ChainPipe,
6423                        ..
6424                    } => true,
6425                    // A function call with placeholder creates a partial application
6426                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
6427                    AstNode::Function { args, .. }
6428                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
6429                    {
6430                        true
6431                    }
6432                    // Anything else (data, function calls, arrays, etc.) is not pure composition
6433                    _ => false,
6434                };
6435
6436                if is_function_composition {
6437                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
6438                    // The original chain is $trim ~> $uppercase (left-associative)
6439                    // We want to create: ($ ~> $trim) ~> $uppercase
6440                    let param_name = "$".to_string();
6441
6442                    // First create $ ~> $trim
6443                    let first_pipe = AstNode::Binary {
6444                        op: BinaryOp::ChainPipe,
6445                        lhs: Box::new(AstNode::Variable(param_name.clone())),
6446                        rhs: chain_lhs.clone(),
6447                    };
6448
6449                    // Then wrap with ~> $uppercase (or the rest of the chain)
6450                    let composed_body = AstNode::Binary {
6451                        op: BinaryOp::ChainPipe,
6452                        lhs: Box::new(first_pipe),
6453                        rhs: chain_rhs.clone(),
6454                    };
6455
6456                    let stored_lambda = StoredLambda {
6457                        params: vec![param_name],
6458                        body: composed_body,
6459                        compiled_body: None, // ChainPipe body is not compilable
6460                        signature: None,
6461                        captured_env: self.capture_current_environment(),
6462                        captured_data: Some(data.clone()),
6463                        thunk: false,
6464                    };
6465                    self.context.bind_lambda(var_name.clone(), stored_lambda);
6466
6467                    // Return a lambda marker value (include _lambda_id for later lookup)
6468                    let lambda_repr = JValue::lambda(
6469                        var_name.as_str(),
6470                        vec!["$".to_string()],
6471                        Some(var_name.clone()),
6472                        None::<String>,
6473                    );
6474                    return Ok(lambda_repr);
6475                }
6476                // If not function composition, fall through to normal evaluation below
6477            }
6478
6479            // Evaluate the RHS
6480            let value = self.evaluate_internal(rhs, data)?;
6481
6482            // If the value is a lambda, copy the stored lambda to the new variable name
6483            if let Some(stored) = self.lookup_lambda_from_value(&value) {
6484                self.context.bind_lambda(var_name.clone(), stored);
6485            }
6486
6487            // Bind even if undefined (null) so inner scopes can shadow outer variables
6488            self.context.bind(var_name, value.clone());
6489            return Ok(value);
6490        }
6491
6492        // Special handling for 'In' operator - check for array filtering
6493        // Must evaluate lhs first to determine if this is array filtering
6494        if op == BinaryOp::In {
6495            let left = self.evaluate_internal(lhs, data)?;
6496
6497            // Check if this is array filtering: array[predicate]
6498            if matches!(left, JValue::Array(_)) {
6499                // Try evaluating rhs in current context to see if it's a simple index
6500                let right_result = self.evaluate_internal(rhs, data);
6501
6502                if let Ok(JValue::Number(_)) = right_result {
6503                    // Simple numeric index: array[n]
6504                    return self.array_index(&left, &right_result.unwrap());
6505                } else {
6506                    // This is array filtering: array[predicate]
6507                    // Evaluate the predicate for each array item
6508                    return self.array_filter(lhs, rhs, &left, data);
6509                }
6510            }
6511        }
6512
6513        // Special handling for logical operators (short-circuit evaluation)
6514        if op == BinaryOp::And {
6515            let left = self.evaluate_internal(lhs, data)?;
6516            if !self.is_truthy(&left) {
6517                // Short-circuit: if left is falsy, return false without evaluating right
6518                return Ok(JValue::Bool(false));
6519            }
6520            let right = self.evaluate_internal(rhs, data)?;
6521            return Ok(JValue::Bool(self.is_truthy(&right)));
6522        }
6523
6524        if op == BinaryOp::Or {
6525            let left = self.evaluate_internal(lhs, data)?;
6526            if self.is_truthy(&left) {
6527                // Short-circuit: if left is truthy, return true without evaluating right
6528                return Ok(JValue::Bool(true));
6529            }
6530            let right = self.evaluate_internal(rhs, data)?;
6531            return Ok(JValue::Bool(self.is_truthy(&right)));
6532        }
6533
6534        // Check if operands are explicit null literals (vs undefined from variables)
6535        let left_is_explicit_null = matches!(lhs, AstNode::Null);
6536        let right_is_explicit_null = matches!(rhs, AstNode::Null);
6537
6538        // Standard evaluation: evaluate both operands
6539        let left = self.evaluate_internal(lhs, data)?;
6540        let right = self.evaluate_internal(rhs, data)?;
6541
6542        match op {
6543            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6544            BinaryOp::Subtract => {
6545                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6546            }
6547            BinaryOp::Multiply => {
6548                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6549            }
6550            BinaryOp::Divide => {
6551                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6552            }
6553            BinaryOp::Modulo => {
6554                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6555            }
6556
6557            BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
6558            BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
6559            BinaryOp::LessThan => {
6560                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6561            }
6562            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6563                &left,
6564                &right,
6565                left_is_explicit_null,
6566                right_is_explicit_null,
6567            ),
6568            BinaryOp::GreaterThan => {
6569                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6570            }
6571            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6572                &left,
6573                &right,
6574                left_is_explicit_null,
6575                right_is_explicit_null,
6576            ),
6577
6578            // And/Or handled above with short-circuit evaluation
6579            BinaryOp::And | BinaryOp::Or => unreachable!(),
6580
6581            BinaryOp::Concatenate => self.concatenate(&left, &right),
6582            BinaryOp::Range => self.range(&left, &right),
6583            BinaryOp::In => self.in_operator(&left, &right),
6584
6585            // Focus binding: should be resolved by ast_transform pass (Task 2)
6586            BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6587                "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6588            )),
6589
6590            // Index binding: should be resolved by ast_transform pass (Task 4,
6591            // which retired the dedicated AstNode::IndexBind variant in favor
6592            // of this generic Binary marker, mirroring FocusBind above)
6593            BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6594                "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6595            )),
6596
6597            // These operators are all handled as special cases earlier in evaluate_binary_op
6598            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6599                unreachable!()
6600            }
6601        }
6602    }
6603
6604    /// Evaluate a unary operation
6605    fn evaluate_unary_op(
6606        &mut self,
6607        op: crate::ast::UnaryOp,
6608        operand: &AstNode,
6609        data: &JValue,
6610    ) -> Result<JValue, EvaluatorError> {
6611        use crate::ast::UnaryOp;
6612
6613        let value = self.evaluate_internal(operand, data)?;
6614
6615        match op {
6616            UnaryOp::Negate => match value {
6617                // undefined returns undefined
6618                JValue::Null | JValue::Undefined => Ok(JValue::Null),
6619                JValue::Number(n) => Ok(JValue::Number(-n)),
6620                _ => Err(EvaluatorError::TypeError(
6621                    "D1002: Cannot negate non-number value".to_string(),
6622                )),
6623            },
6624            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6625        }
6626    }
6627
6628    /// Try to fuse an aggregate function call with its Path argument.
6629    /// Handles patterns like:
6630    /// - $sum(arr.field) → iterate arr, extract field, accumulate
6631    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6632    ///
6633    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6634    fn try_fused_aggregate(
6635        &mut self,
6636        name: &str,
6637        arg: &AstNode,
6638        data: &JValue,
6639    ) -> Result<Option<JValue>, EvaluatorError> {
6640        // Only applies to numeric aggregates
6641        if !matches!(name, "sum" | "max" | "min" | "average") {
6642            return Ok(None);
6643        }
6644
6645        // Argument must be a Path
6646        let AstNode::Path { steps } = arg else {
6647            return Ok(None);
6648        };
6649
6650        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6651        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6652        if steps.len() != 2 {
6653            return Ok(None);
6654        }
6655
6656        // Last step must be a simple Name (the field to extract)
6657        let field_step = &steps[1];
6658        if !field_step.stages.is_empty() {
6659            return Ok(None);
6660        }
6661        let AstNode::Name(extract_field) = &field_step.node else {
6662            return Ok(None);
6663        };
6664
6665        // First step: Name with optional filter stage
6666        let arr_step = &steps[0];
6667        let AstNode::Name(arr_name) = &arr_step.node else {
6668            return Ok(None);
6669        };
6670
6671        // Get the source array from data
6672        let arr = match data {
6673            JValue::Object(obj) => match obj.get(arr_name) {
6674                Some(JValue::Array(arr)) => arr,
6675                _ => return Ok(None),
6676            },
6677            _ => return Ok(None),
6678        };
6679
6680        // Check for filter stage — try CompiledExpr for the predicate
6681        let filter_compiled = match arr_step.stages.as_slice() {
6682            [] => None,
6683            [Stage::Filter(pred)] => try_compile_expr(pred),
6684            _ => return Ok(None),
6685        };
6686        // If filter stage exists but wasn't compilable, bail out
6687        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6688            return Ok(None);
6689        }
6690
6691        // Build shape cache for the array
6692        let shape = arr.first().and_then(build_shape_cache);
6693
6694        // Fused iteration: filter (optional) + extract + aggregate
6695        let mut total = 0.0f64;
6696        let mut count = 0usize;
6697        let mut max_val = f64::NEG_INFINITY;
6698        let mut min_val = f64::INFINITY;
6699        let mut has_any = false;
6700
6701        for item in arr.iter() {
6702            // Apply compiled filter if present
6703            if let Some(ref compiled) = filter_compiled {
6704                let result = if let Some(ref s) = shape {
6705                    eval_compiled_shaped(compiled, item, None, s, &self.options, self.start_time)?
6706                } else {
6707                    eval_compiled(compiled, item, None, &self.options, self.start_time)?
6708                };
6709                if !compiled_is_truthy(&result) {
6710                    continue;
6711                }
6712            }
6713
6714            // Extract field value
6715            let val = match item {
6716                JValue::Object(obj) => match obj.get(extract_field) {
6717                    Some(JValue::Number(n)) => *n,
6718                    Some(_) | None => continue, // Skip non-numeric / missing
6719                },
6720                _ => continue,
6721            };
6722
6723            has_any = true;
6724            match name {
6725                "sum" => total += val,
6726                "max" => max_val = max_val.max(val),
6727                "min" => min_val = min_val.min(val),
6728                "average" => {
6729                    total += val;
6730                    count += 1;
6731                }
6732                _ => unreachable!(),
6733            }
6734        }
6735
6736        if !has_any {
6737            return Ok(Some(match name {
6738                "sum" => JValue::from(0i64),
6739                "average" | "max" | "min" => JValue::Null,
6740                _ => unreachable!(),
6741            }));
6742        }
6743
6744        Ok(Some(match name {
6745            "sum" => JValue::Number(total),
6746            "max" => JValue::Number(max_val),
6747            "min" => JValue::Number(min_val),
6748            "average" => JValue::Number(total / count as f64),
6749            _ => unreachable!(),
6750        }))
6751    }
6752
6753    /// Evaluate a function call
6754    fn evaluate_function_call(
6755        &mut self,
6756        name: &str,
6757        args: &[AstNode],
6758        is_builtin: bool,
6759        data: &JValue,
6760    ) -> Result<JValue, EvaluatorError> {
6761        use crate::functions;
6762
6763        // Check for partial application (any argument is a Placeholder)
6764        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6765        if has_placeholder {
6766            return self.create_partial_application(name, args, is_builtin, data);
6767        }
6768
6769        // FIRST check if this variable holds a function value (lambda or builtin reference)
6770        // This is critical for:
6771        // 1. Allowing function parameters to shadow stored lambdas
6772        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
6773        // 2. Calling built-in functions passed as parameters
6774        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
6775        if let Some(value) = self.context.lookup(name).cloned() {
6776            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
6777                let mut evaluated_args = Vec::with_capacity(args.len());
6778                for arg in args {
6779                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6780                }
6781                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6782            }
6783            if let JValue::Builtin { name: builtin_name } = &value {
6784                // This is a built-in function reference (e.g., $f bound to $sum)
6785                let mut evaluated_args = Vec::with_capacity(args.len());
6786                for arg in args {
6787                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6788                }
6789                return self.call_builtin_with_values(builtin_name, &evaluated_args);
6790            }
6791        }
6792
6793        // THEN check if this is a stored lambda (user-defined function by name)
6794        // This only applies if not shadowed by a binding above
6795        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
6796            let mut evaluated_args = Vec::with_capacity(args.len());
6797            for arg in args {
6798                evaluated_args.push(self.evaluate_internal(arg, data)?);
6799            }
6800            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6801        }
6802
6803        // If the function was called without $ prefix and it's not a stored lambda,
6804        // it's an error (unknown function without $ prefix)
6805        if !is_builtin && name != "__lambda__" {
6806            return Err(EvaluatorError::ReferenceError(format!(
6807                "Unknown function: {}",
6808                name
6809            )));
6810        }
6811
6812        // Special handling for $exists function
6813        // It needs to know if the argument is explicit null vs undefined
6814        if name == "exists" && args.len() == 1 {
6815            let arg = &args[0];
6816
6817            // Check if it's an explicit null literal
6818            if matches!(arg, AstNode::Null) {
6819                return Ok(JValue::Bool(true)); // Explicit null exists
6820            }
6821
6822            // Check if it's a function reference
6823            if let AstNode::Variable(var_name) = arg {
6824                if self.is_builtin_function(var_name) {
6825                    return Ok(JValue::Bool(true)); // Built-in function exists
6826                }
6827
6828                // Check if it's a stored lambda
6829                if self.context.lookup_lambda(var_name).is_some() {
6830                    return Ok(JValue::Bool(true)); // Lambda exists
6831                }
6832
6833                // Check if the variable is defined
6834                if let Some(val) = self.context.lookup(var_name) {
6835                    // A variable bound to the undefined marker doesn't "exist"
6836                    if val.is_undefined() {
6837                        return Ok(JValue::Bool(false));
6838                    }
6839                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
6840                } else {
6841                    return Ok(JValue::Bool(false)); // Variable is undefined
6842                }
6843            }
6844
6845            // For other expressions, evaluate and check if non-null/non-undefined
6846            let value = self.evaluate_internal(arg, data)?;
6847            return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
6848        }
6849
6850        // Check if any arguments are undefined variables or undefined paths
6851        // Functions like $not() should return undefined when given undefined values
6852        for arg in args {
6853            // Check for undefined variable (e.g., $undefined_var)
6854            if let AstNode::Variable(var_name) = arg {
6855                // Skip built-in function names - they're function references, not undefined variables
6856                if !var_name.is_empty()
6857                    && !self.is_builtin_function(var_name)
6858                    && self.context.lookup(var_name).is_none()
6859                {
6860                    // Undefined variable - for functions that should propagate undefined
6861                    if propagates_undefined(name) {
6862                        return Ok(JValue::Null); // Return undefined
6863                    }
6864                }
6865            }
6866            // Check for simple field name (e.g., blah) that evaluates to undefined
6867            if let AstNode::Name(field_name) = arg {
6868                let field_exists =
6869                    matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
6870                if !field_exists && propagates_undefined(name) {
6871                    return Ok(JValue::Null);
6872                }
6873            }
6874            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
6875            // Field accesses are represented as AstNode::Path. String literals should never
6876            // be checked for undefined propagation.
6877            // Check for Path expressions that evaluate to undefined
6878            if let AstNode::Path { steps } = arg {
6879                // For paths that evaluate to null, we need to determine if it's because:
6880                // 1. A field doesn't exist (undefined) - should propagate as undefined
6881                // 2. A field exists with value null - should throw T0410
6882                //
6883                // We can distinguish these by checking if the path is accessing a field
6884                // that doesn't exist on an object vs one that has an explicit null value.
6885                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
6886                    // Path evaluated to null - now check if it's truly undefined
6887                    // For single-step paths, check if the field exists
6888                    if steps.len() == 1 {
6889                        // Get field name - could be Name (identifier) or String (quoted)
6890                        let field_name = match &steps[0].node {
6891                            AstNode::Name(n) => Some(n.as_str()),
6892                            AstNode::String(s) => Some(s.as_str()),
6893                            _ => None,
6894                        };
6895                        if let Some(field) = field_name {
6896                            match data {
6897                                JValue::Object(obj) => {
6898                                    if !obj.contains_key(field) {
6899                                        // Field doesn't exist - return undefined
6900                                        if propagates_undefined(name) {
6901                                            return Ok(JValue::Null);
6902                                        }
6903                                    }
6904                                    // Field exists with value null - continue to throw T0410
6905                                }
6906                                // Trying to access field on null data - return undefined
6907                                JValue::Null if propagates_undefined(name) => {
6908                                    return Ok(JValue::Null);
6909                                }
6910                                _ => {}
6911                            }
6912                        }
6913                    }
6914                    // For multi-step paths, check if any intermediate step failed
6915                    else if steps.len() > 1 {
6916                        // Evaluate each step to find where it breaks
6917                        let mut current = data;
6918                        let mut failed_due_to_missing_field = false;
6919
6920                        for (i, step) in steps.iter().enumerate() {
6921                            if let AstNode::Name(field_name) = &step.node {
6922                                match current {
6923                                    JValue::Object(obj) => {
6924                                        if let Some(val) = obj.get(field_name) {
6925                                            current = val;
6926                                        } else {
6927                                            // Field doesn't exist
6928                                            failed_due_to_missing_field = true;
6929                                            break;
6930                                        }
6931                                    }
6932                                    JValue::Array(_) => {
6933                                        // Array access - evaluate normally
6934                                        break;
6935                                    }
6936                                    JValue::Null => {
6937                                        // Hit null in the middle of the path
6938                                        if i > 0 {
6939                                            // Previous field had null value - not undefined
6940                                            failed_due_to_missing_field = false;
6941                                        }
6942                                        break;
6943                                    }
6944                                    _ => break,
6945                                }
6946                            }
6947                        }
6948
6949                        if failed_due_to_missing_field && propagates_undefined(name) {
6950                            return Ok(JValue::Null);
6951                        }
6952                    }
6953                }
6954            }
6955        }
6956
6957        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
6958        // try to fuse filter+extract+aggregate into a single pass.
6959        if args.len() == 1 {
6960            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
6961                return Ok(result);
6962            }
6963        }
6964
6965        let mut evaluated_args = Vec::with_capacity(args.len());
6966        for arg in args {
6967            evaluated_args.push(self.evaluate_internal(arg, data)?);
6968        }
6969
6970        // JSONata feature: when a function is called with no arguments but expects
6971        // at least one, use the current context value (data) as the implicit first argument
6972        // This also applies when functions expecting N arguments receive N-1 arguments,
6973        // in which case the context value becomes the first argument
6974        let context_functions_zero_arg = [
6975            "string",
6976            "number",
6977            "boolean",
6978            "uppercase",
6979            "lowercase",
6980            "fromMillis",
6981        ];
6982        let context_functions_missing_first = [
6983            "substringBefore",
6984            "substringAfter",
6985            "contains",
6986            "split",
6987            "replace",
6988        ];
6989
6990        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
6991            // Use the current context value as the implicit argument
6992            evaluated_args.push(data.clone());
6993        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
6994            // These functions expect 2+ arguments, but received 1
6995            // Only insert context if it's a compatible type (string for string functions)
6996            // Otherwise, let the function throw T0411 for wrong argument count
6997            if matches!(data, JValue::String(_)) {
6998                evaluated_args.insert(0, data.clone());
6999            }
7000        }
7001
7002        // Special handling for $string() with no explicit arguments
7003        // After context insertion, check if the argument is null (undefined context)
7004        if name == "string"
7005            && args.is_empty()
7006            && !evaluated_args.is_empty()
7007            && evaluated_args[0].is_null()
7008        {
7009            // Context was null/undefined, so return undefined
7010            return Ok(JValue::Null);
7011        }
7012
7013        match name {
7014            "string" => {
7015                if evaluated_args.len() > 2 {
7016                    return Err(EvaluatorError::EvaluationError(
7017                        "string() takes at most 2 arguments".to_string(),
7018                    ));
7019                }
7020
7021                let prettify = if evaluated_args.len() == 2 {
7022                    match &evaluated_args[1] {
7023                        JValue::Bool(b) => Some(*b),
7024                        _ => {
7025                            return Err(EvaluatorError::TypeError(
7026                                "string() prettify parameter must be a boolean".to_string(),
7027                            ))
7028                        }
7029                    }
7030                } else {
7031                    None
7032                };
7033
7034                Ok(functions::string::string(&evaluated_args[0], prettify)?)
7035            }
7036            "length" => {
7037                if evaluated_args.len() != 1 {
7038                    return Err(EvaluatorError::EvaluationError(
7039                        "length() requires exactly 1 argument".to_string(),
7040                    ));
7041                }
7042                match &evaluated_args[0] {
7043                    JValue::String(s) => Ok(functions::string::length(s)?),
7044                    _ => Err(EvaluatorError::TypeError(
7045                        "T0410: Argument 1 of function length does not match function signature"
7046                            .to_string(),
7047                    )),
7048                }
7049            }
7050            "uppercase" => {
7051                if evaluated_args.len() != 1 {
7052                    return Err(EvaluatorError::EvaluationError(
7053                        "uppercase() requires exactly 1 argument".to_string(),
7054                    ));
7055                }
7056                if evaluated_args[0].is_undefined() {
7057                    return Ok(JValue::Undefined);
7058                }
7059                match &evaluated_args[0] {
7060                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
7061                    _ => Err(EvaluatorError::TypeError(
7062                        "T0410: Argument 1 of function uppercase does not match function signature"
7063                            .to_string(),
7064                    )),
7065                }
7066            }
7067            "lowercase" => {
7068                if evaluated_args.len() != 1 {
7069                    return Err(EvaluatorError::EvaluationError(
7070                        "lowercase() requires exactly 1 argument".to_string(),
7071                    ));
7072                }
7073                if evaluated_args[0].is_undefined() {
7074                    return Ok(JValue::Undefined);
7075                }
7076                match &evaluated_args[0] {
7077                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
7078                    _ => Err(EvaluatorError::TypeError(
7079                        "T0410: Argument 1 of function lowercase does not match function signature"
7080                            .to_string(),
7081                    )),
7082                }
7083            }
7084            "number" => {
7085                if evaluated_args.is_empty() {
7086                    return Err(EvaluatorError::EvaluationError(
7087                        "number() requires at least 1 argument".to_string(),
7088                    ));
7089                }
7090                if evaluated_args.len() > 1 {
7091                    return Err(EvaluatorError::TypeError(
7092                        "T0410: Argument 2 of function number does not match function signature"
7093                            .to_string(),
7094                    ));
7095                }
7096                if evaluated_args[0].is_undefined() {
7097                    return Ok(JValue::Undefined);
7098                }
7099                Ok(functions::numeric::number(&evaluated_args[0])?)
7100            }
7101            "sum" => {
7102                if evaluated_args.len() != 1 {
7103                    return Err(EvaluatorError::EvaluationError(
7104                        "sum() requires exactly 1 argument".to_string(),
7105                    ));
7106                }
7107                // Return undefined if argument is undefined
7108                if evaluated_args[0].is_undefined() {
7109                    return Ok(JValue::Undefined);
7110                }
7111                match &evaluated_args[0] {
7112                    JValue::Null => Ok(JValue::Null),
7113                    JValue::Array(arr) => {
7114                        // Use zero-clone iterator-based sum
7115                        Ok(aggregation::sum(arr)?)
7116                    }
7117                    // Non-array values: extract number directly
7118                    JValue::Number(n) => Ok(JValue::Number(*n)),
7119                    other => Ok(functions::numeric::sum(&[other.clone()])?),
7120                }
7121            }
7122            "count" => {
7123                if evaluated_args.len() != 1 {
7124                    return Err(EvaluatorError::EvaluationError(
7125                        "count() requires exactly 1 argument".to_string(),
7126                    ));
7127                }
7128                // Return 0 if argument is undefined
7129                if evaluated_args[0].is_undefined() {
7130                    return Ok(JValue::from(0i64));
7131                }
7132                match &evaluated_args[0] {
7133                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
7134                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
7135                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
7136                }
7137            }
7138            "substring" => {
7139                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7140                    return Err(EvaluatorError::EvaluationError(
7141                        "substring() requires 2 or 3 arguments".to_string(),
7142                    ));
7143                }
7144                if evaluated_args[0].is_undefined() {
7145                    return Ok(JValue::Undefined);
7146                }
7147                match (&evaluated_args[0], &evaluated_args[1]) {
7148                    (JValue::String(s), JValue::Number(start)) => {
7149                        let length = if evaluated_args.len() == 3 {
7150                            match &evaluated_args[2] {
7151                                JValue::Number(l) => Some(*l as i64),
7152                                _ => return Err(EvaluatorError::TypeError(
7153                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
7154                                )),
7155                            }
7156                        } else {
7157                            None
7158                        };
7159                        Ok(functions::string::substring(s, *start as i64, length)?)
7160                    }
7161                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7162                        "T0410: Argument 2 of function substring does not match function signature"
7163                            .to_string(),
7164                    )),
7165                    _ => Err(EvaluatorError::TypeError(
7166                        "T0410: Argument 1 of function substring does not match function signature"
7167                            .to_string(),
7168                    )),
7169                }
7170            }
7171            "substringBefore" => {
7172                if evaluated_args.len() != 2 {
7173                    return Err(EvaluatorError::TypeError(
7174                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
7175                    ));
7176                }
7177                if evaluated_args[0].is_undefined() {
7178                    return Ok(JValue::Undefined);
7179                }
7180                match (&evaluated_args[0], &evaluated_args[1]) {
7181                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
7182                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7183                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
7184                    )),
7185                    _ => Err(EvaluatorError::TypeError(
7186                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
7187                    )),
7188                }
7189            }
7190            "substringAfter" => {
7191                if evaluated_args.len() != 2 {
7192                    return Err(EvaluatorError::TypeError(
7193                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
7194                    ));
7195                }
7196                if evaluated_args[0].is_undefined() {
7197                    return Ok(JValue::Undefined);
7198                }
7199                match (&evaluated_args[0], &evaluated_args[1]) {
7200                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
7201                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7202                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
7203                    )),
7204                    _ => Err(EvaluatorError::TypeError(
7205                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
7206                    )),
7207                }
7208            }
7209            "pad" => {
7210                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7211                    return Err(EvaluatorError::EvaluationError(
7212                        "pad() requires 2 or 3 arguments".to_string(),
7213                    ));
7214                }
7215
7216                // First argument: string to pad
7217                let string = match &evaluated_args[0] {
7218                    JValue::String(s) => s.clone(),
7219                    JValue::Null => return Ok(JValue::Null),
7220                    JValue::Undefined => return Ok(JValue::Undefined),
7221                    _ => {
7222                        return Err(EvaluatorError::TypeError(
7223                            "pad() first argument must be a string".to_string(),
7224                        ))
7225                    }
7226                };
7227
7228                // Second argument: width (negative = left pad, positive = right pad)
7229                let width = match &evaluated_args.get(1) {
7230                    Some(JValue::Number(n)) => *n as i32,
7231                    _ => {
7232                        return Err(EvaluatorError::TypeError(
7233                            "pad() second argument must be a number".to_string(),
7234                        ))
7235                    }
7236                };
7237
7238                // Third argument: padding string (optional, defaults to space)
7239                let pad_string = match evaluated_args.get(2) {
7240                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
7241                    _ => Rc::from(" "),
7242                };
7243
7244                let abs_width = width.unsigned_abs() as usize;
7245                // Count Unicode characters (code points), not bytes
7246                let char_count = string.chars().count();
7247
7248                if char_count >= abs_width {
7249                    // String is already long enough
7250                    return Ok(JValue::string(string));
7251                }
7252
7253                let padding_needed = abs_width - char_count;
7254
7255                let pad_chars: Vec<char> = pad_string.chars().collect();
7256                let mut padding = String::with_capacity(padding_needed);
7257                for i in 0..padding_needed {
7258                    padding.push(pad_chars[i % pad_chars.len()]);
7259                }
7260
7261                let result = if width < 0 {
7262                    // Left pad (negative width)
7263                    format!("{}{}", padding, string)
7264                } else {
7265                    // Right pad (positive width)
7266                    format!("{}{}", string, padding)
7267                };
7268
7269                Ok(JValue::string(result))
7270            }
7271
7272            "trim" => {
7273                if evaluated_args.is_empty() {
7274                    return Ok(JValue::Null); // undefined
7275                }
7276                if evaluated_args.len() != 1 {
7277                    return Err(EvaluatorError::EvaluationError(
7278                        "trim() requires at most 1 argument".to_string(),
7279                    ));
7280                }
7281                match &evaluated_args[0] {
7282                    JValue::Null => Ok(JValue::Null),
7283                    JValue::String(s) => Ok(functions::string::trim(s)?),
7284                    _ => Err(EvaluatorError::TypeError(
7285                        "trim() requires a string argument".to_string(),
7286                    )),
7287                }
7288            }
7289            "contains" => {
7290                if evaluated_args.len() != 2 {
7291                    return Err(EvaluatorError::EvaluationError(
7292                        "contains() requires exactly 2 arguments".to_string(),
7293                    ));
7294                }
7295                if evaluated_args[0].is_null() {
7296                    return Ok(JValue::Null);
7297                }
7298                if evaluated_args[0].is_undefined() {
7299                    return Ok(JValue::Undefined);
7300                }
7301                match &evaluated_args[0] {
7302                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
7303                    _ => Err(EvaluatorError::TypeError(
7304                        "contains() requires a string as the first argument".to_string(),
7305                    )),
7306                }
7307            }
7308            "split" => {
7309                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7310                    return Err(EvaluatorError::EvaluationError(
7311                        "split() requires 2 or 3 arguments".to_string(),
7312                    ));
7313                }
7314                if evaluated_args[0].is_null() {
7315                    return Ok(JValue::Null);
7316                }
7317                if evaluated_args[0].is_undefined() {
7318                    return Ok(JValue::Undefined);
7319                }
7320                match &evaluated_args[0] {
7321                    JValue::String(s) => {
7322                        let limit = if evaluated_args.len() == 3 {
7323                            match &evaluated_args[2] {
7324                                JValue::Number(n) => {
7325                                    let f = *n;
7326                                    // Negative limit is an error
7327                                    if f < 0.0 {
7328                                        return Err(EvaluatorError::EvaluationError(
7329                                            "D3020: Third argument of split function must be a positive number".to_string(),
7330                                        ));
7331                                    }
7332                                    // Floor the value for non-integer limits
7333                                    Some(f.floor() as usize)
7334                                }
7335                                _ => {
7336                                    return Err(EvaluatorError::TypeError(
7337                                        "split() limit must be a number".to_string(),
7338                                    ))
7339                                }
7340                            }
7341                        } else {
7342                            None
7343                        };
7344                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
7345                    }
7346                    _ => Err(EvaluatorError::TypeError(
7347                        "split() requires a string as the first argument".to_string(),
7348                    )),
7349                }
7350            }
7351            "join" => {
7352                // Special case: if first arg is undefined, return undefined
7353                // But if separator (2nd arg) is undefined, use empty string (default)
7354                if evaluated_args.is_empty() {
7355                    return Err(EvaluatorError::TypeError(
7356                        "T0410: Argument 1 of function $join does not match function signature"
7357                            .to_string(),
7358                    ));
7359                }
7360                if evaluated_args[0].is_null() {
7361                    return Ok(JValue::Null);
7362                }
7363                if evaluated_args[0].is_undefined() {
7364                    return Ok(JValue::Undefined);
7365                }
7366
7367                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
7368                // The signature handles coercion and validation
7369                use crate::signature::Signature;
7370
7371                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
7372                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7373                })?;
7374
7375                let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
7376                    Ok(args) => args,
7377                    Err(crate::signature::SignatureError::UndefinedArgument) => {
7378                        // This can happen if the separator is undefined
7379                        // In that case, just validate the first arg and use default separator
7380                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
7381                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7382                        })?;
7383
7384                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
7385                            Ok(args) => args,
7386                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
7387                                index,
7388                                expected,
7389                            }) => {
7390                                return Err(EvaluatorError::TypeError(format!(
7391                                    "T0412: Argument {} of function $join must be an array of {}",
7392                                    index, expected
7393                                )));
7394                            }
7395                            Err(e) => {
7396                                return Err(EvaluatorError::TypeError(format!(
7397                                    "Signature validation failed: {}",
7398                                    e
7399                                )));
7400                            }
7401                        }
7402                    }
7403                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
7404                        index,
7405                        expected,
7406                    }) => {
7407                        return Err(EvaluatorError::TypeError(
7408                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
7409                        ));
7410                    }
7411                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
7412                        index,
7413                        expected,
7414                    }) => {
7415                        return Err(EvaluatorError::TypeError(format!(
7416                            "T0412: Argument {} of function $join must be an array of {}",
7417                            index, expected
7418                        )));
7419                    }
7420                    Err(e) => {
7421                        return Err(EvaluatorError::TypeError(format!(
7422                            "Signature validation failed: {}",
7423                            e
7424                        )));
7425                    }
7426                };
7427
7428                // After coercion, first arg is guaranteed to be an array of strings
7429                match &coerced_args[0] {
7430                    JValue::Array(arr) => {
7431                        let separator = if coerced_args.len() == 2 {
7432                            match &coerced_args[1] {
7433                                JValue::String(s) => Some(&**s),
7434                                JValue::Null => None, // Undefined separator -> use empty string
7435                                _ => None,            // Signature should have validated this
7436                            }
7437                        } else {
7438                            None // No separator provided -> use empty string
7439                        };
7440                        Ok(functions::string::join(arr, separator)?)
7441                    }
7442                    JValue::Null => Ok(JValue::Null),
7443                    _ => unreachable!("Signature validation should ensure array type"),
7444                }
7445            }
7446            "replace" => {
7447                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
7448                    return Err(EvaluatorError::EvaluationError(
7449                        "replace() requires 3 or 4 arguments".to_string(),
7450                    ));
7451                }
7452                if evaluated_args[0].is_null() {
7453                    return Ok(JValue::Null);
7454                }
7455                if evaluated_args[0].is_undefined() {
7456                    return Ok(JValue::Undefined);
7457                }
7458
7459                // Check if replacement (3rd arg) is a function/lambda
7460                let replacement_is_lambda = matches!(
7461                    evaluated_args[2],
7462                    JValue::Lambda { .. } | JValue::Builtin { .. }
7463                );
7464
7465                if replacement_is_lambda {
7466                    // Lambda replacement mode
7467                    return self.replace_with_lambda(
7468                        &evaluated_args[0],
7469                        &evaluated_args[1],
7470                        &evaluated_args[2],
7471                        if evaluated_args.len() == 4 {
7472                            Some(&evaluated_args[3])
7473                        } else {
7474                            None
7475                        },
7476                        data,
7477                    );
7478                }
7479
7480                // String replacement mode
7481                match (&evaluated_args[0], &evaluated_args[2]) {
7482                    (JValue::String(s), JValue::String(replacement)) => {
7483                        let limit = if evaluated_args.len() == 4 {
7484                            match &evaluated_args[3] {
7485                                JValue::Number(n) => {
7486                                    let lim_f64 = *n;
7487                                    if lim_f64 < 0.0 {
7488                                        return Err(EvaluatorError::EvaluationError(format!(
7489                                            "D3011: Limit must be non-negative, got {}",
7490                                            lim_f64
7491                                        )));
7492                                    }
7493                                    Some(lim_f64 as usize)
7494                                }
7495                                _ => {
7496                                    return Err(EvaluatorError::TypeError(
7497                                        "replace() limit must be a number".to_string(),
7498                                    ))
7499                                }
7500                            }
7501                        } else {
7502                            None
7503                        };
7504                        Ok(functions::string::replace(
7505                            s,
7506                            &evaluated_args[1],
7507                            replacement,
7508                            limit,
7509                        )?)
7510                    }
7511                    _ => Err(EvaluatorError::TypeError(
7512                        "replace() requires string arguments".to_string(),
7513                    )),
7514                }
7515            }
7516            "match" => {
7517                // $match(str, pattern [, limit])
7518                // Returns array of match objects for regex matches or custom matcher function
7519                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7520                    return Err(EvaluatorError::EvaluationError(
7521                        "match() requires 1 to 3 arguments".to_string(),
7522                    ));
7523                }
7524                if evaluated_args[0].is_null() {
7525                    return Ok(JValue::Null);
7526                }
7527                if evaluated_args[0].is_undefined() {
7528                    return Ok(JValue::Undefined);
7529                }
7530
7531                let s = match &evaluated_args[0] {
7532                    JValue::String(s) => s.clone(),
7533                    _ => {
7534                        return Err(EvaluatorError::TypeError(
7535                            "match() first argument must be a string".to_string(),
7536                        ))
7537                    }
7538                };
7539
7540                // Get optional limit
7541                let limit = if evaluated_args.len() == 3 {
7542                    match &evaluated_args[2] {
7543                        JValue::Number(n) => Some(*n as usize),
7544                        JValue::Null => None,
7545                        _ => {
7546                            return Err(EvaluatorError::TypeError(
7547                                "match() limit must be a number".to_string(),
7548                            ))
7549                        }
7550                    }
7551                } else {
7552                    None
7553                };
7554
7555                // Check if second argument is a custom matcher function (lambda)
7556                let pattern_value = evaluated_args.get(1);
7557                let is_custom_matcher = pattern_value.is_some_and(|val| {
7558                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7559                });
7560
7561                if is_custom_matcher {
7562                    // Custom matcher function support
7563                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7564                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
7565                }
7566
7567                // Get regex pattern from second argument
7568                let (pattern, flags) = match pattern_value {
7569                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7570                        EvaluatorError::TypeError(
7571                            "match() second argument must be a regex pattern or matcher function"
7572                                .to_string(),
7573                        )
7574                    })?,
7575                    None => (".*".to_string(), "".to_string()),
7576                };
7577
7578                // Build regex
7579                let is_global = flags.contains('g');
7580                let regex_pattern = if flags.contains('i') {
7581                    format!("(?i){}", pattern)
7582                } else {
7583                    pattern.clone()
7584                };
7585
7586                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
7587                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7588                })?;
7589
7590                let mut results = Vec::new();
7591                let mut count = 0;
7592
7593                for caps in re.captures_iter(&s) {
7594                    if let Some(lim) = limit {
7595                        if count >= lim {
7596                            break;
7597                        }
7598                    }
7599
7600                    let full_match = caps.get(0).unwrap();
7601                    let mut match_obj = IndexMap::new();
7602                    match_obj.insert(
7603                        "match".to_string(),
7604                        JValue::string(full_match.as_str().to_string()),
7605                    );
7606                    match_obj.insert(
7607                        "index".to_string(),
7608                        JValue::Number(full_match.start() as f64),
7609                    );
7610
7611                    // Collect capture groups
7612                    let mut groups: Vec<JValue> = Vec::new();
7613                    for i in 1..caps.len() {
7614                        if let Some(group) = caps.get(i) {
7615                            groups.push(JValue::string(group.as_str().to_string()));
7616                        } else {
7617                            groups.push(JValue::Null);
7618                        }
7619                    }
7620                    if !groups.is_empty() {
7621                        match_obj.insert("groups".to_string(), JValue::array(groups));
7622                    }
7623
7624                    results.push(JValue::object(match_obj));
7625                    count += 1;
7626
7627                    // If not global, only return first match
7628                    if !is_global {
7629                        break;
7630                    }
7631                }
7632
7633                if results.is_empty() {
7634                    Ok(JValue::Null)
7635                } else if results.len() == 1 && !is_global {
7636                    // Single match (non-global) returns the match object directly
7637                    Ok(results.into_iter().next().unwrap())
7638                } else {
7639                    Ok(JValue::array(results))
7640                }
7641            }
7642            "max" => {
7643                if evaluated_args.len() != 1 {
7644                    return Err(EvaluatorError::EvaluationError(
7645                        "max() requires exactly 1 argument".to_string(),
7646                    ));
7647                }
7648                // Check for undefined
7649                if evaluated_args[0].is_undefined() {
7650                    return Ok(JValue::Undefined);
7651                }
7652                match &evaluated_args[0] {
7653                    JValue::Null => Ok(JValue::Null),
7654                    JValue::Array(arr) => {
7655                        // Use zero-clone iterator-based max
7656                        Ok(aggregation::max(arr)?)
7657                    }
7658                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7659                    _ => Err(EvaluatorError::TypeError(
7660                        "max() requires an array or number argument".to_string(),
7661                    )),
7662                }
7663            }
7664            "min" => {
7665                if evaluated_args.len() != 1 {
7666                    return Err(EvaluatorError::EvaluationError(
7667                        "min() requires exactly 1 argument".to_string(),
7668                    ));
7669                }
7670                // Check for undefined
7671                if evaluated_args[0].is_undefined() {
7672                    return Ok(JValue::Undefined);
7673                }
7674                match &evaluated_args[0] {
7675                    JValue::Null => Ok(JValue::Null),
7676                    JValue::Array(arr) => {
7677                        // Use zero-clone iterator-based min
7678                        Ok(aggregation::min(arr)?)
7679                    }
7680                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7681                    _ => Err(EvaluatorError::TypeError(
7682                        "min() requires an array or number argument".to_string(),
7683                    )),
7684                }
7685            }
7686            "average" => {
7687                if evaluated_args.len() != 1 {
7688                    return Err(EvaluatorError::EvaluationError(
7689                        "average() requires exactly 1 argument".to_string(),
7690                    ));
7691                }
7692                // Return undefined if argument is undefined
7693                if evaluated_args[0].is_undefined() {
7694                    return Ok(JValue::Undefined);
7695                }
7696                match &evaluated_args[0] {
7697                    JValue::Null => Ok(JValue::Null),
7698                    JValue::Array(arr) => {
7699                        // Use zero-clone iterator-based average
7700                        Ok(aggregation::average(arr)?)
7701                    }
7702                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7703                    _ => Err(EvaluatorError::TypeError(
7704                        "average() requires an array or number argument".to_string(),
7705                    )),
7706                }
7707            }
7708            "abs" => {
7709                if evaluated_args.len() != 1 {
7710                    return Err(EvaluatorError::EvaluationError(
7711                        "abs() requires exactly 1 argument".to_string(),
7712                    ));
7713                }
7714                match &evaluated_args[0] {
7715                    JValue::Null => Ok(JValue::Null),
7716                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
7717                    _ => Err(EvaluatorError::TypeError(
7718                        "abs() requires a number argument".to_string(),
7719                    )),
7720                }
7721            }
7722            "floor" => {
7723                if evaluated_args.len() != 1 {
7724                    return Err(EvaluatorError::EvaluationError(
7725                        "floor() requires exactly 1 argument".to_string(),
7726                    ));
7727                }
7728                match &evaluated_args[0] {
7729                    JValue::Null => Ok(JValue::Null),
7730                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
7731                    _ => Err(EvaluatorError::TypeError(
7732                        "floor() requires a number argument".to_string(),
7733                    )),
7734                }
7735            }
7736            "ceil" => {
7737                if evaluated_args.len() != 1 {
7738                    return Err(EvaluatorError::EvaluationError(
7739                        "ceil() requires exactly 1 argument".to_string(),
7740                    ));
7741                }
7742                match &evaluated_args[0] {
7743                    JValue::Null => Ok(JValue::Null),
7744                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
7745                    _ => Err(EvaluatorError::TypeError(
7746                        "ceil() requires a number argument".to_string(),
7747                    )),
7748                }
7749            }
7750            "round" => {
7751                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7752                    return Err(EvaluatorError::EvaluationError(
7753                        "round() requires 1 or 2 arguments".to_string(),
7754                    ));
7755                }
7756                match &evaluated_args[0] {
7757                    JValue::Null => Ok(JValue::Null),
7758                    JValue::Number(n) => {
7759                        let precision = if evaluated_args.len() == 2 {
7760                            match &evaluated_args[1] {
7761                                JValue::Number(p) => Some(*p as i32),
7762                                _ => {
7763                                    return Err(EvaluatorError::TypeError(
7764                                        "round() precision must be a number".to_string(),
7765                                    ))
7766                                }
7767                            }
7768                        } else {
7769                            None
7770                        };
7771                        Ok(functions::numeric::round(*n, precision)?)
7772                    }
7773                    _ => Err(EvaluatorError::TypeError(
7774                        "round() requires a number argument".to_string(),
7775                    )),
7776                }
7777            }
7778            "sqrt" => {
7779                if evaluated_args.len() != 1 {
7780                    return Err(EvaluatorError::EvaluationError(
7781                        "sqrt() requires exactly 1 argument".to_string(),
7782                    ));
7783                }
7784                match &evaluated_args[0] {
7785                    JValue::Null => Ok(JValue::Null),
7786                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
7787                    _ => Err(EvaluatorError::TypeError(
7788                        "sqrt() requires a number argument".to_string(),
7789                    )),
7790                }
7791            }
7792            "power" => {
7793                if evaluated_args.len() != 2 {
7794                    return Err(EvaluatorError::EvaluationError(
7795                        "power() requires exactly 2 arguments".to_string(),
7796                    ));
7797                }
7798                if evaluated_args[0].is_null() {
7799                    return Ok(JValue::Null);
7800                }
7801                if evaluated_args[0].is_undefined() {
7802                    return Ok(JValue::Undefined);
7803                }
7804                match (&evaluated_args[0], &evaluated_args[1]) {
7805                    (JValue::Number(base), JValue::Number(exp)) => {
7806                        Ok(functions::numeric::power(*base, *exp)?)
7807                    }
7808                    _ => Err(EvaluatorError::TypeError(
7809                        "power() requires number arguments".to_string(),
7810                    )),
7811                }
7812            }
7813            "formatNumber" => {
7814                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7815                    return Err(EvaluatorError::EvaluationError(
7816                        "formatNumber() requires 2 or 3 arguments".to_string(),
7817                    ));
7818                }
7819                if evaluated_args[0].is_null() {
7820                    return Ok(JValue::Null);
7821                }
7822                if evaluated_args[0].is_undefined() {
7823                    return Ok(JValue::Undefined);
7824                }
7825                match (&evaluated_args[0], &evaluated_args[1]) {
7826                    (JValue::Number(num), JValue::String(picture)) => {
7827                        let options = if evaluated_args.len() == 3 {
7828                            Some(&evaluated_args[2])
7829                        } else {
7830                            None
7831                        };
7832                        Ok(functions::numeric::format_number(*num, picture, options)?)
7833                    }
7834                    _ => Err(EvaluatorError::TypeError(
7835                        "formatNumber() requires a number and a string".to_string(),
7836                    )),
7837                }
7838            }
7839            "formatBase" => {
7840                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7841                    return Err(EvaluatorError::EvaluationError(
7842                        "formatBase() requires 1 or 2 arguments".to_string(),
7843                    ));
7844                }
7845                // Handle undefined input
7846                if evaluated_args[0].is_null() {
7847                    return Ok(JValue::Null);
7848                }
7849                if evaluated_args[0].is_undefined() {
7850                    return Ok(JValue::Undefined);
7851                }
7852                match &evaluated_args[0] {
7853                    JValue::Number(num) => {
7854                        let radix = if evaluated_args.len() == 2 {
7855                            match &evaluated_args[1] {
7856                                JValue::Number(r) => Some(r.trunc() as i64),
7857                                _ => {
7858                                    return Err(EvaluatorError::TypeError(
7859                                        "formatBase() radix must be a number".to_string(),
7860                                    ))
7861                                }
7862                            }
7863                        } else {
7864                            None
7865                        };
7866                        Ok(functions::numeric::format_base(*num, radix)?)
7867                    }
7868                    _ => Err(EvaluatorError::TypeError(
7869                        "formatBase() requires a number".to_string(),
7870                    )),
7871                }
7872            }
7873            "formatInteger" => {
7874                if evaluated_args.len() != 2 {
7875                    return Err(EvaluatorError::EvaluationError(
7876                        "formatInteger() requires exactly 2 arguments".to_string(),
7877                    ));
7878                }
7879                match (&evaluated_args[0], &evaluated_args[1]) {
7880                    (JValue::Number(n), JValue::String(picture)) => {
7881                        Ok(crate::datetime::format_integer(*n, picture)?)
7882                    }
7883                    (JValue::Null, _) => Ok(JValue::Null),
7884                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7885                    _ => Err(EvaluatorError::TypeError(
7886                        "formatInteger() requires a number and a string".to_string(),
7887                    )),
7888                }
7889            }
7890            "parseInteger" => {
7891                if evaluated_args.len() != 2 {
7892                    return Err(EvaluatorError::EvaluationError(
7893                        "parseInteger() requires exactly 2 arguments".to_string(),
7894                    ));
7895                }
7896                match (&evaluated_args[0], &evaluated_args[1]) {
7897                    (JValue::String(value), JValue::String(picture)) => {
7898                        Ok(crate::datetime::parse_integer(value, picture)?)
7899                    }
7900                    (JValue::Null, _) => Ok(JValue::Null),
7901                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7902                    _ => Err(EvaluatorError::TypeError(
7903                        "parseInteger() requires a string and a string".to_string(),
7904                    )),
7905                }
7906            }
7907            "append" => {
7908                if evaluated_args.len() != 2 {
7909                    return Err(EvaluatorError::EvaluationError(
7910                        "append() requires exactly 2 arguments".to_string(),
7911                    ));
7912                }
7913                // Handle null/undefined arguments
7914                let first = &evaluated_args[0];
7915                let second = &evaluated_args[1];
7916
7917                // If second arg is null/undefined, return first as-is (no change)
7918                if second.is_null() || second.is_undefined() {
7919                    return Ok(first.clone());
7920                }
7921
7922                // If first arg is null/undefined, return second as-is (appending to nothing gives second)
7923                if first.is_null() || first.is_undefined() {
7924                    return Ok(second.clone());
7925                }
7926
7927                // Convert both to arrays if needed, then append
7928                let arr = match first {
7929                    JValue::Array(a) => a.to_vec(),
7930                    other => vec![other.clone()], // Wrap non-array in array
7931                };
7932
7933                // Pre-check combined size before concatenating, mirroring
7934                // jsonata-js's append() (`arg1.length + arg2.length > options.sequence`).
7935                let second_len = match second {
7936                    JValue::Array(a) => a.len(),
7937                    _ => 1,
7938                };
7939                check_sequence_length(arr.len() + second_len, &self.options)?;
7940
7941                Ok(functions::array::append(&arr, second)?)
7942            }
7943            "reverse" => {
7944                if evaluated_args.len() != 1 {
7945                    return Err(EvaluatorError::EvaluationError(
7946                        "reverse() requires exactly 1 argument".to_string(),
7947                    ));
7948                }
7949                match &evaluated_args[0] {
7950                    JValue::Null => Ok(JValue::Null),
7951                    JValue::Undefined => Ok(JValue::Undefined),
7952                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
7953                    _ => Err(EvaluatorError::TypeError(
7954                        "reverse() requires an array argument".to_string(),
7955                    )),
7956                }
7957            }
7958            "shuffle" => {
7959                if evaluated_args.len() != 1 {
7960                    return Err(EvaluatorError::EvaluationError(
7961                        "shuffle() requires exactly 1 argument".to_string(),
7962                    ));
7963                }
7964                if evaluated_args[0].is_null() {
7965                    return Ok(JValue::Null);
7966                }
7967                if evaluated_args[0].is_undefined() {
7968                    return Ok(JValue::Undefined);
7969                }
7970                match &evaluated_args[0] {
7971                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
7972                    _ => Err(EvaluatorError::TypeError(
7973                        "shuffle() requires an array argument".to_string(),
7974                    )),
7975                }
7976            }
7977
7978            "sift" => {
7979                // $sift(object, function) or $sift(function) - filter object by predicate
7980                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7981                    return Err(EvaluatorError::EvaluationError(
7982                        "sift() requires 1 or 2 arguments".to_string(),
7983                    ));
7984                }
7985
7986                // Determine which argument is the function
7987                let func_arg = if evaluated_args.len() == 1 {
7988                    &args[0]
7989                } else {
7990                    &args[1]
7991                };
7992
7993                // Detect how many parameters the callback expects
7994                let param_count = self.get_callback_param_count(func_arg);
7995
7996                // Helper function to sift a single object
7997                let sift_object = |evaluator: &mut Self,
7998                                   obj: &IndexMap<String, JValue>,
7999                                   func_node: &AstNode,
8000                                   context_data: &JValue,
8001                                   param_count: usize|
8002                 -> Result<JValue, EvaluatorError> {
8003                    // Only create the object value if callback uses 3 parameters
8004                    let obj_value = if param_count >= 3 {
8005                        Some(JValue::object(obj.clone()))
8006                    } else {
8007                        None
8008                    };
8009
8010                    let mut result = IndexMap::new();
8011                    for (key, value) in obj.iter() {
8012                        // Build argument list based on what callback expects
8013                        let call_args = match param_count {
8014                            1 => vec![value.clone()],
8015                            2 => vec![value.clone(), JValue::string(key.clone())],
8016                            _ => vec![
8017                                value.clone(),
8018                                JValue::string(key.clone()),
8019                                obj_value.as_ref().unwrap().clone(),
8020                            ],
8021                        };
8022
8023                        let pred_result =
8024                            evaluator.apply_function(func_node, &call_args, context_data)?;
8025                        if evaluator.is_truthy(&pred_result) {
8026                            result.insert(key.clone(), value.clone());
8027                        }
8028                    }
8029                    // Return undefined for empty results (will be filtered by function application)
8030                    if result.is_empty() {
8031                        Ok(JValue::Undefined)
8032                    } else {
8033                        Ok(JValue::object(result))
8034                    }
8035                };
8036
8037                // Handle partial application - if only 1 arg, use current context as object
8038                if evaluated_args.len() == 1 {
8039                    // $sift(function) - use current context data as object
8040                    match data {
8041                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
8042                        JValue::Array(arr) => {
8043                            // Map sift over each object in the array
8044                            let mut results = Vec::new();
8045                            for item in arr.iter() {
8046                                if let JValue::Object(o) = item {
8047                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
8048                                    // sift_object returns undefined for empty results
8049                                    if !sifted.is_undefined() {
8050                                        results.push(sifted);
8051                                    }
8052                                }
8053                            }
8054                            Ok(JValue::array(results))
8055                        }
8056                        JValue::Null => Ok(JValue::Null),
8057                        _ => Ok(JValue::Undefined),
8058                    }
8059                } else {
8060                    // $sift(object, function)
8061                    match &evaluated_args[0] {
8062                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
8063                        JValue::Null => Ok(JValue::Null),
8064                        _ => Err(EvaluatorError::TypeError(
8065                            "sift() first argument must be an object".to_string(),
8066                        )),
8067                    }
8068                }
8069            }
8070
8071            "zip" => {
8072                if evaluated_args.is_empty() {
8073                    return Err(EvaluatorError::EvaluationError(
8074                        "zip() requires at least 1 argument".to_string(),
8075                    ));
8076                }
8077
8078                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
8079                // If any argument is null/undefined, return empty array
8080                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
8081                for arg in &evaluated_args {
8082                    match arg {
8083                        JValue::Array(arr) => {
8084                            if arr.is_empty() {
8085                                // Empty array means result is empty
8086                                return Ok(JValue::array(vec![]));
8087                            }
8088                            arrays.push(arr.to_vec());
8089                        }
8090                        JValue::Null | JValue::Undefined => {
8091                            // Null/undefined means result is empty
8092                            return Ok(JValue::array(vec![]));
8093                        }
8094                        other => {
8095                            // Wrap non-array values in single-element array
8096                            arrays.push(vec![other.clone()]);
8097                        }
8098                    }
8099                }
8100
8101                if arrays.is_empty() {
8102                    return Ok(JValue::array(vec![]));
8103                }
8104
8105                // Find the length of the shortest array
8106                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
8107
8108                // Zip the arrays together
8109                let mut result = Vec::with_capacity(min_len);
8110                for i in 0..min_len {
8111                    let mut tuple = Vec::with_capacity(arrays.len());
8112                    for array in &arrays {
8113                        tuple.push(array[i].clone());
8114                    }
8115                    result.push(JValue::array(tuple));
8116                }
8117
8118                Ok(JValue::array(result))
8119            }
8120
8121            "sort" => {
8122                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8123                    return Err(EvaluatorError::EvaluationError(
8124                        "sort() requires 1 or 2 arguments".to_string(),
8125                    ));
8126                }
8127
8128                // Use pre-evaluated first argument (avoid double evaluation)
8129                let array_value = &evaluated_args[0];
8130
8131                // Handle undefined input
8132                if array_value.is_null() {
8133                    return Ok(JValue::Null);
8134                }
8135                if array_value.is_undefined() {
8136                    return Ok(JValue::Undefined);
8137                }
8138
8139                let mut arr = match array_value {
8140                    JValue::Array(arr) => arr.to_vec(),
8141                    other => vec![other.clone()],
8142                };
8143
8144                if args.len() == 2 {
8145                    // Sort using the comparator from raw args (need unevaluated lambda AST)
8146                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
8147                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
8148                    Ok(JValue::array(arr))
8149                } else {
8150                    // Default sort (no comparator)
8151                    Ok(functions::array::sort(&arr)?)
8152                }
8153            }
8154            "distinct" => {
8155                if evaluated_args.len() != 1 {
8156                    return Err(EvaluatorError::EvaluationError(
8157                        "distinct() requires exactly 1 argument".to_string(),
8158                    ));
8159                }
8160                match &evaluated_args[0] {
8161                    JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
8162                    // Non-array input, and arrays of length <= 1, pass through
8163                    // unchanged (jsonata-js functions.js:
8164                    // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
8165                    other => Ok(other.clone()),
8166                }
8167            }
8168            "exists" => {
8169                if evaluated_args.len() != 1 {
8170                    return Err(EvaluatorError::EvaluationError(
8171                        "exists() requires exactly 1 argument".to_string(),
8172                    ));
8173                }
8174                Ok(functions::array::exists(&evaluated_args[0])?)
8175            }
8176            "keys" => {
8177                if evaluated_args.len() != 1 {
8178                    return Err(EvaluatorError::EvaluationError(
8179                        "keys() requires exactly 1 argument".to_string(),
8180                    ));
8181                }
8182
8183                // Helper to unwrap single-element arrays
8184                let unwrap_single = |keys: Vec<JValue>| -> JValue {
8185                    if keys.len() == 1 {
8186                        keys.into_iter().next().unwrap()
8187                    } else {
8188                        JValue::array(keys)
8189                    }
8190                };
8191
8192                match &evaluated_args[0] {
8193                    JValue::Null => Ok(JValue::Null),
8194                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
8195                    JValue::Object(obj) => {
8196                        // Return undefined for empty objects
8197                        if obj.is_empty() {
8198                            Ok(JValue::Null)
8199                        } else {
8200                            let keys: Vec<JValue> =
8201                                obj.keys().map(|k| JValue::string(k.clone())).collect();
8202                            check_sequence_length(keys.len(), &self.options)?;
8203                            Ok(unwrap_single(keys))
8204                        }
8205                    }
8206                    JValue::Array(arr) => {
8207                        // For arrays, collect keys from all objects
8208                        let mut all_keys = Vec::new();
8209                        for item in arr.iter() {
8210                            // Skip lambda/builtin values
8211                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
8212                                continue;
8213                            }
8214                            if let JValue::Object(obj) = item {
8215                                for key in obj.keys() {
8216                                    if !all_keys.contains(&JValue::string(key.clone())) {
8217                                        all_keys.push(JValue::string(key.clone()));
8218                                    }
8219                                }
8220                            }
8221                        }
8222                        if all_keys.is_empty() {
8223                            Ok(JValue::Null)
8224                        } else {
8225                            check_sequence_length(all_keys.len(), &self.options)?;
8226                            Ok(unwrap_single(all_keys))
8227                        }
8228                    }
8229                    // Non-object types return undefined
8230                    _ => Ok(JValue::Null),
8231                }
8232            }
8233            "lookup" => {
8234                if evaluated_args.len() != 2 {
8235                    return Err(EvaluatorError::EvaluationError(
8236                        "lookup() requires exactly 2 arguments".to_string(),
8237                    ));
8238                }
8239                if evaluated_args[0].is_null() {
8240                    return Ok(JValue::Null);
8241                }
8242                if evaluated_args[0].is_undefined() {
8243                    return Ok(JValue::Undefined);
8244                }
8245
8246                let key = match &evaluated_args[1] {
8247                    JValue::String(k) => &**k,
8248                    _ => {
8249                        return Err(EvaluatorError::TypeError(
8250                            "lookup() requires a string key".to_string(),
8251                        ))
8252                    }
8253                };
8254
8255                // Helper function to recursively lookup in values
8256                fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
8257                    match val {
8258                        JValue::Array(arr) => {
8259                            let mut results = Vec::new();
8260                            for item in arr.iter() {
8261                                let nested = lookup_recursive(item, key);
8262                                results.extend(nested.iter().cloned());
8263                            }
8264                            results
8265                        }
8266                        JValue::Object(obj) => {
8267                            if let Some(v) = obj.get(key) {
8268                                vec![v.clone()]
8269                            } else {
8270                                vec![]
8271                            }
8272                        }
8273                        _ => vec![],
8274                    }
8275                }
8276
8277                let results = lookup_recursive(&evaluated_args[0], key);
8278                if results.is_empty() {
8279                    Ok(JValue::Null)
8280                } else if results.len() == 1 {
8281                    Ok(results[0].clone())
8282                } else {
8283                    check_sequence_length(results.len(), &self.options)?;
8284                    Ok(JValue::array(results))
8285                }
8286            }
8287            "spread" => {
8288                if evaluated_args.len() != 1 {
8289                    return Err(EvaluatorError::EvaluationError(
8290                        "spread() requires exactly 1 argument".to_string(),
8291                    ));
8292                }
8293                match &evaluated_args[0] {
8294                    JValue::Null => Ok(JValue::Null),
8295                    // Not a container - pass through unchanged (e.g. so $string() still
8296                    // sees the function value and applies its own function->"" rule).
8297                    lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
8298                    JValue::Object(obj) => {
8299                        // functions::object::spread() always returns an array with one
8300                        // element per key (mirrors jsonata-js's push-per-key loop through
8301                        // this.createSequence()), so it needs the same cap as the
8302                        // array-fanout branch below and as the "keys" arm's single-object
8303                        // branch.
8304                        check_sequence_length(obj.len(), &self.options)?;
8305                        Ok(functions::object::spread(obj)?)
8306                    }
8307                    JValue::Array(arr) => {
8308                        // Spread each object in the array
8309                        let mut result = Vec::new();
8310                        for item in arr.iter() {
8311                            match item {
8312                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
8313                                    // Skip lambdas in array
8314                                    continue;
8315                                }
8316                                JValue::Object(obj) => {
8317                                    let spread_result = functions::object::spread(obj)?;
8318                                    if let JValue::Array(spread_items) = spread_result {
8319                                        result.extend(spread_items.iter().cloned());
8320                                    } else {
8321                                        result.push(spread_result);
8322                                    }
8323                                }
8324                                // Non-objects in array are returned unchanged
8325                                other => result.push(other.clone()),
8326                            }
8327                        }
8328                        check_sequence_length(result.len(), &self.options)?;
8329                        Ok(JValue::array(result))
8330                    }
8331                    // Non-objects are returned unchanged
8332                    other => Ok(other.clone()),
8333                }
8334            }
8335            "merge" => {
8336                if evaluated_args.is_empty() {
8337                    return Err(EvaluatorError::EvaluationError(
8338                        "merge() requires at least 1 argument".to_string(),
8339                    ));
8340                }
8341                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
8342                // vs multiple object arguments: $merge(obj1, obj2)
8343                if evaluated_args.len() == 1 {
8344                    match &evaluated_args[0] {
8345                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
8346                        JValue::Null => Ok(JValue::Null),
8347                        JValue::Undefined => Ok(JValue::Undefined),
8348                        JValue::Object(_) => {
8349                            // Single object - just return it
8350                            Ok(evaluated_args[0].clone())
8351                        }
8352                        _ => Err(EvaluatorError::TypeError(
8353                            "merge() requires objects or an array of objects".to_string(),
8354                        )),
8355                    }
8356                } else {
8357                    Ok(functions::object::merge(&evaluated_args)?)
8358                }
8359            }
8360
8361            "map" => {
8362                if args.len() != 2 {
8363                    return Err(EvaluatorError::EvaluationError(
8364                        "map() requires exactly 2 arguments".to_string(),
8365                    ));
8366                }
8367
8368                // Evaluate the array argument
8369                let array = self.evaluate_internal(&args[0], data)?;
8370
8371                match array {
8372                    JValue::Array(arr) => {
8373                        // Detect how many parameters the callback expects
8374                        let param_count = self.get_callback_param_count(&args[1]);
8375
8376                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
8377                        if param_count == 1 {
8378                            if let AstNode::Lambda {
8379                                params,
8380                                body,
8381                                signature: None,
8382                                thunk: false,
8383                            } = &args[1]
8384                            {
8385                                let var_refs: Vec<&str> =
8386                                    params.iter().map(|s| s.as_str()).collect();
8387                                if let Some(compiled) =
8388                                    try_compile_expr_with_allowed_vars(body, &var_refs)
8389                                {
8390                                    let param_name = params[0].as_str();
8391                                    let mut result = Vec::with_capacity(arr.len());
8392                                    let mut vars = HashMap::new();
8393                                    for item in arr.iter() {
8394                                        vars.insert(param_name, item);
8395                                        let mapped = eval_compiled(
8396                                            &compiled,
8397                                            data,
8398                                            Some(&vars),
8399                                            &self.options,
8400                                            self.start_time,
8401                                        )?;
8402                                        if !mapped.is_undefined() {
8403                                            result.push(mapped);
8404                                        }
8405                                    }
8406                                    check_sequence_length(result.len(), &self.options)?;
8407                                    return Ok(JValue::array(result));
8408                                }
8409                            }
8410                            // Stored lambda variable fast path: $var with pre-compiled body
8411                            if let AstNode::Variable(var_name) = &args[1] {
8412                                if let Some(stored) = self.context.lookup_lambda(var_name) {
8413                                    if let Some(ref ce) = stored.compiled_body.clone() {
8414                                        let param_name = stored.params[0].clone();
8415                                        let captured_data = stored.captured_data.clone();
8416                                        let captured_env_clone = stored.captured_env.clone();
8417                                        let ce_clone = ce.clone();
8418                                        if !captured_env_clone.values().any(|v| {
8419                                            matches!(
8420                                                v,
8421                                                JValue::Lambda { .. } | JValue::Builtin { .. }
8422                                            )
8423                                        }) {
8424                                            let call_data = captured_data.as_ref().unwrap_or(data);
8425                                            let mut result = Vec::with_capacity(arr.len());
8426                                            let mut vars: HashMap<&str, &JValue> =
8427                                                captured_env_clone
8428                                                    .iter()
8429                                                    .map(|(k, v)| (k.as_str(), v))
8430                                                    .collect();
8431                                            for item in arr.iter() {
8432                                                vars.insert(param_name.as_str(), item);
8433                                                let mapped = eval_compiled(
8434                                                    &ce_clone,
8435                                                    call_data,
8436                                                    Some(&vars),
8437                                                    &self.options,
8438                                                    self.start_time,
8439                                                )?;
8440                                                if !mapped.is_undefined() {
8441                                                    result.push(mapped);
8442                                                }
8443                                            }
8444                                            check_sequence_length(result.len(), &self.options)?;
8445                                            return Ok(JValue::array(result));
8446                                        }
8447                                    }
8448                                }
8449                            }
8450                        }
8451
8452                        // Only create the array value if callback uses 3 parameters
8453                        let arr_value = if param_count >= 3 {
8454                            Some(JValue::Array(arr.clone()))
8455                        } else {
8456                            None
8457                        };
8458
8459                        let mut result = Vec::with_capacity(arr.len());
8460                        for (index, item) in arr.iter().enumerate() {
8461                            // Build argument list based on what callback expects
8462                            let call_args = match param_count {
8463                                1 => vec![item.clone()],
8464                                2 => vec![item.clone(), JValue::Number(index as f64)],
8465                                _ => vec![
8466                                    item.clone(),
8467                                    JValue::Number(index as f64),
8468                                    arr_value.as_ref().unwrap().clone(),
8469                                ],
8470                            };
8471
8472                            let mapped = self.apply_function(&args[1], &call_args, data)?;
8473                            // Filter out undefined results but keep explicit null (JSONata map semantics)
8474                            // undefined comes from missing else clause, null is explicit
8475                            if !mapped.is_undefined() {
8476                                result.push(mapped);
8477                            }
8478                        }
8479                        check_sequence_length(result.len(), &self.options)?;
8480                        Ok(JValue::array(result))
8481                    }
8482                    JValue::Null => Ok(JValue::Null),
8483                    JValue::Undefined => Ok(JValue::Undefined),
8484                    _ => Err(EvaluatorError::TypeError(
8485                        "map() first argument must be an array".to_string(),
8486                    )),
8487                }
8488            }
8489
8490            "filter" => {
8491                if args.len() != 2 {
8492                    return Err(EvaluatorError::EvaluationError(
8493                        "filter() requires exactly 2 arguments".to_string(),
8494                    ));
8495                }
8496
8497                // Evaluate the array argument
8498                let array = self.evaluate_internal(&args[0], data)?;
8499
8500                // Handle undefined input - return undefined
8501                if array.is_undefined() {
8502                    return Ok(JValue::Undefined);
8503                }
8504
8505                // Handle null input
8506                if array.is_null() {
8507                    return Ok(JValue::Undefined);
8508                }
8509
8510                // Coerce non-array values to single-element arrays
8511                // Track if input was a single value to unwrap result appropriately
8512                // Use references to avoid upfront cloning of all elements
8513                let single_holder;
8514                let (items, was_single_value): (&[JValue], bool) = match &array {
8515                    JValue::Array(arr) => (arr.as_slice(), false),
8516                    _ => {
8517                        single_holder = [array];
8518                        (&single_holder[..], true)
8519                    }
8520                };
8521
8522                // Detect how many parameters the callback expects
8523                let param_count = self.get_callback_param_count(&args[1]);
8524
8525                // CompiledExpr fast path: direct lambda with 1 param, compilable body
8526                if param_count == 1 {
8527                    if let AstNode::Lambda {
8528                        params,
8529                        body,
8530                        signature: None,
8531                        thunk: false,
8532                    } = &args[1]
8533                    {
8534                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8535                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8536                        {
8537                            let param_name = params[0].as_str();
8538                            let mut result = Vec::with_capacity(items.len() / 2);
8539                            let mut vars = HashMap::new();
8540                            for item in items.iter() {
8541                                vars.insert(param_name, item);
8542                                let pred_result = eval_compiled(
8543                                    &compiled,
8544                                    data,
8545                                    Some(&vars),
8546                                    &self.options,
8547                                    self.start_time,
8548                                )?;
8549                                if compiled_is_truthy(&pred_result) {
8550                                    result.push(item.clone());
8551                                }
8552                            }
8553                            if was_single_value {
8554                                if result.len() == 1 {
8555                                    return Ok(result.remove(0));
8556                                } else if result.is_empty() {
8557                                    return Ok(JValue::Undefined);
8558                                }
8559                            }
8560                            check_sequence_length(result.len(), &self.options)?;
8561                            return Ok(JValue::array(result));
8562                        }
8563                    }
8564                    // Stored lambda variable fast path: $var with pre-compiled body
8565                    if let AstNode::Variable(var_name) = &args[1] {
8566                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8567                            if let Some(ref ce) = stored.compiled_body.clone() {
8568                                let param_name = stored.params[0].clone();
8569                                let captured_data = stored.captured_data.clone();
8570                                let captured_env_clone = stored.captured_env.clone();
8571                                let ce_clone = ce.clone();
8572                                if !captured_env_clone.values().any(|v| {
8573                                    matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8574                                }) {
8575                                    let call_data = captured_data.as_ref().unwrap_or(data);
8576                                    let mut result = Vec::with_capacity(items.len() / 2);
8577                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
8578                                        .iter()
8579                                        .map(|(k, v)| (k.as_str(), v))
8580                                        .collect();
8581                                    for item in items.iter() {
8582                                        vars.insert(param_name.as_str(), item);
8583                                        let pred_result = eval_compiled(
8584                                            &ce_clone,
8585                                            call_data,
8586                                            Some(&vars),
8587                                            &self.options,
8588                                            self.start_time,
8589                                        )?;
8590                                        if compiled_is_truthy(&pred_result) {
8591                                            result.push(item.clone());
8592                                        }
8593                                    }
8594                                    if was_single_value {
8595                                        if result.len() == 1 {
8596                                            return Ok(result.remove(0));
8597                                        } else if result.is_empty() {
8598                                            return Ok(JValue::Undefined);
8599                                        }
8600                                    }
8601                                    check_sequence_length(result.len(), &self.options)?;
8602                                    return Ok(JValue::array(result));
8603                                }
8604                            }
8605                        }
8606                    }
8607                }
8608
8609                // Only create the array value if callback uses 3 parameters
8610                let arr_value = if param_count >= 3 {
8611                    Some(JValue::array(items.to_vec()))
8612                } else {
8613                    None
8614                };
8615
8616                let mut result = Vec::with_capacity(items.len() / 2);
8617
8618                for (index, item) in items.iter().enumerate() {
8619                    // Build argument list based on what callback expects
8620                    let call_args = match param_count {
8621                        1 => vec![item.clone()],
8622                        2 => vec![item.clone(), JValue::Number(index as f64)],
8623                        _ => vec![
8624                            item.clone(),
8625                            JValue::Number(index as f64),
8626                            arr_value.as_ref().unwrap().clone(),
8627                        ],
8628                    };
8629
8630                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8631                    if self.is_truthy(&predicate_result) {
8632                        result.push(item.clone());
8633                    }
8634                }
8635
8636                // If input was a single value, return the single matching item
8637                // (or undefined if no match)
8638                if was_single_value {
8639                    if result.len() == 1 {
8640                        return Ok(result.remove(0));
8641                    } else if result.is_empty() {
8642                        return Ok(JValue::Undefined);
8643                    }
8644                }
8645
8646                check_sequence_length(result.len(), &self.options)?;
8647                Ok(JValue::array(result))
8648            }
8649
8650            "reduce" => {
8651                if args.len() < 2 || args.len() > 3 {
8652                    return Err(EvaluatorError::EvaluationError(
8653                        "reduce() requires 2 or 3 arguments".to_string(),
8654                    ));
8655                }
8656
8657                // Check that the callback function has at least 2 parameters
8658                if let AstNode::Lambda { params, .. } = &args[1] {
8659                    if params.len() < 2 {
8660                        return Err(EvaluatorError::EvaluationError(
8661                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
8662                        ));
8663                    }
8664                } else if let AstNode::Function { name, .. } = &args[1] {
8665                    // For now, we can't validate built-in function signatures here
8666                    // But user-defined functions via lambda will be validated above
8667                    let _ = name; // avoid unused warning
8668                }
8669
8670                // Evaluate the array argument
8671                let array = self.evaluate_internal(&args[0], data)?;
8672
8673                // Convert single value to array (JSONata reduce accepts single values)
8674                // Use references to avoid upfront cloning of all elements
8675                let single_holder;
8676                let items: &[JValue] = match &array {
8677                    JValue::Array(arr) => arr.as_slice(),
8678                    JValue::Null => return Ok(JValue::Null),
8679                    _ => {
8680                        single_holder = [array];
8681                        &single_holder[..]
8682                    }
8683                };
8684
8685                if items.is_empty() {
8686                    // Return initial value if provided, otherwise null
8687                    return if args.len() == 3 {
8688                        self.evaluate_internal(&args[2], data)
8689                    } else {
8690                        Ok(JValue::Null)
8691                    };
8692                }
8693
8694                // Get initial accumulator
8695                let mut accumulator = if args.len() == 3 {
8696                    self.evaluate_internal(&args[2], data)?
8697                } else {
8698                    items[0].clone()
8699                };
8700
8701                let start_idx = if args.len() == 3 { 0 } else { 1 };
8702
8703                // Detect how many parameters the callback expects
8704                let param_count = self.get_callback_param_count(&args[1]);
8705
8706                // CompiledExpr fast path: direct lambda with 2 params, compilable body
8707                if param_count == 2 {
8708                    if let AstNode::Lambda {
8709                        params,
8710                        body,
8711                        signature: None,
8712                        thunk: false,
8713                    } = &args[1]
8714                    {
8715                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8716                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8717                        {
8718                            let acc_name = params[0].as_str();
8719                            let item_name = params[1].as_str();
8720                            for item in items[start_idx..].iter() {
8721                                let vars: HashMap<&str, &JValue> =
8722                                    HashMap::from([(acc_name, &accumulator), (item_name, item)]);
8723                                accumulator = eval_compiled(
8724                                    &compiled,
8725                                    data,
8726                                    Some(&vars),
8727                                    &self.options,
8728                                    self.start_time,
8729                                )?;
8730                            }
8731                            return Ok(accumulator);
8732                        }
8733                    }
8734                    // Stored lambda variable fast path: $var with pre-compiled body
8735                    if let AstNode::Variable(var_name) = &args[1] {
8736                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8737                            if stored.params.len() == 2 {
8738                                if let Some(ref ce) = stored.compiled_body.clone() {
8739                                    let acc_param = stored.params[0].clone();
8740                                    let item_param = stored.params[1].clone();
8741                                    let captured_data = stored.captured_data.clone();
8742                                    let captured_env_clone = stored.captured_env.clone();
8743                                    let ce_clone = ce.clone();
8744                                    if !captured_env_clone.values().any(|v| {
8745                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8746                                    }) {
8747                                        let call_data = captured_data.as_ref().unwrap_or(data);
8748                                        for item in items[start_idx..].iter() {
8749                                            let mut vars: HashMap<&str, &JValue> =
8750                                                captured_env_clone
8751                                                    .iter()
8752                                                    .map(|(k, v)| (k.as_str(), v))
8753                                                    .collect();
8754                                            vars.insert(acc_param.as_str(), &accumulator);
8755                                            vars.insert(item_param.as_str(), item);
8756                                            // Evaluate and drop vars before assigning accumulator
8757                                            // to satisfy borrow checker (vars borrows accumulator)
8758                                            let new_acc = eval_compiled(
8759                                                &ce_clone,
8760                                                call_data,
8761                                                Some(&vars),
8762                                                &self.options,
8763                                                self.start_time,
8764                                            )?;
8765                                            drop(vars);
8766                                            accumulator = new_acc;
8767                                        }
8768                                        return Ok(accumulator);
8769                                    }
8770                                }
8771                            }
8772                        }
8773                    }
8774                }
8775
8776                // Only create the array value if callback uses 4 parameters
8777                let arr_value = if param_count >= 4 {
8778                    Some(JValue::array(items.to_vec()))
8779                } else {
8780                    None
8781                };
8782
8783                // Apply function to each element
8784                for (idx, item) in items[start_idx..].iter().enumerate() {
8785                    // For reduce, the function receives (accumulator, value, index, array)
8786                    // Callbacks may use any subset of these parameters
8787                    let actual_idx = start_idx + idx;
8788
8789                    // Build argument list based on what callback expects
8790                    let call_args = match param_count {
8791                        2 => vec![accumulator.clone(), item.clone()],
8792                        3 => vec![
8793                            accumulator.clone(),
8794                            item.clone(),
8795                            JValue::Number(actual_idx as f64),
8796                        ],
8797                        _ => vec![
8798                            accumulator.clone(),
8799                            item.clone(),
8800                            JValue::Number(actual_idx as f64),
8801                            arr_value.as_ref().unwrap().clone(),
8802                        ],
8803                    };
8804
8805                    accumulator = self.apply_function(&args[1], &call_args, data)?;
8806                }
8807
8808                Ok(accumulator)
8809            }
8810
8811            "single" => {
8812                if args.is_empty() || args.len() > 2 {
8813                    return Err(EvaluatorError::EvaluationError(
8814                        "single() requires 1 or 2 arguments".to_string(),
8815                    ));
8816                }
8817
8818                // Evaluate the array argument
8819                let array = self.evaluate_internal(&args[0], data)?;
8820
8821                // Convert to array (wrap single values)
8822                let arr = match array {
8823                    JValue::Array(arr) => arr.to_vec(),
8824                    JValue::Null => return Ok(JValue::Null),
8825                    other => vec![other],
8826                };
8827
8828                if args.len() == 1 {
8829                    // No predicate - array must have exactly 1 element
8830                    match arr.len() {
8831                        0 => Err(EvaluatorError::EvaluationError(
8832                            "single() argument is empty".to_string(),
8833                        )),
8834                        1 => Ok(arr.into_iter().next().unwrap()),
8835                        count => Err(EvaluatorError::EvaluationError(format!(
8836                            "single() argument has {} values (expected exactly 1)",
8837                            count
8838                        ))),
8839                    }
8840                } else {
8841                    // With predicate - find exactly 1 matching element
8842                    let arr_value = JValue::array(arr.clone());
8843                    let mut matches = Vec::new();
8844                    for (index, item) in arr.into_iter().enumerate() {
8845                        // Apply predicate function with (item, index, array)
8846                        let predicate_result = self.apply_function(
8847                            &args[1],
8848                            &[
8849                                item.clone(),
8850                                JValue::Number(index as f64),
8851                                arr_value.clone(),
8852                            ],
8853                            data,
8854                        )?;
8855                        if self.is_truthy(&predicate_result) {
8856                            matches.push(item);
8857                        }
8858                    }
8859
8860                    match matches.len() {
8861                        0 => Err(EvaluatorError::EvaluationError(
8862                            "single() predicate matches no values".to_string(),
8863                        )),
8864                        1 => Ok(matches.into_iter().next().unwrap()),
8865                        count => Err(EvaluatorError::EvaluationError(format!(
8866                            "single() predicate matches {} values (expected exactly 1)",
8867                            count
8868                        ))),
8869                    }
8870                }
8871            }
8872
8873            "each" => {
8874                // $each(object, function) - iterate over object, applying function to each value/key pair
8875                // Returns an array of the function results
8876                if args.is_empty() || args.len() > 2 {
8877                    return Err(EvaluatorError::EvaluationError(
8878                        "each() requires 1 or 2 arguments".to_string(),
8879                    ));
8880                }
8881
8882                // Determine which argument is the object and which is the function
8883                let (obj_value, func_arg) = if args.len() == 1 {
8884                    // Single argument: use current data as object
8885                    (data.clone(), &args[0])
8886                } else {
8887                    // Two arguments: first is object, second is function
8888                    (self.evaluate_internal(&args[0], data)?, &args[1])
8889                };
8890
8891                // Detect how many parameters the callback expects
8892                let param_count = self.get_callback_param_count(func_arg);
8893
8894                match obj_value {
8895                    JValue::Object(obj) => {
8896                        let mut result = Vec::new();
8897                        for (key, value) in obj.iter() {
8898                            // Build argument list based on what callback expects
8899                            // The callback receives the value as the first argument and key as second
8900                            let call_args = match param_count {
8901                                1 => vec![value.clone()],
8902                                _ => vec![value.clone(), JValue::string(key.clone())],
8903                            };
8904
8905                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
8906                            // Skip undefined results (similar to map behavior)
8907                            if !fn_result.is_null() && !fn_result.is_undefined() {
8908                                result.push(fn_result);
8909                            }
8910                        }
8911                        check_sequence_length(result.len(), &self.options)?;
8912                        Ok(JValue::array(result))
8913                    }
8914                    JValue::Null => Ok(JValue::Null),
8915                    _ => Err(EvaluatorError::TypeError(
8916                        "each() first argument must be an object".to_string(),
8917                    )),
8918                }
8919            }
8920
8921            "not" => {
8922                if evaluated_args.len() != 1 {
8923                    return Err(EvaluatorError::EvaluationError(
8924                        "not() requires exactly 1 argument".to_string(),
8925                    ));
8926                }
8927                // $not(x) returns the logical negation of x
8928                // null is falsy, so $not(null) = true; undefined stays undefined
8929                if evaluated_args[0].is_undefined() {
8930                    return Ok(JValue::Undefined);
8931                }
8932                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
8933            }
8934            "boolean" => {
8935                if evaluated_args.len() != 1 {
8936                    return Err(EvaluatorError::EvaluationError(
8937                        "boolean() requires exactly 1 argument".to_string(),
8938                    ));
8939                }
8940                if evaluated_args[0].is_undefined() {
8941                    return Ok(JValue::Undefined);
8942                }
8943                Ok(functions::boolean::boolean(&evaluated_args[0])?)
8944            }
8945            "type" => {
8946                if evaluated_args.len() != 1 {
8947                    return Err(EvaluatorError::EvaluationError(
8948                        "type() requires exactly 1 argument".to_string(),
8949                    ));
8950                }
8951                // Return type string
8952                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
8953                // We use a special marker object to distinguish undefined from null
8954                match &evaluated_args[0] {
8955                    JValue::Null => Ok(JValue::string("null")),
8956                    JValue::Bool(_) => Ok(JValue::string("boolean")),
8957                    JValue::Number(_) => Ok(JValue::string("number")),
8958                    JValue::String(_) => Ok(JValue::string("string")),
8959                    JValue::Array(_) => Ok(JValue::string("array")),
8960                    JValue::Object(_) => Ok(JValue::string("object")),
8961                    JValue::Undefined => Ok(JValue::Undefined),
8962                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
8963                        Ok(JValue::string("function"))
8964                    }
8965                    JValue::Regex { .. } => Ok(JValue::string("regex")),
8966                }
8967            }
8968
8969            "base64encode" => {
8970                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8971                    return Ok(JValue::Null);
8972                }
8973                if evaluated_args.len() != 1 {
8974                    return Err(EvaluatorError::EvaluationError(
8975                        "base64encode() requires exactly 1 argument".to_string(),
8976                    ));
8977                }
8978                match &evaluated_args[0] {
8979                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
8980                    _ => Err(EvaluatorError::TypeError(
8981                        "base64encode() requires a string argument".to_string(),
8982                    )),
8983                }
8984            }
8985            "base64decode" => {
8986                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8987                    return Ok(JValue::Null);
8988                }
8989                if evaluated_args.len() != 1 {
8990                    return Err(EvaluatorError::EvaluationError(
8991                        "base64decode() requires exactly 1 argument".to_string(),
8992                    ));
8993                }
8994                match &evaluated_args[0] {
8995                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
8996                    _ => Err(EvaluatorError::TypeError(
8997                        "base64decode() requires a string argument".to_string(),
8998                    )),
8999                }
9000            }
9001            "encodeUrlComponent" => {
9002                if evaluated_args.len() != 1 {
9003                    return Err(EvaluatorError::EvaluationError(
9004                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
9005                    ));
9006                }
9007                if evaluated_args[0].is_null() {
9008                    return Ok(JValue::Null);
9009                }
9010                if evaluated_args[0].is_undefined() {
9011                    return Ok(JValue::Undefined);
9012                }
9013                match &evaluated_args[0] {
9014                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
9015                    _ => Err(EvaluatorError::TypeError(
9016                        "encodeUrlComponent() requires a string argument".to_string(),
9017                    )),
9018                }
9019            }
9020            "decodeUrlComponent" => {
9021                if evaluated_args.len() != 1 {
9022                    return Err(EvaluatorError::EvaluationError(
9023                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
9024                    ));
9025                }
9026                if evaluated_args[0].is_null() {
9027                    return Ok(JValue::Null);
9028                }
9029                if evaluated_args[0].is_undefined() {
9030                    return Ok(JValue::Undefined);
9031                }
9032                match &evaluated_args[0] {
9033                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
9034                    _ => Err(EvaluatorError::TypeError(
9035                        "decodeUrlComponent() requires a string argument".to_string(),
9036                    )),
9037                }
9038            }
9039            "encodeUrl" => {
9040                if evaluated_args.len() != 1 {
9041                    return Err(EvaluatorError::EvaluationError(
9042                        "encodeUrl() requires exactly 1 argument".to_string(),
9043                    ));
9044                }
9045                if evaluated_args[0].is_null() {
9046                    return Ok(JValue::Null);
9047                }
9048                if evaluated_args[0].is_undefined() {
9049                    return Ok(JValue::Undefined);
9050                }
9051                match &evaluated_args[0] {
9052                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
9053                    _ => Err(EvaluatorError::TypeError(
9054                        "encodeUrl() requires a string argument".to_string(),
9055                    )),
9056                }
9057            }
9058            "decodeUrl" => {
9059                if evaluated_args.len() != 1 {
9060                    return Err(EvaluatorError::EvaluationError(
9061                        "decodeUrl() requires exactly 1 argument".to_string(),
9062                    ));
9063                }
9064                if evaluated_args[0].is_null() {
9065                    return Ok(JValue::Null);
9066                }
9067                if evaluated_args[0].is_undefined() {
9068                    return Ok(JValue::Undefined);
9069                }
9070                match &evaluated_args[0] {
9071                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
9072                    _ => Err(EvaluatorError::TypeError(
9073                        "decodeUrl() requires a string argument".to_string(),
9074                    )),
9075                }
9076            }
9077
9078            "error" => {
9079                // $error(message) - throw error with custom message
9080                if evaluated_args.is_empty() {
9081                    // No message provided
9082                    return Err(EvaluatorError::EvaluationError(
9083                        "D3137: $error() function evaluated".to_string(),
9084                    ));
9085                }
9086
9087                match &evaluated_args[0] {
9088                    JValue::String(s) => {
9089                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
9090                    }
9091                    _ => Err(EvaluatorError::TypeError(
9092                        "T0410: Argument 1 of function error does not match function signature"
9093                            .to_string(),
9094                    )),
9095                }
9096            }
9097            "assert" => {
9098                // $assert(condition, message) - throw error if condition is false
9099                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9100                    return Err(EvaluatorError::EvaluationError(
9101                        "assert() requires 1 or 2 arguments".to_string(),
9102                    ));
9103                }
9104
9105                // First argument must be a boolean
9106                let condition = match &evaluated_args[0] {
9107                    JValue::Bool(b) => *b,
9108                    _ => {
9109                        return Err(EvaluatorError::TypeError(
9110                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
9111                        ));
9112                    }
9113                };
9114
9115                if !condition {
9116                    let message = if evaluated_args.len() == 2 {
9117                        match &evaluated_args[1] {
9118                            JValue::String(s) => s.clone(),
9119                            _ => Rc::from("$assert() statement failed"),
9120                        }
9121                    } else {
9122                        Rc::from("$assert() statement failed")
9123                    };
9124                    return Err(EvaluatorError::EvaluationError(format!(
9125                        "D3141: {}",
9126                        message
9127                    )));
9128                }
9129
9130                Ok(JValue::Null)
9131            }
9132
9133            "eval" => {
9134                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
9135                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9136                    return Err(EvaluatorError::EvaluationError(
9137                        "T0410: Argument 1 of function $eval must be a string".to_string(),
9138                    ));
9139                }
9140
9141                // If the first argument is null/undefined, return undefined
9142                if evaluated_args[0].is_null() {
9143                    return Ok(JValue::Null);
9144                }
9145                if evaluated_args[0].is_undefined() {
9146                    return Ok(JValue::Undefined);
9147                }
9148
9149                // First argument must be a string expression
9150                let expr_str = match &evaluated_args[0] {
9151                    JValue::String(s) => &**s,
9152                    _ => {
9153                        return Err(EvaluatorError::EvaluationError(
9154                            "T0410: Argument 1 of function $eval must be a string".to_string(),
9155                        ));
9156                    }
9157                };
9158
9159                // Parse the expression
9160                let parsed_ast = match parser::parse(expr_str) {
9161                    Ok(ast) => ast,
9162                    Err(e) => {
9163                        // D3120 is the error code for parse errors in $eval
9164                        return Err(EvaluatorError::EvaluationError(format!(
9165                            "D3120: The expression passed to $eval cannot be parsed: {}",
9166                            e
9167                        )));
9168                    }
9169                };
9170
9171                // Determine the context to use for evaluation
9172                let eval_context = if evaluated_args.len() == 2 {
9173                    &evaluated_args[1]
9174                } else {
9175                    data
9176                };
9177
9178                // Evaluate the parsed expression
9179                match self.evaluate_internal(&parsed_ast, eval_context) {
9180                    Ok(result) => Ok(result),
9181                    Err(e) => {
9182                        // D3121 is the error code for evaluation errors in $eval
9183                        let err_msg = e.to_string();
9184                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
9185                            Err(EvaluatorError::EvaluationError(format!(
9186                                "D3121: {}",
9187                                err_msg
9188                            )))
9189                        } else {
9190                            Err(e)
9191                        }
9192                    }
9193                }
9194            }
9195
9196            "now" => {
9197                if !evaluated_args.is_empty() {
9198                    return Err(EvaluatorError::EvaluationError(
9199                        "now() takes no arguments".to_string(),
9200                    ));
9201                }
9202                Ok(crate::datetime::now())
9203            }
9204
9205            "millis" => {
9206                if !evaluated_args.is_empty() {
9207                    return Err(EvaluatorError::EvaluationError(
9208                        "millis() takes no arguments".to_string(),
9209                    ));
9210                }
9211                Ok(crate::datetime::millis())
9212            }
9213
9214            "toMillis" => {
9215                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9216                    return Err(EvaluatorError::EvaluationError(
9217                        "toMillis() requires 1 or 2 arguments".to_string(),
9218                    ));
9219                }
9220
9221                match &evaluated_args[0] {
9222                    JValue::String(s) => {
9223                        // Optional second argument is a picture string for custom parsing
9224                        if evaluated_args.len() == 2 {
9225                            match &evaluated_args[1] {
9226                                JValue::String(picture) => {
9227                                    // Use custom picture format parsing
9228                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
9229                                }
9230                                JValue::Null => Ok(JValue::Null),
9231                                JValue::Undefined => Ok(JValue::Undefined),
9232                                _ => Err(EvaluatorError::TypeError(
9233                                    "toMillis() second argument must be a string".to_string(),
9234                                )),
9235                            }
9236                        } else {
9237                            // Use ISO 8601 partial date parsing
9238                            Ok(crate::datetime::to_millis(s)?)
9239                        }
9240                    }
9241                    JValue::Null => Ok(JValue::Null),
9242                    JValue::Undefined => Ok(JValue::Undefined),
9243                    _ => Err(EvaluatorError::TypeError(
9244                        "toMillis() requires a string argument".to_string(),
9245                    )),
9246                }
9247            }
9248
9249            "fromMillis" => {
9250                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
9251                    return Err(EvaluatorError::EvaluationError(
9252                        "fromMillis() requires 1 to 3 arguments".to_string(),
9253                    ));
9254                }
9255
9256                match &evaluated_args[0] {
9257                    JValue::Number(n) => {
9258                        let millis = (if n.fract() == 0.0 {
9259                            Ok(*n as i64)
9260                        } else {
9261                            Err(())
9262                        })
9263                        .map_err(|_| {
9264                            EvaluatorError::TypeError(
9265                                "fromMillis() requires an integer".to_string(),
9266                            )
9267                        })?;
9268
9269                        let picture = match evaluated_args.get(1) {
9270                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9271                            Some(JValue::String(s)) => Some(s.to_string()),
9272                            Some(_) => {
9273                                return Err(EvaluatorError::TypeError(
9274                                    "fromMillis() second argument must be a string".to_string(),
9275                                ))
9276                            }
9277                        };
9278                        let timezone = match evaluated_args.get(2) {
9279                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9280                            Some(JValue::String(s)) => Some(s.to_string()),
9281                            Some(_) => {
9282                                return Err(EvaluatorError::TypeError(
9283                                    "fromMillis() third argument must be a string".to_string(),
9284                                ))
9285                            }
9286                        };
9287
9288                        Ok(crate::datetime::from_millis_with_picture(
9289                            millis,
9290                            picture.as_deref(),
9291                            timezone.as_deref(),
9292                        )?)
9293                    }
9294                    JValue::Null => Ok(JValue::Null),
9295                    JValue::Undefined => Ok(JValue::Undefined),
9296                    _ => Err(EvaluatorError::TypeError(
9297                        "fromMillis() requires a number argument".to_string(),
9298                    )),
9299                }
9300            }
9301
9302            _ => Err(EvaluatorError::ReferenceError(format!(
9303                "Unknown function: {}",
9304                name
9305            ))),
9306        }
9307    }
9308
9309    /// Apply a function (lambda or expression) to values
9310    ///
9311    /// This handles both:
9312    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
9313    /// 2. Simple expressions: price * 2 - evaluates with values as context
9314    fn apply_function(
9315        &mut self,
9316        func_node: &AstNode,
9317        values: &[JValue],
9318        data: &JValue,
9319    ) -> Result<JValue, EvaluatorError> {
9320        match func_node {
9321            AstNode::Lambda {
9322                params,
9323                body,
9324                signature,
9325                thunk,
9326            } => {
9327                // Direct lambda - invoke it
9328                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
9329            }
9330            AstNode::Function {
9331                name,
9332                args,
9333                is_builtin,
9334            } => {
9335                // Function call - check if it has placeholders (partial application)
9336                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
9337
9338                if has_placeholder {
9339                    // This is a partial application - evaluate it to get the lambda value
9340                    let partial_lambda =
9341                        self.create_partial_application(name, args, *is_builtin, data)?;
9342
9343                    // Now invoke the partial lambda with the provided values
9344                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
9345                        return self.invoke_stored_lambda(&stored, values, data);
9346                    }
9347                    Err(EvaluatorError::EvaluationError(
9348                        "Failed to apply partial application".to_string(),
9349                    ))
9350                } else {
9351                    // Regular function call without placeholders
9352                    // Evaluate it and apply if it returns a function
9353                    let result = self.evaluate_internal(func_node, data)?;
9354
9355                    // Check if result is a lambda value
9356                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
9357                        return self.invoke_stored_lambda(&stored, values, data);
9358                    }
9359
9360                    // Otherwise just return the result
9361                    Ok(result)
9362                }
9363            }
9364            AstNode::Variable(var_name) => {
9365                // Check if this variable holds a stored lambda
9366                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
9367                    self.invoke_stored_lambda(&stored_lambda, values, data)
9368                } else if let Some(value) = self.context.lookup(var_name).cloned() {
9369                    // Check if this variable holds a lambda value
9370                    // This handles lambdas passed as bound arguments in partial applications
9371                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
9372                        return self.invoke_stored_lambda(&stored, values, data);
9373                    }
9374                    // Regular variable value - evaluate with first value as context
9375                    if values.is_empty() {
9376                        self.evaluate_internal(func_node, data)
9377                    } else {
9378                        self.evaluate_internal(func_node, &values[0])
9379                    }
9380                } else if self.is_builtin_function(var_name) {
9381                    // This is a built-in function reference (e.g., $string, $number)
9382                    // Call it directly with the provided values (already evaluated)
9383                    self.call_builtin_with_values(var_name, values)
9384                } else {
9385                    // Unknown variable - evaluate with first value as context
9386                    if values.is_empty() {
9387                        self.evaluate_internal(func_node, data)
9388                    } else {
9389                        self.evaluate_internal(func_node, &values[0])
9390                    }
9391                }
9392            }
9393            _ => {
9394                // For non-lambda expressions, evaluate with first value as context
9395                if values.is_empty() {
9396                    self.evaluate_internal(func_node, data)
9397                } else {
9398                    self.evaluate_internal(func_node, &values[0])
9399                }
9400            }
9401        }
9402    }
9403
9404    /// Execute a transform operator on the bound $ value
9405    fn execute_transform(
9406        &mut self,
9407        location: &AstNode,
9408        update: &AstNode,
9409        delete: Option<&AstNode>,
9410        _original_data: &JValue,
9411    ) -> Result<JValue, EvaluatorError> {
9412        // Get the input value from $ binding
9413        let input = self
9414            .context
9415            .lookup("$")
9416            .ok_or_else(|| {
9417                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
9418            })?
9419            .clone();
9420
9421        // Evaluate location expression on the input to get objects to transform
9422        let located_objects = self.evaluate_internal(location, &input)?;
9423
9424        // Collect target objects into a vector for comparison
9425        let targets: Vec<JValue> = match located_objects {
9426            JValue::Array(arr) => arr.to_vec(),
9427            JValue::Object(_) => vec![located_objects],
9428            JValue::Null => Vec::new(),
9429            other => vec![other],
9430        };
9431
9432        // Validate update parameter - must be an object constructor
9433        // We need to check this before evaluation in case of errors
9434        // For now, we'll validate after evaluation in the transform helper
9435
9436        // Parse delete field names if provided
9437        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
9438            let delete_val = self.evaluate_internal(delete_node, &input)?;
9439            match delete_val {
9440                JValue::Array(arr) => arr
9441                    .iter()
9442                    .filter_map(|v| match v {
9443                        JValue::String(s) => Some(s.to_string()),
9444                        _ => None,
9445                    })
9446                    .collect(),
9447                JValue::String(s) => vec![s.to_string()],
9448                JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
9449                _ => {
9450                    // Delete parameter must be an array of strings or a string
9451                    return Err(EvaluatorError::EvaluationError(
9452                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
9453                    ));
9454                }
9455            }
9456        } else {
9457            Vec::new()
9458        };
9459
9460        // Recursive helper to apply transformation throughout the structure
9461        fn apply_transform_deep(
9462            evaluator: &mut Evaluator,
9463            value: &JValue,
9464            targets: &[JValue],
9465            update: &AstNode,
9466            delete_fields: &[String],
9467        ) -> Result<JValue, EvaluatorError> {
9468            // Check if this value is one of the targets to transform
9469            // Use JValue's PartialEq for semantic equality comparison
9470            if targets.iter().any(|t| t == value) {
9471                // Transform this object
9472                if let JValue::Object(map_rc) = value.clone() {
9473                    let mut map = (*map_rc).clone();
9474                    let update_val = evaluator.evaluate_internal(update, value)?;
9475                    // Validate that update evaluates to an object or null (undefined)
9476                    match update_val {
9477                        JValue::Object(update_map) => {
9478                            for (key, val) in update_map.iter() {
9479                                map.insert(key.clone(), val.clone());
9480                            }
9481                        }
9482                        JValue::Null | JValue::Undefined => {
9483                            // Null/undefined means no updates, just continue to deletions
9484                        }
9485                        _ => {
9486                            return Err(EvaluatorError::EvaluationError(
9487                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
9488                            ));
9489                        }
9490                    }
9491                    for field in delete_fields {
9492                        map.shift_remove(field);
9493                    }
9494                    return Ok(JValue::object(map));
9495                }
9496                return Ok(value.clone());
9497            }
9498
9499            // Otherwise, recursively process children to find and transform targets
9500            match value {
9501                JValue::Object(map) => {
9502                    let mut new_map = IndexMap::new();
9503                    for (k, v) in map.iter() {
9504                        new_map.insert(
9505                            k.clone(),
9506                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
9507                        );
9508                    }
9509                    Ok(JValue::object(new_map))
9510                }
9511                JValue::Array(arr) => {
9512                    let mut new_arr = Vec::new();
9513                    for item in arr.iter() {
9514                        new_arr.push(apply_transform_deep(
9515                            evaluator,
9516                            item,
9517                            targets,
9518                            update,
9519                            delete_fields,
9520                        )?);
9521                    }
9522                    Ok(JValue::array(new_arr))
9523                }
9524                _ => Ok(value.clone()),
9525            }
9526        }
9527
9528        // Apply transformation recursively starting from input
9529        apply_transform_deep(self, &input, &targets, update, &delete_fields)
9530    }
9531
9532    /// Helper to invoke a lambda with given parameters
9533    fn invoke_lambda(
9534        &mut self,
9535        params: &[String],
9536        body: &AstNode,
9537        signature: Option<&String>,
9538        values: &[JValue],
9539        data: &JValue,
9540        thunk: bool,
9541    ) -> Result<JValue, EvaluatorError> {
9542        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
9543    }
9544
9545    /// Invoke a lambda with optional captured environment (for closures)
9546    fn invoke_lambda_with_env(
9547        &mut self,
9548        params: &[String],
9549        body: &AstNode,
9550        signature: Option<&String>,
9551        values: &[JValue],
9552        data: &JValue,
9553        captured_env: Option<&HashMap<String, JValue>>,
9554        captured_data: Option<&JValue>,
9555        thunk: bool,
9556    ) -> Result<JValue, EvaluatorError> {
9557        // If this is a thunk (has tail calls), use TCO trampoline
9558        if thunk {
9559            let stored = StoredLambda {
9560                params: params.to_vec(),
9561                body: body.clone(),
9562                compiled_body: None, // Thunks use TCO, not the compiled fast path
9563                signature: signature.cloned(),
9564                captured_env: captured_env.cloned().unwrap_or_default(),
9565                captured_data: captured_data.cloned(),
9566                thunk,
9567            };
9568            return self.invoke_lambda_with_tco(&stored, values, data);
9569        }
9570
9571        // Validate signature if present, and get coerced arguments
9572        // Push a new scope for this lambda invocation
9573        self.context.push_scope();
9574
9575        // First apply captured environment (for closures)
9576        if let Some(env) = captured_env {
9577            for (name, value) in env {
9578                self.context.bind(name.clone(), value.clone());
9579            }
9580        }
9581
9582        if let Some(sig_str) = signature {
9583            // Validate and coerce arguments with signature
9584            let coerced_values = match crate::signature::Signature::parse(sig_str) {
9585                Ok(sig) => match sig.validate_and_coerce(values, data) {
9586                    Ok(coerced) => coerced,
9587                    Err(e) => {
9588                        self.context.pop_scope();
9589                        match e {
9590                            crate::signature::SignatureError::UndefinedArgument => {
9591                                return Ok(JValue::Null);
9592                            }
9593                            crate::signature::SignatureError::ArgumentTypeMismatch {
9594                                index,
9595                                expected,
9596                            } => {
9597                                return Err(EvaluatorError::TypeError(
9598                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9599                                    ));
9600                            }
9601                            crate::signature::SignatureError::ArrayTypeMismatch {
9602                                index,
9603                                expected,
9604                            } => {
9605                                return Err(EvaluatorError::TypeError(format!(
9606                                    "T0412: Argument {} of function must be an array of {}",
9607                                    index, expected
9608                                )));
9609                            }
9610                            crate::signature::SignatureError::ContextTypeMismatch {
9611                                index,
9612                                expected,
9613                            } => {
9614                                return Err(EvaluatorError::TypeError(format!(
9615                                    "T0411: Context value at argument {} does not match function signature (expected {})",
9616                                    index, expected
9617                                )));
9618                            }
9619                            _ => {
9620                                return Err(EvaluatorError::TypeError(format!(
9621                                    "Signature validation failed: {}",
9622                                    e
9623                                )));
9624                            }
9625                        }
9626                    }
9627                },
9628                Err(e) => {
9629                    self.context.pop_scope();
9630                    return Err(EvaluatorError::EvaluationError(format!(
9631                        "Invalid signature: {}",
9632                        e
9633                    )));
9634                }
9635            };
9636            // Bind coerced values to params
9637            for (i, param) in params.iter().enumerate() {
9638                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9639                self.context.bind(param.clone(), value);
9640            }
9641        } else {
9642            // No signature - bind directly from values slice (no allocation)
9643            for (i, param) in params.iter().enumerate() {
9644                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9645                self.context.bind(param.clone(), value);
9646            }
9647        }
9648
9649        // Check if this is a partial application (body is a special marker string)
9650        if let AstNode::String(body_str) = body {
9651            if body_str.starts_with("__partial_call:") {
9652                // Parse the partial call info
9653                let parts: Vec<&str> = body_str.split(':').collect();
9654                if parts.len() >= 4 {
9655                    let func_name = parts[1];
9656                    let is_builtin = parts[2] == "true";
9657                    let total_args: usize = parts[3].parse().unwrap_or(0);
9658
9659                    // Get placeholder positions from captured env
9660                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
9661                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
9662                            positions
9663                                .iter()
9664                                .filter_map(|v| v.as_f64().map(|n| n as usize))
9665                                .collect()
9666                        } else {
9667                            vec![]
9668                        }
9669                    } else {
9670                        vec![]
9671                    };
9672
9673                    // Reconstruct the full argument list
9674                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
9675
9676                    // Fill in bound arguments from captured environment
9677                    if let Some(env) = captured_env {
9678                        for (key, value) in env {
9679                            if key.starts_with("__bound_arg_") {
9680                                if let Ok(pos) = key[12..].parse::<usize>() {
9681                                    if pos < total_args {
9682                                        full_args[pos] = value.clone();
9683                                    }
9684                                }
9685                            }
9686                        }
9687                    }
9688
9689                    // Fill in placeholder positions with provided values
9690                    for (i, &pos) in placeholder_positions.iter().enumerate() {
9691                        if pos < total_args {
9692                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
9693                            full_args[pos] = value;
9694                        }
9695                    }
9696
9697                    // Pop lambda scope, then push a new scope for temp args
9698                    self.context.pop_scope();
9699                    self.context.push_scope();
9700
9701                    // Build AST nodes for the function call arguments
9702                    let mut temp_args: Vec<AstNode> = Vec::new();
9703                    for (i, value) in full_args.iter().enumerate() {
9704                        let temp_name = format!("__temp_arg_{}", i);
9705                        self.context.bind(temp_name.clone(), value.clone());
9706                        temp_args.push(AstNode::Variable(temp_name));
9707                    }
9708
9709                    // Call the original function
9710                    let result =
9711                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
9712
9713                    // Pop temp scope
9714                    self.context.pop_scope();
9715
9716                    return result;
9717                }
9718            }
9719        }
9720
9721        // Evaluate lambda body (normal case)
9722        // Use captured_data for lexical scoping if available, otherwise use call-site data
9723        let body_data = captured_data.unwrap_or(data);
9724        let result = self.evaluate_internal(body, body_data)?;
9725
9726        // Pop lambda scope, preserving any lambdas referenced by the return value
9727        // Fast path: scalar results can never contain lambda references
9728        let is_scalar = matches!(
9729            &result,
9730            JValue::Number(_)
9731                | JValue::Bool(_)
9732                | JValue::String(_)
9733                | JValue::Null
9734                | JValue::Undefined
9735        );
9736        if is_scalar {
9737            self.context.pop_scope();
9738        } else {
9739            let lambdas_to_keep = self.extract_lambda_ids(&result);
9740            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9741        }
9742
9743        Ok(result)
9744    }
9745
9746    /// Invoke a lambda with tail call optimization using a trampoline
9747    /// This method uses an iterative loop to handle tail-recursive calls without
9748    /// growing the stack, enabling deep recursion for tail-recursive functions.
9749    fn invoke_lambda_with_tco(
9750        &mut self,
9751        stored_lambda: &StoredLambda,
9752        initial_args: &[JValue],
9753        data: &JValue,
9754    ) -> Result<JValue, EvaluatorError> {
9755        let mut current_lambda = stored_lambda.clone();
9756        let mut current_args = initial_args.to_vec();
9757        let mut current_data = data.clone();
9758
9759        // Maximum number of tail call iterations to prevent infinite loops
9760        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
9761        const MAX_TCO_ITERATIONS: usize = 100_000;
9762        let mut iterations = 0;
9763
9764        // Push a persistent scope for the TCO trampoline loop.
9765        // This scope persists across all iterations so that lambdas defined
9766        // in one iteration (like recursive $iter) remain available in subsequent ones.
9767        self.context.push_scope();
9768
9769        // Trampoline loop - keeps evaluating until we get a final value
9770        let result = loop {
9771            iterations += 1;
9772            // The hardcoded iteration cap is a backstop for when no timeout is
9773            // configured; it must not preempt a configured timeout (which is the
9774            // more specific, user-controlled guardrail). Without this gate, an
9775            // infinite tail-recursive loop with a cheap per-iteration body hits
9776            // this cap in single-digit-to-tens of milliseconds and reports the
9777            // misleading "U1001: Stack overflow" (TCO does not grow the stack;
9778            // there is no depth-500 stack here) instead of D1012, for *any*
9779            // realistic `timeout_ms` (100ms, 1s, the docs' own 5000ms default) -
9780            // defeating the purpose of the timeout guardrail for exactly the
9781            // scenario it exists to catch (see jsonata-js's own `$inf := function
9782            // (){$inf()}; $inf()` guardrails-documentation example).
9783            if self.options.timeout_ms.is_none() && iterations > MAX_TCO_ITERATIONS {
9784                self.context.pop_scope();
9785                return Err(EvaluatorError::EvaluationError(
9786                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
9787                ));
9788            }
9789            if let Err(e) = check_loop_timeout(&self.options, self.start_time) {
9790                self.context.pop_scope();
9791                return Err(e);
9792            }
9793
9794            // Evaluate the lambda body within the persistent scope
9795            let result =
9796                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
9797
9798            match result {
9799                LambdaResult::JValue(v) => break v,
9800                LambdaResult::TailCall { lambda, args, data } => {
9801                    // Continue with the tail call - no stack growth
9802                    current_lambda = *lambda;
9803                    current_args = args;
9804                    current_data = data;
9805                }
9806            }
9807        };
9808
9809        // Pop the persistent TCO scope, preserving lambdas referenced by the result
9810        let lambdas_to_keep = self.extract_lambda_ids(&result);
9811        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9812
9813        Ok(result)
9814    }
9815
9816    /// Evaluate a lambda body, detecting tail calls for TCO
9817    /// Returns either a final value or a tail call continuation.
9818    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
9819    /// manages the persistent scope for the trampoline loop.
9820    fn invoke_lambda_body_for_tco(
9821        &mut self,
9822        lambda: &StoredLambda,
9823        values: &[JValue],
9824        data: &JValue,
9825    ) -> Result<LambdaResult, EvaluatorError> {
9826        // Validate signature if present
9827        let coerced_values = if let Some(sig_str) = &lambda.signature {
9828            match crate::signature::Signature::parse(sig_str) {
9829                Ok(sig) => match sig.validate_and_coerce(values, data) {
9830                    Ok(coerced) => coerced,
9831                    Err(e) => match e {
9832                        crate::signature::SignatureError::UndefinedArgument => {
9833                            return Ok(LambdaResult::JValue(JValue::Null));
9834                        }
9835                        crate::signature::SignatureError::ArgumentTypeMismatch {
9836                            index,
9837                            expected,
9838                        } => {
9839                            return Err(EvaluatorError::TypeError(
9840                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9841                                    ));
9842                        }
9843                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
9844                            return Err(EvaluatorError::TypeError(format!(
9845                                "T0412: Argument {} of function must be an array of {}",
9846                                index, expected
9847                            )));
9848                        }
9849                        crate::signature::SignatureError::ContextTypeMismatch {
9850                            index,
9851                            expected,
9852                        } => {
9853                            return Err(EvaluatorError::TypeError(format!(
9854                                "T0411: Context value at argument {} does not match function signature (expected {})",
9855                                index, expected
9856                            )));
9857                        }
9858                        _ => {
9859                            return Err(EvaluatorError::TypeError(format!(
9860                                "Signature validation failed: {}",
9861                                e
9862                            )));
9863                        }
9864                    },
9865                },
9866                Err(e) => {
9867                    return Err(EvaluatorError::EvaluationError(format!(
9868                        "Invalid signature: {}",
9869                        e
9870                    )));
9871                }
9872            }
9873        } else {
9874            values.to_vec()
9875        };
9876
9877        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
9878        // Apply captured environment
9879        for (name, value) in &lambda.captured_env {
9880            self.context.bind(name.clone(), value.clone());
9881        }
9882
9883        // Bind parameters
9884        for (i, param) in lambda.params.iter().enumerate() {
9885            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
9886            self.context.bind(param.clone(), value);
9887        }
9888
9889        // Evaluate the body with tail call detection
9890        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
9891        self.evaluate_for_tco(&lambda.body, body_data)
9892    }
9893
9894    /// Evaluate an expression for TCO, detecting tail calls
9895    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
9896    fn evaluate_for_tco(
9897        &mut self,
9898        node: &AstNode,
9899        data: &JValue,
9900    ) -> Result<LambdaResult, EvaluatorError> {
9901        match node {
9902            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
9903            AstNode::Conditional {
9904                condition,
9905                then_branch,
9906                else_branch,
9907            } => {
9908                let cond_value = self.evaluate_internal(condition, data)?;
9909                let is_truthy = self.is_truthy(&cond_value);
9910
9911                if is_truthy {
9912                    self.evaluate_for_tco(then_branch, data)
9913                } else if let Some(else_expr) = else_branch {
9914                    self.evaluate_for_tco(else_expr, data)
9915                } else {
9916                    Ok(LambdaResult::JValue(JValue::Null))
9917                }
9918            }
9919
9920            // Block: evaluate all but last normally, last for TCO
9921            AstNode::Block(exprs) => {
9922                if exprs.is_empty() {
9923                    return Ok(LambdaResult::JValue(JValue::Null));
9924                }
9925
9926                // Evaluate all expressions except the last
9927                let mut result = JValue::Null;
9928                for (i, expr) in exprs.iter().enumerate() {
9929                    if i == exprs.len() - 1 {
9930                        // Last expression - evaluate for TCO
9931                        return self.evaluate_for_tco(expr, data);
9932                    } else {
9933                        result = self.evaluate_internal(expr, data)?;
9934                    }
9935                }
9936                Ok(LambdaResult::JValue(result))
9937            }
9938
9939            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
9940            AstNode::Binary {
9941                op: BinaryOp::ColonEqual,
9942                lhs,
9943                rhs,
9944            } => {
9945                // This is var := value; get the variable name
9946                let var_name = match lhs.as_ref() {
9947                    AstNode::Variable(name) => name.clone(),
9948                    _ => {
9949                        // Not a simple variable binding, evaluate normally
9950                        let result = self.evaluate_internal(node, data)?;
9951                        return Ok(LambdaResult::JValue(result));
9952                    }
9953                };
9954
9955                // Check if RHS is a lambda - store it specially
9956                if let AstNode::Lambda {
9957                    params,
9958                    body,
9959                    signature,
9960                    thunk,
9961                } = rhs.as_ref()
9962                {
9963                    let captured_env = self.capture_environment_for(body, params);
9964                    let compiled_body = if !thunk {
9965                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9966                        try_compile_expr_with_allowed_vars(body, &var_refs)
9967                    } else {
9968                        None
9969                    };
9970                    let stored_lambda = StoredLambda {
9971                        params: params.clone(),
9972                        body: (**body).clone(),
9973                        compiled_body,
9974                        signature: signature.clone(),
9975                        captured_env,
9976                        captured_data: Some(data.clone()),
9977                        thunk: *thunk,
9978                    };
9979                    self.context.bind_lambda(var_name, stored_lambda);
9980                    let lambda_repr =
9981                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
9982                    return Ok(LambdaResult::JValue(lambda_repr));
9983                }
9984
9985                // Evaluate the RHS
9986                let value = self.evaluate_internal(rhs, data)?;
9987                self.context.bind(var_name, value.clone());
9988                Ok(LambdaResult::JValue(value))
9989            }
9990
9991            // Function call - this is where TCO happens
9992            AstNode::Function { name, args, .. } => {
9993                // Check if this is a call to a stored lambda (user function)
9994                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
9995                    if stored_lambda.thunk {
9996                        let mut evaluated_args = Vec::with_capacity(args.len());
9997                        for arg in args {
9998                            evaluated_args.push(self.evaluate_internal(arg, data)?);
9999                        }
10000                        return Ok(LambdaResult::TailCall {
10001                            lambda: Box::new(stored_lambda),
10002                            args: evaluated_args,
10003                            data: data.clone(),
10004                        });
10005                    }
10006                }
10007                // Not a thunk lambda - evaluate normally
10008                let result = self.evaluate_internal(node, data)?;
10009                Ok(LambdaResult::JValue(result))
10010            }
10011
10012            // Call node (calling a lambda value)
10013            AstNode::Call { procedure, args } => {
10014                // Evaluate the procedure to get the callable
10015                let callable = self.evaluate_internal(procedure, data)?;
10016
10017                // Check if it's a lambda with TCO
10018                if let JValue::Lambda { lambda_id, .. } = &callable {
10019                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
10020                        if stored_lambda.thunk {
10021                            let mut evaluated_args = Vec::with_capacity(args.len());
10022                            for arg in args {
10023                                evaluated_args.push(self.evaluate_internal(arg, data)?);
10024                            }
10025                            return Ok(LambdaResult::TailCall {
10026                                lambda: Box::new(stored_lambda),
10027                                args: evaluated_args,
10028                                data: data.clone(),
10029                            });
10030                        }
10031                    }
10032                }
10033                // Not a thunk - evaluate normally
10034                let result = self.evaluate_internal(node, data)?;
10035                Ok(LambdaResult::JValue(result))
10036            }
10037
10038            // Variable reference that might be a function call
10039            // This handles cases like $f($x) where $f is referenced by name
10040            AstNode::Variable(_) => {
10041                let result = self.evaluate_internal(node, data)?;
10042                Ok(LambdaResult::JValue(result))
10043            }
10044
10045            // Any other expression - evaluate normally
10046            _ => {
10047                let result = self.evaluate_internal(node, data)?;
10048                Ok(LambdaResult::JValue(result))
10049            }
10050        }
10051    }
10052
10053    /// Match with custom matcher function
10054    ///
10055    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
10056    /// The matcher function is called with the string and returns:
10057    /// { match: string, start: number, end: number, groups: [], next: function }
10058    /// The next function is called repeatedly to get subsequent matches
10059    fn match_with_custom_matcher(
10060        &mut self,
10061        str_value: &str,
10062        matcher_node: &AstNode,
10063        limit: Option<usize>,
10064        data: &JValue,
10065    ) -> Result<JValue, EvaluatorError> {
10066        let mut results = Vec::new();
10067        let mut count = 0;
10068
10069        // Call the matcher function with the string
10070        let str_val = JValue::string(str_value.to_string());
10071        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
10072
10073        // Iterate through matches following the 'next' chain
10074        while !current_match.is_undefined() && !current_match.is_null() {
10075            // Check limit
10076            if let Some(lim) = limit {
10077                if count >= lim {
10078                    break;
10079                }
10080            }
10081
10082            // Extract match information from the result object
10083            if let JValue::Object(ref match_obj) = current_match {
10084                // Validate that this is a proper match object
10085                let has_match = match_obj.contains_key("match");
10086                let has_start = match_obj.contains_key("start");
10087                let has_end = match_obj.contains_key("end");
10088                let has_groups = match_obj.contains_key("groups");
10089                let has_next = match_obj.contains_key("next");
10090
10091                if !has_match && !has_start && !has_end && !has_groups && !has_next {
10092                    // Invalid matcher result - T1010 error
10093                    return Err(EvaluatorError::EvaluationError(
10094                        "T1010: The matcher function did not return the correct object structure"
10095                            .to_string(),
10096                    ));
10097                }
10098
10099                // Build the result match object (match, index, groups)
10100                let mut result_obj = IndexMap::new();
10101
10102                if let Some(match_val) = match_obj.get("match") {
10103                    result_obj.insert("match".to_string(), match_val.clone());
10104                }
10105
10106                if let Some(start_val) = match_obj.get("start") {
10107                    result_obj.insert("index".to_string(), start_val.clone());
10108                }
10109
10110                if let Some(groups_val) = match_obj.get("groups") {
10111                    result_obj.insert("groups".to_string(), groups_val.clone());
10112                }
10113
10114                results.push(JValue::object(result_obj));
10115                count += 1;
10116
10117                // Get the next match by calling the 'next' function
10118                if let Some(next_func) = match_obj.get("next") {
10119                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
10120                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
10121                        continue;
10122                    }
10123                }
10124
10125                // No next function or couldn't call it - stop iteration
10126                break;
10127            } else {
10128                // Not a valid match object
10129                break;
10130            }
10131        }
10132
10133        // Return results
10134        if results.is_empty() {
10135            Ok(JValue::Undefined)
10136        } else {
10137            Ok(JValue::array(results))
10138        }
10139    }
10140
10141    /// Replace with lambda/function callback
10142    ///
10143    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
10144    /// The function receives a match object with: match, start, end, groups
10145    fn replace_with_lambda(
10146        &mut self,
10147        str_value: &JValue,
10148        pattern_value: &JValue,
10149        lambda_value: &JValue,
10150        limit_value: Option<&JValue>,
10151        data: &JValue,
10152    ) -> Result<JValue, EvaluatorError> {
10153        // Extract string
10154        let s = match str_value {
10155            JValue::String(s) => &**s,
10156            _ => {
10157                return Err(EvaluatorError::TypeError(
10158                    "replace() requires string arguments".to_string(),
10159                ))
10160            }
10161        };
10162
10163        // Extract regex pattern
10164        let (pattern, flags) =
10165            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
10166                EvaluatorError::TypeError(
10167                    "replace() pattern must be a regex when using lambda replacement".to_string(),
10168                )
10169            })?;
10170
10171        // Build regex
10172        let re = crate::functions::string::build_regex(&pattern, &flags)?;
10173
10174        // Parse limit
10175        let limit = if let Some(lim_val) = limit_value {
10176            match lim_val {
10177                JValue::Number(n) => {
10178                    let lim_f64 = *n;
10179                    if lim_f64 < 0.0 {
10180                        return Err(EvaluatorError::EvaluationError(format!(
10181                            "D3011: Limit must be non-negative, got {}",
10182                            lim_f64
10183                        )));
10184                    }
10185                    Some(lim_f64 as usize)
10186                }
10187                _ => {
10188                    return Err(EvaluatorError::TypeError(
10189                        "replace() limit must be a number".to_string(),
10190                    ))
10191                }
10192            }
10193        } else {
10194            None
10195        };
10196
10197        // Iterate through matches and replace using lambda
10198        let mut result = String::new();
10199        let mut last_end = 0;
10200        let mut count = 0;
10201
10202        for cap in re.captures_iter(s) {
10203            // Check limit
10204            if let Some(lim) = limit {
10205                if count >= lim {
10206                    break;
10207                }
10208            }
10209
10210            let m = cap.get(0).unwrap();
10211            let match_start = m.start();
10212            let match_end = m.end();
10213            let match_str = m.as_str();
10214
10215            // Add text before match
10216            result.push_str(&s[last_end..match_start]);
10217
10218            // Build match object
10219            let groups: Vec<JValue> = (1..cap.len())
10220                .map(|i| {
10221                    cap.get(i)
10222                        .map(|m| JValue::string(m.as_str().to_string()))
10223                        .unwrap_or(JValue::Null)
10224                })
10225                .collect();
10226
10227            let mut match_map = IndexMap::new();
10228            match_map.insert("match".to_string(), JValue::string(match_str));
10229            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
10230            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
10231            match_map.insert("groups".to_string(), JValue::array(groups));
10232            let match_obj = JValue::object(match_map);
10233
10234            // Invoke lambda with match object
10235            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
10236                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
10237            })?;
10238            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
10239            let replacement_str = match lambda_result {
10240                JValue::String(s) => s,
10241                _ => {
10242                    return Err(EvaluatorError::TypeError(format!(
10243                        "D3012: Replacement function must return a string, got {:?}",
10244                        lambda_result
10245                    )))
10246                }
10247            };
10248
10249            // Add replacement
10250            result.push_str(&replacement_str);
10251
10252            last_end = match_end;
10253            count += 1;
10254        }
10255
10256        // Add remaining text after last match
10257        result.push_str(&s[last_end..]);
10258
10259        Ok(JValue::string(result))
10260    }
10261
10262    /// Capture the current environment bindings for closure support
10263    fn capture_current_environment(&self) -> HashMap<String, JValue> {
10264        self.context.all_bindings()
10265    }
10266
10267    /// Capture only the variables referenced by a lambda body (selective capture).
10268    /// This avoids cloning the entire environment when only a few variables are needed.
10269    fn capture_environment_for(
10270        &self,
10271        body: &AstNode,
10272        params: &[String],
10273    ) -> HashMap<String, JValue> {
10274        let free_vars = Self::collect_free_variables(body, params);
10275        if free_vars.is_empty() {
10276            return HashMap::new();
10277        }
10278        let mut result = HashMap::new();
10279        for var_name in &free_vars {
10280            if let Some(value) = self.context.lookup(var_name) {
10281                result.insert(var_name.clone(), value.clone());
10282            }
10283        }
10284        result
10285    }
10286
10287    /// Collect all free variables in an AST node that are not bound by the given params.
10288    /// A "free variable" is one that is referenced but not defined within the expression.
10289    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
10290        let mut free_vars = HashSet::new();
10291        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
10292        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
10293        free_vars
10294    }
10295
10296    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
10297        match node {
10298            AstNode::Variable(name) => {
10299                if !bound.contains(name.as_str()) {
10300                    free.insert(name.clone());
10301                }
10302            }
10303            AstNode::Function { name, args, .. } => {
10304                // Function name references a variable (e.g., $f(...))
10305                if !bound.contains(name.as_str()) {
10306                    free.insert(name.clone());
10307                }
10308                for arg in args {
10309                    Self::collect_free_vars_walk(arg, bound, free);
10310                }
10311            }
10312            AstNode::Lambda { params, body, .. } => {
10313                // Inner lambda introduces new bindings
10314                let mut inner_bound = bound.clone();
10315                for p in params {
10316                    inner_bound.insert(p.as_str());
10317                }
10318                Self::collect_free_vars_walk(body, &inner_bound, free);
10319            }
10320            AstNode::Binary { op, lhs, rhs } => {
10321                Self::collect_free_vars_walk(lhs, bound, free);
10322                Self::collect_free_vars_walk(rhs, bound, free);
10323                // For ColonEqual, note: the binding is visible after this expr in blocks,
10324                // but block handling takes care of that separately
10325                let _ = op;
10326            }
10327            AstNode::Unary { operand, .. } => {
10328                Self::collect_free_vars_walk(operand, bound, free);
10329            }
10330            AstNode::Path { steps } => {
10331                for step in steps {
10332                    Self::collect_free_vars_walk(&step.node, bound, free);
10333                    for stage in &step.stages {
10334                        match stage {
10335                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
10336                            // An index stage binds a variable; it introduces no
10337                            // free variable references.
10338                            Stage::Index(_) => {}
10339                        }
10340                    }
10341                }
10342            }
10343            AstNode::Call { procedure, args } => {
10344                Self::collect_free_vars_walk(procedure, bound, free);
10345                for arg in args {
10346                    Self::collect_free_vars_walk(arg, bound, free);
10347                }
10348            }
10349            AstNode::Conditional {
10350                condition,
10351                then_branch,
10352                else_branch,
10353            } => {
10354                Self::collect_free_vars_walk(condition, bound, free);
10355                Self::collect_free_vars_walk(then_branch, bound, free);
10356                if let Some(else_expr) = else_branch {
10357                    Self::collect_free_vars_walk(else_expr, bound, free);
10358                }
10359            }
10360            AstNode::Block(exprs) => {
10361                let mut block_bound = bound.clone();
10362                for expr in exprs {
10363                    Self::collect_free_vars_walk(expr, &block_bound, free);
10364                    // Bindings introduced via := become bound for subsequent expressions
10365                    if let AstNode::Binary {
10366                        op: BinaryOp::ColonEqual,
10367                        lhs,
10368                        ..
10369                    } = expr
10370                    {
10371                        if let AstNode::Variable(var_name) = lhs.as_ref() {
10372                            block_bound.insert(var_name.as_str());
10373                        }
10374                    }
10375                }
10376            }
10377            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
10378                for expr in exprs {
10379                    Self::collect_free_vars_walk(expr, bound, free);
10380                }
10381            }
10382            AstNode::Object(pairs) => {
10383                for (key, value) in pairs {
10384                    Self::collect_free_vars_walk(key, bound, free);
10385                    Self::collect_free_vars_walk(value, bound, free);
10386                }
10387            }
10388            AstNode::ObjectTransform { input, pattern } => {
10389                Self::collect_free_vars_walk(input, bound, free);
10390                for (key, value) in pattern {
10391                    Self::collect_free_vars_walk(key, bound, free);
10392                    Self::collect_free_vars_walk(value, bound, free);
10393                }
10394            }
10395            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
10396                Self::collect_free_vars_walk(expr, bound, free);
10397            }
10398            AstNode::Sort { input, terms } => {
10399                Self::collect_free_vars_walk(input, bound, free);
10400                for (expr, _) in terms {
10401                    Self::collect_free_vars_walk(expr, bound, free);
10402                }
10403            }
10404            AstNode::Transform {
10405                location,
10406                update,
10407                delete,
10408            } => {
10409                Self::collect_free_vars_walk(location, bound, free);
10410                Self::collect_free_vars_walk(update, bound, free);
10411                if let Some(del) = delete {
10412                    Self::collect_free_vars_walk(del, bound, free);
10413                }
10414            }
10415            // Leaf nodes with no variable references
10416            AstNode::String(_)
10417            | AstNode::Name(_)
10418            | AstNode::Number(_)
10419            | AstNode::Boolean(_)
10420            | AstNode::Null
10421            | AstNode::Undefined
10422            | AstNode::Placeholder
10423            | AstNode::Regex { .. }
10424            | AstNode::Wildcard
10425            | AstNode::Descendant
10426            | AstNode::Parent(_)
10427            | AstNode::ParentVariable(_) => {}
10428        }
10429    }
10430
10431    /// Check if a name refers to a built-in function
10432    fn is_builtin_function(&self, name: &str) -> bool {
10433        matches!(
10434            name,
10435            // String functions
10436            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
10437            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
10438            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
10439            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
10440
10441            // Numeric functions
10442            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
10443            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
10444
10445            // Aggregation functions
10446            "sum" | "max" | "min" | "average" |
10447
10448            // Boolean/logic functions
10449            "boolean" | "not" | "exists" |
10450
10451            // Array functions
10452            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
10453
10454            // Object functions
10455            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
10456
10457            // Higher-order functions
10458            "map" | "filter" | "reduce" | "singletonArray" |
10459
10460            // Date/time functions
10461            "now" | "millis" | "fromMillis" | "toMillis"
10462        )
10463    }
10464
10465    /// Call a built-in function directly with pre-evaluated Values
10466    /// This is used when passing built-in functions to higher-order functions like $map
10467    fn call_builtin_with_values(
10468        &mut self,
10469        name: &str,
10470        values: &[JValue],
10471    ) -> Result<JValue, EvaluatorError> {
10472        use crate::functions;
10473
10474        if values.is_empty() {
10475            return Err(EvaluatorError::EvaluationError(format!(
10476                "{}() requires at least 1 argument",
10477                name
10478            )));
10479        }
10480
10481        let arg = &values[0];
10482
10483        match name {
10484            "string" => Ok(functions::string::string(arg, None)?),
10485            "number" => Ok(functions::numeric::number(arg)?),
10486            "boolean" => Ok(functions::boolean::boolean(arg)?),
10487            "not" => {
10488                let b = functions::boolean::boolean(arg)?;
10489                match b {
10490                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
10491                    _ => Err(EvaluatorError::TypeError(
10492                        "not() requires a boolean".to_string(),
10493                    )),
10494                }
10495            }
10496            "exists" => Ok(JValue::Bool(!arg.is_null())),
10497            "abs" => match arg {
10498                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
10499                _ => Err(EvaluatorError::TypeError(
10500                    "abs() requires a number argument".to_string(),
10501                )),
10502            },
10503            "floor" => match arg {
10504                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
10505                _ => Err(EvaluatorError::TypeError(
10506                    "floor() requires a number argument".to_string(),
10507                )),
10508            },
10509            "ceil" => match arg {
10510                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
10511                _ => Err(EvaluatorError::TypeError(
10512                    "ceil() requires a number argument".to_string(),
10513                )),
10514            },
10515            "round" => match arg {
10516                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
10517                _ => Err(EvaluatorError::TypeError(
10518                    "round() requires a number argument".to_string(),
10519                )),
10520            },
10521            "sqrt" => match arg {
10522                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
10523                _ => Err(EvaluatorError::TypeError(
10524                    "sqrt() requires a number argument".to_string(),
10525                )),
10526            },
10527            "uppercase" => match arg {
10528                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
10529                JValue::Null => Ok(JValue::Null),
10530                _ => Err(EvaluatorError::TypeError(
10531                    "uppercase() requires a string argument".to_string(),
10532                )),
10533            },
10534            "lowercase" => match arg {
10535                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
10536                JValue::Null => Ok(JValue::Null),
10537                _ => Err(EvaluatorError::TypeError(
10538                    "lowercase() requires a string argument".to_string(),
10539                )),
10540            },
10541            "trim" => match arg {
10542                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
10543                JValue::Null => Ok(JValue::Null),
10544                _ => Err(EvaluatorError::TypeError(
10545                    "trim() requires a string argument".to_string(),
10546                )),
10547            },
10548            "length" => match arg {
10549                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
10550                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10551                JValue::Null => Ok(JValue::Null),
10552                _ => Err(EvaluatorError::TypeError(
10553                    "length() requires a string or array argument".to_string(),
10554                )),
10555            },
10556            "sum" => match arg {
10557                JValue::Array(arr) => {
10558                    let mut total = 0.0;
10559                    for item in arr.iter() {
10560                        match item {
10561                            JValue::Number(n) => {
10562                                total += *n;
10563                            }
10564                            _ => {
10565                                return Err(EvaluatorError::TypeError(
10566                                    "sum() requires all array elements to be numbers".to_string(),
10567                                ));
10568                            }
10569                        }
10570                    }
10571                    Ok(JValue::Number(total))
10572                }
10573                JValue::Number(n) => Ok(JValue::Number(*n)),
10574                JValue::Null => Ok(JValue::Null),
10575                _ => Err(EvaluatorError::TypeError(
10576                    "sum() requires an array of numbers".to_string(),
10577                )),
10578            },
10579            "count" => {
10580                match arg {
10581                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10582                    JValue::Null => Ok(JValue::Number(0.0)),
10583                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10584                }
10585            }
10586            "max" => match arg {
10587                JValue::Array(arr) => {
10588                    let mut max_val: Option<f64> = None;
10589                    for item in arr.iter() {
10590                        if let JValue::Number(n) = item {
10591                            let f = *n;
10592                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
10593                        }
10594                    }
10595                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10596                }
10597                JValue::Number(n) => Ok(JValue::Number(*n)),
10598                JValue::Null => Ok(JValue::Null),
10599                _ => Err(EvaluatorError::TypeError(
10600                    "max() requires an array of numbers".to_string(),
10601                )),
10602            },
10603            "min" => match arg {
10604                JValue::Array(arr) => {
10605                    let mut min_val: Option<f64> = None;
10606                    for item in arr.iter() {
10607                        if let JValue::Number(n) = item {
10608                            let f = *n;
10609                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
10610                        }
10611                    }
10612                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10613                }
10614                JValue::Number(n) => Ok(JValue::Number(*n)),
10615                JValue::Null => Ok(JValue::Null),
10616                _ => Err(EvaluatorError::TypeError(
10617                    "min() requires an array of numbers".to_string(),
10618                )),
10619            },
10620            "average" => match arg {
10621                JValue::Array(arr) => {
10622                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
10623                    if nums.is_empty() {
10624                        Ok(JValue::Null)
10625                    } else {
10626                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
10627                        Ok(JValue::Number(avg))
10628                    }
10629                }
10630                JValue::Number(n) => Ok(JValue::Number(*n)),
10631                JValue::Null => Ok(JValue::Null),
10632                _ => Err(EvaluatorError::TypeError(
10633                    "average() requires an array of numbers".to_string(),
10634                )),
10635            },
10636            "append" => {
10637                // append(array1, array2) - append second array to first
10638                if values.len() < 2 {
10639                    return Err(EvaluatorError::EvaluationError(
10640                        "append() requires 2 arguments".to_string(),
10641                    ));
10642                }
10643                let first = &values[0];
10644                let second = &values[1];
10645
10646                // Convert first to array if needed
10647                let mut result = match first {
10648                    JValue::Array(arr) => arr.to_vec(),
10649                    JValue::Null => vec![],
10650                    other => vec![other.clone()],
10651                };
10652
10653                // Append second (flatten if array)
10654                match second {
10655                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
10656                    JValue::Null => {}
10657                    other => result.push(other.clone()),
10658                }
10659
10660                check_sequence_length(result.len(), &self.options)?;
10661                Ok(JValue::array(result))
10662            }
10663            "reverse" => match arg {
10664                JValue::Array(arr) => {
10665                    let mut reversed = arr.to_vec();
10666                    reversed.reverse();
10667                    Ok(JValue::array(reversed))
10668                }
10669                JValue::Null => Ok(JValue::Null),
10670                _ => Err(EvaluatorError::TypeError(
10671                    "reverse() requires an array".to_string(),
10672                )),
10673            },
10674            "keys" => match arg {
10675                JValue::Object(obj) => {
10676                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
10677                    check_sequence_length(keys.len(), &self.options)?;
10678                    Ok(JValue::array(keys))
10679                }
10680                JValue::Null => Ok(JValue::Null),
10681                _ => Err(EvaluatorError::TypeError(
10682                    "keys() requires an object".to_string(),
10683                )),
10684            },
10685
10686            // Add more functions as needed
10687            _ => Err(EvaluatorError::ReferenceError(format!(
10688                "Built-in function {} cannot be called with values directly",
10689                name
10690            ))),
10691        }
10692    }
10693
10694    /// Collect all descendant values recursively
10695    fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
10696        let mut descendants = Vec::new();
10697
10698        match value {
10699            JValue::Null => {
10700                // Null has no descendants, return empty
10701                return descendants;
10702            }
10703            JValue::Object(obj) => {
10704                // Include the current object
10705                descendants.push(value.clone());
10706
10707                for val in obj.values() {
10708                    // Recursively collect descendants
10709                    descendants.extend(self.collect_descendants(val));
10710                }
10711            }
10712            JValue::Array(arr) => {
10713                // DO NOT include the array itself - only recurse into elements
10714                // This matches JavaScript behavior: arrays are traversed but not collected
10715                for val in arr.iter() {
10716                    // Recursively collect descendants
10717                    descendants.extend(self.collect_descendants(val));
10718                }
10719            }
10720            _ => {
10721                // For primitives (string, number, boolean), just include the value itself
10722                descendants.push(value.clone());
10723            }
10724        }
10725
10726        descendants
10727    }
10728
10729    /// Evaluate a predicate (array filter or index)
10730    fn evaluate_predicate(
10731        &mut self,
10732        current: &JValue,
10733        predicate: &AstNode,
10734    ) -> Result<JValue, EvaluatorError> {
10735        // Special case: empty brackets [] (represented as Boolean(true))
10736        // This forces the value to be wrapped in an array
10737        if matches!(predicate, AstNode::Boolean(true)) {
10738            return match current {
10739                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
10740                JValue::Null => Ok(JValue::Null),
10741                other => Ok(JValue::array(vec![other.clone()])),
10742            };
10743        }
10744
10745        match current {
10746            JValue::Array(_arr) => {
10747                // Standalone predicates do simple array operations (no mapping over sub-arrays)
10748
10749                // First, try to evaluate predicate as a simple number (array index)
10750                if let AstNode::Number(n) = predicate {
10751                    // Direct array indexing
10752                    return self.array_index(current, &JValue::Number(*n));
10753                }
10754
10755                // Fast path: if predicate is definitely a filter expression (comparison/logical),
10756                // skip speculative numeric evaluation and go directly to filter logic
10757                if Self::is_filter_predicate(predicate) {
10758                    // Try CompiledExpr fast path
10759                    if let Some(compiled) = try_compile_expr(predicate) {
10760                        let shape = _arr.first().and_then(build_shape_cache);
10761                        let mut filtered = Vec::with_capacity(_arr.len());
10762                        for item in _arr.iter() {
10763                            let result = if let Some(ref s) = shape {
10764                                eval_compiled_shaped(
10765                                    &compiled,
10766                                    item,
10767                                    None,
10768                                    s,
10769                                    &self.options,
10770                                    self.start_time,
10771                                )?
10772                            } else {
10773                                eval_compiled(
10774                                    &compiled,
10775                                    item,
10776                                    None,
10777                                    &self.options,
10778                                    self.start_time,
10779                                )?
10780                            };
10781                            if compiled_is_truthy(&result) {
10782                                filtered.push(item.clone());
10783                            }
10784                        }
10785                        return Ok(JValue::array(filtered));
10786                    }
10787                    // Fallback: full AST evaluation per element
10788                    let mut filtered = Vec::new();
10789                    for item in _arr.iter() {
10790                        let item_result = self.evaluate_internal(predicate, item)?;
10791                        if self.is_truthy(&item_result) {
10792                            filtered.push(item.clone());
10793                        }
10794                    }
10795                    return Ok(JValue::array(filtered));
10796                }
10797
10798                // Try to evaluate the predicate to see if it's a numeric index
10799                // If evaluation succeeds and yields a number, use it as an index
10800                // If evaluation fails (e.g., comparison error), treat as filter
10801                match self.evaluate_internal(predicate, current) {
10802                    Ok(JValue::Number(_)) => {
10803                        // It's a numeric index
10804                        let pred_result = self.evaluate_internal(predicate, current)?;
10805                        return self.array_index(current, &pred_result);
10806                    }
10807                    Ok(JValue::Array(indices)) => {
10808                        // Multiple array selectors [[indices]]
10809                        // Check if array contains any non-numeric values
10810                        let has_non_numeric =
10811                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
10812
10813                        if has_non_numeric {
10814                            // If array contains non-numeric values, return entire array
10815                            return Ok(current.clone());
10816                        }
10817
10818                        // Collect numeric indices, handling negative indices
10819                        let arr_len = _arr.len() as i64;
10820                        let mut resolved_indices: Vec<i64> = indices
10821                            .iter()
10822                            .filter_map(|v| {
10823                                if let JValue::Number(n) = v {
10824                                    let idx = *n as i64;
10825                                    // Resolve negative indices
10826                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
10827                                    // Only include valid indices
10828                                    if actual_idx >= 0 && actual_idx < arr_len {
10829                                        Some(actual_idx)
10830                                    } else {
10831                                        None
10832                                    }
10833                                } else {
10834                                    None
10835                                }
10836                            })
10837                            .collect();
10838
10839                        // Sort and deduplicate indices
10840                        resolved_indices.sort();
10841                        resolved_indices.dedup();
10842
10843                        // Select elements at each sorted index
10844                        let result: Vec<JValue> = resolved_indices
10845                            .iter()
10846                            .map(|&idx| _arr[idx as usize].clone())
10847                            .collect();
10848
10849                        return Ok(JValue::array(result));
10850                    }
10851                    Ok(_) => {
10852                        // Evaluated successfully but not a number - might be a filter
10853                        // Fall through to filter logic
10854                    }
10855                    Err(_) => {
10856                        // Evaluation failed - it's likely a filter expression
10857                        // Fall through to filter logic
10858                    }
10859                }
10860
10861                // Try CompiledExpr fast path for filter expressions
10862                if let Some(compiled) = try_compile_expr(predicate) {
10863                    let shape = _arr.first().and_then(build_shape_cache);
10864                    let mut filtered = Vec::with_capacity(_arr.len());
10865                    for item in _arr.iter() {
10866                        let result = if let Some(ref s) = shape {
10867                            eval_compiled_shaped(
10868                                &compiled,
10869                                item,
10870                                None,
10871                                s,
10872                                &self.options,
10873                                self.start_time,
10874                            )?
10875                        } else {
10876                            eval_compiled(&compiled, item, None, &self.options, self.start_time)?
10877                        };
10878                        if compiled_is_truthy(&result) {
10879                            filtered.push(item.clone());
10880                        }
10881                    }
10882                    return Ok(JValue::array(filtered));
10883                }
10884
10885                // It's a filter expression - evaluate the predicate for each array element
10886                let mut filtered = Vec::new();
10887                for item in _arr.iter() {
10888                    let item_result = self.evaluate_internal(predicate, item)?;
10889
10890                    // If result is truthy, include this item
10891                    if self.is_truthy(&item_result) {
10892                        filtered.push(item.clone());
10893                    }
10894                }
10895
10896                Ok(JValue::array(filtered))
10897            }
10898            JValue::Object(obj) => {
10899                // For objects, predicate can be either:
10900                // 1. A string - property access (computed property name)
10901                // 2. A boolean expression - filter (return object if truthy)
10902                let pred_result = self.evaluate_internal(predicate, current)?;
10903
10904                // If it's a string, use it as a key for property access
10905                if let JValue::String(key) = &pred_result {
10906                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
10907                }
10908
10909                // Otherwise, treat as a filter expression
10910                // If the predicate is truthy, return the object; otherwise return undefined
10911                if self.is_truthy(&pred_result) {
10912                    Ok(current.clone())
10913                } else {
10914                    Ok(JValue::Undefined)
10915                }
10916            }
10917            _ => {
10918                // For primitive values (string, number, boolean):
10919                // In JSONata, scalars are treated as single-element arrays when indexed.
10920                // So value[0] returns value, value[1] returns undefined.
10921
10922                // First check if predicate is a numeric literal
10923                if let AstNode::Number(n) = predicate {
10924                    // For scalars, index 0 or -1 returns the value, others return undefined
10925                    let idx = n.floor() as i64;
10926                    if idx == 0 || idx == -1 {
10927                        return Ok(current.clone());
10928                    } else {
10929                        return Ok(JValue::Undefined);
10930                    }
10931                }
10932
10933                // Try to evaluate the predicate to see if it's a numeric index
10934                let pred_result = self.evaluate_internal(predicate, current)?;
10935
10936                if let JValue::Number(n) = &pred_result {
10937                    // It's a numeric index - treat scalar as single-element array
10938                    let idx = n.floor() as i64;
10939                    if idx == 0 || idx == -1 {
10940                        return Ok(current.clone());
10941                    } else {
10942                        return Ok(JValue::Undefined);
10943                    }
10944                }
10945
10946                // For non-numeric predicates, treat as a filter:
10947                // value[true] returns value, value[false] returns undefined
10948                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
10949                if self.is_truthy(&pred_result) {
10950                    Ok(current.clone())
10951                } else {
10952                    // Return undefined (not null) so $map can filter it out
10953                    Ok(JValue::Undefined)
10954                }
10955            }
10956        }
10957    }
10958
10959    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
10960    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
10961    fn evaluate_sort_term(
10962        &mut self,
10963        term_expr: &AstNode,
10964        element: &JValue,
10965    ) -> Result<JValue, EvaluatorError> {
10966        // For tuples (from index binding), extract the actual value from @ field
10967        let actual_element = if let JValue::Object(obj) = element {
10968            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
10969                obj.get("@").cloned().unwrap_or(JValue::Null)
10970            } else {
10971                element.clone()
10972            }
10973        } else {
10974            element.clone()
10975        };
10976
10977        // For simple field access (Path with single Name step), check if field exists
10978        if let AstNode::Path { steps } = term_expr {
10979            if steps.len() == 1 && steps[0].stages.is_empty() {
10980                if let AstNode::Name(field_name) = &steps[0].node {
10981                    // Check if the field exists in the element
10982                    if let JValue::Object(obj) = &actual_element {
10983                        return match obj.get(field_name) {
10984                            Some(val) => Ok(val.clone()),  // Field exists (may be null)
10985                            None => Ok(JValue::Undefined), // Field is missing
10986                        };
10987                    } else {
10988                        // Not an object - return undefined
10989                        return Ok(JValue::Undefined);
10990                    }
10991                }
10992            }
10993        }
10994
10995        // For complex expressions, evaluate against the tuple's `@` value (the
10996        // real element), not the wrapper. The tuple's carried focus/index/ancestor
10997        // bindings are reachable via context (bound by evaluate_sort), so a term
10998        // like `$`, `%.Price`, or `$pos` still resolves correctly.
10999        let result = self.evaluate_internal(term_expr, &actual_element)?;
11000
11001        // If the result is null from a complex expression, we can't easily tell if it's
11002        // "missing field" or "explicit null". For now, treat null results as undefined
11003        // to maintain compatibility with existing tests.
11004        // TODO: For full JS compatibility, would need deeper analysis of the expression
11005        if result.is_null() {
11006            return Ok(JValue::Undefined);
11007        }
11008
11009        Ok(result)
11010    }
11011
11012    /// Evaluate sort operator
11013    fn evaluate_sort(
11014        &mut self,
11015        data: &JValue,
11016        terms: &[(AstNode, bool)],
11017    ) -> Result<JValue, EvaluatorError> {
11018        // If data is null, return null
11019        if data.is_null() {
11020            return Ok(JValue::Null);
11021        }
11022
11023        // If data is not an array, return it as-is (can't sort a single value)
11024        let array = match data {
11025            JValue::Array(arr) => arr.clone(),
11026            other => return Ok(other.clone()),
11027        };
11028
11029        // If empty array, return as-is
11030        if array.is_empty() {
11031            return Ok(JValue::Array(array));
11032        }
11033
11034        // Evaluate sort keys for each element
11035        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
11036
11037        for (idx, element) in array.iter().enumerate() {
11038            let mut sort_keys = Vec::new();
11039
11040            // When sorting a tuple stream (the input path had a `%`/`@`/`#`
11041            // step, so each element is a `{@, !label, $var, __tuple__}`
11042            // wrapper), bind its carried ancestor/focus/index keys into scope
11043            // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
11044            // create_tuple_stream's per-tuple frame binding. Sort terms attach
11045            // to a synthetic step after the last input step, so `%` refers to
11046            // the last input step's ancestry, carried under `!label` here.
11047            // Saves/restores rather than blindly unbinding, so a tuple key
11048            // that collides with a live outer `:=` binding doesn't get
11049            // deleted once this row's sort terms are evaluated.
11050            let tuple_bindings = match element {
11051                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11052                    Some(self.bind_tuple_keys(obj))
11053                }
11054                _ => None,
11055            };
11056
11057            // When sorting a tuple stream, `$` and the term's data context are the
11058            // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
11059            // otherwise a term like `^($)` would try to order by the wrapper
11060            // object and raise T2008. The carried focus/index/ancestor keys stay
11061            // reachable via the context bindings established just above.
11062            let term_data = match element {
11063                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11064                    obj.get("@").cloned().unwrap_or(JValue::Null)
11065                }
11066                other => other.clone(),
11067            };
11068
11069            // Evaluate each sort term with $ bound to the element
11070            for (term_expr, _ascending) in terms {
11071                // Save current $ binding
11072                let saved_dollar = self.context.lookup("$").cloned();
11073
11074                // Bind $ to current element
11075                self.context.bind("$".to_string(), term_data.clone());
11076
11077                // Evaluate the sort expression, distinguishing missing fields from explicit null
11078                let sort_value = self.evaluate_sort_term(term_expr, element)?;
11079
11080                // Restore $ binding
11081                if let Some(val) = saved_dollar {
11082                    self.context.bind("$".to_string(), val);
11083                } else {
11084                    self.context.unbind("$");
11085                }
11086
11087                sort_keys.push(sort_value);
11088            }
11089
11090            if let Some(tuple_bindings) = tuple_bindings {
11091                tuple_bindings.restore(self);
11092            }
11093
11094            indexed_array.push((idx, sort_keys));
11095        }
11096
11097        // Validate that all sort keys are comparable (same type, or undefined)
11098        // Undefined values (missing fields) are allowed and sort to the end
11099        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
11100        for term_idx in 0..terms.len() {
11101            let mut first_valid_type: Option<&str> = None;
11102
11103            for (_idx, sort_keys) in &indexed_array {
11104                let sort_value = &sort_keys[term_idx];
11105
11106                // Skip undefined markers (missing fields) - these are allowed and sort to end
11107                if sort_value.is_undefined() {
11108                    continue;
11109                }
11110
11111                // Get the type name for this value
11112                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
11113                let value_type = match sort_value {
11114                    JValue::Number(_) => "number",
11115                    JValue::String(_) => "string",
11116                    JValue::Bool(_) => "boolean",
11117                    JValue::Array(_) => "array",
11118                    JValue::Object(_) => "object", // This catches non-undefined objects
11119                    JValue::Null => "null",        // Explicit null from data
11120                    _ => "unknown",
11121                };
11122
11123                // Check that sort keys are only numbers or strings
11124                // Null, boolean, array, and object types are not valid for sorting
11125                if value_type != "number" && value_type != "string" {
11126                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
11127                }
11128
11129                // Check if this matches the first valid type we saw
11130                if let Some(first_type) = first_valid_type {
11131                    if first_type != value_type {
11132                        return Err(EvaluatorError::TypeError(format!(
11133                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
11134                            first_type, value_type
11135                        )));
11136                    }
11137                } else {
11138                    first_valid_type = Some(value_type);
11139                }
11140            }
11141        }
11142
11143        // Sort the indexed array
11144        indexed_array.sort_by(|a, b| {
11145            // Compare sort keys in order
11146            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
11147                let left = &a.1[i];
11148                let right = &b.1[i];
11149
11150                let cmp = self.compare_values(left, right);
11151
11152                if cmp != std::cmp::Ordering::Equal {
11153                    return if *ascending { cmp } else { cmp.reverse() };
11154                }
11155            }
11156
11157            // If all keys are equal, maintain original order (stable sort)
11158            a.0.cmp(&b.0)
11159        });
11160
11161        // Extract sorted elements
11162        let sorted: Vec<JValue> = indexed_array
11163            .iter()
11164            .map(|(idx, _)| array[*idx].clone())
11165            .collect();
11166
11167        Ok(JValue::array(sorted))
11168    }
11169
11170    /// Compare two values for sorting (JSONata semantics)
11171    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
11172        // Handle undefined markers first - they sort to the end
11173        let left_undef = left.is_undefined();
11174        let right_undef = right.is_undefined();
11175
11176        if left_undef && right_undef {
11177            return Ordering::Equal;
11178        }
11179        if left_undef {
11180            return Ordering::Greater; // Undefined sorts last
11181        }
11182        if right_undef {
11183            return Ordering::Less;
11184        }
11185
11186        match (left, right) {
11187            // Nulls also sort last (explicit null in data)
11188            (JValue::Null, JValue::Null) => Ordering::Equal,
11189            (JValue::Null, _) => Ordering::Greater,
11190            (_, JValue::Null) => Ordering::Less,
11191
11192            // Numbers
11193            (JValue::Number(a), JValue::Number(b)) => {
11194                let a_f64 = *a;
11195                let b_f64 = *b;
11196                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
11197            }
11198
11199            // Strings
11200            (JValue::String(a), JValue::String(b)) => a.cmp(b),
11201
11202            // Booleans
11203            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
11204
11205            // Arrays (lexicographic comparison)
11206            (JValue::Array(a), JValue::Array(b)) => {
11207                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
11208                    let cmp = self.compare_values(a_elem, b_elem);
11209                    if cmp != Ordering::Equal {
11210                        return cmp;
11211                    }
11212                }
11213                a.len().cmp(&b.len())
11214            }
11215
11216            // Different types: use type ordering
11217            // null < bool < number < string < array < object
11218            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
11219            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
11220            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
11221            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
11222
11223            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
11224            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
11225            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
11226            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
11227
11228            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
11229            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
11230            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
11231            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
11232
11233            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
11234            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
11235            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
11236            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
11237
11238            (JValue::Object(_), _) => Ordering::Greater,
11239            _ => Ordering::Equal,
11240        }
11241    }
11242
11243    /// Check if a value is truthy (JSONata semantics).
11244    fn is_truthy(&self, value: &JValue) -> bool {
11245        match value {
11246            JValue::Null | JValue::Undefined => false,
11247            JValue::Bool(b) => *b,
11248            JValue::Number(n) => *n != 0.0,
11249            JValue::String(s) => !s.is_empty(),
11250            JValue::Array(arr) => !arr.is_empty(),
11251            JValue::Object(obj) => !obj.is_empty(),
11252            _ => false,
11253        }
11254    }
11255
11256    /// Check if a value is truthy for the default operator (?:)
11257    /// This has special semantics:
11258    /// - Lambda/function objects are not values, so they're falsy
11259    /// - Arrays containing only falsy elements are falsy
11260    /// - Otherwise, use standard truthiness
11261    fn is_truthy_for_default(&self, value: &JValue) -> bool {
11262        match value {
11263            // Lambda/function values are not data values, so they're falsy
11264            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
11265            // Arrays need special handling - check if all elements are falsy
11266            JValue::Array(arr) => {
11267                if arr.is_empty() {
11268                    return false;
11269                }
11270                // Array is truthy only if it contains at least one truthy element
11271                arr.iter().any(|elem| self.is_truthy(elem))
11272            }
11273            // For all other types, use standard truthiness
11274            _ => self.is_truthy(value),
11275        }
11276    }
11277
11278    /// Unwrap singleton arrays to scalar values
11279    /// This is used when no explicit array-keeping operation (like []) was used
11280    fn unwrap_singleton(&self, value: JValue) -> JValue {
11281        match value {
11282            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
11283            _ => value,
11284        }
11285    }
11286
11287    /// Extract lambda IDs from a value (used for closure preservation)
11288    /// Finds any lambda_id references in the value so they can be preserved
11289    /// when exiting a block scope
11290    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
11291        // Fast path: scalars can never contain lambda references
11292        match value {
11293            JValue::Number(_)
11294            | JValue::Bool(_)
11295            | JValue::String(_)
11296            | JValue::Null
11297            | JValue::Undefined
11298            | JValue::Regex { .. }
11299            | JValue::Builtin { .. } => return Vec::new(),
11300            _ => {}
11301        }
11302        let mut ids = Vec::new();
11303        self.collect_lambda_ids(value, &mut ids);
11304        ids
11305    }
11306
11307    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
11308        match value {
11309            JValue::Lambda { lambda_id, .. } => {
11310                let id_str = lambda_id.to_string();
11311                if !ids.contains(&id_str) {
11312                    ids.push(id_str);
11313                    // Transitively follow the stored lambda's captured_env
11314                    // to find all referenced lambdas. This is critical for
11315                    // closures like the Y-combinator where returned lambdas
11316                    // capture other lambdas in their environment.
11317                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
11318                        let env_values: Vec<JValue> =
11319                            stored.captured_env.values().cloned().collect();
11320                        for env_value in &env_values {
11321                            self.collect_lambda_ids(env_value, ids);
11322                        }
11323                    }
11324                }
11325            }
11326            JValue::Object(map) => {
11327                // Recurse into object values
11328                for v in map.values() {
11329                    self.collect_lambda_ids(v, ids);
11330                }
11331            }
11332            JValue::Array(arr) => {
11333                // Recurse into array elements
11334                for v in arr.iter() {
11335                    self.collect_lambda_ids(v, ids);
11336                }
11337            }
11338            _ => {}
11339        }
11340    }
11341
11342    /// Equality comparison (JSONata semantics)
11343    fn equals(&self, left: &JValue, right: &JValue) -> bool {
11344        crate::functions::array::values_equal(left, right)
11345    }
11346
11347    /// Addition
11348    fn add(
11349        &self,
11350        left: &JValue,
11351        right: &JValue,
11352        left_is_explicit_null: bool,
11353        right_is_explicit_null: bool,
11354    ) -> Result<JValue, EvaluatorError> {
11355        match (left, right) {
11356            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
11357            // Explicit null literal with number -> T2002 error
11358            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
11359                Err(EvaluatorError::TypeError(
11360                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11361                ))
11362            }
11363            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
11364                Err(EvaluatorError::TypeError(
11365                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
11366                ))
11367            }
11368            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
11369                Err(EvaluatorError::TypeError(
11370                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11371                ))
11372            }
11373            // Undefined variable (null/undefined) with number -> undefined result
11374            (JValue::Null | JValue::Undefined, JValue::Number(_))
11375            | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
11376            // Boolean with anything (including undefined) -> T2001 error
11377            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
11378                "T2001: The left side of the '+' operator must evaluate to a number or a string"
11379                    .to_string(),
11380            )),
11381            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
11382                "T2001: The right side of the '+' operator must evaluate to a number or a string"
11383                    .to_string(),
11384            )),
11385            // Undefined with undefined -> undefined
11386            (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
11387                Ok(JValue::Null)
11388            }
11389            _ => Err(EvaluatorError::TypeError(format!(
11390                "Cannot add {:?} and {:?}",
11391                left, right
11392            ))),
11393        }
11394    }
11395
11396    /// Subtraction
11397    fn subtract(
11398        &self,
11399        left: &JValue,
11400        right: &JValue,
11401        left_is_explicit_null: bool,
11402        right_is_explicit_null: bool,
11403    ) -> Result<JValue, EvaluatorError> {
11404        match (left, right) {
11405            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
11406            // Explicit null literal -> error
11407            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11408                "T2002: The left side of the - operator must evaluate to a number".to_string(),
11409            )),
11410            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11411                "T2002: The right side of the - operator must evaluate to a number".to_string(),
11412            )),
11413            // Undefined variables -> undefined result
11414            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11415                Ok(JValue::Null)
11416            }
11417            _ => Err(EvaluatorError::TypeError(format!(
11418                "Cannot subtract {:?} and {:?}",
11419                left, right
11420            ))),
11421        }
11422    }
11423
11424    /// Multiplication
11425    fn multiply(
11426        &self,
11427        left: &JValue,
11428        right: &JValue,
11429        left_is_explicit_null: bool,
11430        right_is_explicit_null: bool,
11431    ) -> Result<JValue, EvaluatorError> {
11432        match (left, right) {
11433            (JValue::Number(a), JValue::Number(b)) => {
11434                let result = *a * *b;
11435                // Check for overflow to Infinity
11436                if result.is_infinite() {
11437                    return Err(EvaluatorError::EvaluationError(
11438                        "D1001: Number out of range".to_string(),
11439                    ));
11440                }
11441                Ok(JValue::Number(result))
11442            }
11443            // Explicit null literal -> error
11444            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11445                "T2002: The left side of the * operator must evaluate to a number".to_string(),
11446            )),
11447            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11448                "T2002: The right side of the * operator must evaluate to a number".to_string(),
11449            )),
11450            // Undefined variables -> undefined result
11451            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11452                Ok(JValue::Null)
11453            }
11454            _ => Err(EvaluatorError::TypeError(format!(
11455                "Cannot multiply {:?} and {:?}",
11456                left, right
11457            ))),
11458        }
11459    }
11460
11461    /// Division
11462    fn divide(
11463        &self,
11464        left: &JValue,
11465        right: &JValue,
11466        left_is_explicit_null: bool,
11467        right_is_explicit_null: bool,
11468    ) -> Result<JValue, EvaluatorError> {
11469        match (left, right) {
11470            (JValue::Number(a), JValue::Number(b)) => {
11471                let denominator = *b;
11472                if denominator == 0.0 {
11473                    return Err(EvaluatorError::EvaluationError(
11474                        "Division by zero".to_string(),
11475                    ));
11476                }
11477                Ok(JValue::Number(*a / denominator))
11478            }
11479            // Explicit null literal -> error
11480            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11481                "T2002: The left side of the / operator must evaluate to a number".to_string(),
11482            )),
11483            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11484                "T2002: The right side of the / operator must evaluate to a number".to_string(),
11485            )),
11486            // Undefined variables -> undefined result
11487            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11488                Ok(JValue::Null)
11489            }
11490            _ => Err(EvaluatorError::TypeError(format!(
11491                "Cannot divide {:?} and {:?}",
11492                left, right
11493            ))),
11494        }
11495    }
11496
11497    /// Modulo
11498    fn modulo(
11499        &self,
11500        left: &JValue,
11501        right: &JValue,
11502        left_is_explicit_null: bool,
11503        right_is_explicit_null: bool,
11504    ) -> Result<JValue, EvaluatorError> {
11505        match (left, right) {
11506            (JValue::Number(a), JValue::Number(b)) => {
11507                let denominator = *b;
11508                if denominator == 0.0 {
11509                    return Err(EvaluatorError::EvaluationError(
11510                        "Division by zero".to_string(),
11511                    ));
11512                }
11513                Ok(JValue::Number(*a % denominator))
11514            }
11515            // Explicit null literal -> error
11516            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11517                "T2002: The left side of the % operator must evaluate to a number".to_string(),
11518            )),
11519            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11520                "T2002: The right side of the % operator must evaluate to a number".to_string(),
11521            )),
11522            // Undefined variables -> undefined result
11523            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11524                Ok(JValue::Null)
11525            }
11526            _ => Err(EvaluatorError::TypeError(format!(
11527                "Cannot compute modulo of {:?} and {:?}",
11528                left, right
11529            ))),
11530        }
11531    }
11532
11533    /// Get human-readable type name for error messages
11534    fn type_name(value: &JValue) -> &'static str {
11535        match value {
11536            JValue::Null => "null",
11537            JValue::Bool(_) => "boolean",
11538            JValue::Number(_) => "number",
11539            JValue::String(_) => "string",
11540            JValue::Array(_) => "array",
11541            JValue::Object(_) => "object",
11542            _ => "unknown",
11543        }
11544    }
11545
11546    /// Ordered comparison with null/type checking shared across <, <=, >, >=
11547    ///
11548    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
11549    /// `compare_strs` receives (left_str, right_str) for string operands.
11550    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
11551    fn ordered_compare(
11552        &self,
11553        left: &JValue,
11554        right: &JValue,
11555        left_is_explicit_null: bool,
11556        right_is_explicit_null: bool,
11557        op_symbol: &str,
11558        compare_nums: fn(f64, f64) -> bool,
11559        compare_strs: fn(&str, &str) -> bool,
11560    ) -> Result<JValue, EvaluatorError> {
11561        match (left, right) {
11562            (JValue::Number(a), JValue::Number(b)) => {
11563                Ok(JValue::Bool(compare_nums(*a, *b)))
11564            }
11565            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
11566            // Both null/undefined -> return undefined
11567            (JValue::Null, JValue::Null) => Ok(JValue::Null),
11568            // Explicit null literal with any type (except null) -> T2010 error
11569            (JValue::Null, _) if left_is_explicit_null => {
11570                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11571            }
11572            (_, JValue::Null) if right_is_explicit_null => {
11573                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11574            }
11575            // Boolean with undefined -> T2010 error
11576            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
11577                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11578            }
11579            // Number or String with undefined (not explicit null) -> undefined result
11580            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
11581            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
11582                Ok(JValue::Null)
11583            }
11584            // String vs Number -> T2009
11585            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
11586                Err(EvaluatorError::EvaluationError(format!(
11587                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
11588                    op_symbol
11589                )))
11590            }
11591            // Boolean comparisons -> T2010
11592            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
11593                Err(EvaluatorError::EvaluationError(format!(
11594                    "T2010: Cannot compare {} and {}",
11595                    Self::type_name(left), Self::type_name(right)
11596                )))
11597            }
11598            // Other type mismatches
11599            _ => Err(EvaluatorError::EvaluationError(format!(
11600                "T2010: Cannot compare {} and {}",
11601                Self::type_name(left), Self::type_name(right)
11602            ))),
11603        }
11604    }
11605
11606    /// Less than comparison
11607    fn less_than(
11608        &self,
11609        left: &JValue,
11610        right: &JValue,
11611        left_is_explicit_null: bool,
11612        right_is_explicit_null: bool,
11613    ) -> Result<JValue, EvaluatorError> {
11614        self.ordered_compare(
11615            left,
11616            right,
11617            left_is_explicit_null,
11618            right_is_explicit_null,
11619            "<",
11620            |a, b| a < b,
11621            |a, b| a < b,
11622        )
11623    }
11624
11625    /// Less than or equal comparison
11626    fn less_than_or_equal(
11627        &self,
11628        left: &JValue,
11629        right: &JValue,
11630        left_is_explicit_null: bool,
11631        right_is_explicit_null: bool,
11632    ) -> Result<JValue, EvaluatorError> {
11633        self.ordered_compare(
11634            left,
11635            right,
11636            left_is_explicit_null,
11637            right_is_explicit_null,
11638            "<=",
11639            |a, b| a <= b,
11640            |a, b| a <= b,
11641        )
11642    }
11643
11644    /// Greater than comparison
11645    fn greater_than(
11646        &self,
11647        left: &JValue,
11648        right: &JValue,
11649        left_is_explicit_null: bool,
11650        right_is_explicit_null: bool,
11651    ) -> Result<JValue, EvaluatorError> {
11652        self.ordered_compare(
11653            left,
11654            right,
11655            left_is_explicit_null,
11656            right_is_explicit_null,
11657            ">",
11658            |a, b| a > b,
11659            |a, b| a > b,
11660        )
11661    }
11662
11663    /// Greater than or equal comparison
11664    fn greater_than_or_equal(
11665        &self,
11666        left: &JValue,
11667        right: &JValue,
11668        left_is_explicit_null: bool,
11669        right_is_explicit_null: bool,
11670    ) -> Result<JValue, EvaluatorError> {
11671        self.ordered_compare(
11672            left,
11673            right,
11674            left_is_explicit_null,
11675            right_is_explicit_null,
11676            ">=",
11677            |a, b| a >= b,
11678            |a, b| a >= b,
11679        )
11680    }
11681
11682    /// Convert a value to a string for concatenation
11683    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
11684        match value {
11685            JValue::String(s) => Ok(s.to_string()),
11686            JValue::Null => Ok(String::new()),
11687            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
11688                match crate::functions::string::string(value, None) {
11689                    Ok(JValue::String(s)) => Ok(s.to_string()),
11690                    Ok(JValue::Null) => Ok(String::new()),
11691                    _ => Err(EvaluatorError::TypeError(
11692                        "Cannot concatenate complex types".to_string(),
11693                    )),
11694                }
11695            }
11696            _ => Ok(String::new()),
11697        }
11698    }
11699
11700    /// String concatenation
11701    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11702        let left_str = Self::value_to_concat_string(left)?;
11703        let right_str = Self::value_to_concat_string(right)?;
11704        Ok(JValue::string(format!("{}{}", left_str, right_str)))
11705    }
11706
11707    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
11708    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11709        // Check left operand is a number or null
11710        let start_f64 = match left {
11711            JValue::Number(n) => Some(*n),
11712            JValue::Null | JValue::Undefined => None,
11713            _ => {
11714                return Err(EvaluatorError::EvaluationError(
11715                    "T2003: Left operand of range operator must be a number".to_string(),
11716                ));
11717            }
11718        };
11719
11720        // Check left operand is an integer (if it's a number)
11721        if let Some(val) = start_f64 {
11722            if val.fract() != 0.0 {
11723                return Err(EvaluatorError::EvaluationError(
11724                    "T2003: Left operand of range operator must be an integer".to_string(),
11725                ));
11726            }
11727        }
11728
11729        // Check right operand is a number or null
11730        let end_f64 = match right {
11731            JValue::Number(n) => Some(*n),
11732            JValue::Null | JValue::Undefined => None,
11733            _ => {
11734                return Err(EvaluatorError::EvaluationError(
11735                    "T2004: Right operand of range operator must be a number".to_string(),
11736                ));
11737            }
11738        };
11739
11740        // Check right operand is an integer (if it's a number)
11741        if let Some(val) = end_f64 {
11742            if val.fract() != 0.0 {
11743                return Err(EvaluatorError::EvaluationError(
11744                    "T2004: Right operand of range operator must be an integer".to_string(),
11745                ));
11746            }
11747        }
11748
11749        // If either operand is null, return empty array
11750        if start_f64.is_none() || end_f64.is_none() {
11751            return Ok(JValue::array(vec![]));
11752        }
11753
11754        let start = start_f64.unwrap() as i64;
11755        let end = end_f64.unwrap() as i64;
11756
11757        // Check range size limit (10 million elements max)
11758        let size = if start <= end {
11759            (end - start + 1) as usize
11760        } else {
11761            0
11762        };
11763        if size > 10_000_000 {
11764            return Err(EvaluatorError::EvaluationError(
11765                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
11766            ));
11767        }
11768        check_sequence_length(size, &self.options)?;
11769
11770        let mut result = Vec::with_capacity(size);
11771        if start <= end {
11772            for i in start..=end {
11773                result.push(JValue::Number(i as f64));
11774            }
11775        }
11776        // Note: if start > end, return empty array (not reversed)
11777        Ok(JValue::array(result))
11778    }
11779
11780    /// In operator (checks if left is in right array/object)
11781    /// Array indexing: array[index]
11782    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
11783        match (array, index) {
11784            (JValue::Array(arr), JValue::Number(n)) => {
11785                let idx = *n as i64;
11786                let len = arr.len() as i64;
11787
11788                // Handle negative indexing (offset from end)
11789                let actual_idx = if idx < 0 { len + idx } else { idx };
11790
11791                if actual_idx < 0 || actual_idx >= len {
11792                    Ok(JValue::Undefined)
11793                } else {
11794                    Ok(arr[actual_idx as usize].clone())
11795                }
11796            }
11797            _ => Err(EvaluatorError::TypeError(
11798                "Array indexing requires array and number".to_string(),
11799            )),
11800        }
11801    }
11802
11803    /// Array filtering: array[predicate]
11804    /// Evaluates the predicate for each item in the array and returns items where predicate is true
11805    fn array_filter(
11806        &mut self,
11807        _lhs_node: &AstNode,
11808        rhs_node: &AstNode,
11809        array: &JValue,
11810        _original_data: &JValue,
11811    ) -> Result<JValue, EvaluatorError> {
11812        match array {
11813            JValue::Array(arr) => {
11814                // Pre-allocate with estimated capacity (assume ~50% will match)
11815                let mut filtered = Vec::with_capacity(arr.len() / 2);
11816
11817                for item in arr.iter() {
11818                    // Evaluate the predicate in the context of this array item
11819                    // The item becomes the new "current context" ($)
11820                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
11821
11822                    // Check if the predicate is truthy
11823                    if self.is_truthy(&predicate_result) {
11824                        filtered.push(item.clone());
11825                    }
11826                }
11827
11828                Ok(JValue::array(filtered))
11829            }
11830            _ => Err(EvaluatorError::TypeError(
11831                "Array filtering requires an array".to_string(),
11832            )),
11833        }
11834    }
11835
11836    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11837        // If either side is undefined/null, return false (not an error)
11838        // This matches JavaScript behavior
11839        if left.is_null() || right.is_null() {
11840            return Ok(JValue::Bool(false));
11841        }
11842
11843        match right {
11844            JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
11845            JValue::Object(obj) => {
11846                if let JValue::String(key) = left {
11847                    Ok(JValue::Bool(obj.contains_key(&**key)))
11848                } else {
11849                    Ok(JValue::Bool(false))
11850                }
11851            }
11852            // If right side is not an array or object (e.g., string, number),
11853            // wrap it in an array for comparison
11854            other => Ok(JValue::Bool(self.equals(left, other))),
11855        }
11856    }
11857
11858    /// Create a partially applied function from a function call with placeholder arguments
11859    /// This evaluates non-placeholder arguments and creates a new lambda that takes
11860    /// the placeholder positions as parameters.
11861    fn create_partial_application(
11862        &mut self,
11863        name: &str,
11864        args: &[AstNode],
11865        is_builtin: bool,
11866        data: &JValue,
11867    ) -> Result<JValue, EvaluatorError> {
11868        // First, look up the function to ensure it exists
11869        let is_lambda = self.context.lookup_lambda(name).is_some()
11870            || (self
11871                .context
11872                .lookup(name)
11873                .map(|v| matches!(v, JValue::Lambda { .. }))
11874                .unwrap_or(false));
11875
11876        // Built-in functions must be called with $ prefix for partial application
11877        // Without $, it's an error (T1007) suggesting the user forgot the $
11878        if !is_lambda && !is_builtin {
11879            // Check if it's a built-in function called without $
11880            if self.is_builtin_function(name) {
11881                return Err(EvaluatorError::EvaluationError(format!(
11882                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
11883                    name
11884                )));
11885            }
11886            return Err(EvaluatorError::EvaluationError(
11887                "T1008: Attempted to partially apply a non-function".to_string(),
11888            ));
11889        }
11890
11891        // Evaluate non-placeholder arguments and track placeholder positions
11892        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
11893        let mut placeholder_positions: Vec<usize> = Vec::new();
11894
11895        for (i, arg) in args.iter().enumerate() {
11896            if matches!(arg, AstNode::Placeholder) {
11897                placeholder_positions.push(i);
11898            } else {
11899                let value = self.evaluate_internal(arg, data)?;
11900                bound_args.push((i, value));
11901            }
11902        }
11903
11904        // Generate parameter names for each placeholder
11905        let param_names: Vec<String> = placeholder_positions
11906            .iter()
11907            .enumerate()
11908            .map(|(i, _)| format!("__p{}", i))
11909            .collect();
11910
11911        // Store the partial application info as a special lambda
11912        // When invoked, it will call the original function with bound + placeholder args
11913        let partial_id = format!(
11914            "__partial_{}_{}_{}",
11915            name,
11916            placeholder_positions.len(),
11917            bound_args.len()
11918        );
11919
11920        // Create a stored lambda that represents this partial application
11921        // The body is a marker that we'll interpret specially during invocation
11922        let stored_lambda = StoredLambda {
11923            params: param_names.clone(),
11924            body: AstNode::String(format!(
11925                "__partial_call:{}:{}:{}",
11926                name,
11927                is_builtin,
11928                args.len()
11929            )),
11930            compiled_body: None, // Partial application uses a special body marker
11931            signature: None,
11932            captured_env: {
11933                let mut env = self.capture_current_environment();
11934                // Store the bound arguments in the captured environment
11935                for (pos, value) in &bound_args {
11936                    env.insert(format!("__bound_arg_{}", pos), value.clone());
11937                }
11938                // Store placeholder positions
11939                env.insert(
11940                    "__placeholder_positions".to_string(),
11941                    JValue::array(
11942                        placeholder_positions
11943                            .iter()
11944                            .map(|p| JValue::Number(*p as f64))
11945                            .collect::<Vec<_>>(),
11946                    ),
11947                );
11948                // Store total argument count
11949                env.insert(
11950                    "__total_args".to_string(),
11951                    JValue::Number(args.len() as f64),
11952                );
11953                env
11954            },
11955            captured_data: Some(data.clone()),
11956            thunk: false,
11957        };
11958
11959        self.context.bind_lambda(partial_id.clone(), stored_lambda);
11960
11961        // Return a lambda object that can be invoked
11962        let lambda_obj = JValue::lambda(
11963            partial_id.as_str(),
11964            param_names,
11965            Some(name.to_string()),
11966            None::<String>,
11967        );
11968
11969        Ok(lambda_obj)
11970    }
11971}
11972
11973impl Default for Evaluator {
11974    fn default() -> Self {
11975        Self::new()
11976    }
11977}
11978
11979#[cfg(test)]
11980mod tests {
11981    use super::*;
11982    use crate::ast::{BinaryOp, UnaryOp};
11983
11984    // --- Task 7: tuple-wrapper output leak -----------------------------------
11985    //
11986    // `%`/`@`/`#` are implemented internally via a tuple-stream representation
11987    // (`create_tuple_stream`): each element gets wrapped as
11988    // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
11989    // consume/re-wrap these, but the *final* evaluate() result can still carry
11990    // a lingering wrapper -- confirmed for real by dumping actual output before
11991    // this fix (see task-7-report.md for the raw before/after). These tests
11992    // pin both the bare top-level case (Task 5's brief `#` example) and the
11993    // object/array-construction-nested case (found while verifying the brief's
11994    // illustrative fix against real output -- a plain per-element Array-only
11995    // recursion does not reach into a constructed object's field values).
11996
11997    fn dataset5_for_tuple_tests() -> JValue {
11998        let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
11999        serde_json::from_str::<serde_json::Value>(s).unwrap().into()
12000    }
12001
12002    fn assert_no_tuple_wrapper(value: &JValue) {
12003        match value {
12004            JValue::Object(obj) => {
12005                assert!(
12006                    obj.get("__tuple__").is_none(),
12007                    "tuple wrapper leaked into output: {:?}",
12008                    value
12009                );
12010                for v in obj.values() {
12011                    assert_no_tuple_wrapper(v);
12012                }
12013            }
12014            JValue::Array(arr) => {
12015                for item in arr.iter() {
12016                    assert_no_tuple_wrapper(item);
12017                }
12018            }
12019            _ => {}
12020        }
12021    }
12022
12023    #[test]
12024    fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
12025        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12026        let ast = crate::parser::parse("items#$i").unwrap();
12027        let mut evaluator = Evaluator::new();
12028        let result = evaluator.evaluate(&ast, &data).unwrap();
12029        assert_no_tuple_wrapper(&result);
12030        assert_eq!(
12031            result,
12032            JValue::array(vec![
12033                JValue::from(1i64),
12034                JValue::from(2i64),
12035                JValue::from(3i64)
12036            ])
12037        );
12038    }
12039
12040    #[test]
12041    fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
12042        // Confirmed by Task 6 to evaluate to the correct @-values but stay
12043        // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
12044        let data = dataset5_for_tuple_tests();
12045        let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
12046        let mut evaluator = Evaluator::new();
12047        let result = evaluator.evaluate(&ast, &data).unwrap();
12048        assert_no_tuple_wrapper(&result);
12049        assert_eq!(
12050            result,
12051            JValue::array(vec![
12052                JValue::string("040657863"),
12053                JValue::string("0406654603"),
12054            ])
12055        );
12056    }
12057
12058    #[test]
12059    fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
12060        // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
12061        let data = dataset5_for_tuple_tests();
12062        let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
12063            .unwrap();
12064        let mut evaluator = Evaluator::new();
12065        let result = evaluator.evaluate(&ast, &data).unwrap();
12066        assert_no_tuple_wrapper(&result);
12067        assert_eq!(
12068            result,
12069            JValue::array(vec![
12070                JValue::string("0406654608"),
12071                JValue::string("0406634348"),
12072            ])
12073        );
12074    }
12075
12076    #[test]
12077    fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
12078        // A tuple-producing expression nested inside a constructed object's field
12079        // value: the top-level result is a plain (non-tuple) Object, so a naive
12080        // "unwrap only if the whole value is a tuple wrapper" check would miss
12081        // this -- must recurse into field values too.
12082        let data = dataset5_for_tuple_tests();
12083        let ast =
12084            crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
12085                .unwrap();
12086        let mut evaluator = Evaluator::new();
12087        let result = evaluator.evaluate(&ast, &data).unwrap();
12088        assert_no_tuple_wrapper(&result);
12089        assert_eq!(
12090            result,
12091            JValue::from(serde_json::json!({
12092                "skus": ["040657863", "0406654603"]
12093            }))
12094        );
12095    }
12096
12097    #[test]
12098    fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
12099        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12100        let ast = crate::parser::parse("[items#$i]").unwrap();
12101        let mut evaluator = Evaluator::new();
12102        let result = evaluator.evaluate(&ast, &data).unwrap();
12103        assert_no_tuple_wrapper(&result);
12104    }
12105
12106    #[test]
12107    fn test_evaluate_literals() {
12108        let mut evaluator = Evaluator::new();
12109        let data = JValue::Null;
12110
12111        // String literal
12112        let result = evaluator
12113            .evaluate(&AstNode::string("hello"), &data)
12114            .unwrap();
12115        assert_eq!(result, JValue::string("hello"));
12116
12117        // Number literal
12118        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
12119        assert_eq!(result, JValue::from(42i64));
12120
12121        // Boolean literal
12122        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
12123        assert_eq!(result, JValue::Bool(true));
12124
12125        // Null literal
12126        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
12127        assert_eq!(result, JValue::Null);
12128    }
12129
12130    #[test]
12131    fn test_evaluate_variables() {
12132        let mut evaluator = Evaluator::new();
12133        let data = JValue::Null;
12134
12135        // Bind a variable
12136        evaluator
12137            .context
12138            .bind("x".to_string(), JValue::from(100i64));
12139
12140        // Look up the variable
12141        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
12142        assert_eq!(result, JValue::from(100i64));
12143
12144        // Undefined variable returns null (undefined in JSONata semantics)
12145        let result = evaluator
12146            .evaluate(&AstNode::variable("undefined"), &data)
12147            .unwrap();
12148        assert_eq!(result, JValue::Null);
12149    }
12150
12151    #[test]
12152    fn test_evaluate_path() {
12153        let mut evaluator = Evaluator::new();
12154        let data = JValue::from(serde_json::json!({
12155            "foo": {
12156                "bar": {
12157                    "baz": 42
12158                }
12159            }
12160        }));
12161        // Simple path
12162        let path = AstNode::Path {
12163            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
12164        };
12165        let result = evaluator.evaluate(&path, &data).unwrap();
12166        assert_eq!(
12167            result,
12168            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
12169        );
12170
12171        // Nested path
12172        let path = AstNode::Path {
12173            steps: vec![
12174                PathStep::new(AstNode::Name("foo".to_string())),
12175                PathStep::new(AstNode::Name("bar".to_string())),
12176                PathStep::new(AstNode::Name("baz".to_string())),
12177            ],
12178        };
12179        let result = evaluator.evaluate(&path, &data).unwrap();
12180        assert_eq!(result, JValue::from(42i64));
12181
12182        // Missing path returns undefined (not null - see issue #32)
12183        let path = AstNode::Path {
12184            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
12185        };
12186        let result = evaluator.evaluate(&path, &data).unwrap();
12187        assert_eq!(result, JValue::Undefined);
12188    }
12189
12190    #[test]
12191    fn test_arithmetic_operations() {
12192        let mut evaluator = Evaluator::new();
12193        let data = JValue::Null;
12194
12195        // Addition
12196        let expr = AstNode::Binary {
12197            op: BinaryOp::Add,
12198            lhs: Box::new(AstNode::number(10.0)),
12199            rhs: Box::new(AstNode::number(5.0)),
12200        };
12201        let result = evaluator.evaluate(&expr, &data).unwrap();
12202        assert_eq!(result, JValue::Number(15.0));
12203
12204        // Subtraction
12205        let expr = AstNode::Binary {
12206            op: BinaryOp::Subtract,
12207            lhs: Box::new(AstNode::number(10.0)),
12208            rhs: Box::new(AstNode::number(5.0)),
12209        };
12210        let result = evaluator.evaluate(&expr, &data).unwrap();
12211        assert_eq!(result, JValue::Number(5.0));
12212
12213        // Multiplication
12214        let expr = AstNode::Binary {
12215            op: BinaryOp::Multiply,
12216            lhs: Box::new(AstNode::number(10.0)),
12217            rhs: Box::new(AstNode::number(5.0)),
12218        };
12219        let result = evaluator.evaluate(&expr, &data).unwrap();
12220        assert_eq!(result, JValue::Number(50.0));
12221
12222        // Division
12223        let expr = AstNode::Binary {
12224            op: BinaryOp::Divide,
12225            lhs: Box::new(AstNode::number(10.0)),
12226            rhs: Box::new(AstNode::number(5.0)),
12227        };
12228        let result = evaluator.evaluate(&expr, &data).unwrap();
12229        assert_eq!(result, JValue::Number(2.0));
12230
12231        // Modulo
12232        let expr = AstNode::Binary {
12233            op: BinaryOp::Modulo,
12234            lhs: Box::new(AstNode::number(10.0)),
12235            rhs: Box::new(AstNode::number(3.0)),
12236        };
12237        let result = evaluator.evaluate(&expr, &data).unwrap();
12238        assert_eq!(result, JValue::Number(1.0));
12239    }
12240
12241    #[test]
12242    fn test_division_by_zero() {
12243        let mut evaluator = Evaluator::new();
12244        let data = JValue::Null;
12245
12246        let expr = AstNode::Binary {
12247            op: BinaryOp::Divide,
12248            lhs: Box::new(AstNode::number(10.0)),
12249            rhs: Box::new(AstNode::number(0.0)),
12250        };
12251        let result = evaluator.evaluate(&expr, &data);
12252        assert!(result.is_err());
12253    }
12254
12255    #[test]
12256    fn test_comparison_operations() {
12257        let mut evaluator = Evaluator::new();
12258        let data = JValue::Null;
12259
12260        // Equal
12261        let expr = AstNode::Binary {
12262            op: BinaryOp::Equal,
12263            lhs: Box::new(AstNode::number(5.0)),
12264            rhs: Box::new(AstNode::number(5.0)),
12265        };
12266        assert_eq!(
12267            evaluator.evaluate(&expr, &data).unwrap(),
12268            JValue::Bool(true)
12269        );
12270
12271        // Not equal
12272        let expr = AstNode::Binary {
12273            op: BinaryOp::NotEqual,
12274            lhs: Box::new(AstNode::number(5.0)),
12275            rhs: Box::new(AstNode::number(3.0)),
12276        };
12277        assert_eq!(
12278            evaluator.evaluate(&expr, &data).unwrap(),
12279            JValue::Bool(true)
12280        );
12281
12282        // Less than
12283        let expr = AstNode::Binary {
12284            op: BinaryOp::LessThan,
12285            lhs: Box::new(AstNode::number(3.0)),
12286            rhs: Box::new(AstNode::number(5.0)),
12287        };
12288        assert_eq!(
12289            evaluator.evaluate(&expr, &data).unwrap(),
12290            JValue::Bool(true)
12291        );
12292
12293        // Greater than
12294        let expr = AstNode::Binary {
12295            op: BinaryOp::GreaterThan,
12296            lhs: Box::new(AstNode::number(5.0)),
12297            rhs: Box::new(AstNode::number(3.0)),
12298        };
12299        assert_eq!(
12300            evaluator.evaluate(&expr, &data).unwrap(),
12301            JValue::Bool(true)
12302        );
12303    }
12304
12305    #[test]
12306    fn test_logical_operations() {
12307        let mut evaluator = Evaluator::new();
12308        let data = JValue::Null;
12309
12310        // And - both true
12311        let expr = AstNode::Binary {
12312            op: BinaryOp::And,
12313            lhs: Box::new(AstNode::boolean(true)),
12314            rhs: Box::new(AstNode::boolean(true)),
12315        };
12316        assert_eq!(
12317            evaluator.evaluate(&expr, &data).unwrap(),
12318            JValue::Bool(true)
12319        );
12320
12321        // And - first false
12322        let expr = AstNode::Binary {
12323            op: BinaryOp::And,
12324            lhs: Box::new(AstNode::boolean(false)),
12325            rhs: Box::new(AstNode::boolean(true)),
12326        };
12327        assert_eq!(
12328            evaluator.evaluate(&expr, &data).unwrap(),
12329            JValue::Bool(false)
12330        );
12331
12332        // Or - first true
12333        let expr = AstNode::Binary {
12334            op: BinaryOp::Or,
12335            lhs: Box::new(AstNode::boolean(true)),
12336            rhs: Box::new(AstNode::boolean(false)),
12337        };
12338        assert_eq!(
12339            evaluator.evaluate(&expr, &data).unwrap(),
12340            JValue::Bool(true)
12341        );
12342
12343        // Or - both false
12344        let expr = AstNode::Binary {
12345            op: BinaryOp::Or,
12346            lhs: Box::new(AstNode::boolean(false)),
12347            rhs: Box::new(AstNode::boolean(false)),
12348        };
12349        assert_eq!(
12350            evaluator.evaluate(&expr, &data).unwrap(),
12351            JValue::Bool(false)
12352        );
12353    }
12354
12355    #[test]
12356    fn test_string_concatenation() {
12357        let mut evaluator = Evaluator::new();
12358        let data = JValue::Null;
12359
12360        let expr = AstNode::Binary {
12361            op: BinaryOp::Concatenate,
12362            lhs: Box::new(AstNode::string("Hello")),
12363            rhs: Box::new(AstNode::string(" World")),
12364        };
12365        let result = evaluator.evaluate(&expr, &data).unwrap();
12366        assert_eq!(result, JValue::string("Hello World"));
12367    }
12368
12369    #[test]
12370    fn test_range_operator() {
12371        let mut evaluator = Evaluator::new();
12372        let data = JValue::Null;
12373
12374        // Forward range
12375        let expr = AstNode::Binary {
12376            op: BinaryOp::Range,
12377            lhs: Box::new(AstNode::number(1.0)),
12378            rhs: Box::new(AstNode::number(5.0)),
12379        };
12380        let result = evaluator.evaluate(&expr, &data).unwrap();
12381        assert_eq!(
12382            result,
12383            JValue::array(vec![
12384                JValue::Number(1.0),
12385                JValue::Number(2.0),
12386                JValue::Number(3.0),
12387                JValue::Number(4.0),
12388                JValue::Number(5.0)
12389            ])
12390        );
12391
12392        // Backward range (start > end) returns empty array
12393        let expr = AstNode::Binary {
12394            op: BinaryOp::Range,
12395            lhs: Box::new(AstNode::number(5.0)),
12396            rhs: Box::new(AstNode::number(1.0)),
12397        };
12398        let result = evaluator.evaluate(&expr, &data).unwrap();
12399        assert_eq!(result, JValue::array(vec![]));
12400    }
12401
12402    #[test]
12403    fn test_in_operator() {
12404        let mut evaluator = Evaluator::new();
12405        let data = JValue::Null;
12406
12407        // In array
12408        let expr = AstNode::Binary {
12409            op: BinaryOp::In,
12410            lhs: Box::new(AstNode::number(3.0)),
12411            rhs: Box::new(AstNode::Array(vec![
12412                AstNode::number(1.0),
12413                AstNode::number(2.0),
12414                AstNode::number(3.0),
12415            ])),
12416        };
12417        let result = evaluator.evaluate(&expr, &data).unwrap();
12418        assert_eq!(result, JValue::Bool(true));
12419
12420        // Not in array
12421        let expr = AstNode::Binary {
12422            op: BinaryOp::In,
12423            lhs: Box::new(AstNode::number(5.0)),
12424            rhs: Box::new(AstNode::Array(vec![
12425                AstNode::number(1.0),
12426                AstNode::number(2.0),
12427                AstNode::number(3.0),
12428            ])),
12429        };
12430        let result = evaluator.evaluate(&expr, &data).unwrap();
12431        assert_eq!(result, JValue::Bool(false));
12432    }
12433
12434    #[test]
12435    fn test_unary_operations() {
12436        let mut evaluator = Evaluator::new();
12437        let data = JValue::Null;
12438
12439        // Negation
12440        let expr = AstNode::Unary {
12441            op: UnaryOp::Negate,
12442            operand: Box::new(AstNode::number(5.0)),
12443        };
12444        let result = evaluator.evaluate(&expr, &data).unwrap();
12445        assert_eq!(result, JValue::Number(-5.0));
12446
12447        // Not
12448        let expr = AstNode::Unary {
12449            op: UnaryOp::Not,
12450            operand: Box::new(AstNode::boolean(true)),
12451        };
12452        let result = evaluator.evaluate(&expr, &data).unwrap();
12453        assert_eq!(result, JValue::Bool(false));
12454    }
12455
12456    #[test]
12457    fn test_array_construction() {
12458        let mut evaluator = Evaluator::new();
12459        let data = JValue::Null;
12460
12461        let expr = AstNode::Array(vec![
12462            AstNode::number(1.0),
12463            AstNode::number(2.0),
12464            AstNode::number(3.0),
12465        ]);
12466        let result = evaluator.evaluate(&expr, &data).unwrap();
12467        // Whole number literals are preserved as integers
12468        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
12469    }
12470
12471    #[test]
12472    fn test_object_construction() {
12473        let mut evaluator = Evaluator::new();
12474        let data = JValue::Null;
12475
12476        let expr = AstNode::Object(vec![
12477            (AstNode::string("name"), AstNode::string("Alice")),
12478            (AstNode::string("age"), AstNode::number(30.0)),
12479        ]);
12480        let result = evaluator.evaluate(&expr, &data).unwrap();
12481        // Whole number literals are preserved as integers
12482        let mut expected = IndexMap::new();
12483        expected.insert("name".to_string(), JValue::string("Alice"));
12484        expected.insert("age".to_string(), JValue::Number(30.0));
12485        assert_eq!(result, JValue::object(expected));
12486    }
12487
12488    #[test]
12489    fn test_conditional() {
12490        let mut evaluator = Evaluator::new();
12491        let data = JValue::Null;
12492
12493        // True condition
12494        let expr = AstNode::Conditional {
12495            condition: Box::new(AstNode::boolean(true)),
12496            then_branch: Box::new(AstNode::string("yes")),
12497            else_branch: Some(Box::new(AstNode::string("no"))),
12498        };
12499        let result = evaluator.evaluate(&expr, &data).unwrap();
12500        assert_eq!(result, JValue::string("yes"));
12501
12502        // False condition
12503        let expr = AstNode::Conditional {
12504            condition: Box::new(AstNode::boolean(false)),
12505            then_branch: Box::new(AstNode::string("yes")),
12506            else_branch: Some(Box::new(AstNode::string("no"))),
12507        };
12508        let result = evaluator.evaluate(&expr, &data).unwrap();
12509        assert_eq!(result, JValue::string("no"));
12510
12511        // No else branch returns undefined (not null)
12512        let expr = AstNode::Conditional {
12513            condition: Box::new(AstNode::boolean(false)),
12514            then_branch: Box::new(AstNode::string("yes")),
12515            else_branch: None,
12516        };
12517        let result = evaluator.evaluate(&expr, &data).unwrap();
12518        assert_eq!(result, JValue::Undefined);
12519    }
12520
12521    #[test]
12522    fn test_block_expression() {
12523        let mut evaluator = Evaluator::new();
12524        let data = JValue::Null;
12525
12526        let expr = AstNode::Block(vec![
12527            AstNode::number(1.0),
12528            AstNode::number(2.0),
12529            AstNode::number(3.0),
12530        ]);
12531        let result = evaluator.evaluate(&expr, &data).unwrap();
12532        // Block returns the last expression; whole numbers are preserved as integers
12533        assert_eq!(result, JValue::from(3i64));
12534    }
12535
12536    #[test]
12537    fn test_function_calls() {
12538        let mut evaluator = Evaluator::new();
12539        let data = JValue::Null;
12540
12541        // uppercase function
12542        let expr = AstNode::Function {
12543            name: "uppercase".to_string(),
12544            args: vec![AstNode::string("hello")],
12545            is_builtin: true,
12546        };
12547        let result = evaluator.evaluate(&expr, &data).unwrap();
12548        assert_eq!(result, JValue::string("HELLO"));
12549
12550        // lowercase function
12551        let expr = AstNode::Function {
12552            name: "lowercase".to_string(),
12553            args: vec![AstNode::string("HELLO")],
12554            is_builtin: true,
12555        };
12556        let result = evaluator.evaluate(&expr, &data).unwrap();
12557        assert_eq!(result, JValue::string("hello"));
12558
12559        // length function
12560        let expr = AstNode::Function {
12561            name: "length".to_string(),
12562            args: vec![AstNode::string("hello")],
12563            is_builtin: true,
12564        };
12565        let result = evaluator.evaluate(&expr, &data).unwrap();
12566        assert_eq!(result, JValue::from(5i64));
12567
12568        // sum function
12569        let expr = AstNode::Function {
12570            name: "sum".to_string(),
12571            args: vec![AstNode::Array(vec![
12572                AstNode::number(1.0),
12573                AstNode::number(2.0),
12574                AstNode::number(3.0),
12575            ])],
12576            is_builtin: true,
12577        };
12578        let result = evaluator.evaluate(&expr, &data).unwrap();
12579        assert_eq!(result, JValue::Number(6.0));
12580
12581        // count function
12582        let expr = AstNode::Function {
12583            name: "count".to_string(),
12584            args: vec![AstNode::Array(vec![
12585                AstNode::number(1.0),
12586                AstNode::number(2.0),
12587                AstNode::number(3.0),
12588            ])],
12589            is_builtin: true,
12590        };
12591        let result = evaluator.evaluate(&expr, &data).unwrap();
12592        assert_eq!(result, JValue::from(3i64));
12593    }
12594
12595    #[test]
12596    fn test_complex_nested_data() {
12597        let mut evaluator = Evaluator::new();
12598        let data = JValue::from(serde_json::json!({
12599            "users": [
12600                {"name": "Alice", "age": 30},
12601                {"name": "Bob", "age": 25},
12602                {"name": "Charlie", "age": 35}
12603            ],
12604            "metadata": {
12605                "total": 3,
12606                "version": "1.0"
12607            }
12608        }));
12609        // Access nested field
12610        let path = AstNode::Path {
12611            steps: vec![
12612                PathStep::new(AstNode::Name("metadata".to_string())),
12613                PathStep::new(AstNode::Name("version".to_string())),
12614            ],
12615        };
12616        let result = evaluator.evaluate(&path, &data).unwrap();
12617        assert_eq!(result, JValue::string("1.0"));
12618    }
12619
12620    #[test]
12621    fn test_error_handling() {
12622        let mut evaluator = Evaluator::new();
12623        let data = JValue::Null;
12624
12625        // Type error: adding string and number
12626        let expr = AstNode::Binary {
12627            op: BinaryOp::Add,
12628            lhs: Box::new(AstNode::string("hello")),
12629            rhs: Box::new(AstNode::number(5.0)),
12630        };
12631        let result = evaluator.evaluate(&expr, &data);
12632        assert!(result.is_err());
12633
12634        // Reference error: undefined function
12635        let expr = AstNode::Function {
12636            name: "undefined_function".to_string(),
12637            args: vec![],
12638            is_builtin: false,
12639        };
12640        let result = evaluator.evaluate(&expr, &data);
12641        assert!(result.is_err());
12642    }
12643
12644    #[test]
12645    fn test_truthiness() {
12646        let evaluator = Evaluator::new();
12647
12648        assert!(!evaluator.is_truthy(&JValue::Null));
12649        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
12650        assert!(evaluator.is_truthy(&JValue::Bool(true)));
12651        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
12652        assert!(evaluator.is_truthy(&JValue::from(1i64)));
12653        assert!(!evaluator.is_truthy(&JValue::string("")));
12654        assert!(evaluator.is_truthy(&JValue::string("hello")));
12655        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
12656        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
12657    }
12658
12659    #[test]
12660    fn test_integration_with_parser() {
12661        use crate::parser::parse;
12662
12663        let mut evaluator = Evaluator::new();
12664        let data = JValue::from(serde_json::json!({
12665            "price": 10,
12666            "quantity": 5
12667        }));
12668        // Test simple path
12669        let ast = parse("price").unwrap();
12670        let result = evaluator.evaluate(&ast, &data).unwrap();
12671        assert_eq!(result, JValue::from(10i64));
12672
12673        // Test arithmetic
12674        let ast = parse("price * quantity").unwrap();
12675        let result = evaluator.evaluate(&ast, &data).unwrap();
12676        // Note: Arithmetic operations produce f64 results in JSON
12677        assert_eq!(result, JValue::Number(50.0));
12678
12679        // Test comparison
12680        let ast = parse("price > 5").unwrap();
12681        let result = evaluator.evaluate(&ast, &data).unwrap();
12682        assert_eq!(result, JValue::Bool(true));
12683    }
12684
12685    #[test]
12686    fn test_evaluate_dollar_function_uppercase() {
12687        use crate::parser::parse;
12688
12689        let mut evaluator = Evaluator::new();
12690        let ast = parse(r#"$uppercase("hello")"#).unwrap();
12691        let empty = JValue::object(IndexMap::new());
12692        let result = evaluator.evaluate(&ast, &empty).unwrap();
12693        assert_eq!(result, JValue::string("HELLO"));
12694    }
12695
12696    #[test]
12697    fn test_evaluate_dollar_function_sum() {
12698        use crate::parser::parse;
12699
12700        let mut evaluator = Evaluator::new();
12701        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
12702        let empty = JValue::object(IndexMap::new());
12703        let result = evaluator.evaluate(&ast, &empty).unwrap();
12704        assert_eq!(result, JValue::Number(15.0));
12705    }
12706
12707    #[test]
12708    fn test_evaluate_nested_dollar_functions() {
12709        use crate::parser::parse;
12710
12711        let mut evaluator = Evaluator::new();
12712        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
12713        let empty = JValue::object(IndexMap::new());
12714        let result = evaluator.evaluate(&ast, &empty).unwrap();
12715        // length() returns an integer, not a float
12716        assert_eq!(result, JValue::Number(5.0));
12717    }
12718
12719    #[test]
12720    fn test_array_mapping() {
12721        use crate::parser::parse;
12722
12723        let mut evaluator = Evaluator::new();
12724        let data: JValue = serde_json::from_str(
12725            r#"{
12726            "products": [
12727                {"id": 1, "name": "Laptop", "price": 999.99},
12728                {"id": 2, "name": "Mouse", "price": 29.99},
12729                {"id": 3, "name": "Keyboard", "price": 79.99}
12730            ]
12731        }"#,
12732        )
12733        .map(|v: serde_json::Value| JValue::from(v))
12734        .unwrap();
12735
12736        // Test mapping over array to extract field
12737        let ast = parse("products.name").unwrap();
12738        let result = evaluator.evaluate(&ast, &data).unwrap();
12739        assert_eq!(
12740            result,
12741            JValue::array(vec![
12742                JValue::string("Laptop"),
12743                JValue::string("Mouse"),
12744                JValue::string("Keyboard")
12745            ])
12746        );
12747
12748        // Test mapping over array to extract prices
12749        let ast = parse("products.price").unwrap();
12750        let result = evaluator.evaluate(&ast, &data).unwrap();
12751        assert_eq!(
12752            result,
12753            JValue::array(vec![
12754                JValue::Number(999.99),
12755                JValue::Number(29.99),
12756                JValue::Number(79.99)
12757            ])
12758        );
12759
12760        // Test with $sum function on mapped array
12761        let ast = parse("$sum(products.price)").unwrap();
12762        let result = evaluator.evaluate(&ast, &data).unwrap();
12763        assert_eq!(result, JValue::Number(1109.97));
12764    }
12765
12766    #[test]
12767    fn test_empty_brackets() {
12768        use crate::parser::parse;
12769
12770        let mut evaluator = Evaluator::new();
12771
12772        // Test empty brackets on simple value - should wrap in array
12773        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
12774        let ast = parse("foo[]").unwrap();
12775        let result = evaluator.evaluate(&ast, &data).unwrap();
12776        assert_eq!(
12777            result,
12778            JValue::array(vec![JValue::string("bar")]),
12779            "Empty brackets should wrap value in array"
12780        );
12781
12782        // Test empty brackets on array - should return array as-is
12783        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
12784        let ast2 = parse("arr[]").unwrap();
12785        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
12786        assert_eq!(
12787            result2,
12788            JValue::array(vec![
12789                JValue::Number(1.0),
12790                JValue::Number(2.0),
12791                JValue::Number(3.0)
12792            ]),
12793            "Empty brackets should preserve array"
12794        );
12795    }
12796
12797    // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
12798    // Expected values below are ground-truthed against jsonata-js 2.x.
12799
12800    #[test]
12801    fn test_index_bind_makes_variable_available_in_next_step() {
12802        // `#$o` binds each Order's position; `$o` must resolve in the later step.
12803        let data: JValue = serde_json::json!({
12804            "Account": {
12805                "Order": [
12806                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
12807                    {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
12808                ]
12809            }
12810        })
12811        .into();
12812        let ast =
12813            crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
12814        let mut evaluator = Evaluator::new();
12815        let result = evaluator.evaluate(&ast, &data).unwrap();
12816        assert_eq!(
12817            result,
12818            serde_json::json!([
12819                {"name": "Hat", "idx": 0},
12820                {"name": "Cap", "idx": 1},
12821                {"name": "Sock", "idx": 1}
12822            ])
12823            .into()
12824        );
12825    }
12826
12827    #[test]
12828    fn test_index_bind_with_predicate_stage() {
12829        // Mirrors reference joins/index[13]: index binding, then a predicate on
12830        // the next step, carrying the index binding through.
12831        let data: JValue = serde_json::json!({
12832            "Account": {
12833                "Order": [
12834                    {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
12835                    {"Product": [{"ProductID": 9, "Name": "C"}]}
12836                ]
12837            }
12838        })
12839        .into();
12840        let ast =
12841            crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
12842                .unwrap();
12843        let mut evaluator = Evaluator::new();
12844        let result = evaluator.evaluate(&ast, &data).unwrap();
12845        assert_eq!(
12846            result,
12847            serde_json::json!([
12848                {"n": "B", "idx": 0},
12849                {"n": "C", "idx": 1}
12850            ])
12851            .into()
12852        );
12853    }
12854
12855    #[test]
12856    fn test_focus_bind_makes_variable_available_in_next_step() {
12857        // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
12858        // does NOT advance the context `@`); the variable itself is what carries
12859        // forward. This asserts the real jsonata-js behaviour.
12860        let data: JValue = serde_json::json!({
12861            "Account": {
12862                "Order": [
12863                    {"OrderID": "o1"},
12864                    {"OrderID": "o2"}
12865                ]
12866            }
12867        })
12868        .into();
12869        let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
12870        let mut evaluator = Evaluator::new();
12871        let result = evaluator.evaluate(&ast, &data).unwrap();
12872        assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
12873    }
12874
12875    #[test]
12876    fn test_parent_reference_resolves_to_enclosing_step_value() {
12877        let data: JValue = serde_json::json!({
12878            "Account": {
12879                "Order": [
12880                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
12881                ]
12882            }
12883        })
12884        .into();
12885        let ast =
12886            crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
12887                .unwrap();
12888        let mut evaluator = Evaluator::new();
12889        let result = evaluator.evaluate(&ast, &data).unwrap();
12890        assert_eq!(
12891            result,
12892            serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
12893        );
12894    }
12895
12896    // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
12897    // a tuple-carried `$name`/`!label` key straight into the top scope and then
12898    // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
12899    // same-named outer `:=` binding that happened to be live in that scope
12900    // frame. Expected values below are verified against jsonata-js (2.2.1
12901    // reference, `tests/jsonata-js`).
12902
12903    #[test]
12904    fn test_chained_focus_bind_does_not_clobber_outer_variable() {
12905        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12906        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
12907        let mut evaluator = Evaluator::new();
12908        let result = evaluator.evaluate(&ast, &data).unwrap();
12909        assert_eq!(result, serde_json::json!("OUT").into());
12910    }
12911
12912    #[test]
12913    fn test_chained_index_bind_does_not_clobber_outer_variable() {
12914        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12915        let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
12916        let mut evaluator = Evaluator::new();
12917        let result = evaluator.evaluate(&ast, &data).unwrap();
12918        assert_eq!(result, serde_json::json!("OUT").into());
12919    }
12920
12921    #[test]
12922    fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
12923        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12924        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
12925        let mut evaluator = Evaluator::new();
12926        let result = evaluator.evaluate(&ast, &data).unwrap();
12927        assert_eq!(result, serde_json::json!("OUT").into());
12928    }
12929
12930    #[test]
12931    fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
12932        let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
12933        let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
12934        let mut evaluator = Evaluator::new();
12935        let result = evaluator.evaluate(&ast, &data).unwrap();
12936        assert_eq!(result, serde_json::json!("OUT").into());
12937    }
12938}